[
  {
    "path": ".dockerignore",
    "content": "# Ignore files and directories starting with a dot\n\n# Ignore specific files\n.dockerignore\n.git\n\n# Ignore build artifacts\nlogs/\n_output/\n# Ignore non-essential documentation\nREADME.md\nREADME-zh_CN.md\nCONTRIBUTING.md\nCHANGELOG/\n# LICENSE\n\n# Ignore testing and linting configuration\n.golangci.yml\n\n\n# Ignore assets\nassets/\n\n# Ignore components\ncomponents/\n\n# Ignore tools and scripts\n.github/\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.sh text eol=lf\n"
  },
  {
    "path": ".github/.codecov.yml",
    "content": "coverage:\n  status:\n    project:\n      default: false # disable the default status that measures entire project\n      pkg: # declare a new status context \"pkg\"\n        paths:\n          - pkg/* # only include coverage in \"pkg/\" folder\n        informational: true # Always pass check\n      tools: # declare a new status context \"tools\"\n        paths:\n          - tools/* # only include coverage in \"tools/\" folder\n        informational: true # Always pass check\n      test: # declare a new status context \"test\"\n        paths:\n          - test/* # only include coverage in \"test/\" folder\n        informational: true # Always pass check\n      \n      # internal: # declare a new status context \"internal\"\n      #   paths:\n      #     - internal/* # only include coverage in \"internal/\" folder\n      #   informational: true # Always pass check\n      # cmd: # declare a new status context \"cmd\"\n      #   paths:\n      #     - cmd/* # only include coverage in \"cmd/\" folder\n      #   informational: true # Always pass check\n    patch: off # disable the commit only checks\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug Report\ntitle: \"[BUG] \"\nlabels: [\"bug\"]\ndescription: \"Create a detailed report to help us identify and resolve issues.\"\n# assignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: \"Thank you for taking the time to fill out the bug report. Please provide as much information as possible to help us understand and replicate the bug.\"\n\n  - type: input\n    id: openim-server-version\n    attributes:\n      label: OpenIM Server Version\n      description: \"Please provide the version number of OpenIM Server you are using.\"\n      placeholder: \"e.g., 3.8.0\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: operating-system\n    attributes:\n      label: Operating System and CPU Architecture\n      description: \"Please select the operating system and describe the CPU architecture.\"\n      options:\n        - Linux (AMD)\n        - Linux (ARM)\n        - Windows (AMD)\n        - Windows (ARM)\n        - macOS (AMD)\n        - macOS (ARM)\n    validations:\n      required: true\n\n  - type: dropdown\n    id: deployment-method\n    attributes:\n      label: Deployment Method\n      description: \"Please specify how OpenIM Server was deployed.\"\n      options:\n        - Source Code Deployment\n        - Docker Deployment\n    validations:\n      required: true\n      \n  - type: textarea\n    id: bug-description-reproduction\n    attributes:\n      label: Bug Description and Steps to Reproduce\n      description: \"Provide a detailed description of the bug and a step-by-step guide on how to reproduce it.\"\n      placeholder: \"Describe the bug in detail here...\\n\\nSteps to reproduce the bug on the server:\\n1. Start the server with specific configurations (mention any relevant config details).\\n2. Make an API call to '...' endpoint with the following payload '...'.\\n3. Observe the behavior and note any error messages or logs.\\n4. Mention any additional setup relevant to the bug (e.g., database version, external service dependencies).\"\n    validations:\n      required: true\n\n  - type: markdown\n    attributes:\n      value: \"If possible, please add screenshots to help explain your problem.\"\n\n  - type: textarea\n    id: screenshots-link\n    attributes:\n      label: Screenshots Link\n      description: \"If applicable, please provide any links to screenshots here.\"\n      placeholder: \"Paste your screenshot URL here, e.g., http://imgur.com/example\""
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  # - name: \"Bug Report\"\n  #   description: \"Report a bug in the project\"\n  #   file: \"bug-report.yml\"\n  - name: 📢 Connect on slack\n    url: https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A\n    about: Support OpenIM-related requests or issues, get in touch with developers and help on slack\n  - name: 🌐 OpenIM Blog\n    url: https://www.openim.io/\n    about: Open the OpenIM community blog\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/deployment.yml",
    "content": "name: Deployment issue\ntitle: \"[Deployment] \"\nlabels: [\"deployment\"]\ndescription: \"Create a detailed report to help us identify and resolve deployment issues.\"\n# assignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: \"Thank you for taking the time to fill out the deployment issue report. Please provide as much information as possible to help us understand and resolve the issue.\"\n\n  - type: input\n    id: openim-server-version\n    attributes:\n      label: OpenIM Server Version\n      description: \"Please provide the version number of OpenIM Server you are using.\"\n      placeholder: \"e.g., 3.8.0\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: operating-system\n    attributes:\n      label: Operating System and CPU Architecture\n      description: \"Please select the operating system and describe the CPU architecture.\"\n      options:\n        - Linux (AMD)\n        - Linux (ARM)\n        - Windows (AMD)\n        - Windows (ARM)\n        - macOS (AMD)\n        - macOS (ARM)\n    validations:\n      required: true\n\n  - type: dropdown\n    id: deployment-method\n    attributes:\n      label: Deployment Method\n      description: \"Please specify how OpenIM Server was deployed.\"\n      options:\n        - Source Code Deployment\n        - Docker Deployment\n    validations:\n      required: true\n      \n  - type: textarea\n    id: issue-description-reproduction\n    attributes:\n      label: Issue Description and Steps to Reproduce\n      description: \"Provide a detailed description of the issue and a step-by-step guide on how to reproduce it.\"\n      placeholder: \"Describe the issue in detail here...\\n\\nSteps to reproduce the issue on the server:\\n1. Start the server with specific configurations (mention any relevant config details).\\n2. Make an API call to '...' endpoint with the following payload '...'.\\n3. Observe the behavior and note any error messages or logs.\\n4. Mention any additional setup relevant to the bug (e.g., database version, external service dependencies).\"\n    validations:\n      required: true\n\n  - type: markdown\n    attributes:\n      value: \"If possible, please add screenshots to help explain your problem.\"\n\n  - type: textarea\n    id: screenshots-link\n    attributes:\n      label: Screenshots Link\n      description: \"If applicable, please provide any links to screenshots here.\"\n      placeholder: \"Paste your screenshot URL here, e.g., http://imgur.com/example\""
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.md",
    "content": "---\nname: Documentation Update\nabout: Propose updates to documentation, including README files and other docs.\ntitle: \"[DOC]: \"  # Prefix for the title to help identify documentation issues\nlabels: documentation  # Labels to be automatically added\nassignees: ''  # Optionally, specify maintainers or teams to be auto-assigned\n\n---\n\n## Documentation Updates\nDescribe the documentation that needs to be updated or corrected. Please specify the files and sections if possible.\n\n## Motivation\nExplain why these updates are necessary. What is missing, misleading, or outdated?\n\n## Suggested Changes\nDetail the changes that you propose. If you are suggesting large changes, include examples or mockups of what the updated documentation should look like.\n\n## Additional Information\nInclude any other information that might be relevant, such as links to discussions or related issues in the repository.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yml",
    "content": "name: Feature Request\ntitle: \"[FEATURE REQUEST] \"\nlabels: [\"feature request\",\"enhancement\"]\ndescription: \"Propose a new feature or improvement that you believe will help enhance the project.\"\n# assignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: \"Thank you for taking the time to propose a feature request. Please fill in as much detail as possible to help us understand why this feature is necessary and how it should work.\"\n\n  - type: textarea\n    id: feature-reason\n    attributes:\n      label: Why this feature?\n      description: \"Explain why this feature is needed. What problem does it solve? How does it benefit the project and its users?\"\n      placeholder: \"Describe the need for this feature...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: solution-proposal\n    attributes:\n      label: Suggested Solution\n      description: \"Describe your proposed solution for this feature. How do you envision it working?\"\n      placeholder: \"Detail your solution here...\"\n    validations:\n      required: true\n\n  - type: markdown\n    attributes:\n      value: \"Please provide any other relevant information or screenshots that could help illustrate your idea.\"\n\n  - type: textarea\n    id: additional-info\n    attributes:\n      label: Additional Information\n      description: \"Include any additional information, links, or screenshots that might be relevant to your feature request.\"\n      placeholder: \"Add more context or links to relevant resources...\"\n\n  - type: markdown\n    attributes:\n      value: \"Thank you for contributing to the project! We appreciate your input and will review your suggestion as soon as possible.\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/other.yml",
    "content": "name: 🐧 Other\ndescription: Use this for any other issues. Please do NOT create blank issues\ntitle: \"[Other]: <give this problem a name>\"\nlabels: [\"other\"]\n# assignees: []\n\nbody:\n  - type: markdown\n    attributes:\n      value: \"# Other issue\"\n  - type: textarea\n    id: issuedescription\n    attributes:\n      label: What would you like to share?\n      description: Provide a clear and concise explanation of your issue.\n    validations:\n      required: true\n  - type: textarea\n    id: extrainfo\n    attributes:\n      label: Additional information\n      description: Is there anything else we should know about this issue?\n    validations:\n      required: false\n  - type: markdown\n    attributes:\n      value: |\n        You can also join our Discord community [here](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n        Feel free to check out other cool repositories of the openim Community [here](https://github.com/openimsdk)\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/rfc.md",
    "content": "---\nname: RFC - Feature Proposal\nabout: Submit a proposal for a significant feature to invite community discussion.\ntitle: \"[RFC]: \"  # Prefix for the title to help identify RFC proposals\nlabels: rfc, proposal  # Labels to be automatically added\nassignees: ''  # Optionally, specify maintainers or teams to be auto-assigned\n\n---\n\n## Proposal Overview\nBriefly describe the content and objectives of your proposal.\n\n## Motivation\nWhy is this new feature necessary? What is the background of this problem?\n\n## Detailed Design\nDescribe the technical details of the proposal, including implementation steps, code snippets, or architecture diagrams.\n\n## Alternatives Considered\nHave other alternatives been considered? Why is this approach preferred over others?\n\n## Impact\nHow will this proposal affect existing practices and community users?\n\n## Additional Information\nInclude any other relevant information such as related discussions, prior related work, etc.\n"
  },
  {
    "path": ".github/sync-release.yml",
    "content": "openimsdk/openim-docker:\n  - source: ./config\n    dest: ./openim-server/release/config\n    replace: true\n  - source: ./docs\n    dest: ./openim-server/release/docs\n    replace: true\n  - source: ./scripts\n    dest: ./openim-server/release/scripts\n    replace: true\n  - source: ./scripts\n    dest: ./scripts\n    replace: false\n  - source: ./Makefile\n    dest: ./Makefile\n    replace: false\n"
  },
  {
    "path": ".github/workflows/auto-assign-issue.yml",
    "content": "name: Assign issue to comment author\non:\n  issue_comment:\n    types: [created]\njobs:\n  assign-issue:\n    if: |\n      contains(github.event.comment.body, '/assign') || contains(github.event.comment.body, '/accept') &&\n      !contains(github.event.comment.user.login, 'openim-robot')\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Assign the issue\n        run: |\n          export LETASE_MILESTONES=$(curl 'https://api.github.com/repos/$OWNER/$PEPO/milestones' | jq -r 'last(.[]).title')\n          gh issue edit ${{ github.event.issue.number }} --add-assignee \"${{ github.event.comment.user.login }}\"\n          gh issue edit ${{ github.event.issue.number }} --add-label \"accepted\"\n          gh issue comment $ISSUE --body \"@${{ github.event.comment.user.login }} Glad to see you accepted this issue🤲, this issue has been assigned to you. I set the milestones for this issue to [$LETASE_MILESTONES](https://github.com/$OWNER/$PEPO/milestones), We are looking forward to your PR!\"\n\n        # gh issue edit ${{ github.event.issue.number }} --milestone \"$LETASE_MILESTONES\"\n        env:\n          GH_TOKEN: ${{ secrets.BOT_TOKEN }}\n          ISSUE: ${{ github.event.issue.html_url }}\n          OWNER: ${{ github.repository_owner }}\n          REPO: ${{ github.event.repository.name }}\n"
  },
  {
    "path": ".github/workflows/auto-invite-comment.yml",
    "content": "name: Invite users to join OpenIM Community.\non:\n  issue_comment:\n    types:\n      - created\njobs:\n  issue_comment:\n    name: Invite users to join OpenIM Community\n    if: ${{ github.event.comment.body == '/invite' || github.event.comment.body == '/close' || github.event.comment.body == '/comment' }}\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - name: Invite user to join OpenIM Community\n        uses: peter-evans/create-or-update-comment@v4\n        with:\n          token: ${{ secrets.BOT_GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            We value close connections with our users, developers, and contributors here at Open-IM-Server. With a large community and maintainer team, we're always here to help and support you. Whether you're looking to join our community or have any questions or suggestions, we welcome you to get in touch with us.\n\n            Our most recommended way to get in touch is through [Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A). Even if you're in China, Slack is usually not blocked by firewalls, making it an easy way to connect with us. Our Slack community is the ideal place to discuss and share ideas and suggestions with other users and developers of Open-IM-Server. You can ask technical questions, seek help, or share your experiences with other users of Open-IM-Server.\n\n            In addition to Slack, we also offer the following ways to get in touch:\n\n            + <a href=\"https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A\" target=\"_blank\"><img src=\"https://img.shields.io/badge/Slack-OpenIM%2B-blueviolet?logo=slack&amp;logoColor=white\"></a> We also have Slack channels for you to communicate and discuss. To join, visit https://slack.com/ and join our [👀 Open-IM-Server slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) team channel.\n            + <a href=\"https://mail.google.com/mail/u/0/?fs=1&tf=cm&to=info@openim.io\" target=\"_blank\"><img src=\"https://img.shields.io/badge/gmail-%40OOpenIMSDKCore?style=social&logo=gmail\"></a> Get in touch with us on [Gmail](https://mail.google.com/mail/u/0/?fs=1&tf=cm&to=winxu81@gmail.com). If you have any questions or issues that need resolving, or any suggestions and feedback for our open source projects, please feel free to contact us via email.\n            + <a href=\"https://doc.rentsoft.cn/\" target=\"_blank\"><img src=\"https://img.shields.io/badge/%E5%8D%9A%E5%AE%A2-%40OpenIMSDKCore-blue?style=social&logo=Octopus%20Deploy\"></a> Read our [blog](https://doc.rentsoft.cn/). Our blog is a great place to stay up-to-date with Open-IM-Server projects and trends. On the blog, we share our latest developments, tech trends, and other interesting information.\n            + <a href=\"https://github.com/OpenIMSDK/OpenIM-Docs/blob/main/docs/images/WechatIMG20.jpeg\" target=\"_blank\"><img src=\"https://img.shields.io/badge/%E5%BE%AE%E4%BF%A1-OpenIMSDKCore-brightgreen?logo=wechat&style=flat-square\"></a> Add [Wechat](https://github.com/OpenIMSDK/OpenIM-Docs/blob/main/docs/images/WechatIMG20.jpeg) and indicate that you are a user or developer of Open-IM-Server. We will process your request as soon as possible.\n\n      # - name: Close Issue\n      #   uses: peter-evans/close-issue@v3\n      #   with:\n      #     token: ${{ secrets.BOT_GITHUB_TOKEN }}\n      #     issue-number: ${{ github.event.issue.number }}\n      #     comment: 🤖 Auto-closing issue, if you still need help please reopen the issue or ask for help in the community above\n      #     labels: |\n      #       accepted\n"
  },
  {
    "path": ".github/workflows/changelog.yml",
    "content": "name: Release Changelog\n\non:\n  release:\n    types: [released]\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  update-changelog:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v4\n\n    - name: Run Go Changelog Generator\n      run: |\n        # Run the Go changelog generator, passing the release tag if available\n        if [ \"${{ github.event.release.tag_name }}\" = \"latest\" ]; then\n          go run tools/changelog/changelog.go > \"${{ github.event.release.tag_name }}-changelog.md\"\n        else\n          go run tools/changelog/changelog.go \"${{ github.event.release.tag_name }}\" > \"${{ github.event.release.tag_name }}-changelog.md\"\n        fi\n\n    - name: Handle changelog files\n      run: |\n        # Ensure that the CHANGELOG directory exists\n        mkdir -p CHANGELOG\n\n        # Extract Major.Minor version by removing the 'v' prefix from the tag name\n        TAG_NAME=${{ github.event.release.tag_name }}\n        CHANGELOG_VERSION_NUMBER=$(echo \"$TAG_NAME\" | sed 's/^v//' | grep -oP '^\\d+\\.\\d+')\n\n        # Define the new changelog file path\n        CHANGELOG_FILENAME=\"CHANGELOG-$CHANGELOG_VERSION_NUMBER.md\"\n        CHANGELOG_PATH=\"CHANGELOG/$CHANGELOG_FILENAME\"\n\n        # Check if the changelog file for the current release already exists\n        if [ -f \"$CHANGELOG_PATH\" ]; then\n          # If the file exists, append the new changelog to the existing one\n          cat \"$CHANGELOG_PATH\" >> \"${TAG_NAME}-changelog.md\"\n          # Overwrite the existing changelog with the updated content\n          mv \"${TAG_NAME}-changelog.md\" \"$CHANGELOG_PATH\"\n        else\n          # If the changelog file doesn't exist, rename the temp changelog file to the new changelog file\n          mv \"${TAG_NAME}-changelog.md\" \"$CHANGELOG_PATH\"\n\n          # Ensure that README.md exists\n          if [ ! -f \"CHANGELOG/README.md\" ]; then\n            echo -e \"# CHANGELOGs\\n\\n\" > CHANGELOG/README.md\n          fi\n          \n            # Add the new changelog entry at the top of the README.md\n            if ! grep -q \"\\[$CHANGELOG_FILENAME\\]\" CHANGELOG/README.md; then\n            sed -i \"3i- [$CHANGELOG_FILENAME](./$CHANGELOG_FILENAME)\" CHANGELOG/README.md\n            # Remove the extra newline character added by sed\n            # sed -i '4d' CHANGELOG/README.md\n            fi\n          fi\n\n    - name: Clean up\n      run: |\n        # Remove any temporary files that were created during the process\n        rm -f \"${{ github.event.release.tag_name }}-changelog.md\"\n\n    - name: Create Pull Request\n      uses: peter-evans/create-pull-request@v7.0.5\n      with:\n        token: ${{ secrets.GITHUB_TOKEN }}\n        commit-message: \"Update CHANGELOG for release ${{ github.event.release.tag_name }}\"\n        title: \"Update CHANGELOG for release ${{ github.event.release.tag_name }}\"\n        body: \"This PR updates the CHANGELOG files for release ${{ github.event.release.tag_name }}\"\n        branch: changelog-${{ github.event.release.tag_name }} \n        base: main \n        delete-branch: true\n        labels: changelog\n"
  },
  {
    "path": ".github/workflows/cla-assistant.yml",
    "content": "name: CLA Assistant\non:\n  issue_comment:\n    types: [created]\n  pull_request_target:\n    types: [opened,closed,synchronize]\n\n# explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings\npermissions:\n  actions: write\n  contents: write # this can be 'read' if the signatures are in remote repository\n  pull-requests: write\n  statuses: write\n\njobs:\n  CLA-Assistant:\n    runs-on: ubuntu-latest\n    steps:\n      - name: \"CLA Assistant\"\n        if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'\n        uses: contributor-assistant/github-action@v2.4.0\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          PERSONAL_ACCESS_TOKEN: ${{ secrets.BOT_TOKEN }}\n        with:\n          path-to-signatures: 'signatures/cla.json'\n          path-to-document: 'https://github.com/OpenIM-Robot/cla/blob/main/README.md' # e.g. a CLA or a DCO document\n          branch: 'main'\n          allowlist: 'bot*,*bot,OpenIM-Robot'\n\n         # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken\n          remote-organization-name: OpenIM-Robot\n          remote-repository-name: cla\n          create-file-commit-message: 'Creating file for storing CLA Signatures'\n          # signed-commit-message: '$contributorName has signed the CLA in $owner/$repo#$pullRequestNo'\n          custom-notsigned-prcomment: '💕 Thank you for your contribution and please kindly read and sign our CLA. [CLA Docs](https://github.com/OpenIM-Robot/cla/blob/main/README.md)'\n          custom-pr-sign-comment: 'I have read the CLA Document and I hereby sign the CLA'\n          custom-allsigned-prcomment: '🤖 All Contributors have signed the [CLA](https://github.com/OpenIM-Robot/cla/blob/main/README.md).<br> The signed information is recorded [**here**](https://github.com/OpenIM-Robot/cla/blob/main/signatures/cla.json)'\n          #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)\n          #use-dco-flag: true - If you are using DCO instead of CLA\n"
  },
  {
    "path": ".github/workflows/cleanup-after-milestone-prs-merged.yml",
    "content": "name: Cleanup After Milestone PRs Merged\n\non:\n  pull_request:\n    types:\n      - closed\n\njobs:\n  handle_pr:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4.2.0\n\n    - name: Get the PR title and extract PR numbers\n      id: extract_pr_numbers\n      run: |\n        # Get the PR title\n        PR_TITLE=\"${{ github.event.pull_request.title }}\"\n\n        echo \"PR Title: $PR_TITLE\"\n\n        # Extract PR numbers from the title\n        PR_NUMBERS=$(echo \"$PR_TITLE\" | grep -oE \"#[0-9]+\" | tr -d '#' | tr '\\n' ' ')\n        echo \"Extracted PR Numbers: $PR_NUMBERS\"\n\n        # Save PR numbers to a file\n        echo \"$PR_NUMBERS\" > pr_numbers.txt\n        echo \"Saved PR Numbers to pr_numbers.txt\"\n\n        # Check if the title matches a specific pattern\n        if echo \"$PR_TITLE\" | grep -qE \"^deps: Merge( #[0-9]+)+ PRs into .+\"; then\n          echo \"proceed=true\" >> $GITHUB_OUTPUT\n        else\n          echo \"proceed=false\" >> $GITHUB_OUTPUT\n        fi\n\n    - name: Use extracted PR numbers and label PRs\n      if: (steps.extract_pr_numbers.outputs.proceed == 'true' || contains(github.event.pull_request.labels.*.name, 'milestone-merge')) && github.event.pull_request.merged == true\n      run: |\n        # Read the previously saved PR numbers\n        PR_NUMBERS=$(cat pr_numbers.txt)\n        echo \"Using extracted PR Numbers: $PR_NUMBERS\"\n\n        # Loop through each PR number and add label\n        for PR_NUMBER in $PR_NUMBERS; do\n          echo \"Adding 'cherry-picked' label to PR #$PR_NUMBER\"\n          curl -X POST \\\n            -H \"Authorization: token ${{ secrets.GITHUB_TOKEN }}\" \\\n            -H \"Accept: application/vnd.github+json\" \\\n            https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/labels \\\n            -d '{\"labels\":[\"cherry-picked\"]}'\n        done\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n    - name: Delete branch after PR close\n      if: steps.extract_pr_numbers.outputs.proceed == 'true' || contains(github.event.pull_request.labels.*.name, 'milestone-merge')\n      run: |\n        BRANCH_NAME=\"${{ github.event.pull_request.head.ref }}\"\n        echo \"Branch to delete: $BRANCH_NAME\"\n        git push origin --delete \"$BRANCH_NAME\"\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ main ]\n  schedule:\n    - cron: '18 19 * * 6'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'go' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]\n        # Learn more:\n        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v3\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v3\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v3"
  },
  {
    "path": ".github/workflows/comment-check.yml",
    "content": "name: Non-English Comments Check\n\non:\n  pull_request:\n    branches:\n      - main\n  workflow_dispatch:\n\njobs:\n  non-english-comments-check:\n    runs-on: ubuntu-latest\n\n    env:\n      # need ignore Dirs\n      EXCLUDE_DIRS: \".git docs tests scripts assets node_modules build\"\n      # need ignore Files\n      EXCLUDE_FILES: \"*.md *.txt *.html *.css *.min.js *.mdx\"\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Search for Non-English comments\n        run: |\n          set -e\n          # Define the regex pattern to match Chinese characters\n          pattern='[\\p{Han}]'\n\n          # Process the directories to be excluded\n          exclude_dirs=\"\"\n          for dir in $EXCLUDE_DIRS; do\n            exclude_dirs=\"$exclude_dirs --exclude-dir=$dir\"\n          done\n\n          # Process the file types to be excluded\n          exclude_files=\"\"\n          for file in $EXCLUDE_FILES; do\n            exclude_files=\"$exclude_files --exclude=$file\"\n          done\n\n          # Use grep to find all comments containing Non-English characters and save to file\n          grep -Pnr \"$pattern\" . $exclude_dirs $exclude_files > non_english_comments.txt || true\n\n      - name: Output non-English comments are found\n        run: |\n          if [ -s non_english_comments.txt ]; then\n            echo \"Non-English comments found in the following locations:\"\n            cat non_english_comments.txt\n            exit 1  # terminate the workflow\n          else\n            echo \"No Non_English comments found.\"\n          fi\n"
  },
  {
    "path": ".github/workflows/docker-build-and-release-services-images.yml",
    "content": "name: Build and release services Docker Images\n\non:\n  push:\n    branches:\n      - release-*\n  release:\n    types: [published]\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Tag version to be used for Docker image\"\n        required: true\n        default: \"v3.8.3\"\n\njobs:\n  build-and-push:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3.8.0\n\n      - name: Log in to Docker Hub\n        uses: docker/login-action@v3.3.0\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Log in to GitHub Container Registry\n        uses: docker/login-action@v3.3.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Log in to Aliyun Container Registry\n        uses: docker/login-action@v3.3.0\n        with:\n          registry: registry.cn-hangzhou.aliyuncs.com\n          username: ${{ secrets.ALIREGISTRY_USERNAME }}\n          password: ${{ secrets.ALIREGISTRY_TOKEN }}\n\n      - name: Extract metadata for Docker (tags, labels)\n        id: meta\n        uses: docker/metadata-action@v5.6.0\n        with:\n          tags: |\n            type=ref,event=tag\n            type=schedule\n            type=ref,event=branch\n            type=semver,pattern={{version}}\n            type=semver,pattern=v{{version}}\n            type=semver,pattern=release-{{raw}}\n            type=sha\n            type=raw,value=${{ github.event.inputs.tag }}\n\n      - name: Build and push Docker images\n        run: |\n          IMG_DIR=\"build/images\"\n          for dir in \"$IMG_DIR\"/*/; do\n              # Find Dockerfile or *.dockerfile in a case-insensitive manner\n              dockerfile=$(find \"$dir\" -maxdepth 1 -type f \\( -iname 'dockerfile' -o -iname '*.dockerfile' \\) | head -n 1)\n              \n              if [ -n \"$dockerfile\" ] && [ -f \"$dockerfile\" ]; then\n                  IMAGE_NAME=$(basename \"$dir\")\n                  echo \"Building Docker image for $IMAGE_NAME with tags:\"\n                  \n                  # Initialize tag arguments\n                  tag_args=()\n\n                  # Read each tag and append --tag arguments\n                  while IFS= read -r tag; do\n                      tag_args+=(--tag \"${{ secrets.DOCKER_USERNAME }}/$IMAGE_NAME:$tag\")\n                      tag_args+=(--tag \"ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:$tag\")\n                      tag_args+=(--tag \"registry.cn-hangzhou.aliyuncs.com/openimsdk/$IMAGE_NAME:$tag\")\n                  done <<< \"${{ steps.meta.outputs.tags }}\"\n\n                  # Build and push the Docker image with all tags\n                  docker buildx build --platform linux/amd64,linux/arm64 \\\n                    --file \"$dockerfile\" \\\n                    \"${tag_args[@]}\" \\\n                    --push \\\n                    \".\"\n              else\n                  echo \"No valid Dockerfile found in $dir\"\n              fi\n          done\n"
  },
  {
    "path": ".github/workflows/go-build-test.yml",
    "content": "name: Go Build Test\n\non:\n  push:\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n\n  workflow_dispatch:\n\njobs:\n  go-build:\n    name: Test with go ${{ matrix.go_version }} on ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n\n    env:\n      SHARE_CONFIG_PATH: config/share.yml\n\n    permissions:\n      contents: write\n      pull-requests: write\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        go_version: [\"1.22.x\"]\n\n    steps:\n      - name: Checkout Server repository\n        uses: actions/checkout@v4\n\n      - name: Set up Go ${{ matrix.go_version }}\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go_version }}\n\n      - name: Get Server dependencies\n        run: |\n          go install github.com/magefile/mage@latest\n          go mod tidy\n          go mod download\n\n      - name: Set up infra services\n        uses: hoverkraft-tech/compose-action@v2.0.1\n        with:\n          compose-file: \"./docker-compose.yml\"\n\n      - name: Modify Server Configuration\n        run: |\n          yq e '.secret = 123456' -i ${{ env.SHARE_CONFIG_PATH }}\n\n      # - name: Get Internal IP Address\n      #   id: get-ip\n      #   run: |\n      #     IP=$(hostname -I | awk '{print $1}')\n      #     echo \"The IP Address is: $IP\"\n      #     echo \"::set-output name=ip::$IP\"\n\n      # - name: Update .env\n      #   run: |\n      #     sed -i 's|externalAddress:.*|externalAddress: \"http://${{ steps.get-ip.outputs.ip }}:10005\"|' config/minio.yml\n      #     cat config/minio.yml\n\n      - name: Build and test Server Services\n        run: |\n          mage build\n          mage start\n          mage check\n\n      - name: Checkout Chat repository\n        uses: actions/checkout@v4\n        with:\n          repository: \"openimsdk/chat\"\n          path: \"chat-repo\"\n\n      - name: Get Chat dependencies\n        run: |\n          cd ${{ github.workspace }}/chat-repo\n          go mod tidy\n          go mod download\n          go install github.com/magefile/mage@latest\n\n      - name: Modify Chat Configuration\n        run: |\n          cd ${{ github.workspace }}/chat-repo\n          yq e '.openIM.secret = 123456' -i ${{ env.SHARE_CONFIG_PATH }}\n\n      - name: Build and test Chat Services\n        run: |\n          cd ${{ github.workspace }}/chat-repo\n          mage build\n          mage start\n          mage check\n\n      - name: Test Server and Chat\n        run: |\n          check_error() {\n            echo \"Response: $1\"\n            errCode=$(echo $1 | jq -r '.errCode')\n            if [ \"$errCode\" != \"0\" ]; then\n              errMsg=$(echo $1 | jq -r '.errMsg')\n              echo \"Error: $errMsg\"\n              exit 1\n            fi\n          }\n\n          # Test register\n          response1=$(curl -X POST -H \"Content-Type: application/json\" -H \"operationID: imAdmin\" -d '{\n            \"verifyCode\": \"666666\",\n            \"platform\": 3,\n            \"autoLogin\": true,\n            \"user\":{\n            \"nickname\": \"test12312\",\n            \"areaCode\":\"+86\",\n            \"phoneNumber\": \"12345678190\",\n            \"password\":\"test123456\"\n            }\n          }' http://127.0.0.1:10008/account/register)\n          check_error \"$response1\"\n          userID1=$(echo $response1 | jq -r '.data.userID')\n          echo \"userID1: $userID1\"\n\n          response2=$(curl -X POST -H \"Content-Type: application/json\" -H \"operationID: imAdmin\" -d '{\n            \"verifyCode\": \"666666\",\n            \"platform\": 3,\n            \"autoLogin\": true,\n            \"user\":{\n            \"nickname\": \"test22312\",\n            \"areaCode\":\"+86\",\n            \"phoneNumber\": \"12345678290\",\n            \"password\":\"test123456\"\n            }\n          }' http://127.0.0.1:10008/account/register)\n          check_error \"$response2\"\n          userID2=$(echo $response2 | jq -r '.data.userID')\n          echo \"userID2: $userID2\"\n\n          # Test login\n          login_response=$(curl -X POST -H \"Content-Type: application/json\" -H \"operationID: imAdmin\"  -d '{\n            \"platform\": 3,\n            \"areaCode\":\"+86\",\n            \"phoneNumber\": \"12345678190\",\n            \"password\":\"test123456\"\n          }' http://localhost:10008/account/login)\n          check_error \"$login_response\"\n\n          # Test get admin token\n          get_admin_token_response=$(curl -X POST -H \"Content-Type: application/json\" -H \"operationID: imAdmin\" -d '{\n            \"secret\": \"123456\",\n            \"platformID\": 2,\n            \"userID\": \"imAdmin\"\n          }' http://127.0.0.1:10002/auth/get_admin_token)\n          check_error \"$get_admin_token_response\"\n          adminToken=$(echo $get_admin_token_response | jq -r '.data.token')\n          echo \"adminToken: $adminToken\"\n\n          # Test send message\n          send_msg_response=$(curl -X POST -H \"Content-Type: application/json\" -H \"operationID: imAdmin\" -H \"token: $adminToken\" -d '{\n            \"sendID\": \"'$userID1'\",\n            \"recvID\": \"'$userID2'\",\n            \"senderPlatformID\": 3,\n            \"content\": {\n              \"content\": \"hello!!\"\n            },\n            \"contentType\": 101,\n            \"sessionType\": 1\n           }' http://127.0.0.1:10002/msg/send_msg)\n           check_error \"$send_msg_response\"\n\n          # Test get users\n          get_users_response=$(curl -X POST -H \"Content-Type: application/json\" -H \"operationID: imAdmin\" -H \"token: $adminToken\" -d '{\n            \"pagination\": {\n               \"pageNumber\": 1,\n               \"showNumber\": 100\n             }\n            }' http://127.0.0.1:10002/user/get_users)\n          check_error \"$get_users_response\"\n\n  go-test:\n    name: Benchmark Test with go ${{ matrix.go_version }} on ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    permissions:\n      contents: write\n    env:\n      SDK_DIR: openim-sdk-core\n      NOTIFICATION_CONFIG_PATH: config/notification.yml\n      SHARE_CONFIG_PATH: config/share.yml\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        go_version: [\"1.22.x\"]\n\n    steps:\n      - name: Checkout Server repository\n        uses: actions/checkout@v4\n\n      - name: Checkout SDK repository\n        uses: actions/checkout@v4\n        with:\n          repository: \"openimsdk/openim-sdk-core\"\n          ref: \"main\"\n          path: ${{ env.SDK_DIR }}\n\n      - name: Set up Go ${{ matrix.go_version }}\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go_version }}\n\n      - name: Get Server dependencies\n        run: |\n          go install github.com/magefile/mage@latest\n          go mod download\n\n      - name: Modify Server Configuration\n        run: |\n          yq e '.groupCreated.isSendMsg = true' -i ${{ env.NOTIFICATION_CONFIG_PATH }}\n          yq e '.friendApplicationApproved.isSendMsg = true' -i ${{ env.NOTIFICATION_CONFIG_PATH }}\n          yq e '.secret = 123456' -i ${{ env.SHARE_CONFIG_PATH }}\n\n      - name: Start Server Services\n        run: |\n          docker compose up -d\n          mage build\n          mage start\n          mage check\n\n      - name: Build test SDK core\n        run: |\n          cd ${{ env.SDK_DIR }}\n          go mod tidy\n          cd integration_test\n          mkdir data\n          go run main.go -lgr 0.8 -imf -crg -ckgn -ckcon -sem -ckmsn -u 20 -su 5 -lg 2 -cg 2 -cgm 3 -sm 10 -gm 10 -reg\n\n  dockerfile-test:\n    name: Build and Test Dockerfile\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go_version: [\"1.22\"]\n\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n\n      - name: Set up Go ${{ matrix.go_version }}\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go_version }}\n\n      - name: Get dependencies\n        run: |\n          go mod tidy\n          go mod download\n          go install github.com/magefile/mage@latest\n\n      - name: Build Docker Image\n        run: |\n          IMAGE_NAME=\"${{ github.event.repository.name }}-test\"\n          CONTAINER_NAME=\"${{ github.event.repository.name }}-container\"\n          docker build -t $IMAGE_NAME .\n\n      - name: Run Docker Container\n        run: |\n          IMAGE_NAME=\"${{ github.event.repository.name }}-test\"\n          CONTAINER_NAME=\"${{ github.event.repository.name }}-container\"\n          docker run --name $CONTAINER_NAME -d $IMAGE_NAME\n          docker ps -a\n\n      - name: Test Docker Container Logs\n        run: |\n          CONTAINER_NAME=\"${{ github.event.repository.name }}-container\"\n          docker logs $CONTAINER_NAME\n"
  },
  {
    "path": ".github/workflows/help-comment-issue.yml",
    "content": "# Copyright © 2023 OpenIM. All rights reserved.\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\nname: Good frist issue add comment\non:\n  issues:\n    types:\n      - labeled\n\njobs:\n  add-comment:\n    if: github.event.label.name == 'help wanted' || github.event.label.name == 'good first issue'\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - name: Add comment\n        uses: peter-evans/create-or-update-comment@v4\n        with:\n          issue-number: ${{ github.event.issue.number }}\n          token: ${{ secrets.BOT_TOKEN }}\n          body: |\n            This issue is available for anyone to work on. **Make sure to reference this issue in your pull request.** :sparkles: Thank you for your contribution! :sparkles:\n            [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) to connect and communicate with our developers.\n            If you wish to accept this assignment, please leave a comment in the comments section: `/accept`.🎯\n"
  },
  {
    "path": ".github/workflows/issue-translator.yml",
    "content": "name: 'issue-translator'\non: \n  issue_comment: \n    types: [created]\n  issues: \n    types: [opened]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: usthe/issues-translate-action@v2.7\n        with:\n          BOT_GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} \n          IS_MODIFY_TITLE: true\n          # not require, default false, . Decide whether to modify the issue title\n          # if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.\n          CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿\n          # not require. Customize the translation robot prefix message."
  },
  {
    "path": ".github/workflows/merge-from-milestone.yml",
    "content": "name: Create Individual PRs from Milestone\n\npermissions:\n  contents: write\n  pull-requests: write\n  issues: write\n\non:\n  workflow_dispatch:\n    inputs:\n      milestone_name:\n        description: \"Milestone name to collect closed PRs from\"\n        required: true\n        default: \"v3.8.4\"\n      target_branch:\n        description: \"Target branch to merge the consolidated PR\"\n        required: true\n        default: \"pre-release-v3.8.4\"\n\nenv:\n  MILESTONE_NAME: ${{ github.event.inputs.milestone_name || 'v3.8.4' }}\n  TARGET_BRANCH: ${{ github.event.inputs.target_branch || 'pre-release-v3.8.4' }}\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  BOT_TOKEN: ${{ secrets.BOT_TOKEN }}\n  LABEL_NAME: cherry-picked\n  TEMP_DIR: /tmp\n\njobs:\n  merge_milestone_prs:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Setup temp directory\n        run: |\n          # Create the temporary directory and initialize necessary files\n          mkdir -p ${{ env.TEMP_DIR }}\n          touch ${{ env.TEMP_DIR }}/pr_numbers.txt\n          touch ${{ env.TEMP_DIR }}/commit_hashes.txt\n          touch ${{ env.TEMP_DIR }}/pr_title.txt\n          touch ${{ env.TEMP_DIR }}/pr_body.txt\n          touch ${{ env.TEMP_DIR }}/created_pr_number.txt\n\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          token: ${{ secrets.BOT_TOKEN }}\n\n      - name: Setup Git User for OpenIM-Robot\n        run: |\n          git config --global user.email \"OpenIM-Robot@users.noreply.github.com\"\n          git config --global user.name \"OpenIM-Robot\"\n\n      - name: Fetch Milestone ID and Filter PR Numbers\n        env:\n          MILESTONE_NAME: ${{ env.MILESTONE_NAME }}\n        run: |\n          # Fetch milestone details and extract milestone ID\n          milestones=$(curl -s -H \"Authorization: token $BOT_TOKEN\" \\\n            -H \"Accept: application/vnd.github+json\" \\\n            \"https://api.github.com/repos/${{ github.repository }}/milestones\")\n          milestone_id=$(echo \"$milestones\" | grep -B3 \"\\\"title\\\": \\\"$MILESTONE_NAME\\\"\" | grep '\"number\":' | head -n1 | grep -o '[0-9]\\+')\n          if [ -z \"$milestone_id\" ]; then\n            echo \"Milestone '$MILESTONE_NAME' not found. Exiting.\"\n            exit 1\n          fi\n          echo \"Milestone ID: $milestone_id\"\n          echo \"MILESTONE_ID=$milestone_id\" >> $GITHUB_ENV\n\n          # Fetch issues for the milestone\n          issues=$(curl -s -H \"Authorization: token $BOT_TOKEN\" \\\n                -H \"Accept: application/vnd.github+json\" \\\n                \"https://api.github.com/repos/${{ github.repository }}/issues?milestone=$milestone_id&state=closed&per_page=100\")\n\n          > ${{ env.TEMP_DIR }}/pr_numbers.txt\n\n          # Filter PRs that do not have the 'cherry-picked' label\n          for pr_number in $(echo \"$issues\" | jq -r '.[] | select(.pull_request != null) | .number'); do\n            labels=$(curl -s -H \"Authorization: token $BOT_TOKEN\" \\\n              -H \"Accept: application/vnd.github+json\" \\\n              \"https://api.github.com/repos/${{ github.repository }}/issues/$pr_number/labels\" | jq -r '.[].name')\n\n            if ! echo \"$labels\" | grep -q \"${LABEL_NAME}\"; then\n              echo \"PR #$pr_number does not have the 'cherry-picked' label. Adding to the list.\"\n              echo \"$pr_number\" >> ${{ env.TEMP_DIR }}/pr_numbers.txt\n            fi\n          done\n\n          sort -n ${{ env.TEMP_DIR }}/pr_numbers.txt -o ${{ env.TEMP_DIR }}/pr_numbers.txt\n\n      - name: Create Individual PRs\n        run: |\n          for pr_number in $(cat ${{ env.TEMP_DIR }}/pr_numbers.txt); do\n            pr_details=$(curl -s -H \"Authorization: token $BOT_TOKEN\" \\\n              -H \"Accept: application/vnd.github+json\" \\\n              \"https://api.github.com/repos/${{ github.repository }}/pulls/$pr_number\")\n            pr_title=$(echo \"$pr_details\" | jq -r '.title')\n            pr_body=$(echo \"$pr_details\" | jq -r '.body')\n            pr_creator=$(echo \"$pr_details\" | jq -r '.user.login')\n            merge_commit=$(echo \"$pr_details\" | jq -r '.merge_commit_sha')\n            short_commit_hash=$(echo \"$merge_commit\" | cut -c 1-7)\n\n            if [ \"$merge_commit\" != \"null\" ]; then\n              git fetch origin\n              \n              echo \"Checking out target branch: $TARGET_BRANCH\"\n              git checkout $TARGET_BRANCH\n\n              echo \"Pulling latest changes from target branch: $TARGET_BRANCH\"\n              git pull origin $TARGET_BRANCH\n              \n              cherry_pick_branch=\"cherry-pick-${short_commit_hash}\"\n              git checkout -b $cherry_pick_branch\n\n              echo \"Cherry-picking commit: $merge_commit\"\n              if ! git cherry-pick \"$merge_commit\" --strategy=recursive -X theirs; then\n                echo \"Conflict detected for $merge_commit. Resolving with incoming changes.\"\n                conflict_files=$(git diff --name-only --diff-filter=U)\n                echo \"Conflicting files:\"\n                echo \"$conflict_files\"\n\n                for file in $conflict_files; do\n                  if [ -f \"$file\" ]; then\n                    echo \"Resolving conflict for $file\"\n                    git add \"$file\"\n                  else\n                    echo \"File $file has been deleted. Skipping.\"\n                    git rm \"$file\"\n                  fi\n                done\n\n                echo \"Conflicts resolved. Continuing cherry-pick.\"\n                git cherry-pick --continue || { echo \"Cherry-pick failed, but continuing to create PR.\"; }\n              else\n                echo \"Cherry-pick successful for commit $merge_commit.\"\n              fi\n\n              git remote set-url origin \"https://${BOT_TOKEN}@github.com/${{ github.repository }}.git\"\n              \n              echo \"Pushing branch: $cherry_pick_branch\"\n              if ! git push origin $cherry_pick_branch --force; then\n                echo \"Push failed, but continuing to create PR...\"\n              fi\n\n              new_pr_title=\"$pr_title [Created by @$pr_creator from #$pr_number]\"\n              new_pr_body=\"$pr_body\n              > This PR is created from original PR #$pr_number.\"\n\n              response=$(curl -s -X POST -H \"Authorization: token $BOT_TOKEN\" \\\n                -H \"Accept: application/vnd.github+json\" \\\n                https://api.github.com/repos/${{ github.repository }}/pulls \\\n                -d \"$(jq -n --arg title \"$new_pr_title\" \\\n                  --arg head \"$cherry_pick_branch\" \\\n                  --arg base \"$TARGET_BRANCH\" \\\n                  --arg body \"$new_pr_body\" \\\n                  '{title: $title, head: $head, base: $base, body: $body}')\")\n\n              new_pr_number=$(echo \"$response\" | jq -r '.number')\n\n              if [[ \"$new_pr_number\" == \"null\" || -z \"$new_pr_number\" ]]; then\n                echo \"Failed to create PR. Response: $response\"\n              \n                git checkout $TARGET_BRANCH\n\n                git branch -D $cherry_pick_branch\n                \n                echo \"Deleted branch: $cherry_pick_branch\"\n                git push origin --delete $cherry_pick_branch\n              else\n                echo \"Created PR #$new_pr_number\"\n\n                curl -s -X POST -H \"Authorization: token $GITHUB_TOKEN\" \\\n                -H \"Accept: application/vnd.github+json\" \\\n                -d '{\"labels\": [\"milestone-merge\"]}' \\\n                \"https://api.github.com/repos/${{ github.repository }}/issues/$new_pr_number/labels\"\n              fi\n\n              echo \"\"\n              echo \"----------------------------------------\"\n              echo \"\"\n            fi\n          done\n"
  },
  {
    "path": ".github/workflows/publish-docker-image.yml",
    "content": "name: Publish Docker image to registries\n\non:\n  push:\n    branches:\n      - release-*\n  release:\n    types: [published]\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Tag version to be used for Docker image\"\n        required: true\n        default: \"v3.8.3\"\n\nenv:\n  GO_VERSION: \"1.22\"\n  IMAGE_NAME: \"openim-server\"\n  # IMAGE_NAME: ${{ github.event.repository.name }}\n  DOCKER_BUILDKIT: 1\n\njobs:\n  publish-docker-images:\n    runs-on: ubuntu-latest\n    if: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.merged == false) }}\n    steps:\n      - name: Checkout main repository\n        uses: actions/checkout@v4\n        with:\n          path: main-repo\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3.3.0\n\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          driver-opts: network=host\n\n      - name: Extract metadata for Docker\n        id: meta\n        uses: docker/metadata-action@v5.6.0\n        with:\n          images: |\n            ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}\n            ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}\n            registry.cn-hangzhou.aliyuncs.com/openimsdk/${{ env.IMAGE_NAME }}\n          tags: |\n            type=ref,event=tag\n            type=schedule\n            type=ref,event=branch\n            type=ref,event=pr\n            type=semver,pattern={{version}}\n            type=semver,pattern=v{{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=sha\n\n      - name: Install skopeo\n        run: |\n          sudo apt-get update && sudo apt-get install -y skopeo\n\n      - name: Build multi-arch images as OCI\n        run: |\n          mkdir -p /tmp/oci-image /tmp/docker-cache\n\n          # Build multi-architecture image and save in OCI format        \n          docker buildx build \\\n            --platform linux/amd64,linux/arm64 \\\n            --output type=oci,dest=/tmp/oci-image/multi-arch.tar \\\n            --cache-to type=local,dest=/tmp/docker-cache \\\n            --cache-from type=gha \\\n            ./main-repo\n\n            # Use skopeo to convert the amd64 image from OCI format to Docker format and load it\n          skopeo copy --override-arch amd64 oci-archive:/tmp/oci-image/multi-arch.tar docker-daemon:${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:local\n\n          # check image\n          docker image ls | grep openim\n\n      - name: Checkout compose repository\n        uses: actions/checkout@v4\n        with:\n          repository: \"openimsdk/openim-docker\"\n          path: \"compose-repo\"\n\n      - name: Get Internal IP Address\n        id: get-ip\n        run: |\n          IP=$(hostname -I | awk '{print $1}')\n          echo \"The IP Address is: $IP\"\n          echo \"ip=$IP\" >> $GITHUB_OUTPUT\n\n      - name: Update .env to use the local image\n        run: |\n          sed -i 's|OPENIM_SERVER_IMAGE=.*|OPENIM_SERVER_IMAGE=${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:local|' ${{ github.workspace }}/compose-repo/.env\n          sed -i 's|MINIO_EXTERNAL_ADDRESS=.*|MINIO_EXTERNAL_ADDRESS=http://${{ steps.get-ip.outputs.ip }}:10005|' ${{ github.workspace }}/compose-repo/.env\n\n      - name: Start services using Docker Compose\n        run: |\n          cd ${{ github.workspace }}/compose-repo\n          docker compose up -d\n\n          docker compose ps\n\n      # - name: Check openim-server health\n      #   run: |\n      #     timeout=300\n      #     interval=30\n      #     elapsed=0\n      #     while [[ $elapsed -le $timeout ]]; do\n      #       if ! docker exec openim-server mage check; then\n      #         echo \"openim-server is not ready, waiting...\"\n      #         sleep $interval\n      #         elapsed=$(($elapsed + $interval))\n      #       else\n      #         echo \"Health check successful\"\n      #         exit 0\n      #       fi\n      #     done\n      #     echo \"Health check failed after 5 minutes\"\n      #     exit 1\n\n      # - name: Check openim-chat health\n      #   if: success()\n      #   run: |\n      #     if ! docker exec openim-chat mage check; then\n      #         echo \"openim-chat check failed\"\n      #         exit 1\n      #       else\n      #         echo \"Health check successful\"\n      #         exit 0\n      #       fi\n\n      - name: Log in to Docker Hub\n        uses: docker/login-action@v3.3.0\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Log in to GitHub Container Registry\n        uses: docker/login-action@v3.3.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Log in to Aliyun Container Registry\n        uses: docker/login-action@v3.3.0\n        with:\n          registry: registry.cn-hangzhou.aliyuncs.com\n          username: ${{ secrets.ALIREGISTRY_USERNAME }}\n          password: ${{ secrets.ALIREGISTRY_TOKEN }}\n\n      - name: Push multi-architecture images\n        if: success()\n        run: |\n          docker buildx build \\\n            --platform linux/amd64,linux/arm64 \\\n            $(echo \"${{ steps.meta.outputs.tags }}\" | sed 's/,/ --tag /g' | sed 's/^/--tag /') \\\n            --cache-from type=local,src=/tmp/docker-cache \\\n            --push \\\n            ./main-repo\n\n      - name: Verify multi-platform support\n        run: |\n          images=(\n            \"${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}\"\n            \"ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}\"\n            \"registry.cn-hangzhou.aliyuncs.com/openimsdk/${{ env.IMAGE_NAME }}\"\n          )\n\n          for image in \"${images[@]}\"; do\n              for tag in $(echo \"${{ steps.meta.outputs.tags }}\" | tr ',' '\\n' | cut -d':' -f2); do\n                  echo \"Verifying multi-arch support for $image:$tag\"\n                  manifest=$(docker manifest inspect \"$image:$tag\" || echo \"error\")\n                  if [[ \"$manifest\" == \"error\" ]]; then\n                      echo \"Manifest not found for $image:$tag\"\n                      exit 1\n                  fi\n                  amd64_found=$(echo \"$manifest\" | jq '.manifests[] | select(.platform.architecture == \"amd64\")')\n                  arm64_found=$(echo \"$manifest\" | jq '.manifests[] | select(.platform.architecture == \"arm64\")')\n                  if [[ -z \"$amd64_found\" ]]; then\n                      echo \"Multi-platform support check failed for $image:$tag - missing amd64\"\n                      exit 1\n                  fi\n                  if [[ -z \"$arm64_found\" ]]; then\n                      echo \"Multi-platform support check failed for $image:$tag - missing arm64\"\n                      exit 1\n                  fi\n                  echo \"✅ $image:$tag supports both amd64 and arm64 architectures\"\n              done\n          done\n"
  },
  {
    "path": ".github/workflows/remove-unused-labels.yml",
    "content": "name: Remove Unused Labels\non:\n  workflow_dispatch:\n\njobs:\n  cleanup:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n      contents: read\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n\n      - name: Fetch All Issues and PRs\n        id: fetch_issues_prs\n        uses: actions/github-script@v7.0.1\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const issues = await github.paginate(github.rest.issues.listForRepo, {\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              state: 'all',\n              per_page: 100\n            });\n\n            const labelsInUse = new Set();\n            issues.forEach(issue => {\n              issue.labels.forEach(label => {\n                labelsInUse.add(label.name);\n              });\n            });\n\n            return JSON.stringify(Array.from(labelsInUse));\n          result-encoding: string\n\n      - name: Fetch All Labels\n        id: fetch_labels\n        uses: actions/github-script@v7.0.1\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const labels = await github.paginate(github.rest.issues.listLabelsForRepo, {\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              per_page: 100\n            });\n\n            return JSON.stringify(labels.map(label => label.name));\n          result-encoding: string\n\n      - name: Remove Unused Labels\n        uses: actions/github-script@v7.0.1\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const labelsInUse = new Set(JSON.parse(process.env.LABELS_IN_USE));\n            const allLabels = JSON.parse(process.env.ALL_LABELS);\n\n            const unusedLabels = allLabels.filter(label => !labelsInUse.has(label));\n\n            for (const label of unusedLabels) {\n              await github.rest.issues.deleteLabel({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                name: label\n              });\n              console.log(`Deleted label: ${label}`);\n            }\n        env:\n          LABELS_IN_USE: ${{ steps.fetch_issues_prs.outputs.result }}\n          ALL_LABELS: ${{ steps.fetch_labels.outputs.result }}\n"
  },
  {
    "path": ".github/workflows/reopen-issue.yml",
    "content": "name: Reopen and Update Stale Issues\n\non:\n  workflow_dispatch:\n\njobs:\n  reopen_stale_issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      contents: read\n\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n\n      - name: Fetch Closed Issues with lifecycle/stale Label\n        id: fetch_issues\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const issues = await github.paginate(github.rest.issues.listForRepo, {\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              state: 'closed',\n              labels: 'lifecycle/stale',\n              per_page: 100\n            });\n            const issueNumbers = issues\n              .filter(issue => !issue.pull_request) // exclude PR\n              .map(issue => issue.number);\n            console.log(`Fetched issues: ${issueNumbers}`);\n            return issueNumbers;\n\n      - name: Set issue numbers\n        id: set_issue_numbers\n        run: |\n          echo \"ISSUE_NUMBERS=${{ steps.fetch_issues.outputs.result }}\" >> $GITHUB_ENV\n          echo \"Issue numbers: ${{ steps.fetch_issues.outputs.result }}\"\n\n      - name: Reopen Issues\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const issueNumbers = JSON.parse(process.env.ISSUE_NUMBERS);\n            console.log(`Reopening issues: ${issueNumbers}`);\n\n            for (const issue_number of issueNumbers) {\n              // Reopen the issue\n              await github.rest.issues.update({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: issue_number,\n                state: 'open'\n              });\n              console.log(`Reopened issue #${issue_number}`);\n            }\n\n      - name: Remove lifecycle/stale Label\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const issueNumbers = JSON.parse(process.env.ISSUE_NUMBERS);\n            console.log(`Removing 'lifecycle/stale' label from issues: ${issueNumbers}`);\n\n            for (const issue_number of issueNumbers) {\n              // Remove the lifecycle/stale label\n              await github.rest.issues.removeLabel({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: issue_number,\n                name: 'lifecycle/stale'\n              });\n              console.log(`Removed label 'lifecycle/stale' from issue #${issue_number}`);\n            }\n"
  },
  {
    "path": ".github/workflows/update-version-file-on-release.yml",
    "content": "name: Update Version File on Release\n\non:\n  release:\n    types: [created]\n\njobs:\n  update-version:\n    runs-on: ubuntu-latest\n    env:\n      TAG_VERSION: ${{ github.event.release.tag_name }}\n    steps:\n      # Step 1: Checkout the original repository's code\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          # submodules: \"recursive\"\n\n      - name: Safe submodule initialization\n        run: |\n          echo \"Checking for submodules...\"\n          if [ -f .gitmodules ]; then\n            if [ -s .gitmodules ]; then\n              echo \"Initializing submodules...\"\n              if git submodule sync --recursive 2>/dev/null; then\n                git submodule update --init --force --recursive || {\n                  echo \"Warning: Some submodules failed to initialize, continuing anyway...\"\n                }\n              else\n                echo \"Warning: Submodule sync failed, continuing without submodules...\"\n              fi\n            else\n              echo \".gitmodules exists but is empty, skipping submodule initialization\"\n            fi\n          else\n            echo \"No .gitmodules file found, no submodules to initialize\"\n          fi\n\n      # Step 2: Set up Git with official account\n      - name: Set up Git\n        run: |\n          git config --global user.name \"github-actions[bot]\"\n          git config --global user.email \"github-actions[bot]@users.noreply.github.com\"\n\n      # Step 3: Check and delete existing tag\n      - name: Check and delete existing tag\n        run: |\n          if git rev-parse ${{ env.TAG_VERSION }} >/dev/null 2>&1; then\n            git tag -d ${{ env.TAG_VERSION }}\n            git push --delete origin ${{ env.TAG_VERSION }}\n          fi\n\n      # Step 4: Update version file\n      - name: Update version file\n        run: |\n          mkdir -p version\n          echo -n \"${{ env.TAG_VERSION }}\" > version/version\n\n      # Step 5: Commit and push changes\n      - name: Commit and push changes\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git add version/version\n          git commit -m \"Update version to ${{ env.TAG_VERSION }}\"\n\n      # Step 6: Update tag\n      - name: Update tag\n        run: |\n          git tag -fa ${{ env.TAG_VERSION }} -m \"Update version to ${{ env.TAG_VERSION }}\"\n          git push origin ${{ env.TAG_VERSION }} --force\n\n      # Step 7: Find and Publish Draft Release\n      - name: Find and Publish Draft Release\n        uses: actions/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const { owner, repo } = context.repo;\n            const tagName = process.env.TAG_VERSION;\n\n            try {\n              let release;\n              try {\n                const response = await github.rest.repos.getReleaseByTag({\n                  owner,\n                  repo,\n                  tag: tagName\n                });\n                release = response.data;\n              } catch (tagError) {\n                core.info(`Release not found by tag, searching all releases...`);\n                const releases = await github.rest.repos.listReleases({\n                  owner,\n                  repo,\n                  per_page: 100\n                });\n            \n                release = releases.data.find(r => r.draft && r.tag_name === tagName);            \n                if (!release) {\n                  throw new Error(`No release found with tag ${tagName}`);\n                }\n              }\n            \n              await github.rest.repos.updateRelease({\n                owner,\n                repo,\n                release_id: release.id,\n                draft: false,\n                prerelease: release.prerelease\n              });\n            \n              const status = release.draft ? \"was draft\" : \"was already published\";\n              core.info(`Release ${tagName} ensured to be published (${status}).`);\n            \n            } catch (error) {\n              core.warning(`Could not find or update release for tag ${tagName}: ${error.message}`);\n            }\n"
  },
  {
    "path": ".github/workflows/user-first-interaction.yml",
    "content": "name: User First Interaction\n\non:\n  issues:\n    types: [opened]\n  pull_request:\n    branches: [main]\n    types: [opened]\n\njobs:\n  check_for_first_interaction:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/first-interaction@v1.3.0\n        with:\n          repo-token: ${{ secrets.BOT_TOKEN }}\n          pr-message: |\n            Hello! Thank you for your contribution.\n\n            If you are fixing a bug, please reference the issue number in the description.\n\n            If you are implementing a feature request, please check with the maintainers that the feature will be accepted first.\n\n            [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) to connect and communicate with our developers.\n\n            Please leave your information in the [✨ discussions](https://github.com/orgs/OpenIMSDK/discussions/426), we expect anyone to join OpenIM developer community.\n\n          issue-message: |\n            Hello! Thank you for filing an issue.\n\n            If this is a bug report, please include relevant logs to help us debug the problem.\n\n            [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) to connect and communicate with our developers.\n        continue-on-error: true\n"
  },
  {
    "path": ".gitignore",
    "content": "# Copyright © 2023 OpenIMSDK.\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# ==============================================================================\n# For the entire design of.gitignore, ignore git commits and ignore files\n#===============================================================================\n#\n\n### OpenIM developer supplement ###\nlogs\n.devcontainer\ncomponents\nout-test\nDockerfile.cross\n\n### Makefile ###\ntmp/\nbin/\noutput/\n_output/\ndeployments/charts/generated-configs/\n\n### OpenIM Config ###\n.env\nconfig/config.yaml\nconfig/notification.yaml\n\n### OpenIM deploy ###\ndeployments/openim-server/charts\n\n# files used by the developer\n.idea.md\n.todo.md\n.note.md\n\n# ==============================================================================\n# Created by https://www.toptal.com/developers/gitignore/api/go,git,vim,tags,test,emacs,backup,jetbrains\n# Edit at https://www.toptal.com/developers/gitignore?templates=go,git,vim,tags,test,emacs,backup,jetbrains\n\n### Backup ###\n*.bak\n*.gho\n*.ori\n*.orig\n*.tmp\n\n### Emacs ###\n# -*- mode: gitignore; -*-\n*~\n\\#*\\#\n/.emacs.desktop\n/.emacs.desktop.lock\n*.elc\nauto-save-list\ntramp\n.\\#*\n\n# Org-mode\n.org-id-locations\n*_archive\n\n# flymake-mode\n*_flymake.*\n\n# eshell files\n/eshell/history\n/eshell/lastdir\n\n# elpa packages\n/elpa/\n\n# reftex files\n*.rel\n\n# AUCTeX auto folder\n/auto/\n\n# cask packages\n.cask/\ndist/\n\n# Flycheck\nflycheck_*.el\n\n# server auth directory\n/server/\n\n# projectiles files\n.projectile\n\n# directory configuration\n.dir-locals.el\n\n# network security\n/network-security.data\n\n### vscode ###\n.vscode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# End of https://www.toptal.com/developers/gitignore/api/vim,jetbrains,vscode,git,go,tags,backup,test\n\n### Git ###\n# Created by git for backups. To disable backups in Git:\n# $ git config --global mergetool.keepBackup false\n\n# Created by git when using merge tools for conflicts\n*.BACKUP.*\n*.BASE.*\n*.LOCAL.*\n*.REMOTE.*\n*_BACKUP_*.txt\n*_BASE_*.txt\n*_LOCAL_*.txt\n*_REMOTE_*.txt\n\n### Go ###\n# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\nvendor/\n\n# Go workspace file\n# go.work\ngo.work.sum\n\n### JetBrains ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### JetBrains Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n### Tags ###\n# Ignore tags created by etags, ctags, gtags (GNU global) and cscope\nTAGS\n.TAGS\n!TAGS/\ntags\n.tags\n!tags/\ngtags.files\nGTAGS\nGRTAGS\nGPATH\nGSYMS\ncscope.files\ncscope.out\ncscope.in.out\ncscope.po.out\n\n\n### Test ###\n### Ignore all files that could be used to test your code and\n### you wouldn't want to push\n\n# Reference https://en.wikipedia.org/wiki/Metasyntactic_variable\n\n# Most common\n*foo\n*bar\n*fubar\n*foobar\n*baz\n\n# Less common\n*qux\n*quux\n*bongo\n*bazola\n*ztesch\n\n# UK, Australia\n*wibble\n*wobble\n*wubble\n*flob\n*blep\n*blah\n*boop\n*beep\n\n# Japanese\n*hoge\n*piyo\n*fuga\n*hogera\n*hogehoge\n\n# Portugal, Spain\n*fulano\n*sicrano\n*beltrano\n*mengano\n*perengano\n*zutano\n\n# France, Italy, the Netherlands\n*toto\n*titi\n*tata\n*tutu\n*pipppo\n*pluto\n*paperino\n*aap\n*noot\n*mies\n\n# Other names that would make sense\n*tests\n*testsdir\n*testsfile\n*testsfiles\n*testdir\n*testfile\n*testfiles\n*testing\n*testingdir\n*testingfile\n*testingfiles\n*temp\n*tempdir\n*tempfile\n*tempfiles\n*tmp\n*tmpdir\n*tmpfile\n*tmpfiles\n*lol\n\n### Vim ###\n# Swap\n[._]*.s[a-v][a-z]\n!*.svg  # comment out if you don't need vector files\n[._]*.sw[a-p]\n[._]s[a-rt-v][a-z]\n[._]ss[a-gi-z]\n[._]sw[a-p]\n\n# Session\nSession.vim\nSessionx.vim\n\n# Temporary\n.netrwhist\n# Auto-generated tag files\n# Persistent undo\n[._]*.un~\n\n# End of https://www.toptal.com/developers/gitignore/api/go,git,vim,tags,test,emacs,backup,jetbrains\n.idea\ndist/"
  },
  {
    "path": ".golangci.yml",
    "content": "# options for analysis running\nrun:\n  # default concurrency is a available CPU number\n  concurrency: 4\n\n  # timeout for analysis, e.g. 30s, 5m, default is 1m\n  timeout: 5m\n\n  # exit code when at least one issue was found, default is 1\n  issues-exit-code: 1\n\n  # include test files or not, default is true\n  tests: true\n\n  # list of build tags, all linters use it. Default is empty list.\n  build-tags:\n    - mytag\n\n  # which dirs to skip: issues from them won't be reported;\n  # can use regexp here: generated.*, regexp is applied on full path;\n  # default value is empty list, but default dirs are skipped independently\n  # from this option's value (see skip-dirs-use-default).\n  # \"/\" will be replaced by current OS file path separator to properly work\n  # on Windows.\n  # skip-dirs:\n  #   - components\n  #   - docs\n  #   - util\n  #   - .*~\n  #   - api/swagger/docs\n\n  \n  #   - server/docs\n  #   - components/mnt/config/certs\n  #   - logs\n\n  # default is true. Enables skipping of directories:\n  #   vendor$, third_party$, testdata$, examples$, Godeps$, builtin$\n  # skip-dirs-use-default: true\n\n  # which files to skip: they will be analyzed, but issues from them\n  # won't be reported. Default value is empty list, but there is\n  # no need to include all autogenerated files, we confidently recognize\n  # autogenerated files. If it's not please let us know.\n  # \"/\" will be replaced by current OS file path separator to properly work\n  # on Windows.\n  # skip-files:\n  #   - \".*\\\\.my\\\\.go$\"\n  #   - _test.go\n  #   - \".*_test.go\"\n  #   - \"mocks/\"\n  #   - \".github/\"\n  #   - \"logs/\"\n  #   - \"_output/\"\n  #   - \"components/\"\n\n  # by default isn't set. If set we pass it to \"go list -mod={option}\". From \"go help modules\":\n  # If invoked with -mod=readonly, the go command is disallowed from the implicit\n  # automatic updating of go.mod described above. Instead, it fails when any changes\n  # to go.mod are needed. This setting is most useful to check that go.mod does\n  # not need updates, such as in a continuous integration and testing system.\n  # If invoked with -mod=vendor, the go command assumes that the vendor\n  # directory holds the correct copies of dependencies and ignores\n  # the dependency descriptions in go.mod.\n  #modules-download-mode: release|readonly|vendor\n\n  # Allow multiple parallel golangci-lint instances running.\n  # If false (default) - golangci-lint acquires file lock on start.\n  allow-parallel-runners: true\n\n\n# output configuration options\noutput:\n  # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is \"colored-line-number\"\n  # format: colored-line-number\n\n  # print lines of code with issue, default is true\n  print-issued-lines: true\n\n  # print linter name in the end of issue text, default is true\n  print-linter-name: true\n\n  # make issues output unique by line, default is true\n  uniq-by-line: true\n\n  # add a prefix to the output file references; default is no prefix\n  path-prefix: \"\"\n\n  # sorts results by: filepath, line and column\n  sort-results: true\n\n# all available settings of specific linters\nlinters-settings:\n  bidichk:\n    # The following configurations check for all mentioned invisible unicode\n    # runes. It can be omitted because all runes are enabled by default.\n    left-to-right-embedding: true\n    right-to-left-embedding: true\n    pop-directional-formatting: true\n    left-to-right-override: true\n    right-to-left-override: true\n    left-to-right-isolate: true\n    right-to-left-isolate: true\n    first-strong-isolate: true\n    pop-directional-isolate: true\n\n  dupl:\n    # tokens count to trigger issue, 150 by default\n    threshold: 200\n  errcheck:\n    # report about not checking of errors in type assertions: `a := b.(MyStruct)`;\n    # default is false: such cases aren't reported by default.\n    check-type-assertions: false\n\n    # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;\n    # default is false: such cases aren't reported by default.\n    check-blank: false\n\n    # [deprecated] comma-separated list of pairs of the form pkg:regex\n    # the regex is used to ignore names within pkg. (default \"fmt:.*\").\n    # see https://github.com/kisielk/errcheck#the-deprecated-method for details\n    #ignore: GenMarkdownTree,os:.*,BindPFlags,WriteTo,Help\n    #ignore: (os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv\n\n    # path to a file containing a list of functions to exclude from checking\n    # see https://github.com/kisielk/errcheck#excluding-functions for details\n    # exclude: errcheck.txt\n\n  errorlint:\n    # Check whether fmt.Errorf uses the %w verb for formatting errors. See the readme for caveats\n    errorf: true\n    # Check for plain type assertions and type switches\n    asserts: true\n    # Check for plain error comparisons\n    comparison: true\n\n  exhaustive:\n    # Program elements to check for exhaustiveness.\n    # Default: [ switch ]\n    check:\n      - switch\n      - map\n    # check switch statements in generated files also\n    check-generated: false\n    # indicates that switch statements are to be considered exhaustive if a\n    # 'default' case is present, even if all enum members aren't listed in the\n    # switch\n    default-signifies-exhaustive: false\n    # enum members matching the supplied regex do not have to be listed in\n    # switch statements to satisfy exhaustiveness\n    ignore-enum-members: \"\"\n    # consider enums only in package scopes, not in inner scopes\n    package-scope-only: false\n\n\n  forbidigo:\n  #   # Forbid the following identifiers (identifiers are written using regexp):\n    forbid:\n      # - ^print.*$\n      - 'fmt\\.Print.*'\n      - fmt.Println.* # too much log noise\n      - ^unsafe\\..*$\n      - ^init$\n      - ^os.Exit$\n      - ^fmt.Print.*$\n      - errors.New.*$\n      - ^fmt.Println.*$\n      - ^panic$\n      - painc\n  #     - ginkgo\\\\.F.* # these are used just for local development\n  #   # Exclude godoc examples from forbidigo checks.  Default is true.\n  #   exclude_godoc_examples: false\n\n  funlen:\n    lines: 220\n    statements: 80\n\n  gocognit:\n    # minimal code complexity to report, 30 by default (but we recommend 10-20)\n    min-complexity: 30\n\n  goconst:\n    # minimal length of string constant, 3 by default\n    min-len: 3\n    # minimal occurrences count to trigger, 3 by default\n    min-occurrences: 3\n    # ignore test files, false by default\n    ignore-tests: false\n    # look for existing constants matching the values, true by default\n    match-constant: true\n    # search also for duplicated numbers, false by default\n    numbers: false\n    # minimum value, only works with goconst.numbers, 3 by default\n    min: 3\n    # maximum value, only works with goconst.numbers, 3 by default\n    max: 3\n    # ignore when constant is not used as function argument, true by default\n    ignore-calls: true\n\n  gocritic:\n    # Which checks should be enabled; can't be combined with 'disabled-checks';\n    # See https://go-critic.github.io/overview#checks-overview\n    # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`\n    # By default list of stable checks is used.\n    enabled-checks:\n      #- rangeValCopy\n      - ruleguard\n\n    # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty\n    disabled-checks:\n      - regexpMust\n      - ifElseChain\n      #- exitAfterDefer\n\n    # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.\n    # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section \"Tags\".\n    enabled-tags:\n      - performance\n    disabled-tags:\n      - experimental\n\n    # Settings passed to gocritic.\n    # The settings key is the name of a supported gocritic checker.\n    # The list of supported checkers can be find in https://go-critic.github.io/overview.\n    settings:\n      captLocal: # must be valid enabled check name\n        # whether to restrict checker to params only (default true)\n        paramsOnly: true\n      elseif:\n        # whether to skip balanced if-else pairs (default true)\n        skipBalanced: true\n      hugeParam:\n        # size in bytes that makes the warning trigger (default 80)\n        sizeThreshold: 80\n      rangeExprCopy:\n        # size in bytes that makes the warning trigger (default 512)\n        sizeThreshold: 512\n        # whether to check test functions (default true)\n        skipTestFuncs: true\n      rangeValCopy:\n        # size in bytes that makes the warning trigger (default 128)\n        sizeThreshold: 32\n        # whether to check test functions (default true)\n        skipTestFuncs: true\n      ruleguard:\n        # path to a gorules file for the ruleguard checker\n        rules: ''\n      underef:\n        # whether to skip (*x).method() calls where x is a pointer receiver (default true)\n        skipRecvDeref: true\n\n  gocyclo:\n    # minimal code complexity to report, 30 by default (but we recommend 10-20)\n    min-complexity: 30\n  cyclop:\n    # the maximal code complexity to report\n    max-complexity: 50\n    # the maximal average package complexity. If it's higher than 0.0 (float) the check is enabled (default 0.0)\n    package-average: 0.0\n    # should ignore tests (default false)\n    skip-tests: false\n  godot:\n    # comments to be checked: `declarations`, `toplevel`, or `all`\n    scope: declarations\n    # list of regexps for excluding particular comment lines from check\n    exclude:\n      # example: exclude comments which contain numbers\n      - '[0-9]+'\n      - 'func\\s+\\w+'\n      - 'FIXME:'\n      - '.*func.*'\n    # check that each sentence starts with a capital letter\n    capital: true\n  godox:\n    # report any comments starting with keywords, this is useful for TODO or FIXME comments that\n    # might be left in the code accidentally and should be resolved before merging\n    keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting\n      #- TODO\n      - BUG\n      - FIXME\n      #- NOTE\n      - OPTIMIZE # marks code that should be optimized before merging\n      - HACK # marks hack-arounds that should be removed before merging\n  gofmt:\n    # simplify code: gofmt with `-s` option, true by default\n    simplify: true\n\n  gofumpt:\n    # Select the Go version to target. The default is `1.18`.\n    go-version: \"1.21\"\n\n    # Choose whether or not to use the extra rules that are disabled\n    # by default\n    extra-rules: false\n\n  # goheader:\n    # values:\n      # const:\n        # define here const type values in format k:v, for example:\n        # COMPANY: MY COMPANY\n      # regexp:\n        # define here regexp type values, for example\n        # AUTHOR: .*@mycompany\\.com\n    # template: # |-\n      # put here copyright header template for source code files, for example:\n      # Note: {{ YEAR }} is a builtin value that returns the year relative to the current machine time.\n      #\n      # {{ AUTHOR }} {{ COMPANY }} {{ YEAR }}\n      # SPDX-License-Identifier: Apache-2.0\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    # template-path:\n      # also as alternative of directive 'template' you may put the path to file with the template source\n\n  goimports:\n    # put imports beginning with prefix after 3rd-party packages;\n    # it's a comma-separated list of prefixes\n    local-prefixes: github.com/openimsdk/open-im-server\n\n  gomnd:\n    # List of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description.\n    # Default: [\"argument\", \"case\", \"condition\", \"operation\", \"return\", \"assign\"]\n    checks:\n      - argument\n      - case\n      - condition\n      - operation\n      - return\n      - assign\n    # List of numbers to exclude from analysis.\n    # The numbers should be written as string.\n    # Values always ignored: \"1\", \"1.0\", \"0\" and \"0.0\"\n    # Default: []\n    ignored-numbers:\n      - '0666'\n      - '0755'\n      - '42'\n    # List of file patterns to exclude from analysis.\n    # Values always ignored: `.+_test.go`\n    # Default: []\n    ignored-files:\n      - 'magic1_.+\\.go$'\n    # List of function patterns to exclude from analysis.\n    # Following functions are always ignored: `time.Date`,\n    # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,\n    # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.\n    # Default: []\n    ignored-functions:\n      - '^math\\.'\n      - '^webhook\\.StatusText$'\n  gomoddirectives:\n    # Allow local `replace` directives. Default is false.\n    replace-local: true\n    # List of allowed `replace` directives. Default is empty.\n    replace-allow-list:\n      - google.golang.org/grpc\n\n    # Allow to not explain why the version has been retracted in the `retract` directives. Default is false.\n    retract-allow-no-explanation: false\n    # Forbid the use of the `exclude` directives. Default is false.\n    exclude-forbidden: false\n\n  gomodguard:\n    allowed:\n      modules:    \n        - gorm.io/gen                                                    # List of allowed modules\n        - gorm.io/gorm\n        - gorm.io/driver/mysql\n        - k8s.io/klog\n        - github.com/allowed/module\n        - go.mongodb.org/mongo-driver/mongo\n        # - gopkg.in/yaml.v2\n      domains:                                                        # List of allowed module domains\n        - google.golang.org\n        - gopkg.in\n        - golang.org\n        - github.com\n        - go.mongodb.org\n        - go.uber.org\n        - openim.io\n        - go.etcd.io\n    blocked:\n      versions:\n        - github.com/MakeNowJust/heredoc:\n            version: \"> 2.0.9\"\n            reason: \"use the latest version\"\n      local_replace_directives: false     # Set to true to raise lint issues for packages that are loaded from a local path via replace directive\n\n  gosec:\n    # To select a subset of rules to run.\n    # Available rules: https://github.com/securego/gosec#available-rules\n    includes:\n      - G401\n      - G306\n      - G101\n    # To specify a set of rules to explicitly exclude.\n    # Available rules: https://github.com/securego/gosec#available-rules\n    excludes:\n      - G204\n    # Exclude generated files\n    exclude-generated: true\n    # Filter out the issues with a lower severity than the given value. Valid options are: low, medium, high.\n    severity: \"low\"\n    # Filter out the issues with a lower confidence than the given value. Valid options are: low, medium, high.\n    confidence: \"low\"\n    # To specify the configuration of rules.\n    # The configuration of rules is not fully documented by gosec:\n    # https://github.com/securego/gosec#configuration\n    # https://github.com/securego/gosec/blob/569328eade2ccbad4ce2d0f21ee158ab5356a5cf/rules/rulelist.go#L60-L102\n    config:\n      G306: \"0600\"\n      G101:\n        pattern: \"(?i)example\"\n        ignore_entropy: false\n        entropy_threshold: \"80.0\"\n        per_char_threshold: \"3.0\"\n        truncate: \"32\"\n\n  gosimple:\n    # Select the Go version to target. The default is '1.13'.\n    go: \"1.20\"\n    # https://staticcheck.io/docs/options#checks\n    checks: [ \"all\" ]\n\n  govet:\n    # settings per analyzer\n    settings:\n      printf: # analyzer name, run `go tool vet help` to see all analyzers\n        funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer\n          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof\n          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf\n          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf\n          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf\n\n    # enable or disable analyzers by name\n    enable:\n      - atomicalign\n    enable-all: false\n    disable:\n      - shadow\n    disable-all: false\n\n  depguard:\n    rules:\n      prevent_unmaintained_packages:\n        list-mode: lax # allow unless explicitely denied\n        files:\n          - $all\n          - \"!$test\"\n        allow:\n          - $gostd\n        deny:\n          - pkg: io/ioutil\n            desc: \"replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil\"\n          - pkg: github.com/OpenIMSDK\n            desc: \"The OpenIM organization has been replaced with lowercase, please do not use uppercase organization name, you will use openimsdk\"\n          - pkg: log\n            desc: \"We have a wrapped log package at openim, we recommend you to use our wrapped log package, https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/logging.md\"\n          - pkg: errors\n            desc: \"We have a wrapped errors package at openim, we recommend you to use our wrapped errors package, https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/error-code.md\"\n\n  importas:\n    # if set to `true`, force to use alias.\n    no-unaliased: true\n    # List of aliases\n    alias:\n      # using `servingv1` alias for `knative.dev/serving/pkg/apis/serving/v1` package\n      - pkg: knative.dev/serving/pkg/apis/serving/v1\n        alias: servingv1\n      - pkg: gopkg.in/yaml.v2\n        alias: yaml\n      # using `autoscalingv1alpha1` alias for `knative.dev/serving/pkg/apis/autoscaling/v1alpha1` package\n      - pkg: knative.dev/serving/pkg/apis/autoscaling/v1alpha1\n        alias: autoscalingv1alpha1\n      # You can specify the package path by regular expression,\n      # and alias by regular expression expansion syntax like below.\n      # see https://github.com/julz/importas#use-regular-expression for details\n      - pkg: knative.dev/serving/pkg/apis/(\\w+)/(v[\\w\\d]+)\n        alias: $1$2\n\n  ireturn:\n    # ireturn allows using `allow` and `reject` settings at the same time.\n    # Both settings are lists of the keywords and regular expressions matched to interface or package names.\n    # keywords:\n    # - `empty` for `interface{}`\n    # - `error` for errors\n    # - `stdlib` for standard library\n    # - `anon` for anonymous interfaces\n\n    # By default, it allows using errors, empty interfaces, anonymous interfaces,\n    # and interfaces provided by the standard library.\n    allow:\n    - anon\n    - error\n    - empty\n    - stdlib\n    # You can specify idiomatic endings for interface\n    - (or|er)$\n\n    # Reject patterns\n    reject:\n    - github.com\\/user\\/package\\/v4\\.Type\n\n  lll:\n    # max line length, lines longer will be reported. Default is 250.\n    # '\\t' is counted as 1 character by default, and can be changed with the tab-width option\n    line-length: 250\n    # tab width in spaces. Default to 1.\n    tab-width: 4\n  misspell:\n    # Correct spellings using locale preferences for US or UK.\n    # Default is to use a neutral variety of English.\n    # Setting locale to US will correct the British spelling of 'colour' to 'color'.\n    locale: US\n    ignore-words:\n      - someword\n  nakedret:\n    # make an issue if func has more lines of code than this setting and it has naked returns; default is 30\n    max-func-lines: 30\n\n  nestif:\n    # minimal complexity of if statements to report, 5 by default\n    min-complexity: 4\n\n  nilnil:\n    # By default, nilnil checks all returned types below.\n    checked-types:\n      - ptr\n      - func\n      - iface\n      - map\n      - chan\n\n  nlreturn:\n    # size of the block (including return statement that is still \"OK\")\n    # so no return split required.\n    block-size: 1\n\n  nolintlint:\n    # Disable to ensure that all nolint directives actually have an effect. Default is true.\n    allow-unused: false\n    # Exclude following linters from requiring an explanation.  Default is [].\n    allow-no-explanation: [ ]\n    # Enable to require an explanation of nonzero length after each nolint directive. Default is false.\n    require-explanation: false\n    # Enable to require nolint directives to mention the specific linter being suppressed. Default is false.\n    require-specific: true\n\n  prealloc:\n    # XXX: we don't recommend using this linter before doing performance profiling.\n    # For most programs usage of prealloc will be a premature optimization.\n\n    # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.\n    # True by default.\n    simple: true\n    range-loops: true # Report preallocation suggestions on range loops, true by default\n    for-loops: false # Report preallocation suggestions on for loops, false by default\n\n  promlinter:\n    # Promlinter cannot infer all metrics name in static analysis.\n    # Enable strict mode will also include the errors caused by failing to parse the args.\n    strict: false\n    # Please refer to https://github.com/yeya24/promlinter#usage for detailed usage.\n    disabled-linters:\n     - \"Help\"\n     - \"MetricUnits\"\n     - \"Counter\"\n     - \"HistogramSummaryReserved\"\n     - \"MetricTypeInName\"\n     - \"ReservedChars\"\n     - \"CamelCase\"\n    \n  predeclared:\n    # comma-separated list of predeclared identifiers to not report on\n    ignore: \"\"\n    # include method names and field names (i.e., qualified names) in checks\n    q: false\n  rowserrcheck:\n    packages:\n      - github.com/jmoiron/sqlx\n\n  revive:\n    # see https://github.com/mgechev/revive#available-rules for details.\n    ignore-generated-header: true\n    severity: warning\n    rules:\n      - name: indent-error-flow\n        severity: warning\n      - name: exported\n        severity: warning\n      - name: var-naming\n        arguments: [ [ \"OpenIM\"] ]\n        # arguments: [ [\"ID\", \"HTTP\", \"URL\", \"URI\", \"API\", \"APIKey\", \"Token\", \"TokenID\", \"TokenSecret\", \"TokenKey\", \"TokenSecret\", \"JWT\", \"JWTToken\", \"JWTTokenID\", \"JWTTokenSecret\", \"JWTTokenKey\", \"JWTTokenSecret\", \"OAuth\", \"OAuthToken\", \"RPC\" ] ]\n      - name: atomic\n      - name: line-length-limit\n        severity: error\n        arguments: [200]\n      - name: unhandled-error\n        arguments : [\"fmt.Printf\", \"myFunction\"]\n\n  staticcheck:\n    # Select the Go version to target. The default is '1.13'.\n    go: \"1.20\"\n    # https://staticcheck.io/docs/options#checks\n    checks: [ \"all\" ]\n\n  stylecheck:\n    # Select the Go version to target. The default is '1.13'.\n    go: \"1.20\"\n\n    # https://staticcheck.io/docs/options#checks\n    checks: [ \"all\", \"-ST1000\", \"-ST1003\", \"-ST1016\", \"-ST1020\", \"-ST1021\", \"-ST1022\" ]\n    # https://staticcheck.io/docs/options#dot_import_whitelist\n    dot-import-whitelist:\n      - fmt\n    # https://staticcheck.io/docs/options#initialisms\n    initialisms: [ \"ACL\", \"API\", \"ASCII\", \"CPU\", \"CSS\", \"DNS\", \"EOF\", \"GUID\", \"HTML\", \"HTTP\", \"HTTPS\", \"ID\", \"IP\", \"JSON\", \"QPS\", \"RAM\", \"RPC\", \"SLA\", \"SMTP\", \"SQL\", \"SSH\", \"TCP\", \"TLS\", \"TTL\", \"UDP\", \"UI\", \"GID\", \"UID\", \"UUID\", \"URI\", \"URL\", \"UTF8\", \"VM\", \"XML\", \"XMPP\", \"XSRF\", \"XSS\" ]\n    # https://staticcheck.io/docs/options#http_status_code_whitelist\n    http-status-code-whitelist: [ \"200\", \"400\", \"404\", \"500\" ]\n\n  tagliatelle:\n    # check the struck tag name case\n    case:\n      # use the struct field name to check the name of the struct tag\n      use-field-name: true\n      rules:\n        # any struct tag type can be used.\n        # support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower`\n        json: camel\n        yaml: camel\n        xml: camel\n        bson: camel\n        avro: snake\n        mapstructure: kebab\n\n  testpackage:\n    # regexp pattern to skip files\n    skip-regexp: (id|export|internal)_test\\.go\n  thelper:\n    # The following configurations enable all checks. It can be omitted because all checks are enabled by default.\n    # You can enable only required checks deleting unnecessary checks.\n    test:\n      first: true\n      name: true\n      begin: true\n    benchmark:\n      first: true\n      name: true\n      begin: true\n    tb:\n      first: true\n      name: true\n      begin: true\n\n  tenv:\n    # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.\n    # By default, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.\n    all: false\n\n  unparam:\n    # Inspect exported functions, default is false. Set to true if no external program/library imports your code.\n    # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:\n    # if it's called for subdir of a project it can't find external interfaces. All text editor integrations\n    # with golangci-lint call it on a directory with the changed file.\n    check-exported: false\n  # unused:\n    # treat code as a program (not a library) and report unused exported identifiers; default is false.\n    # XXX: if you enable this setting, unused will report a lot of false-positives in text editors:\n    # if it's called for subdir of a project it can't find funcs usages. All text editor integrations\n    # with golangci-lint call it on a directory with the changed file.\n  whitespace:\n    multi-if: false   # Enforces newlines (or comments) after every multi-line if statement\n    multi-func: false # Enforces newlines (or comments) after every multi-line function signature\n\n  wrapcheck:\n    # An array of strings that specify substrings of signatures to ignore.\n    # If this set, it will override the default set of ignored signatures.\n    # See https://github.com/tomarrell/wrapcheck#configuration for more information.\n    ignoreSigs:\n      - .Errorf(\n      - errors.New(\n      - errors.Unwrap(\n      - .Wrap(\n      - .WrapMsg(\n      - .Wrapf(\n      - .WithMessage(\n      - .WithMessagef(\n      - .WithStack(\n    ignorePackageGlobs:\n        - encoding/*\n        - github.com/pkg/*\n        - github.com/openimsdk/*\n        - github.com/OpenIMSDK/*\n\n  wsl:\n    # If true append is only allowed to be cuddled if appending value is\n    # matching variables, fields or types on line above. Default is true.\n    strict-append: true\n    # Allow calls and assignments to be cuddled as long as the lines have any\n    # matching variables, fields or types. Default is true.\n    allow-assign-and-call: true\n    # Allow assignments to be cuddled with anything. Default is false.\n    allow-assign-and-anything: false\n    # Allow multiline assignments to be cuddled. Default is true.\n    allow-multiline-assign: true\n    # Allow declarations (var) to be cuddled.\n    allow-cuddle-declarations: false\n    # Allow trailing comments in ending of blocks\n    allow-trailing-comment: false\n    # Force newlines in end of case at this limit (0 = never).\n    force-case-trailing-whitespace: 0\n    # Force cuddling of err checks with err var assignment\n    force-err-cuddling: false\n    # Allow leading comments to be separated with empty liens\n    allow-separated-leading-comment: false\n  makezero:\n    # Allow only slices initialized with a length of zero. Default is false.\n    always: false\n\n  # The custom section can be used to define linter plugins to be loaded at runtime. See README doc\n  #  for more info.\n  #custom:\n    # Each custom linter should have a unique name.\n    #example:\n      # The path to the plugin *.so. Can be absolute or local. Required for each custom linter\n      #path: /path/to/example.so\n      # The description of the linter. Optional, just for documentation purposes.\n      #description: This is an example usage of a plugin linter.\n      # Intended to point to the repo location of the linter. Optional, just for documentation purposes.\n      #original-url: github.com/golangci/example-linter\n\nlinters:\n  # please, do not use `enable-all`: it's deprecated and will be removed soon.\n  # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint\n  # enable-all: true\n  disable-all: true\n  enable:\n    - typecheck     # Basic type checking\n    - gofmt         # Format check\n    - govet         # Go's standard linting tool\n    - gosimple      # Suggestions for simplifying code\n    - errcheck\n    - decorder\n    - ineffassign\n    - forbidigo\n    - revive\n    - reassign\n    - tparallel\n    - unconvert\n    - fieldalignment\n    - dupl\n    - dupword\n    - errname\n    - gci\n    - exhaustive\n    - gocritic\n    - goprintffuncname\n    - gomnd\n    - goconst\n    - gosec\n    - misspell      # Spelling mistakes\n    - staticcheck   # Static analysis\n    - unused        # Checks for unused code\n    # - goimports     # Checks if imports are correctly sorted and formatted\n    - godot         # Checks for comment punctuation\n    - bodyclose     # Ensures HTTP response body is closed\n    - stylecheck    # Style checker for Go code\n    - unused        # Checks for unused code\n    - errcheck      # Checks for missed error returns\n  fast: true\n\nissues:\n  # List of regexps of issue texts to exclude, empty list by default.\n  # But independently from this option we use default exclude patterns,\n  # it can be disabled by `exclude-use-default: false`. To list all\n  # excluded by default patterns execute `golangci-lint run --help`\n  exclude:\n    - tools/.*\n    - test/.*\n    - components/*\n    - third_party/.*\n\n  # Excluding configuration per-path, per-linter, per-text and per-source\n  exclude-rules:\n    - linters:\n      - revive\n      path: (log/.*)\\.go\n\n    - linters:\n      - wrapcheck\n      path: (cmd/.*|pkg/.*)\\.go\n\n    - linters:\n      - typecheck\n        #path: (pkg/storage/.*)\\.go\n      path: (internal/.*|pkg/.*)\\.go\n\n    - path: (cmd/.*|test/.*|tools/.*|internal/pump/pumps/.*)\\.go\n      linters:\n        - forbidigo\n\n    - path: (cmd/[a-z]*/.*|store/.*)\\.go\n      linters:\n        - dupl\n\n    - linters:\n        - gocritic\n      text: (hugeParam:|rangeValCopy:)\n\n    - path: (cmd/[a-z]*/.*)\\.go\n      linters:\n        - lll\n\n    - path: (validator/.*|code/.*|validator/.*|watcher/watcher/.*)\n      linters:\n        - gochecknoinits\n\n    - path: (internal/.*/options|internal/pump|pkg/log/options.go|internal/authzserver|tools/)\n      linters:\n        - tagliatelle\n\n    - path: (pkg/app/.*)\\.go\n      linters:\n        - unused\n        - forbidigo\n\n    # Exclude some staticcheck messages\n    - linters:\n        - staticcheck\n      text: \"SA9003:\"\n\n    # Exclude lll issues for long lines with go:generate\n    - linters:\n        - lll\n      source: \"^//go:generate \"\n\n    - text: \".*[\\u4e00-\\u9fa5]+.*\"\n      linters:\n        - golint\n      source: \"^//.*$\"\n\n  # Independently from option `exclude` we use default exclude patterns,\n  # it can be disabled by this option. To list all\n  # excluded by default patterns execute `golangci-lint run --help`.\n  # Default value for this option is true.\n  exclude-use-default: true\n\n  # The default value is false. If set to true exclude and exclude-rules\n  # regular expressions become case sensitive.\n  exclude-case-sensitive: false\n\n  # The list of ids of default excludes to include or disable. By default it's empty.\n  include:\n    - EXC0002 # disable excluding of issues about comments from golint\n\n  # Maximum issues count per one linter. Set to 0 to disable. Default is 50.\n  max-issues-per-linter: 0\n\n  # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.\n  max-same-issues: 0\n\n  # Show only new issues: if there are unstaged changes or untracked files,\n  # only those changes are analyzed, else only changes in HEAD~ are analyzed.\n  # It's a super-useful option for integration of golangci-lint into existing\n  # large codebase. It's not practical to fix all existing issues at the moment\n  # of integration: much better don't allow issues in new code.\n  # Default is false.\n  new: false\n\n  # Show only new issues created after git revision `REV`\n  # new-from-rev: REV\n\n  # Show only new issues created in git patch with set file path.\n  #new-from-patch: path/to/patch/file\n\n  # Fix found issues (if it's supported by the linter)\n  fix: true\n\nseverity:\n  # Default value is empty string.\n  # Set the default severity for issues. If severity rules are defined and the issues\n  # do not match or no severity is provided to the rule this will be the default\n  # severity applied. Severities should match the supported severity names of the\n  # selected out format.\n  # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity\n  # -   Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity\n  # -       Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message\n  default-severity: error\n\n  # The default value is false.\n  # If set to true severity-rules regular expressions become case sensitive.\n  case-sensitive: false\n\n  # Default value is empty list.\n  # When a list of severity rules are provided, severity information will be added to lint\n  # issues. Severity rules have the same filtering capability as exclude rules except you\n  # are allowed to specify one matcher per severity rule.\n  # Only affects out formats that support setting severity information.\n  rules:\n    - linters:\n      - dupl\n      severity: info\n"
  },
  {
    "path": "CHANGELOG/CHANGELOG-3.8.md",
    "content": "## [v3.8.3-patch.6](https://github.com/openimsdk/open-im-server/releases/tag/v3.8.3-patch.6) \t(2025-07-23)\n\n### Bug Fixes\n* fix: Add friend DB in notification sender [#3438](https://github.com/openimsdk/open-im-server/pull/3438)\n* fix: remove update version file workflows have new line in 3.8.3-patch branch. [#3452](https://github.com/openimsdk/open-im-server/pull/3452)\n* fix: s3 aws init [#3454](https://github.com/openimsdk/open-im-server/pull/3454)\n* fix: use safe submodule init in workflows in v3.8.3-patch. [#3469](https://github.com/openimsdk/open-im-server/pull/3469)\n\n**Full Changelog**: [v3.8.3-patch.5...v3.8.3-patch.6](https://github.com/openimsdk/open-im-server/compare/v3.8.3-patch.5...v3.8.3-patch.6)\n\n## [v3.8.3-patch.5](https://github.com/openimsdk/open-im-server/releases/tag/v3.8.3-patch.5) \t(2025-06-10)\n\n### New Features\n* feat: optimize friend and group applications [#3396](https://github.com/openimsdk/open-im-server/pull/3396)\n\n### Bug Fixes\n* fix: solve unocrrect invite notification [Created [#3219](https://github.com/openimsdk/open-im-server/pull/3219)\n\n### Builds\n* build: update gomake version in dockerfile.[Patch branch] [#3416](https://github.com/openimsdk/open-im-server/pull/3416)\n\n**Full Changelog**: [v3.8.3...v3.8.3-patch.5](https://github.com/openimsdk/open-im-server/compare/v3.8.3...v3.8.3-patch.5)\n\n## [v3.8.3-patch.4](https://github.com/openimsdk/open-im-server/releases/tag/v3.8.3-patch.4) \t(2025-03-13)\n\n### Bug Fixes\n* fix: solve unocrrect invite notificationfrom #3213\n\n**Full Changelog**: [v3.8.3-patch.3...v3.8.3-patch.4](https://github.com/openimsdk/open-im-server/compare/v3.8.3-patch.3...v3.8.3-patch.4)\n\n## [v3.8.3-patch.3](https://github.com/openimsdk/open-im-server/releases/tag/v3.8.3-patch.3) \t(2025-03-07)\n\n### New Features\n* feat: optimizing BatchGetIncrementalGroupMember #3180\n\n### Bug Fixes\n* fix: solve uncorrect notification when set group info #3172\n* fix: the sorting is wrong after canceling the administrator in group settings #3185\n* fix: solve uncorrect GroupMember enter group notification type. #3188\n\n### Refactors\n* refactor: change sendNotification to sendMessage to avoid ambiguity regarding message sending behavior. #3173\n\n**Full Changelog**: [v3.8.3-patch.2...v3.8.3-patch.3](https://github.com/openimsdk/open-im-server/compare/v3.8.3-patch.2...v3.8.3-patch.3)\n\n## [v3.8.3-patch.2](https://github.com/openimsdk/open-im-server/releases/tag/v3.8.3-patch.2) \t(2025-02-28)\n\n### Bug Fixes\n* fix: Offline push does not have a badge && Android offline push (#3146) [#3174](https://github.com/openimsdk/open-im-server/pull/3174)\n\n**Full Changelog**: [v3.8.3-patch.1...v3.8.3-patch.2](https://github.com/openimsdk/open-im-server/compare/v3.8.3-patch.1...v3.8.3-patch.2)\n\n## [v3.8.3-patch.1](https://github.com/openimsdk/open-im-server/releases/tag/v3.8.3-patch.1) \t(2025-02-25)\n\n### New Features\n* feat: add backup volume && optimize log print [Created [#3121](https://github.com/openimsdk/open-im-server/pull/3121)\n\n### Bug Fixes\n* fix: seq conversion failed without exiting [Created [#3120](https://github.com/openimsdk/open-im-server/pull/3120)\n* fix: check error in BatchSetTokenMapByUidPid [Created [#3123](https://github.com/openimsdk/open-im-server/pull/3123)\n* fix: DeleteDoc crash [Created [#3124](https://github.com/openimsdk/open-im-server/pull/3124)\n* fix: the abnormal message has no sending time, causing the SDK to be abnormal [Created [#3126](https://github.com/openimsdk/open-im-server/pull/3126)\n* fix: crash caused [#3127](https://github.com/openimsdk/open-im-server/pull/3127)\n* fix: the user sets the conversation timer cleanup timestamp unit incorrectly [Created [#3128](https://github.com/openimsdk/open-im-server/pull/3128)\n* fix: seq conversion not reading env in docker environment [Created [#3131](https://github.com/openimsdk/open-im-server/pull/3131)\n\n### Builds\n* build: improve workflows contents. [Created [#3125](https://github.com/openimsdk/open-im-server/pull/3125)\n\n**Full Changelog**: [v3.8.3-e-v1.1.5...v3.8.3-patch.1-e-v1.1.5](https://github.com/openimsdk/open-im-server-enterprise/compare/v3.8.3-e-v1.1.5...v3.8.3-patch.1-e-v1.1.5)"
  },
  {
    "path": "CHANGELOG/README.md",
    "content": "# CHANGELOGs\n\n- [CHANGELOG-3.8.md](./CHANGELOG-3.8.md)\n\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n`security@openim.io`.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING-zh_CN.md",
    "content": "\n\n# 如何给 OpenIM 贡献代码（提交 Pull Request）\n\n<p align=\"center\">\n  <a href=\"./CONTRIBUTING.md\">English</a> · \n  <a href=\"./CONTRIBUTING-zh_CN.md\">中文</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-UA.md\">Українська</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-CS.md\">Česky</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-HU.md\">Magyar</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ES.md\">Español</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FA.md\">فارسی</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FR.md\">Français</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-DE.md\">Deutsch</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-PL.md\">Polski</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ID.md\">Indonesian</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FI.md\">Suomi</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ML.md\">മലയാളം</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-JP.md\">日本語</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-NL.md\">Nederlands</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-IT.md\">Italiano</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-RU.md\">Русский</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-PTBR.md\">Português (Brasil)</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-EO.md\">Esperanto</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-KR.md\">한국어</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-AR.md\">العربي</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-VN.md\">Tiếng Việt</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-DA.md\">Dansk</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-GR.md\">Ελληνικά</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-TR.md\">Türkçe</a>\n</p>\n\n本指南将以 [openimsdk/open-im-server](https://github.com/openimsdk/open-im-server) 为例，详细说明如何为 OpenIM 项目贡献代码。我们采用“一问题一分支”的策略，确保每个 Issue 都对应一个专门的分支，以便有效管理代码变更。\n\n### 1. Fork 仓库\n前往 [openimsdk/open-im-server](https://github.com/openimsdk/open-im-server) GitHub 页面，点击右上角的 \"Fork\" 按钮，将仓库 Fork 到你的 GitHub 账户下。\n\n### 2. 克隆仓库\n将你 Fork 的仓库克隆到本地：\n```bash\ngit clone https://github.com/your-username/open-im-server.git\n```\n\n### 3. 设置远程上游\n添加原始仓库为远程上游以便跟踪其更新：\n```bash\ngit remote add upstream https://github.com/openimsdk/open-im-server.git\n```\n\n### 4. 创建 Issue\n在原始仓库中创建一个新的 Issue，详细描述你遇到的问题或希望添加\n\n的新功能。\n\n### 5. 创建新分支\n基于主分支创建一个新分支，并使用描述性的名称与 Issue ID，例如：\n```bash\ngit checkout -b fix-bug-123\n```\n\n### 6. 提交更改\n在你的本地分支上进行更改后，提交这些更改：\n```bash\ngit add .\ngit commit -m \"Describe your changes in detail\"\n```\n\n### 7. 推送分支\n将你的分支推送回你的 GitHub Fork：\n```bash\ngit push origin fix-bug-123\n```\n\n### 8. 创建 Pull Request\n在 GitHub 上转到你的 Fork 仓库，点击 \"Pull Request\" 按钮。确保 PR 描述清楚，并链接到相关的 Issue。\n\n### 9. 签署 CLA\n如果这是你第一次提交 PR，你需要在 PR 的评论中回复：\n```\nI have read the CLA Document and I hereby sign the CLA\n```\n\n### 编程规范\n请参考以下文档以了解关于 Go 语言编程规范的详细信息：\n- [Go 编码规范](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/go-code.md)\n- [代码约定](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/code-conventions.md)\n\n### 日志规范\n- **禁止使用标准的 `log` 包**。\n- 应使用 `\"github.com/openimsdk/tools/log\"` 包来打印日志，该包支持多种日志级别：`debug`、`info`、`warn`、`error`。\n- **错误日志应仅在首次调用的函数中打印**，以防止日志重复，并确保错误的上下文清晰。\n\n### 异常及错误处理\n- **禁止使用 `panic`**：程序中不应使用 `panic`，以避免在遇到不可恢复的错误时突然终止。\n- **错误包裹**：使用 `\"github.com/openimsdk/tools/errs\"` 来包裹错误，保持错误信息的完整性并增加调试便利。\n- **错误传递**：如果函数本身不能处理错误，应将错误返回给调用者，而不是隐藏或忽略这些错误。\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to Contribute to OpenIM (Submitting Pull Requests)\n\n<p align=\"center\">\n  <a href=\"./CONTRIBUTING.md\">English</a> · \n  <a href=\"./CONTRIBUTING-zh_CN.md\">中文</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-UA.md\">Українська</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-CS.md\">Česky</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-HU.md\">Magyar</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ES.md\">Español</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FA.md\">فارسی</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FR.md\">Français</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-DE.md\">Deutsch</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-PL.md\">Polski</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ID.md\">Indonesian</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FI.md\">Suomi</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ML.md\">മലയാളം</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-JP.md\">日本語</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-NL.md\">Nederlands</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-IT.md\">Italiano</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-RU.md\">Русский</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-PTBR.md\">Português (Brasil)</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-EO.md\">Esperanto</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-KR.md\">한국어</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-AR.md\">العربي</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-VN.md\">Tiếng Việt</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-DA.md\">Dansk</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-GR.md\">Ελληνικά</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-TR.md\">Türkçe</a>\n</p>\n\nThis guide will use [openimsdk/open-im-server](https://github.com/openimsdk/open-im-server) as an example to explain in detail how to contribute code to the OpenIM project. We adopt a \"one issue, one branch\" strategy to ensure each issue corresponds to a dedicated branch for effective code change management.\n\n### 1. Fork the Repository\nGo to the [openimsdk/open-im-server](https://github.com/openimsdk/open-im-server) GitHub page, click the \"Fork\" button in the upper right corner to fork the repository to your GitHub account.\n\n### 2. Clone the Repository\nClone the repository you forked to your local machine:\n```bash\ngit clone https://github.com/your-username/open-im-server.git\n```\n\n### 3. Set Upstream Remote\nAdd the original repository as a remote upstream to track updates:\n```bash\ngit remote add upstream https://github.com/openimsdk/open-im-server.git\n```\n\n### 4. Create an Issue\nCreate a new issue in the original repository detailing the problem you encountered or the new feature you wish to add.\n\n### 5. Create a New Branch\nCreate a new branch off the main branch with a descriptive name and Issue ID, for example:\n```bash\ngit checkout -b fix-bug-123\n```\n\n### 6. Commit Changes\nAfter making changes on your local branch, commit these changes:\n```bash\ngit add .\ngit commit -m \"Describe your changes\n\n in detail\"\n```\n\n### 7. Push the Branch\nPush your branch back to your GitHub fork:\n```bash\ngit push origin fix-bug-123\n```\n\n### 8. Create a Pull Request\nGo to your fork on GitHub and click the \"Pull Request\" button. Ensure the PR description is clear and links to the related issue.\n\n### 9. Sign the CLA\nIf this is your first time submitting a PR, you will need to reply in the comments of the PR:\n```\nI have read the CLA Document and I hereby sign the CLA\n```\n\n### Programming Standards\nPlease refer to the following documents for detailed information on Go language programming standards:\n- [Go Coding Standards](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/go-code.md)\n- [Code Conventions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/code-conventions.md)\n\n### Logging Standards\n- **Do not use the standard `log` package**.\n- Use the `\"github.com/openimsdk/tools/log\"` package for logging, which supports multiple log levels: `debug`, `info`, `warn`, `error`.\n- **Error logs should only be printed in the function where they are first actively called** to prevent log duplication and ensure clear error context.\n\n### Exception and Error Handling\n- **Prohibit the use of `panic`**: The code should not use `panic` to avoid abrupt termination when encountering unrecoverable errors.\n- **Error Wrapping**: Use `\"github.com/openimsdk/tools/errs\"` to wrap errors, maintaining the integrity of error information and facilitating debugging.\n- **Error Propagation**: If a function cannot handle an error itself, it should return the error to the caller, rather than hiding or ignoring it.\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n# ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod download\n\n# Install Mage to use for building the application\nRUN go install github.com/magefile/mage@v1.15.0\n\n# Optionally build your application if needed\nRUN mage build\n\n# Using Alpine Linux with Go environment for the final image\nFROM golang:1.22-alpine\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\nCOPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\nCOPY --from=builder /go/bin/mage /usr/local/bin/mage\nCOPY --from=builder $SERVER_DIR/magefile_windows.go $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/magefile_unix.go $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/magefile.go $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/start-config.yml $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/go.mod $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/go.sum $SERVER_DIR/\n\nRUN go get github.com/openimsdk/gomake@v0.0.15-alpha.1\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"mage start && tail -f /dev/null\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\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"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"./assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n[![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20OpenIM%20Guru-006BFF?style=for-the-badge)](https://gurubase.io/g/openim)\n\n<p align=\"center\">\n  <a href=\"./README.md\">English</a> · \n  <a href=\"./README_zh_CN.md\">中文</a> · \n  <a href=\"./docs/readme/README_uk.md\">Українська</a> · \n  <a href=\"./docs/readme/README_cs.md\">Česky</a> · \n  <a href=\"./docs/readme/README_hu.md\">Magyar</a> · \n  <a href=\"./docs/readme/README_es.md\">Español</a> · \n  <a href=\"./docs/readme/README_fa.md\">فارسی</a> · \n  <a href=\"./docs/readme/README_fr.md\">Français</a> · \n  <a href=\"./docs/readme/README_de.md\">Deutsch</a> · \n  <a href=\"./docs/readme/README_pl.md\">Polski</a> · \n  <a href=\"./docs/readme/README_id.md\">Indonesian</a> · \n  <a href=\"./docs/readme/README_fi.md\">Suomi</a> · \n  <a href=\"./docs/readme/README_ml.md\">മലയാളം</a> · \n  <a href=\"./docs/readme/README_ja.md\">日本語</a> · \n  <a href=\"./docs/readme/README_nl.md\">Nederlands</a> · \n  <a href=\"./docs/readme/README_it.md\">Italiano</a> · \n  <a href=\"./docs/readme/README_ru.md\">Русский</a> · \n  <a href=\"./docs/readme/README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./docs/readme/README_eo.md\">Esperanto</a> · \n  <a href=\"./docs/readme/README_ko.md\">한국어</a> · \n  <a href=\"./docs/readme/README_ar.md\">العربي</a> · \n  <a href=\"./docs/readme/README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./docs/readme/README_da.md\">Dansk</a> · \n  <a href=\"./docs/readme/README_el.md\">Ελληνικά</a> · \n  <a href=\"./docs/readme/README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## :busts_in_silhouette: Join Our Community\n\n- 💬 [Follow us on Twitter](https://twitter.com/founder_im63606)\n- 🚀 [Join our Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [Join our WeChat Group](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## Ⓜ️ About OpenIM\n\nUnlike standalone chat applications such as Telegram, Signal, and Rocket.Chat, OpenIM offers an open-source instant messaging solution designed specifically for developers rather than as a directly installable standalone chat app. Comprising OpenIM SDK and OpenIM Server, it provides developers with a complete set of tools and services to integrate instant messaging functions into their applications, including message sending and receiving, user management, and group management. Overall, OpenIM aims to provide developers with the necessary tools and framework to implement efficient instant messaging solutions in their applications.\n\n![App-OpenIM Relationship](./docs/images/oepnim-design.png)\n\n## 🚀 Introduction to OpenIMSDK\n\n**OpenIMSDK**, designed for **OpenIMServer**, is an IM SDK created specifically for integration into client applications. It supports various functionalities and modules:\n\n- 🌟 Main Features:\n\n  - 📦 Local Storage\n  - 🔔 Listener Callbacks\n  - 🛡️ API Wrapping\n  - 🌐 Connection Management\n\n- 📚 Main Modules:\n  1. 🚀 Initialization and Login\n  2. 👤 User Management\n  3. 👫 Friends Management\n  4. 🤖 Group Functions\n  5. 💬 Session Handling\n\nBuilt with Golang and supports cross-platform deployment to ensure a consistent integration experience across all platforms.\n\n👉 **[Explore the GO SDK](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 Introduction to OpenIMServer\n\n- **OpenIMServer** features include:\n  - 🌐 Microservices Architecture: Supports cluster mode, including a gateway and multiple rpc services.\n  - 🚀 Diverse Deployment Options: Supports source code, Kubernetes, or Docker deployment.\n  - Massive User Support: Supports large-scale groups with hundreds of thousands, millions of users, and billions of messages.\n\n### Enhanced Business Functions:\n\n- **REST API**: Provides a REST API for business systems to enhance functionality, such as group creation and message pushing through backend interfaces.\n\n- **Webhooks**: Expands business forms through callbacks, sending requests to business servers before or after certain events.\n\n  ![Overall Architecture](./docs/images/architecture-layers.png)\n\n## :rocket: Quick Start\n\nExperience online for iOS/Android/H5/PC/Web:\n\n👉 **[OpenIM Online Demo](https://www.openim.io/en/commercial)**\n\nTo facilitate user experience, we offer various deployment solutions. You can choose your preferred deployment method from the list below:\n\n- **[Source Code Deployment Guide](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Docker Deployment Guide](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n\n## System Support\n\nSupports Linux, Windows, Mac systems, and ARM and AMD CPU architectures.\n\n## :link: Links\n\n- **[Developer Manual](https://docs.openim.io/)**\n- **[Changelog](https://github.com/openimsdk/open-im-server/blob/main/CHANGELOG.md)**\n\n## :writing_hand: How to Contribute\n\nWe welcome contributions of any kind! Please make sure to read our [Contributor Documentation](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md) before submitting a Pull Request.\n\n- **[Report a Bug](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=bug&template=bug_report.md&title=)**\n- **[Suggest a Feature](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)**\n- **[Submit a Pull Request](https://github.com/openimsdk/open-im-server/pulls)**\n\nThank you for contributing to building a powerful instant messaging solution!\n\n## :closed_book: License\n\nThis software is licensed under the Apache License 2.0\n\n## 🔮 Thanks to our contributors!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "README_zh_CN.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"./assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"./README.md\">English</a> · \n  <a href=\"./README_zh_CN.md\">中文</a> · \n  <a href=\"./docs/readme/README_uk.md\">Українська</a> · \n  <a href=\"./docs/readme/README_cs.md\">Česky</a> · \n  <a href=\"./docs/readme/README_hu.md\">Magyar</a> · \n  <a href=\"./docs/readme/README_es.md\">Español</a> · \n  <a href=\"./docs/readme/README_fa.md\">فارسی</a> · \n  <a href=\"./docs/readme/README_fr.md\">Français</a> · \n  <a href=\"./docs/readme/README_de.md\">Deutsch</a> · \n  <a href=\"./docs/readme/README_pl.md\">Polski</a> · \n  <a href=\"./docs/readme/README_id.md\">Indonesian</a> · \n  <a href=\"./docs/readme/README_fi.md\">Suomi</a> · \n  <a href=\"./docs/readme/README_ml.md\">മലയാളം</a> · \n  <a href=\"./docs/readme/README_ja.md\">日本語</a> · \n  <a href=\"./docs/readme/README_nl.md\">Nederlands</a> · \n  <a href=\"./docs/readme/README_it.md\">Italiano</a> · \n  <a href=\"./docs/readme/README_ru.md\">Русский</a> · \n  <a href=\"./docs/readme/README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./docs/readme/README_eo.md\">Esperanto</a> · \n  <a href=\"./docs/readme/README_ko.md\">한국어</a> · \n  <a href=\"./docs/readme/README_ar.md\">العربي</a> · \n  <a href=\"./docs/readme/README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./docs/readme/README_da.md\">Dansk</a> · \n  <a href=\"./docs/readme/README_el.md\">Ελληνικά</a> · \n  <a href=\"./docs/readme/README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## :busts_in_silhouette: 加入我们的社区\n\n- 💬 [关注我们的 Twitter](https://twitter.com/founder_im63606)\n- 🚀 [加入我们的 Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2hljfom5u-9ZuzP3NfEKW~BJKbpLm0Hw)\n- :eyes: [加入我们的微信群](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## Ⓜ️ 关于 OpenIM\n\n与 Telegram、Signal、Rocket.Chat 等独立聊天应用不同，OpenIM 提供了专为开发者设计的开源即时通讯解决方案，而不是直接安装使用的独立聊天应用。OpenIM 由 OpenIM SDK 和 OpenIM Server 两大部分组成，为开发者提供了一整套集成即时通讯功能的工具和服务，包括消息发送接收、用户管理和群组管理等。总体来说，OpenIM 旨在为开发者提供必要的工具和框架，帮助他们在自己的应用中实现高效的即时通讯解决方案。\n\n![App-OpenIM 关系](./docs/images/oepnim-design.png)\n\n## 🚀 OpenIMSDK 介绍\n\n**OpenIMSDK** 是为 **OpenIMServer** 设计的 IM SDK，专为集成到客户端应用而生。它支持多种功能和模块：\n\n- 🌟 主要功能：\n\n  - 📦 本地存储\n  - 🔔 监听器回调\n  - 🛡️ API 封装\n  - 🌐 连接管理\n\n- 📚 主要模块：\n  1. 🚀 初始化及登录\n  2. 👤 用户管理\n  3. 👫 好友管理\n  4. 🤖 群组功能\n  5. 💬 会话处理\n\n它使用 Golang 构建，并支持跨平台部署，确保在所有平台上提供一致的接入体验。\n\n👉 **[探索 GO SDK](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 OpenIMServer 介绍\n\n- **OpenIMServer** 的特点包括：\n  - 🌐 微服务架构：支持集群模式，包括网关(gateway)和多个 rpc 服务。\n  - 🚀 多样的部署方式：支持源代码、Kubernetes 或 Docker 部署。\n  - 海量用户支持：支持十万级超大群组，千万级用户和百亿级消息。\n\n### 增强的业务功能：\n\n- **REST API**：为业务系统提供 REST API，增加群组创建、消息推送等后台接口功能。\n\n- **Webhooks**：通过事件前后的回调，向业务服务器发送请求，扩展更多的业务形态。\n\n  ![整体架构](./docs/images/architecture-layers.png)\n\n## :rocket: 快速入门\n\n在线体验 iOS/Android/H5/PC/Web：\n\n👉 **[OpenIM 在线演示](https://www.openim.io/en/commercial)**\n\n为了便于用户体验，我们提供了多种部署解决方案，您可以根据以下列表选择适合您的部署方式：\n\n- **[源代码部署指南](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Docker 部署指南](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n\n## 系统支持\n\n支持 Linux、Windows、Mac 系统以及 ARM 和 AMD CPU 架构。\n\n## :link: 相关链接\n\n- **[开发手册](https://docs.openim.io/)**\n- **[更新日志](https://github.com/openimsdk/open-im-server/blob/main/CHANGELOG.md)**\n\n## :writing_hand: 如何贡献\n\n我们欢迎任何形式的贡献！在提交 Pull Request 之前，请确保阅读我们的[贡献者文档](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)\n\n- **[报告 Bug](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=bug&template=bug_report.md&title=)**\n- **[提出新特性](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)**\n- **[提交 Pull Request](https://github.com/openimsdk/open-im-server/pulls)**\n\n感谢您的贡献，一起来打造强大的即时通讯解决方案！\n\n## :closed_book: 开源许可证 License\n\nThis software is licensed under the Apache License 2.0\n\n## 🔮 Thanks to our contributors!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "assets/README.md",
    "content": "# `/assets`\n\nThe `/assets` directory in the OpenIM repository contains various assets such as images, logos, and animated GIFs. These assets serve different purposes and contribute to the functionality and aesthetics of the OpenIM project.\n\n## Directory Structure:\n\n```bash\nassets/\n├── README.md                     # Documentation for the assets directory\n├── images                        # Directory holding images related to OpenIM\n│   ├── architecture.png          # Image depicting the architecture of OpenIM\n│   └── mvc.png                   # Image illustrating the Model-View-Controller (MVC) pattern\n├── intive-slack.png              # Image displaying the Intive Slack logo\n├── logo                          # Directory containing various logo variations for OpenIM\n│   ├── openim-logo-black.png     # OpenIM logo with a black background\n│   ├── openim-logo-blue.png      # OpenIM logo with a blue background\n│   ├── openim-logo-green.png     # OpenIM logo with a green background\n│   ├── openim-logo-purple.png    # OpenIM logo with a purple background\n│   ├── openim-logo-white.png     # OpenIM logo with a white background\n│   ├── openim-logo-yellow.png    # OpenIM logo with a yellow background\n│   └── openim-logo.png           # OpenIM logo with a transparent background\n└── logo-gif                      # Directory containing animated GIF versions of the OpenIM logo\n    └── openim-log.gif            # Animated OpenIM logo with a transparent background\n```\n\n## Copyright Notice:\n\nThe OpenIM logo, including its variations and animated versions, displayed in this repository [OpenIM](https://github.com/openimsdk/open-im-server) under the `/assets/logo` and `/assets/logo-gif` directories, are protected by copyright laws.\n\nThe logo design is credited to @Xx(席欣).\n\nPlease respect the intellectual property rights and refrain from unauthorized use and distribution of these assets."
  },
  {
    "path": "assets/colors.md",
    "content": "# Official Colors\n\nThe openim logo has an official blue color.  When reproducing the logo, please use the official color, when possible.\n\n## Pantone\n\nWhen possible, the Pantone color is preferred for print material.  The official Pantone color is *285C*.\n\n## RGB\n\nWhen used digitally, the official RGB color code is *#326CE5*.\n"
  },
  {
    "path": "assets/demo/README.md",
    "content": "## :star2: Why OpenIM\n\n**🔍 Function screenshot display**\n\n<div align=\"center\">\n\n\n|             multiple message              |               Efficient meetings                |\n| :---------------------------------------: | :---------------------------------------------: |\n| ![multiple-message](./multiple-message.png) | ![efficient-meetings](./efficient-meetings.png) |\n|      **One-to-one and group chats**       |     **Special features - Custom messages**      |\n|      ![group-chat](./group-chat.png)      |   ![special-function](./special-function.png)    |\n\n</div>\n"
  },
  {
    "path": "assets/logo/LICENSE",
    "content": "# The OpenIM logo files are licensed under a choice of either Apache-2.0 or CC-BY-4.0 (Creative Commons Attribution 4.0 International)."
  },
  {
    "path": "assets/logo-gif/LICENSE",
    "content": "# The OpenIM logo files are licensed under a choice of either Apache-2.0 or CC-BY-4.0 (Creative Commons Attribution 4.0 International)."
  },
  {
    "path": "bootstrap.bat",
    "content": "@echo off\nSETLOCAL\n\nmage -version >nul 2>&1\nIF %ERRORLEVEL% EQU 0 (\n    echo Mage is already installed.\n    GOTO DOWNLOAD\n)\n\ngo version >nul 2>&1\nIF NOT %ERRORLEVEL% EQU 0 (\n    echo Go is not installed. Please install Go and try again.\n    exit /b 1\n)\n\necho Installing Mage...\ngo install github.com/magefile/mage@latest\n\nmage -version >nul 2>&1\nIF NOT %ERRORLEVEL% EQU 0 (\n    echo Mage installation failed.\n    echo Please ensure that %GOPATH%/bin is in your PATH.\n    exit /b 1\n)\n\necho Mage installed successfully.\n\n:DOWNLOAD\ngo mod download\n\nENDLOCAL\n"
  },
  {
    "path": "bootstrap.sh",
    "content": "#!/bin/bash\n\nif [[ \":$PATH:\" == *\":$HOME/.local/bin:\"* ]]; then\n    TARGET_DIR=\"$HOME/.local/bin\"\nelse\n    TARGET_DIR=\"/usr/local/bin\"\n    echo \"Using /usr/local/bin as the installation directory. Might require sudo permissions.\"\nfi\n\nif ! command -v mage &> /dev/null; then\n    echo \"Installing Mage to $TARGET_DIR ...\"\n    GOBIN=$TARGET_DIR go install github.com/magefile/mage@latest\nfi\n\nif ! command -v mage &> /dev/null; then\n    echo \"Mage installation failed.\"\n    echo \"Please ensure that $TARGET_DIR is in your \\$PATH.\"\n    exit 1\nfi\n\necho \"Mage installed successfully.\"\n\ngo mod download\n"
  },
  {
    "path": "build/README.md",
    "content": "# Building OpenIM\n\nBuilding OpenIM is easy if you take advantage of the containerized build environment. This document will help guide you through understanding this build process.\n\n## Requirements\n\n1. Docker, using one of the following configurations:\n  * **macOS** Install Docker for Mac. See installation instructions [here](https://docs.docker.com/docker-for-mac/).\n     **Note**: You will want to set the Docker VM to have at least 4GB of initial memory or building will likely fail.\n  * **Linux with local Docker**  Install Docker according to the [instructions](https://docs.docker.com/installation/#installation) for your OS.\n  * **Windows with Docker Desktop WSL2 backend**  Install Docker according to the [instructions](https://docs.docker.com/docker-for-windows/wsl-tech-preview/). Be sure to store your sources in the local Linux file system, not the Windows remote mount at `/mnt/c`.\n  \n  **Note**: You will need to check if Docker CLI plugin buildx is properly installed (`docker-buildx` file should be present in `~/.docker/cli-plugins`). You can install buildx according to the [instructions](https://github.com/docker/buildx/blob/master/README.md#installing).\n\n2. **Optional** [Google Cloud SDK](https://developers.google.com/cloud/sdk/)\n\nYou must install and configure Google Cloud SDK if you want to upload your release to Google Cloud Storage and may safely omit this otherwise.\n\n## Actions\n\nAbout [Images packages](https://github.com/orgs/OpenIMSDK/packages?repo_name=Open-IM-Server)\n\nAll files in the `build/images` directory are not templated and are instead rendered by Github Actions, which is an automated process.\n\nTrigger condition:\n1. create a new tag with the format `vX.Y.Z` (e.g. `v1.0.0`)\n2. push the tag to the remote repository\n3. wait for the build to finish\n4. download the artifacts from the release page\n\n## Make images\n\n**help info:**\n\n```bash\n$ make image.help\n```\n\n**build images:**\n\n```bash\n$ make image\n```\n\n## Overview\n\nWhile it is possible to build OpenIM using a local golang installation, we have a build process that runs in a Docker container.  This simplifies initial set up and provides for a very consistent build and test environment.\n\n\n## Basic Flow\n\nThe scripts directly under [`build/`](.) are used to build and test.  They will ensure that the `openim-build` Docker image is built (based on [`build/build-image/Dockerfile`](../Dockerfile) and after base image's `OPENIM_BUILD_IMAGE_CROSS_TAG` from Dockerfile is replaced with one of those actual tags of the base image, like `v1.13.9-2`) and then execute the appropriate command in that container.  These scripts will both ensure that the right data is cached from run to run for incremental builds and will copy the results back out of the container. You can specify a different registry/name and version for `openim-cross` by setting `OPENIM_CROSS_IMAGE` and `OPENIM_CROSS_VERSION`, see [`common.sh`](common.sh) for more details.\n\nThe `openim-build` container image is built by first creating a \"context\" directory in `_output/images/build-image`.  It is done there instead of at the root of the OpenIM repo to minimize the amount of data we need to package up when building the image.\n\nThere are 3 different containers instances that are run from this image.  The first is a \"data\" container to store all data that needs to persist across to support incremental builds. Next there is an \"rsync\" container that is used to transfer data in and out to the data container.  Lastly there is a \"build\" container that is used for actually doing build actions.  The data container persists across runs while the rsync and build containers are deleted after each use.\n\n`rsync` is used transparently behind the scenes to efficiently move data in and out of the container.  This will use an ephemeral port picked by Docker.  You can modify this by setting the `OPENIM_RSYNC_PORT` env variable.\n\nAll Docker names are suffixed with a hash derived from the file path (to allow concurrent usage on things like CI machines) and a version number.  When the version number changes all state is cleared and clean build is started.  This allows the build infrastructure to be changed and signal to CI systems that old artifacts need to be deleted.\n\n## Build artifacts\nThe build system output all its products to a top level directory in the source repository named `_output`.\nThese include the binary compiled packages (e.g. imctl, openim-api etc.) and archived Docker images.\nIf you intend to run a component with a docker image you will need to import it from this directory with\n"
  },
  {
    "path": "build/goreleaser.yaml",
    "content": "# This is an example .goreleaser.yml file with some sensible defaults.\n# Make sure to check the documentation at https://goreleaser.com\n\nbefore:\n  hooks:\n    - make clean\n    # You may remove this if you don't use go modules.\n    - make tidy\n    - make copyright.add\n    # you may remove this if you don't need go generate\n    - go generate ./...\n\ngit:\n  # What should be used to sort tags when gathering the current and previous\n  # tags if there are more than one tag in the same commit.\n  #\n  # Default: '-version:refname'\n  tag_sort: -version:creatordate\n\n  # What should be used to specify prerelease suffix while sorting tags when gathering\n  # the current and previous tags if there are more than one tag in the same commit.\n  #\n  # Since: v1.17\n  prerelease_suffix: \"-\"\n\n  # Tags to be ignored by GoReleaser.\n  # This means that GoReleaser will not pick up tags that match any of the\n  # provided values as either previous or current tags.\n  #\n  # Templates: allowed.\n  # Since: v1.21.\n  ignore_tags:\n    - nightly\n    # - \"{{.Env.IGNORE_TAG}}\"\n  \nsnapshot:\n  name_template: \"{{ incpatch .Version }}-next\"\n\n# gomod:\n#   proxy: true\n\nreport_sizes: true\n\n# metadata:\n#   mod_timestamp: \"{{ .CommitTimestamp }}\"\n\nbuilds:\n  - binary: openim-api\n    id: openim-api\n    main: ./cmd/openim-api/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-cmdutils\n    id: openim-cmdutils\n    main: ./cmd/openim-cmdutils/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-crontask\n    id: openim-crontask\n    main: ./cmd/openim-crontask/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-msggateway\n    id: openim-msggateway\n    main: ./cmd/openim-msggateway/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-msgtransfer\n    id: openim-msgtransfer\n    main: ./cmd/openim-msgtransfer/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-push\n    id: openim-push\n    main: ./cmd/openim-push/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-rpc-auth\n    id: openim-rpc-auth\n    main: ./cmd/openim-rpc/openim-rpc-auth/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-rpc-conversation\n    id: openim-rpc-conversation\n    main: ./cmd/openim-rpc/openim-rpc-conversation/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-rpc-friend\n    id: openim-rpc-friend\n    main: ./cmd/openim-rpc/openim-rpc-friend/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-rpc-group\n    id: openim-rpc-group\n    main: ./cmd/openim-rpc/openim-rpc-group/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-rpc-msg\n    id: openim-rpc-msg\n    main: ./cmd/openim-rpc/openim-rpc-msg/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-rpc-third\n    id: openim-rpc-third\n    main: ./cmd/openim-rpc/openim-rpc-third/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n  - binary: openim-rpc-user\n    id: openim-rpc-user\n    main: ./cmd/openim-rpc/openim-rpc-user/main.go\n    goos:\n      - darwin\n      - windows\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\n\n# TODO：Need a script, such as the init - release to help binary to find the right directory\n# ,which can be compiled binary\narchives:\n  - format: tar.gz\n    # this name template makes the OS and Arch compatible with the results of uname.\n    name_template: >-\n      {{ .ProjectName }}_\n      {{- title .Os }}_\n      {{- if eq .Arch \"amd64\" }}x86_64\n      {{- else if eq .Arch \"386\" }}i386\n      {{- else }}{{ .Arch }}{{ end }}\n      {{- if .Arm }}v{{ .Arm }}{{ end }}\n\n    # Set this to true if you want all files in the archive to be in a single directory.\n    # If set to true and you extract the archive 'goreleaser_Linux_arm64.tar.gz',\n    # you'll get a folder 'goreleaser_Linux_arm64'.\n    # If set to false, all files are extracted separately.\n    # You can also set it to a custom folder name (templating is supported).\n    wrap_in_directory: true\n\n    # use zip for windows archives\n    files:\n      - CHANGELOG/*\n      - deployment/*\n      - config/*\n      - build/*\n      - scripts/*\n      - Makefile\n      - install.sh\n      - docs/*\n      - src: \"*.md\"\n        dst: docs\n\n        # Strip parent folders when adding files to the archive.\n        strip_parent: true\n\n        # File info.\n        # Not all fields are supported by all formats available formats.\n        #\n        # Default: copied from the source file\n        info:\n          # Templates: allowed (since v1.14)\n          owner: root\n\n          # Templates: allowed (since v1.14)\n          group: root\n\n          # Must be in time.RFC3339Nano format.\n          #\n          # Templates: allowed (since v1.14)\n          mtime: \"{{ .CommitDate }}\"\n\n          # File mode.\n          mode: 0644\n\n    format_overrides:\n    - goos: windows\n      format: zip\n\nchangelog:\n  sort: asc\n  use: github\n  filters:\n    exclude:\n      - \"^test:\"\n      - \"^chore\"\n      - \"merge conflict\"\n      - Merge pull request\n      - Merge remote-tracking branch\n      - Merge branch\n      - go mod tidy\n  groups:\n    - title: Dependency updates\n      regexp: '^.*?(feat|fix)\\(deps\\)!?:.+$'\n      order: 300\n    - title: \"New Features\"\n      regexp: '^.*?feat(\\([[:word:]]+\\))??!?:.+$'\n      order: 100\n    - title: \"Security updates\"\n      regexp: '^.*?sec(\\([[:word:]]+\\))??!?:.+$'\n      order: 150\n    - title: \"Bug fixes\"\n      regexp: '^.*?fix(\\([[:word:]]+\\))??!?:.+$'\n      order: 200\n    - title: \"Documentation updates\"\n      regexp: ^.*?doc(\\([[:word:]]+\\))??!?:.+$\n      order: 400\n    - title: \"Build process updates\"\n      regexp: ^.*?build(\\([[:word:]]+\\))??!?:.+$\n      order: 400\n    - title: Other work\n      order: 9999\n\n# dockers:\n#   - image_templates:\n#       - \"openimsdk/open-im-server:{{ .Tag }}-amd64\"\n#       - \"ghcr.io/goreleaser/goreleaser:{{ .Tag }}-amd64\"\n#     dockerfile: build/images/openim-api/Dockerfile.release\n#     ids: \n#       - openim-api\n#     use: buildx\n#     build_flag_templates:\n#       - \"--pull\"\n#       - \"--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/openimsdk/open-im-server/main/README.md\"\n#       - \"--label=io.artifacthub.package.logo-url=hhttps://github.com/openimsdk/open-im-server/blob/main/assets/logo/openim-logo-green.png\"\n#       - '--label=io.artifacthub.package.maintainers=[{\"name\":\"Xinwei Xiong\",\"email\":\"3293172751nss@gmail.com\"}]'\n#       - \"--label=io.artifacthub.package.license=Apace-2.0\"\n#       - \"--label=org.opencontainers.image.description=OpenIM Open source top instant messaging system\"\n#       - \"--label=org.opencontainers.image.created={{.Date}}\"\n#       - \"--label=org.opencontainers.image.name={{.ProjectName}}\"\n#       - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n#       - \"--label=org.opencontainers.image.version={{.Version}}\"\n#       - \"--label=org.opencontainers.image.source={{.GitURL}}\"\n#       - \"--platform=linux/amd64\"\n#     extra_files:\n#       - scripts/entrypoint.sh\n#   - image_templates:\n#       - \"goreleaser/goreleaser:{{ .Tag }}-arm64\"\n#       - \"ghcr.io/goreleaser/goreleaser:{{ .Tag }}-arm64\"\n#     dockerfile: build/images/openim-api/Dockerfile.release\n#     use: buildx\n#     build_flag_templates:\n#       - \"--pull\"\n#       - \"--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/openimsdk/open-im-server/main/README.md\"\n#       - \"--label=io.artifacthub.package.logo-url=hhttps://github.com/openimsdk/open-im-server/blob/main/assets/logo/openim-logo-green.png\"\n#       - '--label=io.artifacthub.package.maintainers=[{\"name\":\"Xinwei Xiong\",\"email\":\"3293172751nss@gmail.com\"}]'\n#       - \"--label=io.artifacthub.package.license=Apace-2.0\"\n#       - \"--label=org.opencontainers.image.description=OpenIM Open source top instant messaging system\"\n#       - \"--label=org.opencontainers.image.created={{.Date}}\"\n#       - \"--label=org.opencontainers.image.name={{.ProjectName}}\"\n#       - \"--label=org.opencontainers.image.revision={{.FullCommit}}\"\n#       - \"--label=org.opencontainers.image.version={{.Version}}\"\n#       - \"--label=org.opencontainers.image.source={{.GitURL}}\"\n#       - \"--platform=linux/arm64\"\n#     goarch: arm64\n#     extra_files:\n#       - scripts/entrypoint.sh\n\n# docker_manifests:\n#   - name_template: \"goreleaser/goreleaser:{{ .Tag }}\"\n#     image_templates:\n#       - \"goreleaser/goreleaser:{{ .Tag }}-amd64\"\n#       - \"goreleaser/goreleaser:{{ .Tag }}-arm64\"\n#   - name_template: \"ghcr.io/goreleaser/goreleaser:{{ .Tag }}\"\n#     image_templates:\n#       - \"ghcr.io/goreleaser/goreleaser:{{ .Tag }}-amd64\"\n#       - \"ghcr.io/goreleaser/goreleaser:{{ .Tag }}-arm64\"\n#   - name_template: \"goreleaser/goreleaser:latest\"\n#     image_templates:\n#       - \"goreleaser/goreleaser:{{ .Tag }}-amd64\"\n#       - \"goreleaser/goreleaser:{{ .Tag }}-arm64\"\n#   - name_template: \"ghcr.io/goreleaser/goreleaser:latest\"\n#     image_templates:\n#       - \"ghcr.io/goreleaser/goreleaser:{{ .Tag }}-amd64\"\n#       - \"ghcr.io/goreleaser/goreleaser:{{ .Tag }}-arm64\"\n\nnfpms:\n  - id: packages\n    builds:\n      - openim-api\n      - openim-cmdutils\n      - openim-crontask\n      - openim-msggateway\n      - openim-msgtransfer\n      - openim-push\n      - openim-rpc-auth\n      - openim-rpc-conversation\n      - openim-rpc-friend\n      - openim-rpc-group\n      - openim-rpc-msg\n      - openim-rpc-third\n      - openim-rpc-user\n    # Your app's vendor.\n    vendor: OpenIMSDK\n    homepage: https://github.com/openimsdk/open-im-server\n    maintainer: kubbot <https://github.com/kubbot>\n    description: |-\n      Auto sync github labels\n      kubbot && openimbot\n    license: MIT\n    formats:\n      - apk\n      - deb\n      - rpm\n      - termux.deb # Since: v1.11\n      - archlinux # Since: v1.13\n    dependencies:\n      - git\n    recommends:\n      - golang\n\n\n# The lines beneath this are called `modelines`. See `:help modeline`\n# Feel free to remove those if you don't want/use them.\n# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\n# vim: set ts=2 sw=2 tw=0 fo=cnqoj\n\n# Default: './dist'\ndist: ./_output/dist\n\n# .goreleaser.yaml\nmilestones:\n  # You can have multiple milestone configs\n  -\n    # Repository for the milestone\n    # Default is extracted from the origin remote URL\n    repo:\n      owner: OpenIMSDK\n      name: Open-IM-Server\n\n    # Whether to close the milestone\n    close: true\n\n    # Fail release on errors, such as missing milestone.\n    fail_on_error: false\n\n    # Name of the milestone\n    #\n    # Default: '{{ .Tag }}'\n    name_template: \"Current Release\"\n\n# publishers:\n#   - name: \"fury.io\"\n#     ids:\n#       - packages\n#     dir: \"{{ dir .ArtifactPath }}\"\n#     cmd: |\n#       bash -c '\n#       if [[ \"{{ .Tag }}\" =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then\n#         curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/{{ .Env.USERNAME }}/\n#       else\n#         echo \"Skipping deployment: Non-production release detected\"\n#       fi'\n\nchecksum:\n  name_template: \"{{ .ProjectName }}_checksums.txt\"\n  algorithm: sha256\n\nrelease:\n  prerelease: auto\n"
  },
  {
    "path": "build/images/Dockerfile",
    "content": "# # Copyright © 2023 OpenIM. All rights reserved.\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# FROM BASE_IMAGE\n\n# WORKDIR ${SERVER_WORKDIR}\n\n# # Set HTTP proxy\n# ARG BINARY_NAME\n\n# COPY BINARY_NAME ./bin/BINARY_NAME\n\n# ENTRYPOINT [\"./bin/BINARY_NAME\"]"
  },
  {
    "path": "build/images/openim-api/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\nRUN go build -o _output/openim-api ./cmd/openim-api\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\nCOPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-api\"]\n"
  },
  {
    "path": "build/images/openim-crontask/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-crontask ./cmd/openim-crontask\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-crontask\"]\n"
  },
  {
    "path": "build/images/openim-msggateway/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-msggateway ./cmd/openim-msggateway\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-msggateway\"]\n"
  },
  {
    "path": "build/images/openim-msgtransfer/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-msgtransfer ./cmd/openim-msgtransfer\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-msgtransfer\"]\n"
  },
  {
    "path": "build/images/openim-push/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-push ./cmd/openim-push\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-push\"]\n"
  },
  {
    "path": "build/images/openim-rpc-auth/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-rpc-auth ./cmd/openim-rpc/openim-rpc-auth\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-rpc-auth\"]\n"
  },
  {
    "path": "build/images/openim-rpc-conversation/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-rpc-conversation ./cmd/openim-rpc/openim-rpc-conversation\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-rpc-conversation\"]\n"
  },
  {
    "path": "build/images/openim-rpc-friend/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-rpc-friend ./cmd/openim-rpc/openim-rpc-friend\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-rpc-friend\"]\n"
  },
  {
    "path": "build/images/openim-rpc-group/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-rpc-group ./cmd/openim-rpc/openim-rpc-group\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-rpc-group\"]\n"
  },
  {
    "path": "build/images/openim-rpc-msg/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-rpc-msg ./cmd/openim-rpc/openim-rpc-msg\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-rpc-msg\"]\n"
  },
  {
    "path": "build/images/openim-rpc-third/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\n\n\nRUN go build -o _output/openim-rpc-third ./cmd/openim-rpc/openim-rpc-third\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-rpc-third\"]\n"
  },
  {
    "path": "build/images/openim-rpc-user/Dockerfile",
    "content": "# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod tidy\n\nRUN go build -o _output/openim-rpc-user ./cmd/openim-rpc/openim-rpc-user\n\n\n# Using Alpine Linux for the final image\nFROM alpine:latest\n\n# Install necessary packages, such as bash\nRUN apk add --no-cache bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\n# COPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"_output/openim-rpc-user\"]\n"
  },
  {
    "path": "build/images/openim-tools/component/Dockerfile",
    "content": "# # Copyright © 2023 OpenIM. All rights reserved.\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# # OpenIM base image: https://github.com/openim-sigs/openim-base-image\n\n# # Set go mod installation source and proxy\n\n# FROM golang:1.20 AS builder\n\n# \n\n# WORKDIR /openim/openim-server\n\n# \n# ENV GOPROXY=$GOPROXY\n\n# COPY go.mod go.sum ./\n# RUN go mod download\n\n# COPY . .\n\n# RUN make clean\n# RUN make build BINS=component\n\n# # FROM ghcr.io/openim-sigs/openim-bash-image:latest\n# FROM ghcr.io/openim-sigs/openim-bash-image:latest\n\n# WORKDIR /openim/openim-server\n\n# COPY --from=builder /openim/openim-server/_output/bin/tools /openim/openim-server/_output/bin/tools/\n# COPY --from=builder /openim/openim-server/config /openim/openim-server/config\n\n# ENV OPENIM_SERVER_CONFIG_NAME=/openim/openim-server/config\n\n# RUN mv ${OPENIM_SERVER_BINDIR}/platforms/$(get_os)/$(get_arch)/component /usr/bin/component\n\n# ENTRYPOINT [\"bash\", \"-c\", \"component -c $OPENIM_SERVER_CONFIG_NAME\"]\n\n\n# Use Go 1.22 Alpine as the base image for building the application\nFROM golang:1.22-alpine AS builder\n# Define the base directory for the application as an environment variable\nENV SERVER_DIR=/openim-server\n\n# Set the working directory inside the container based on the environment variable\nWORKDIR $SERVER_DIR\n\n# Set the Go proxy to improve dependency resolution speed\n\n#ENV GOPROXY=https://goproxy.io,direct\n\n# Copy all files from the current directory into the container\nCOPY . .\n\nRUN go mod download\n\n# Install Mage to use for building the application\nRUN go install github.com/magefile/mage@v1.15.0\n\n# ENV BINS=openim-rpc-user\n\n# Optionally build your application if needed\n# RUN mage build ${BINS} check-free-memory seq || true\nRUN mage build check-free-memory seq || true\n\n# Using Alpine Linux with Go environment for the final image\nFROM golang:1.22-alpine\n\n# Install necessary packages, such as bash\nRUN apk add bash\n\n# Set the environment and work directory\nENV SERVER_DIR=/openim-server\nWORKDIR $SERVER_DIR\n\n\n# Copy the compiled binaries and mage from the builder image to the final image\nCOPY --from=builder $SERVER_DIR/_output $SERVER_DIR/_output\nCOPY --from=builder $SERVER_DIR/config $SERVER_DIR/config\nCOPY --from=builder /go/bin/mage /usr/local/bin/mage\nCOPY --from=builder $SERVER_DIR/magefile_windows.go $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/magefile_unix.go $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/magefile.go $SERVER_DIR/\n# COPY --from=builder $SERVER_DIR/start-config.yml $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/go.mod $SERVER_DIR/\nCOPY --from=builder $SERVER_DIR/go.sum $SERVER_DIR/\n\n\nRUN echo -e \"serviceBinaries:\\n  \\n\" \\\n    > $SERVER_DIR/start-config.yml && \\\n    echo -e \"toolBinaries:\\n  - check-free-memory\\n  - seq\\n\" >> $SERVER_DIR/start-config.yml && \\\n    echo \"maxFileDescriptors: 10000\" >> $SERVER_DIR/start-config.yml\n\nRUN go get github.com/openimsdk/gomake@v0.0.15-alpha.1\n\n# Set the command to run when the container starts\nENTRYPOINT [\"sh\", \"-c\", \"mage start && tail -f /dev/null\"]\n"
  },
  {
    "path": "cmd/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/mitchellh/mapstructure\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/api\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/msggateway\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/msgtransfer\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/auth\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/conversation\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/group\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/msg\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/relation\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/third\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/user\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/tools/cron\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/discovery/standalone\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/spf13/viper\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc init() {\n\tconfig.SetStandalone()\n\tprommetrics.RegistryAll()\n}\n\nfunc main() {\n\tvar configPath string\n\tflag.StringVar(&configPath, \"c\", \"\", \"config path\")\n\tflag.Parse()\n\tif configPath == \"\" {\n\t\t_, _ = fmt.Fprintln(os.Stderr, \"config path is empty\")\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\tcmd := newCmds(configPath)\n\tputCmd(cmd, false, auth.Start)\n\tputCmd(cmd, false, conversation.Start)\n\tputCmd(cmd, false, relation.Start)\n\tputCmd(cmd, false, group.Start)\n\tputCmd(cmd, false, msg.Start)\n\tputCmd(cmd, false, third.Start)\n\tputCmd(cmd, false, user.Start)\n\tputCmd(cmd, false, push.Start)\n\tputCmd(cmd, true, msggateway.Start)\n\tputCmd(cmd, true, msgtransfer.Start)\n\tputCmd(cmd, true, api.Start)\n\tputCmd(cmd, true, cron.Start)\n\tctx := context.Background()\n\tif err := cmd.run(ctx); err != nil {\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"server exit %s\", err)\n\t\tos.Exit(1)\n\t\treturn\n\t}\n}\n\nfunc newCmds(confPath string) *cmds {\n\treturn &cmds{confPath: confPath}\n}\n\ntype cmdName struct {\n\tName  string\n\tFunc  func(ctx context.Context) error\n\tBlock bool\n}\ntype cmds struct {\n\tconfPath string\n\tcmds     []cmdName\n\tconfig   config.AllConfig\n\tconf     map[string]reflect.Value\n}\n\nfunc (x *cmds) getTypePath(typ reflect.Type) string {\n\treturn path.Join(typ.PkgPath(), typ.Name())\n}\n\nfunc (x *cmds) initDiscovery() {\n\tx.config.Discovery.Enable = \"standalone\"\n\tvof := reflect.ValueOf(&x.config.Discovery.RpcService).Elem()\n\ttof := reflect.TypeOf(&x.config.Discovery.RpcService).Elem()\n\tnum := tof.NumField()\n\tfor i := 0; i < num; i++ {\n\t\tfield := tof.Field(i)\n\t\tif !field.IsExported() {\n\t\t\tcontinue\n\t\t}\n\t\tif field.Type.Kind() != reflect.String {\n\t\t\tcontinue\n\t\t}\n\t\tvof.Field(i).SetString(field.Name)\n\t}\n}\n\nfunc (x *cmds) initAllConfig() error {\n\tx.conf = make(map[string]reflect.Value)\n\tvof := reflect.ValueOf(&x.config).Elem()\n\tnum := vof.NumField()\n\tfor i := 0; i < num; i++ {\n\t\tfield := vof.Field(i)\n\t\tfor ptr := true; ptr; {\n\t\t\tif field.Kind() == reflect.Ptr {\n\t\t\t\tfield = field.Elem()\n\t\t\t} else {\n\t\t\t\tptr = false\n\t\t\t}\n\t\t}\n\t\tx.conf[x.getTypePath(field.Type())] = field\n\t\tval := field.Addr().Interface()\n\t\tname := val.(interface{ GetConfigFileName() string }).GetConfigFileName()\n\t\tconfData, err := os.ReadFile(filepath.Join(x.confPath, name))\n\t\tif err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tv := viper.New()\n\t\tv.SetConfigType(\"yaml\")\n\t\tif err := v.ReadConfig(bytes.NewReader(confData)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\topt := func(conf *mapstructure.DecoderConfig) {\n\t\t\tconf.TagName = config.StructTagName\n\t\t}\n\t\tif err := v.Unmarshal(val, opt); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tx.initDiscovery()\n\tx.config.Redis.Disable = false\n\tx.config.LocalCache = config.LocalCache{}\n\tconfig.InitNotification(&x.config.Notification)\n\treturn nil\n}\n\nfunc (x *cmds) parseConf(conf any) error {\n\tvof := reflect.ValueOf(conf)\n\tfor {\n\t\tif vof.Kind() == reflect.Ptr {\n\t\t\tvof = vof.Elem()\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\ttof := vof.Type()\n\tnumField := vof.NumField()\n\tfor i := 0; i < numField; i++ {\n\t\ttypeField := tof.Field(i)\n\t\tif !typeField.IsExported() {\n\t\t\tcontinue\n\t\t}\n\t\tfield := vof.Field(i)\n\t\tpkt := x.getTypePath(field.Type())\n\t\tval, ok := x.conf[pkt]\n\t\tif !ok {\n\t\t\tswitch field.Interface().(type) {\n\t\t\tcase config.Index:\n\t\t\tcase config.Path:\n\t\t\t\tfield.SetString(x.confPath)\n\t\t\tcase config.AllConfig:\n\t\t\t\tfield.Set(reflect.ValueOf(x.config))\n\t\t\tcase *config.AllConfig:\n\t\t\t\tfield.Set(reflect.ValueOf(&x.config))\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"config field %s %s not found\", vof.Type().Name(), typeField.Name)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tfield.Set(val)\n\t}\n\treturn nil\n}\n\nfunc (x *cmds) add(name string, block bool, fn func(ctx context.Context) error) {\n\tx.cmds = append(x.cmds, cmdName{Name: name, Block: block, Func: fn})\n}\n\nfunc (x *cmds) initLog() error {\n\tconf := x.config.Log\n\tif err := log.InitLoggerFromConfig(\n\t\t\"openim-server\",\n\t\tprogram.GetProcessName(),\n\t\t\"\", \"\",\n\t\tconf.RemainLogLevel,\n\t\tconf.IsStdout,\n\t\tconf.IsJson,\n\t\tconf.StorageLocation,\n\t\tconf.RemainRotationCount,\n\t\tconf.RotationTime,\n\t\tstrings.TrimSpace(version.Version),\n\t\tconf.IsSimplify,\n\t); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n\n}\n\nfunc (x *cmds) run(ctx context.Context) error {\n\tif len(x.cmds) == 0 {\n\t\treturn fmt.Errorf(\"no command to run\")\n\t}\n\tif err := x.initAllConfig(); err != nil {\n\t\treturn err\n\t}\n\tif err := x.initLog(); err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithCancelCause(ctx)\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tlog.ZError(ctx, \"context server exit cause\", context.Cause(ctx))\n\t}()\n\n\tif prometheus := x.config.API.Prometheus; prometheus.Enable {\n\t\tvar (\n\t\t\tport int\n\t\t\terr  error\n\t\t)\n\t\tif !prometheus.AutoSetPorts {\n\t\t\tport, err = datautil.GetElemByIndex(prometheus.Ports, 0)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tlistener, err := net.Listen(\"tcp\", fmt.Sprintf(\":%d\", port))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"prometheus listen %d error %w\", port, err)\n\t\t}\n\t\tdefer listener.Close()\n\t\tlog.ZDebug(ctx, \"prometheus start\", \"addr\", listener.Addr())\n\t\tgo func() {\n\t\t\terr := prommetrics.Start(listener)\n\t\t\tif err == nil {\n\t\t\t\terr = fmt.Errorf(\"http done\")\n\t\t\t}\n\t\t\tcancel(fmt.Errorf(\"prometheus %w\", err))\n\t\t}()\n\t}\n\n\tgo func() {\n\t\tsigs := make(chan os.Signal, 1)\n\t\tsignal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase val := <-sigs:\n\t\t\tlog.ZDebug(ctx, \"recv signal\", \"signal\", val.String())\n\t\t\tcancel(fmt.Errorf(\"signal %s\", val.String()))\n\t\t}\n\t}()\n\n\tfor i := range x.cmds {\n\t\tcmd := x.cmds[i]\n\t\tif cmd.Block {\n\t\t\tcontinue\n\t\t}\n\t\tif err := cmd.Func(ctx); err != nil {\n\t\t\tcancel(fmt.Errorf(\"server %s exit %w\", cmd.Name, err))\n\t\t\treturn err\n\t\t}\n\t\tgo func() {\n\t\t\tif cmd.Block {\n\t\t\t\tcancel(fmt.Errorf(\"server %s exit\", cmd.Name))\n\t\t\t}\n\t\t}()\n\t}\n\n\tvar wait cmdManger\n\tfor i := range x.cmds {\n\t\tcmd := x.cmds[i]\n\t\tif !cmd.Block {\n\t\t\tcontinue\n\t\t}\n\t\twait.Start(cmd.Name)\n\t\tgo func() {\n\t\t\tdefer wait.Shutdown(cmd.Name)\n\t\t\tif err := cmd.Func(ctx); err != nil {\n\t\t\t\tcancel(fmt.Errorf(\"server %s exit %w\", cmd.Name, err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcancel(fmt.Errorf(\"server %s exit\", cmd.Name))\n\t\t}()\n\t}\n\t<-ctx.Done()\n\texitCause := context.Cause(ctx)\n\tlog.ZWarn(ctx, \"notification of service closure\", exitCause)\n\tdone := wait.Wait()\n\ttimeout := time.NewTimer(time.Second * 10)\n\tdefer timeout.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-timeout.C:\n\t\t\tlog.ZWarn(ctx, \"server exit timeout\", nil, \"running\", wait.Running())\n\t\t\treturn exitCause\n\t\tcase _, ok := <-done:\n\t\t\tif ok {\n\t\t\t\tlog.ZWarn(ctx, \"waiting for the service to exit\", nil, \"running\", wait.Running())\n\t\t\t} else {\n\t\t\t\tlog.ZInfo(ctx, \"all server exit done\")\n\t\t\t\treturn exitCause\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc putCmd[C any](cmd *cmds, block bool, fn func(ctx context.Context, config *C, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error) {\n\tname := path.Base(runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name())\n\tif index := strings.Index(name, \".\"); index >= 0 {\n\t\tname = name[:index]\n\t}\n\tcmd.add(name, block, func(ctx context.Context) error {\n\t\tvar conf C\n\t\tif err := cmd.parseConf(&conf); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn fn(ctx, &conf, standalone.GetSvcDiscoveryRegistry(), standalone.GetServiceRegistrar())\n\t})\n}\n\ntype cmdManger struct {\n\tlock  sync.Mutex\n\tdone  chan struct{}\n\tcount int\n\tnames map[string]struct{}\n}\n\nfunc (x *cmdManger) Start(name string) {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tif x.names == nil {\n\t\tx.names = make(map[string]struct{})\n\t}\n\tif x.done == nil {\n\t\tx.done = make(chan struct{}, 1)\n\t}\n\tif _, ok := x.names[name]; ok {\n\t\tpanic(fmt.Errorf(\"cmd %s already exists\", name))\n\t}\n\tx.count++\n\tx.names[name] = struct{}{}\n}\n\nfunc (x *cmdManger) Shutdown(name string) {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tif _, ok := x.names[name]; !ok {\n\t\tpanic(fmt.Errorf(\"cmd %s not exists\", name))\n\t}\n\tdelete(x.names, name)\n\tx.count--\n\tif x.count == 0 {\n\t\tclose(x.done)\n\t} else {\n\t\tselect {\n\t\tcase x.done <- struct{}{}:\n\t\tdefault:\n\t\t}\n\t}\n}\n\nfunc (x *cmdManger) Wait() <-chan struct{} {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tif x.count == 0 || x.done == nil {\n\t\ttmp := make(chan struct{})\n\t\tclose(tmp)\n\t\treturn tmp\n\t}\n\treturn x.done\n}\n\nfunc (x *cmdManger) Running() []string {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tnames := make([]string, 0, len(x.names))\n\tfor name := range x.names {\n\t\tnames = append(names, name)\n\t}\n\treturn names\n}\n"
  },
  {
    "path": "cmd/openim-api/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t_ \"net/http/pprof\"\n)\n\nfunc main() {\n\tif err := cmd.NewApiCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-cmdutils/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tmsgUtilsCmd := cmd.NewMsgUtilsCmd(\"openIMCmdUtils\", \"openIM cmd utils\", nil)\n\tgetCmd := cmd.NewGetCmd()\n\tfixCmd := cmd.NewFixCmd()\n\tclearCmd := cmd.NewClearCmd()\n\tseqCmd := cmd.NewSeqCmd()\n\tmsgCmd := cmd.NewMsgCmd()\n\tgetCmd.AddCommand(seqCmd.GetSeqCmd(), msgCmd.GetMsgCmd())\n\tgetCmd.AddSuperGroupIDFlag()\n\tgetCmd.AddUserIDFlag()\n\tgetCmd.AddConfigDirFlag()\n\tgetCmd.AddIndexFlag()\n\tgetCmd.AddBeginSeqFlag()\n\tgetCmd.AddLimitFlag()\n\t// openIM get seq --userID=xxx\n\t// openIM get seq --superGroupID=xxx\n\t// openIM get msg --userID=xxx --beginSeq=100 --limit=10\n\t// openIM get msg --superGroupID=xxx --beginSeq=100 --limit=10\n\n\tfixCmd.AddCommand(seqCmd.FixSeqCmd())\n\tfixCmd.AddSuperGroupIDFlag()\n\tfixCmd.AddUserIDFlag()\n\tfixCmd.AddConfigDirFlag()\n\tfixCmd.AddIndexFlag()\n\tfixCmd.AddFixAllFlag()\n\t// openIM fix seq --userID=xxx\n\t// openIM fix seq --superGroupID=xxx\n\t// openIM fix seq --fixAll\n\n\tclearCmd.AddCommand(msgCmd.ClearMsgCmd())\n\tclearCmd.AddSuperGroupIDFlag()\n\tclearCmd.AddUserIDFlag()\n\tclearCmd.AddConfigDirFlag()\n\tclearCmd.AddIndexFlag()\n\tclearCmd.AddClearAllFlag()\n\tclearCmd.AddBeginSeqFlag()\n\tclearCmd.AddLimitFlag()\n\t// openIM clear msg --userID=xxx --beginSeq=100 --limit=10\n\t// openIM clear msg --superGroupID=xxx --beginSeq=100 --limit=10\n\t// openIM clear msg --clearAll\n\tmsgUtilsCmd.AddCommand(&getCmd.Command, &fixCmd.Command, &clearCmd.Command)\n\tif err := msgUtilsCmd.Execute(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-crontask/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewCronTaskCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-msggateway/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewMsgGatewayCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-msgtransfer/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewMsgTransferCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-push/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewPushRpcCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-rpc/openim-rpc-auth/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewAuthRpcCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-rpc/openim-rpc-conversation/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewConversationRpcCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-rpc/openim-rpc-friend/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewFriendRpcCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-rpc/openim-rpc-group/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewGroupRpcCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-rpc/openim-rpc-msg/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewMsgRpcCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-rpc/openim-rpc-third/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewThirdRpcCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/openim-rpc/openim-rpc-user/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nfunc main() {\n\tif err := cmd.NewUserRpcCmd().Exec(); err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n}\n"
  },
  {
    "path": "config/README.md",
    "content": "# \t\t\t\t\t\tOpenIM Configuration File Descriptions and Common Configuration Modifications\n\n## External Component Configurations\n\n| Configuration File | Description                                                 |\n| ------------------ |-------------------------------------------------------------|\n| **kafka.yml**      | Configuration for Kafka username, password, address, etc.   |\n| **redis.yml**      | Configuration for Redis password, address, etc.             |\n| **minio.yml**      | Configuration for MinIO username, password, address, etc.   |\n| **mongodb.yml**    | Configuration for MongoDB username, password, address, etc. |\n| **discovery.yml**  | Service discovery and etcd credentials and address.         |\n\n## OpenIMServer Related Configurations\n| Configuration File              | Description                                    |\n| ------------------------------- | ---------------------------------------------- |\n| **log.yml**                     | Configuration for logging levels and storage directory                   |\n| **notification.yml**            | Event notification settings (e.g., add friend, create group)           |\n| **share.yml**                   | Common settings for all services (e.g., secrets)            |\n| **webhooks.yml**                | Webhook URLs and related settings                           |\n| **local-cache.yml**             | Local cache settings (generally do not modify)                 |\n| **openim-rpc-third.yml**        | openim-rpc-third listen IP, port, and object storage settings  |\n| **openim-rpc-user.yml**         | openim-rpc-user listen IP and port settings              |\n| **openim-api.yml**              | openim-api listen IP, port, and other settings               |\n| **openim-crontask.yml**         | openim-crontask scheduled task settings                   |\n| **openim-msggateway.yml**       | openim-msggateway listen IP, port, and other settings           |\n| **openim-msgtransfer.yml**      | Settings for openim-msgtransfer service                   |\n| **openim-push.yml**             | openim-push listen IP, port, and offline push settings        |\n| **openim-rpc-auth.yml**         | openim-rpc-auth listen IP, port, token validity settings |\n| **openim-rpc-conversation.yml** | openim-rpc-conversation listen IP and port settings     |\n| **openim-rpc-friend.yml**       | openim-rpc-friend listen IP and port settings           |\n| **openim-rpc-group.yml**        | openim-rpc-group listen IP and port settings           |\n| **openim-rpc-msg.yml**          | openim-rpc-msg listen IP and port settings         |\n\n\n## Monitoring and Alerting Related Configurations\n| Configuration File             | Description     |\n| ------------------------------ | --------------- |\n| **prometheus.yml**             | Prometheus configuration |\n| **instance-down-rules.yml**    | Alert rules       |\n| **alertmanager.yml**           | Alertmanager configuration   |\n| **email.tmpl**                 | Email alert template   |\n| **grefana-template/Demo.json** | Default Grafana dashboard |\n\n## Common Configuration Modifications\n| Configuration Item                                              | Configuration File                |\n| -------------------------------------------------------- | ----------------------- |\n| Configure MinIO as object storage (focus on the externalAddress field) | `minio.yml`             |\n| Adjust log level and number of log files                              | `log.yml`               |\n| Enable or disable friend verification when sending messages                           | `openim-rpc-msg.yml`    |\n| OpenIMServer secret                                         | `share.yml`             |\n| Configure OSS, COS, AWS, or Kodo as object storage               | `openim-rpc-third.yml`  |\n| Multi-end mutual kick strategy and max concurrent connections per gateway                 | `openim-msggateway.yml` |\n| Offline message push configuration                                            | `openim-push.yml`       |\n| Configure webhooks for callback notifications (e.g., before/after message send)         | `webhooks.yml`          |\n| Whether new group members can view historical messages                          | `openim-rpc-group.yml`  |\n| Token expiration time settings                                      | `openim-rpc-auth.yml`     |\n| Scheduled task settings (e.g., how long to retain messages)                      | `openim-crontask.yml`   |\n\n## Starting Multiple Instances of a Service and Maximum File Descriptors\n\n\nTo start multiple instances of an OpenIM service, simply add the corresponding port numbers and modify the `start-config.yml` file in the project’s root directory, \nthen restart the service. For example, to start 2 instances of `openim-rpc-user`:\n\n```yaml\nrpc:\n  registerIP: ''\n  listenIP: 0.0.0.0\n  ports: [ 10110, 10111 ]\n\nprometheus:\n  enable: true\n  ports: [ 20100, 20101 ]\n```\n\nModify`start-config.yml`:\n\n```yaml\nserviceBinaries:\n  openim-rpc-user: 2\n```\n\nTo set the maximum number of open file descriptors (typically one per online user):\n\n```\nmaxFileDescriptors: 10000\n```\n"
  },
  {
    "path": "config/README_zh_CN.md",
    "content": "# \t\t\t\t\t\tOpenIM配置文件说明以及常用配置修改说明\n\n## 外部组件相关配置\n\n| Configuration File | Description                        |\n| ------------------ | ---------------------------------- |\n| **kafka.yml**      | Kafka用户名、密码、地址等配置      |\n| **redis.yml**      | Redis密码、地址等配置              |\n| **minio.yml**      | MinIO用户名、密码、地址等配置      |\n| **mongodb.yml**    | MongoDB用户名、密码、地址等配置    |\n| **discovery.yml**  | 服务发现以及etcd用户名、密码、地址 |\n\n## OpenIMServer相关配置\n| Configuration File              | Description                                    |\n| ------------------------------- | ---------------------------------------------- |\n| **log.yml**                     | 日志级别及存储目录等配置                       |\n| **notification.yml**            | 添加好友、创建群组等事件通知配置               |\n| **share.yml**                   | 各服务所需的公共配置，如secret等               |\n| **webhooks.yml**                | Webhook中URL等配置                             |\n| **local-cache.yml**             | 本地缓存配置，一般不用修改                     |\n| **openim-rpc-third.yml**        | openim-rpc-third监听IP、端口及对象存储配置     |\n| **openim-rpc-user.yml**         | openim-rpc-user监听IP、端口配置                |\n| **openim-api.yml**              | openim-api监听IP、端口等配置                   |\n| **openim-crontask.yml**         | openim-crontask定时任务配置                    |\n| **openim-msggateway.yml**       | openim-msggateway监听IP、端口等配置            |\n| **openim-msgtransfer.yml**      | openim-msgtransfer服务配置                     |\n| **openim-push.yml**             | openim-push监听IP、端口及离线推送配置          |\n| **openim-rpc-auth.yml**         | openim-rpc-auth监听IP、端口及token有效期等配置 |\n| **openim-rpc-conversation.yml** | openim-rpc-conversation监听IP、端口等配置      |\n| **openim-rpc-friend.yml**       | openim-rpc-friend监听IP、端口等配置            |\n| **openim-rpc-group.yml**        | openim-rpc-group监听IP、端口等配置             |\n| **openim-rpc-msg.yml**          | openim-rpc-msg服务的监听IP、端口等配置         |\n\n\n## 监控告警相关配置\n| Configuration File             | Description     |\n| ------------------------------ | --------------- |\n| **prometheus.yml**             | prometheus配置  |\n| **instance-down-rules.yml**    | 告警规则        |\n| **alertmanager.yml**           | 告警管理配置    |\n| **email.tmpl**                 | 邮件告警模版    |\n| **grefana-template/Demo.json** | 默认的dashboard |\n\n## 常用配置修改\n| 修改配置项                                               | 配置文件                |\n| -------------------------------------------------------- | ----------------------- |\n| 使用minio作为对象存储时配置，重点关注externalAddress字段 | `minio.yml`             |\n| 日志级别及日志文件数量调整                               | `log.yml`               |\n| 发送消息是否需要验证好友关系                             | `openim-rpc-msg.yml`    |\n| OpenIMServer秘钥                                         | `share.yml`             |\n| 使用oss, cos, aws, kodo作为对象存储时配置                | `openim-rpc-third.yml`  |\n| 多端互踢策略，单个gateway同时最大连接数                  | `openim-msggateway.yml` |\n| 消息离线推送                                             | `openim-push.yml`       |\n| 配置webhook来通知回调服务器，如消息发送前后回调          | `webhooks.yml`          |\n| 新入群用户是否可以查看历史消息                           | `openim-rpc-group.yml`  |\n| token 过期时间设置                                       | `openim-rpc-auth.yml`     |\n| 定时任务设置，例如消息保存多长时间                       | `openim-crontask.yml`   |\n\n## 启动某个服务的多个实例和最大文件句柄数\n\n\n若要启动某个OpenIM的多个实例，只需增加对应的端口数，并修改项目根目录下的`start-config.yml`文件，重启服务即可生效。例如，启动2个`openim-rpc-user`实例的配置如下：\n\n```yaml\nrpc:\n  registerIP: ''\n  listenIP: 0.0.0.0\n  ports: [ 10110, 10111 ]\n\nprometheus:\n  enable: true\n  ports: [ 20100, 20101 ]\n```\n\n修改`start-config.yml`:\n\n```yaml\nserviceBinaries:\n  openim-rpc-user: 2\n```\n\n修改最大同时打开的文件句柄数，一般是每个在线用户占用一个\n\n```\nmaxFileDescriptors: 10000\n```\n"
  },
  {
    "path": "config/alertmanager.yml",
    "content": "global:\n  resolve_timeout: 5m\n  smtp_from: alert@openim.io\n  smtp_smarthost: smtp.163.com:465\n  smtp_auth_username: alert@openim.io\n  smtp_auth_password: YOURAUTHPASSWORD\n  smtp_require_tls: false\n  smtp_hello: xxx\n\ntemplates:\n  - /etc/alertmanager/email.tmpl\n\nroute:\n  group_by: [ 'alertname' ]\n  group_wait: 5s\n  group_interval: 5s\n  repeat_interval: 5m\n  receiver: email\n  routes:\n    - matchers:\n        - alertname = \"XXX\"\n      group_by: [ 'instance' ]\n      group_wait: 5s\n      group_interval: 5s\n      repeat_interval: 5m\n      receiver: email\n\nreceivers:\n  - name: email\n    email_configs:\n      - to: 'alert@example.com'\n        html: '{{ template \"email.to.html\" . }}'\n        headers: { Subject: \"[OPENIM-SERVER]Alarm\" }\n        send_resolved: true\n"
  },
  {
    "path": "config/discovery.yml",
    "content": "enable: etcd\netcd:\n  rootDirectory: openim\n  address: [localhost:12379]\n  ## Attention: If you set auth in etcd\n  ## you must also update the username and password in Chat project.\n  username:\n  password:\n\nkubernetes:\n  namespace: default\n\nrpcService:\n  user: user-rpc-service\n  friend: friend-rpc-service\n  msg: msg-rpc-service\n  push: push-rpc-service\n  messageGateway: messagegateway-rpc-service\n  group: group-rpc-service\n  auth: auth-rpc-service\n  conversation: conversation-rpc-service\n  third: third-rpc-service\n"
  },
  {
    "path": "config/email.tmpl",
    "content": "{{ define \"email.to.html\" }}\n{{ if eq .Status \"firing\" }}\n    {{ range .Alerts }}\n    <!-- Begin of OpenIM Alert -->\n    <div style=\"border:1px solid #ccc; padding:10px; margin-bottom:10px;\">\n        <h3>OpenIM Alert</h3>\n        <p><strong>Alert Status:</strong> firing</p>\n        <p><strong>Alert Program:</strong> Prometheus Alert</p>\n        <p><strong>Severity Level:</strong> {{ .Labels.severity }}</p>\n        <p><strong>Alert Type:</strong> {{ .Labels.alertname }}</p>\n        <p><strong>Affected Host:</strong> {{ .Labels.instance }}</p>\n        <p><strong>Affected Service:</strong> {{ .Labels.job }}</p>\n        <p><strong>Alert Subject:</strong> {{ .Annotations.summary }}</p>\n        <p><strong>Trigger Time:</strong> {{ .StartsAt.Format \"2006-01-02 15:04:05\" }}</p>\n    </div>\n    {{ end }}\n\n\n{{ else if eq .Status \"resolved\" }}\n    {{ range .Alerts }}\n    <!-- Begin of OpenIM Alert -->\n    <div style=\"border:1px solid #ccc; padding:10px; margin-bottom:10px;\">\n        <h3>OpenIM Alert</h3>\n        <p><strong>Alert Status:</strong> resolved</p>\n        <p><strong>Alert Program:</strong> Prometheus Alert</p>\n        <p><strong>Severity Level:</strong> {{ .Labels.severity }}</p>\n        <p><strong>Alert Type:</strong> {{ .Labels.alertname }}</p>\n        <p><strong>Affected Host:</strong> {{ .Labels.instance }}</p>\n        <p><strong>Affected Service:</strong> {{ .Labels.job }}</p>\n        <p><strong>Alert Subject:</strong> {{ .Annotations.summary }}</p>\n        <p><strong>Trigger Time:</strong> {{ .StartsAt.Format \"2006-01-02 15:04:05\" }}</p>\n    </div>\n    {{ end }}\n<!-- End of OpenIM Alert -->\n{{ end }}\n{{ end }}\n"
  },
  {
    "path": "config/grafana-template/Demo.json",
    "content": "{\n  \"__inputs\": [\n    {\n      \"name\": \"DS_PROMETHEUS\",\n      \"label\": \"prometheus\",\n      \"description\": \"\",\n      \"type\": \"datasource\",\n      \"pluginId\": \"prometheus\",\n      \"pluginName\": \"Prometheus\"\n    }\n  ],\n  \"__elements\": {},\n  \"__requires\": [\n    {\n      \"type\": \"grafana\",\n      \"id\": \"grafana\",\n      \"name\": \"Grafana\",\n      \"version\": \"11.0.1\"\n    },\n    {\n      \"type\": \"datasource\",\n      \"id\": \"prometheus\",\n      \"name\": \"Prometheus\",\n      \"version\": \"1.0.0\"\n    },\n    {\n      \"type\": \"panel\",\n      \"id\": \"timeseries\",\n      \"name\": \"Time series\",\n      \"version\": \"\"\n    }\n  ],\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": {\n          \"type\": \"grafana\",\n          \"uid\": \"-- Grafana --\"\n        },\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"fiscalYearStartMonth\": 0,\n  \"graphTooltip\": 0,\n  \"id\": null,\n  \"links\": [],\n  \"liveNow\": false,\n  \"panels\": [\n    {\n      \"collapsed\": false,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 35,\n      \"panels\": [],\n      \"title\": \"Server\",\n      \"type\": \"row\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"Is the service up.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"stepBefore\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 2,\n            \"pointSize\": 9,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"bool_on_off\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 12,\n        \"x\": 6,\n        \"y\": 1\n      },\n      \"id\": 1,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"up\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"$legendName\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"UP\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of online users and login users within the time frame.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"online users\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"#37bbff\",\n                  \"mode\": \"fixed\",\n                  \"seriesBy\": \"last\"\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 12\n      },\n      \"id\": 37,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"online_user_num\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"online users\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"increase(user_login_total[$time])\",\n          \"hide\": false,\n          \"instant\": false,\n          \"legendFormat\": \"login num\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Login Information\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of register users within the time frame.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"register users\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"#7437ff\",\n                  \"mode\": \"fixed\",\n                  \"seriesBy\": \"last\"\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 12\n      },\n      \"id\": 59,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"user_register_total\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"register users\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Register num\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of chat msg success.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 23\n      },\n      \"id\": 38,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"increase(single_chat_msg_process_success_total[$time])\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"single msgs\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"increase(group_chat_msg_process_success_total[$time])\",\n          \"hide\": false,\n          \"instant\": false,\n          \"legendFormat\": \"group msgs\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Chat Msg Success Num\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of chat msg failed .\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"single msgs\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"#ff00dc\",\n                  \"mode\": \"fixed\",\n                  \"seriesBy\": \"last\"\n                }\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"group msgs\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"#0cffef\",\n                  \"mode\": \"fixed\"\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 23\n      },\n      \"id\": 39,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"increase(single_chat_msg_process_failed_total[$time])\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"single msgs\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"increase(group_chat_msg_process_failed_total[$time])\",\n          \"hide\": false,\n          \"instant\": false,\n          \"legendFormat\": \"group msgs\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Chat Msg Failed Num\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of msg failed offline pushed.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed msgs\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"dark-red\",\n                  \"mode\": \"fixed\",\n                  \"seriesBy\": \"last\"\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 8,\n        \"x\": 0,\n        \"y\": 33\n      },\n      \"id\": 42,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"increase(msg_offline_push_failed_total[$time])\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"addr:{{instance}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Msg Offline Push Failed Num\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of failed set seq.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed msgs\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"semi-dark-green\",\n                  \"mode\": \"fixed\",\n                  \"seriesBy\": \"last\"\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 8,\n        \"x\": 8,\n        \"y\": 33\n      },\n      \"id\": 43,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"increase(seq_set_failed_total[$time])\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"addr: {{instance}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Seq Set Failed Num\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of messages that take a long time to send.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"failed msgs\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"color\",\n                \"value\": {\n                  \"fixedColor\": \"dark-red\",\n                  \"mode\": \"fixed\",\n                  \"seriesBy\": \"last\"\n                }\n              }\n            ]\n          }\n        ]\n      },\n      \"gridPos\": {\n        \"h\": 11,\n        \"w\": 8,\n        \"x\": 16,\n        \"y\": 33\n      },\n      \"id\": 60,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"msg_long_time_push_total\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"addr:{{instance}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        }\n      ],\n      \"title\": \"Long Time Send Msg Total\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of successfully inserted messages.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 12,\n        \"x\": 0,\n        \"y\": 44\n      },\n      \"id\": 44,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"increase(msg_insert_redis_success_total[$time])\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"redis: {{instance}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"increase(msg_insert_mongo_success_total[$time])\",\n          \"hide\": false,\n          \"instant\": false,\n          \"legendFormat\": \"mongo: {{instance}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Msg Success Insert Num\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"datasource\": {\n        \"type\": \"prometheus\",\n        \"uid\": \"${DS_PROMETHEUS}\"\n      },\n      \"description\": \"This metric represents the number of failed insertion messages.\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"color\": {\n            \"mode\": \"palette-classic\"\n          },\n          \"custom\": {\n            \"axisBorderShow\": false,\n            \"axisCenteredZero\": false,\n            \"axisColorMode\": \"text\",\n            \"axisLabel\": \"\",\n            \"axisPlacement\": \"auto\",\n            \"barAlignment\": 0,\n            \"drawStyle\": \"line\",\n            \"fillOpacity\": 0,\n            \"gradientMode\": \"none\",\n            \"hideFrom\": {\n              \"legend\": false,\n              \"tooltip\": false,\n              \"viz\": false\n            },\n            \"insertNulls\": false,\n            \"lineInterpolation\": \"linear\",\n            \"lineStyle\": {\n              \"fill\": \"solid\"\n            },\n            \"lineWidth\": 1,\n            \"pointSize\": 5,\n            \"scaleDistribution\": {\n              \"type\": \"linear\"\n            },\n            \"showPoints\": \"auto\",\n            \"spanNulls\": false,\n            \"stacking\": {\n              \"group\": \"A\",\n              \"mode\": \"none\"\n            },\n            \"thresholdsStyle\": {\n              \"mode\": \"off\"\n            }\n          },\n          \"fieldMinMax\": false,\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          },\n          \"unit\": \"none\"\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 10,\n        \"w\": 12,\n        \"x\": 12,\n        \"y\": 44\n      },\n      \"id\": 45,\n      \"options\": {\n        \"legend\": {\n          \"calcs\": [],\n          \"displayMode\": \"list\",\n          \"placement\": \"bottom\",\n          \"showLegend\": true\n        },\n        \"tooltip\": {\n          \"maxHeight\": 600,\n          \"mode\": \"single\",\n          \"sort\": \"none\"\n        }\n      },\n      \"targets\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"exemplar\": false,\n          \"expr\": \"increase(msg_insert_redis_failed_total[$time])\",\n          \"format\": \"time_series\",\n          \"hide\": false,\n          \"instant\": false,\n          \"interval\": \"\",\n          \"legendFormat\": \"redis: {{instance}}\",\n          \"range\": true,\n          \"refId\": \"A\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"editorMode\": \"code\",\n          \"expr\": \"increase(msg_insert_mongo_failed_total[$time])\",\n          \"hide\": false,\n          \"instant\": false,\n          \"legendFormat\": \"mongo: {{instance}}\",\n          \"range\": true,\n          \"refId\": \"B\"\n        }\n      ],\n      \"title\": \"Msg Failed Insert Num\",\n      \"type\": \"timeseries\"\n    },\n    {\n      \"collapsed\": true,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 54\n      },\n      \"id\": 22,\n      \"panels\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of call of all API.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 9,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 13\n          },\n          \"id\": 29,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path) (api_count)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"API Requests Total\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of call of all API within the time frame.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": [\n              {\n                \"__systemRef\": \"hideSeriesFrom\",\n                \"matcher\": {\n                  \"id\": \"byNames\",\n                  \"options\": {\n                    \"mode\": \"exclude\",\n                    \"names\": [\n                      \"/friend/get_friend_list\"\n                    ],\n                    \"prefix\": \"All except:\",\n                    \"readOnly\": true\n                  }\n                },\n                \"properties\": [\n                  {\n                    \"id\": \"custom.hideFrom\",\n                    \"value\": {\n                      \"legend\": false,\n                      \"tooltip\": false,\n                      \"viz\": true\n                    }\n                  }\n                ]\n              }\n            ]\n          },\n          \"gridPos\": {\n            \"h\": 9,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 13\n          },\n          \"id\": 48,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path) (increase(api_count[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"API Requests Num\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of err return of API.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 14,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 22\n          },\n          \"id\": 24,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path) (api_count{code != \\\"0\\\"})\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"API Error Total\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of err return of API with err code.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 14,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 22\n          },\n          \"id\": 23,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path, code) (api_count{code != \\\"0\\\"})\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{path}}: code={{code}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"API Error Total With Code\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the qps of API.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"reqps\"\n            },\n            \"overrides\": [\n              {\n                \"matcher\": {\n                  \"id\": \"byName\",\n                  \"options\": \"Value\"\n                },\n                \"properties\": [\n                  {\n                    \"id\": \"color\",\n                    \"value\": {\n                      \"fixedColor\": \"#1ed9d4\",\n                      \"mode\": \"fixed\"\n                    }\n                  }\n                ]\n              }\n            ]\n          },\n          \"gridPos\": {\n            \"h\": 9,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 36\n          },\n          \"id\": 51,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum(rate(api_count[1m]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"qps\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"API QPS\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of err return of API within the time frame.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 12,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 45\n          },\n          \"id\": 49,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path) (increase(api_count{code != \\\"0\\\"}[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"API Error Num\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of err return of API with err code within the time frame..\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 12,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 45\n          },\n          \"id\": 50,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path, code) (increase(api_count{code != \\\"0\\\"}[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{path}}: code={{code}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"API Error Num With Code\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"title\": \"API\",\n      \"type\": \"row\"\n    },\n    {\n      \"collapsed\": true,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 55\n      },\n      \"id\": 28,\n      \"panels\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of call of all RPC.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 10,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 14\n          },\n          \"id\": 21,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path) (rpc_count)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"RPC Total Count\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the error return of RPC.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 10,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 24\n          },\n          \"id\": 31,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path) (rpc_count{code!=\\\"0\\\"})\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"RPC Error Count\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the error return of RPC with code.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 10,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 24\n          },\n          \"id\": 33,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path, code) (rpc_count{code!=\\\"0\\\"})\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{path}}: code={{code}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"RPC Error Count With Code\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of call of all RPC within the time frame.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 9,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 34\n          },\n          \"id\": 52,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path) (increase(rpc_count[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"RPC Total Num\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of RPC calls within the time frame, aggregated by name.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 13,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 43\n          },\n          \"id\": 30,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (name) (increase(rpc_count[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"RPC Num by Name\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of call of RPC within the time frame, aggregated by address.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 13,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 43\n          },\n          \"id\": 32,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (instance) (increase(rpc_count[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"RPC Num by Address\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the error return of RPC within the time frame within the time frame.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 10,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 56\n          },\n          \"id\": 54,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path) (increase(rpc_count{code!=\\\"0\\\"}[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"__auto\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"RPC Error Num\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the error return of RPC with code within the time frame within the time frame.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 10,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 56\n          },\n          \"id\": 53,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (path, code) (increase(rpc_count{code!=\\\"0\\\"}[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{path}}: code={{code}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"RPC Error Num With Code\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"title\": \"RPC\",\n      \"type\": \"row\"\n    },\n    {\n      \"collapsed\": true,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 56\n      },\n      \"id\": 25,\n      \"panels\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of HTTP requests.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 15\n          },\n          \"id\": 27,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (method, path) (http_count)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{method}}: {{path}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"HTTP Total Count\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of HTTP requests with status.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 15\n          },\n          \"id\": 26,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (method, path, status) (http_count)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{method}}: {{path}}: {{status}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"HTTP Total Count With Status\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of HTTP requests within the time frame.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 26\n          },\n          \"id\": 55,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (method, path) (increase(http_count[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{method}}: {{path}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"HTTP Total Num\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of HTTP requests with status within the time frame.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 26\n          },\n          \"id\": 56,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"right\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum by (method, path, status) (increase(http_count[$time]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{method}}: {{path}}: {{status}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"HTTP Total Num With Status\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the qps of HTTP.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"reqps\"\n            },\n            \"overrides\": [\n              {\n                \"matcher\": {\n                  \"id\": \"byName\",\n                  \"options\": \"Value\"\n                },\n                \"properties\": [\n                  {\n                    \"id\": \"color\",\n                    \"value\": {\n                      \"fixedColor\": \"#1ed9d4\",\n                      \"mode\": \"fixed\"\n                    }\n                  }\n                ]\n              }\n            ]\n          },\n          \"gridPos\": {\n            \"h\": 9,\n            \"w\": 24,\n            \"x\": 0,\n            \"y\": 37\n          },\n          \"id\": 57,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"sum(rate(http_count[1m]))\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"qps\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"HTTP QPS\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"title\": \"HTTP\",\n      \"type\": \"row\"\n    },\n    {\n      \"collapsed\": true,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 57\n      },\n      \"id\": 6,\n      \"panels\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the proportion of CPU runtime within 1 second. It is calculated as the average CPU runtime over 1 minute.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"percent\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 5\n          },\n          \"id\": 5,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"10.3.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  rate(process_cpu_seconds_total{job=~\\\"$rpcNameFilter\\\"}[1m])*100,\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}}: {{instance}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"CPU Usage Percentage\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the proportion of CPU runtime within 1 second. It is calculated as the average CPU runtime over 1 minute.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"percent\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 5\n          },\n          \"id\": 4,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"10.3.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  rate(process_cpu_seconds_total{job!~\\\"$rpcNameFilter\\\"}[1m])*100,\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}}: {{instance}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"CPU Usage Percentage\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of open file descriptors.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 16\n          },\n          \"id\": 7,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"10.3.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  process_open_fds{job=~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}}: {{instance}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Open File Descriptors\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of open file descriptors.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 16\n          },\n          \"id\": 8,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"10.3.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  process_open_fds{job!~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}}: {{instance}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Open File Descriptors\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of process virtual memory bytes.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 27\n          },\n          \"id\": 9,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"10.3.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  process_virtual_memory_bytes{job=~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}}: {{instance}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Virtual Memory bytes\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of process virtual memory bytes.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\",\n                    \"value\": null\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 27\n          },\n          \"id\": 10,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"10.3.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  process_virtual_memory_bytes{job!~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}}: {{instance}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Virtual Memory bytes\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of process resident memory bytes.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 38\n          },\n          \"id\": 11,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"10.3.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  process_resident_memory_bytes{job=~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}}: {{instance}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Resident Memory bytes\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of process resident memory bytes.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 38\n          },\n          \"id\": 12,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"pluginVersion\": \"10.3.7\",\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  process_resident_memory_bytes{job!~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"{{job}}: {{instance}}\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Resident Memory bytes\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"title\": \"Process\",\n      \"type\": \"row\"\n    },\n    {\n      \"collapsed\": true,\n      \"gridPos\": {\n        \"h\": 1,\n        \"w\": 24,\n        \"x\": 0,\n        \"y\": 58\n      },\n      \"id\": 3,\n      \"panels\": [\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"Measures the frequency of garbage collection operations in the Go environment, averaged over the last five minutes.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"s\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 6\n          },\n          \"id\": 58,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  rate(go_gc_duration_seconds_count{job=~\\\"$rpcNameFilter\\\"}[5m]),\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"GC Rate Per Second\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"Measures the frequency of garbage collection operations in the Go environment, averaged over the last five minutes.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"s\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 6\n          },\n          \"id\": 2,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"expr\": \"label_replace(\\r\\n  rate(go_gc_duration_seconds_count{job!~\\\"$rpcNameFilter\\\"}[5m]),\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"hide\": false,\n              \"instant\": false,\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"GC Rate Per Second\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of goroutines.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 17\n          },\n          \"id\": 13,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  go_goroutines{job=~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Goroutines\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of goroutines.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"none\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 17\n          },\n          \"id\": 14,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  go_goroutines{job!~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Goroutines\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of bytes allocated and still in use.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 28\n          },\n          \"id\": 15,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  go_memstats_alloc_bytes{job=~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Go Alloc Bytes \",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of bytes allocated and still in use.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 28\n          },\n          \"id\": 16,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  go_memstats_alloc_bytes{job!~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Go Alloc Bytes \",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of bytes used by the profiling bucket hash table.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 39\n          },\n          \"id\": 17,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  go_memstats_buck_hash_sys_bytes{job=~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Go Buck Hash Sys Bytes \",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of bytes used by the profiling bucket hash table.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 39\n          },\n          \"id\": 18,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  go_memstats_buck_hash_sys_bytes{job!~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Go Buck Hash Sys Bytes \",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of bytes in use by mcache structures.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 0,\n            \"y\": 50\n          },\n          \"id\": 19,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  go_memstats_mcache_inuse_bytes{job=~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Go Mcache Bytes\",\n          \"type\": \"timeseries\"\n        },\n        {\n          \"datasource\": {\n            \"type\": \"prometheus\",\n            \"uid\": \"${DS_PROMETHEUS}\"\n          },\n          \"description\": \"This metric represents the number of bytes in use by mcache structures.\",\n          \"fieldConfig\": {\n            \"defaults\": {\n              \"color\": {\n                \"mode\": \"palette-classic\"\n              },\n              \"custom\": {\n                \"axisBorderShow\": false,\n                \"axisCenteredZero\": false,\n                \"axisColorMode\": \"text\",\n                \"axisLabel\": \"\",\n                \"axisPlacement\": \"auto\",\n                \"barAlignment\": 0,\n                \"drawStyle\": \"line\",\n                \"fillOpacity\": 0,\n                \"gradientMode\": \"none\",\n                \"hideFrom\": {\n                  \"legend\": false,\n                  \"tooltip\": false,\n                  \"viz\": false\n                },\n                \"insertNulls\": false,\n                \"lineInterpolation\": \"linear\",\n                \"lineStyle\": {\n                  \"fill\": \"solid\"\n                },\n                \"lineWidth\": 1,\n                \"pointSize\": 5,\n                \"scaleDistribution\": {\n                  \"type\": \"linear\"\n                },\n                \"showPoints\": \"auto\",\n                \"spanNulls\": false,\n                \"stacking\": {\n                  \"group\": \"A\",\n                  \"mode\": \"none\"\n                },\n                \"thresholdsStyle\": {\n                  \"mode\": \"off\"\n                }\n              },\n              \"fieldMinMax\": false,\n              \"mappings\": [],\n              \"thresholds\": {\n                \"mode\": \"absolute\",\n                \"steps\": [\n                  {\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"color\": \"red\",\n                    \"value\": 80\n                  }\n                ]\n              },\n              \"unit\": \"bytes\"\n            },\n            \"overrides\": []\n          },\n          \"gridPos\": {\n            \"h\": 11,\n            \"w\": 12,\n            \"x\": 12,\n            \"y\": 50\n          },\n          \"id\": 20,\n          \"options\": {\n            \"legend\": {\n              \"calcs\": [],\n              \"displayMode\": \"list\",\n              \"placement\": \"bottom\",\n              \"showLegend\": true\n            },\n            \"tooltip\": {\n              \"maxHeight\": 600,\n              \"mode\": \"single\",\n              \"sort\": \"none\"\n            }\n          },\n          \"targets\": [\n            {\n              \"datasource\": {\n                \"type\": \"prometheus\",\n                \"uid\": \"${DS_PROMETHEUS}\"\n              },\n              \"editorMode\": \"code\",\n              \"exemplar\": false,\n              \"expr\": \"label_replace(\\r\\n  go_memstats_mcache_inuse_bytes{job!~\\\"$rpcNameFilter\\\"},\\r\\n  \\\"job\\\",\\r\\n  \\\"$1\\\",\\r\\n  \\\"job\\\",\\r\\n  \\\".*openim-(.*)\\\"\\r\\n)\",\n              \"format\": \"time_series\",\n              \"hide\": false,\n              \"instant\": false,\n              \"interval\": \"\",\n              \"legendFormat\": \"$legendName\",\n              \"range\": true,\n              \"refId\": \"A\"\n            }\n          ],\n          \"title\": \"Go Mcache Bytes\",\n          \"type\": \"timeseries\"\n        }\n      ],\n      \"title\": \"GO infomation\",\n      \"type\": \"row\"\n    }\n  ],\n  \"refresh\": \"5s\",\n  \"schemaVersion\": 39,\n  \"tags\": [],\n  \"templating\": {\n    \"list\": [\n      {\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"openimserver-openim-rpc.*\",\n          \"value\": \"openimserver-openim-rpc.*\"\n        },\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"filter\",\n        \"multi\": false,\n        \"name\": \"rpcNameFilter\",\n        \"options\": [\n          {\n            \"selected\": true,\n            \"text\": \"openimserver-openim-rpc.*\",\n            \"value\": \"openimserver-openim-rpc.*\"\n          }\n        ],\n        \"query\": \"openimserver-openim-rpc.*\",\n        \"queryValue\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"custom\"\n      },\n      {\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"{{job}}: {{instance}}\",\n          \"value\": \"{{job}}: {{instance}}\"\n        },\n        \"description\": \"common legend name\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"legend\",\n        \"multi\": false,\n        \"name\": \"legendName\",\n        \"options\": [\n          {\n            \"selected\": true,\n            \"text\": \"{{job}}: {{instance}}\",\n            \"value\": \"{{job}}: {{instance}}\"\n          }\n        ],\n        \"query\": \"{{job}}: {{instance}}\",\n        \"queryValue\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"custom\"\n      },\n      {\n        \"current\": {\n          \"selected\": false,\n          \"text\": \"5m\",\n          \"value\": \"5m\"\n        },\n        \"description\": \"Global promQL time range.\",\n        \"hide\": 0,\n        \"includeAll\": false,\n        \"label\": \"time\",\n        \"multi\": false,\n        \"name\": \"time\",\n        \"options\": [\n          {\n            \"selected\": false,\n            \"text\": \"1m\",\n            \"value\": \"1m\"\n          },\n          {\n            \"selected\": true,\n            \"text\": \"5m\",\n            \"value\": \"5m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"30m\",\n            \"value\": \"30m\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1h\",\n            \"value\": \"1h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"3h\",\n            \"value\": \"3h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"6h\",\n            \"value\": \"6h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"12h\",\n            \"value\": \"12h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"24h\",\n            \"value\": \"24h\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1w\",\n            \"value\": \"1w\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"4w\",\n            \"value\": \"4w\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"12w\",\n            \"value\": \"12w\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"24w\",\n            \"value\": \"24w\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"1y\",\n            \"value\": \"1y\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"2y\",\n            \"value\": \"2y\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"4y\",\n            \"value\": \"4y\"\n          },\n          {\n            \"selected\": false,\n            \"text\": \"10y\",\n            \"value\": \"10y\"\n          }\n        ],\n        \"query\": \"1m,5m,30m,1h,3h,6h,12h,24h,1w,4w,12w,24w,1y,2y,4y,10y\",\n        \"queryValue\": \"\",\n        \"skipUrlSync\": false,\n        \"type\": \"custom\"\n      }\n    ]\n  },\n  \"time\": {\n    \"from\": \"now-15m\",\n    \"to\": \"now\"\n  },\n  \"timeRangeUpdatedDuringEditOrView\": false,\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"Demo\",\n  \"uid\": \"a506d250-b606-4702-86a7-ac6aa1d069a1\",\n  \"version\": 2,\n  \"weekStart\": \"\"\n}"
  },
  {
    "path": "config/instance-down-rules.yml",
    "content": "groups:\n  - name: instance_down\n    rules:\n      - alert: InstanceDown\n        expr: up == 0\n        for: 1m\n        labels:\n          severity: critical\n        annotations:\n          summary: \"Instance {{ $labels.instance }} down\"\n          description: \"{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minutes.\"\n\n  - name: database_insert_failure_alerts\n    rules:\n      - alert: DatabaseInsertFailed\n        expr: (increase(msg_insert_redis_failed_total[5m]) > 0) or (increase(msg_insert_mongo_failed_total[5m]) > 0)\n        for: 1m\n        labels:\n          severity: critical\n        annotations:\n          summary: \"Increase in MsgInsertRedisFailedCounter or MsgInsertMongoFailedCounter detected\"\n          description: \"Either MsgInsertRedisFailedCounter or MsgInsertMongoFailedCounter has increased in the last 5 minutes, indicating failures in message insert operations to Redis or MongoDB,maybe the redis or mongodb is crash.\"\n\n  - name: registrations_few\n    rules:\n      - alert: RegistrationsFew\n        expr: increase(user_login_total[1h]) == 0\n        for: 1m\n        labels:\n          severity: info\n        annotations:\n          summary: \"Too few registrations within the time frame\"\n          description: \"The number of registrations in the last hour is 0. There might be some issues.\"\n\n  - name: messages_few\n    rules:\n      - alert: MessagesFew\n        expr: (increase(single_chat_msg_process_success_total[1h])+increase(group_chat_msg_process_success_total[1h])) == 0\n        for: 1m\n        labels:\n          severity: info\n        annotations:\n          summary: \"Too few messages within the time frame\"\n          description: \"The number of messages sent in the last hour is 0. There might be some issues.\"\n"
  },
  {
    "path": "config/kafka.yml",
    "content": "## Kafka authentication\nusername:\npassword:\n\n# Producer acknowledgment settings\nproducerAck:\n# Compression type to use (e.g., none, gzip, snappy)\ncompressType: none\n# List of Kafka broker addresses\naddress: [localhost:19094]\n# Kafka topic for Redis integration\ntoRedisTopic: toRedis\n# Kafka topic for MongoDB integration\ntoMongoTopic: toMongo\n# Kafka topic for push notifications\ntoPushTopic: toPush\n# Kafka topic for offline push notifications\ntoOfflinePushTopic: toOfflinePush\n# Consumer group ID for Redis topic\ntoRedisGroupID: redis\n# Consumer group ID for MongoDB topic\ntoMongoGroupID: mongo\n# Consumer group ID for push notifications topic\ntoPushGroupID: push\n# Consumer group ID for offline push notifications topic\ntoOfflinePushGroupID: offlinePush\n# TLS (Transport Layer Security) configuration\ntls:\n  # Enable or disable TLS\n  enableTLS: false\n  # CA certificate file path\n  caCrt:\n  # Client certificate file path\n  clientCrt:\n  # Client key file path\n  clientKey:\n  # Client key password\n  clientKeyPwd:\n  # Whether to skip TLS verification (not recommended for production)\n  insecureSkipVerify: false\n"
  },
  {
    "path": "config/local-cache.yml",
    "content": "auth:\n  topic: DELETE_CACHE_AUTH\n  slotNum: 100\n  slotSize: 2000\n  successExpire: 300\n  failedExpire: 5\n\nuser:\n  topic: DELETE_CACHE_USER\n  slotNum: 100\n  slotSize: 2000\n  successExpire: 300\n  failedExpire: 5\n\ngroup:\n  topic: DELETE_CACHE_GROUP\n  slotNum: 100\n  slotSize: 2000\n  successExpire: 300\n  failedExpire: 5\n\nfriend:\n  topic: DELETE_CACHE_FRIEND\n  slotNum: 100\n  slotSize: 2000\n  successExpire: 300\n  failedExpire: 5\n\nconversation:\n  topic: DELETE_CACHE_CONVERSATION\n  slotNum: 100\n  slotSize: 2000\n  successExpire: 300\n  failedExpire: 5\n"
  },
  {
    "path": "config/log.yml",
    "content": "# Log storage path, default is acceptable, change to a full path if modification is needed\nstorageLocation: ../../../../logs/\n# Log rotation period (in hours), default is acceptable\nrotationTime: 24\n# Number of log files to retain, default is acceptable\nremainRotationCount: 2\n# Log level settings: 3 for production environment; 6 for more verbose logging in debugging environments\nremainLogLevel: 6\n# Whether to output to standard output, default is acceptable\nisStdout: false\n# Whether to log in JSON format, default is acceptable\nisJson: false\n# output simplify log when KeyAndValues's value len is bigger than 50 in rpc method log\nisSimplify: true"
  },
  {
    "path": "config/minio.yml",
    "content": "# Name of the bucket in MinIO\nbucket: openim\n# Access key ID for MinIO authentication\naccessKeyID: root\n# Secret access key for MinIO authentication\nsecretAccessKey: openIM123\n# Session token for MinIO authentication (optional)\nsessionToken: \n# Internal address of the MinIO server\ninternalAddress: localhost:10005\n# External address of the MinIO server, accessible from outside. Supports both HTTP and HTTPS using a domain name\nexternalAddress: http://external_ip:10005\n# Flag to enable or disable public read access to the bucket\npublicRead: false\n\n\n"
  },
  {
    "path": "config/mongodb.yml",
    "content": "# URI for database connection, leave empty if using address and credential settings directly\nuri:\n# List of MongoDB server addresses\naddress: [localhost:37017]\n# Name of the database\ndatabase: openim_v3\n# Username for database authentication\nusername: openIM\n# Password for database authentication\npassword: openIM123\n# Authentication source for database authentication, if use root user, set it to admin\nauthSource: openim_v3\n# Maximum number of connections in the connection pool\nmaxPoolSize: 100\n# Maximum number of retry attempts for a failed database connection\nmaxRetry: 10\n# MongoDB Mode, including \"standalone\", \"replicaSet\"\nmongoMode: \"standalone\"\n\n# The following configurations only take effect when mongoMode is set to \"replicaSet\"\nreplicaSet:\n  name: rs0\n  hosts: [127.0.0.1:37017, 127.0.0.1:37018, 127.0.0.1:37019]\n  # Read concern level: \"local\", \"available\", \"majority\", \"linearizable\", \"snapshot\"\n  readConcern: majority\n  # maximum staleness of data in seconds\n  maxStaleness: 90s\n\n# The following configurations only take effect when mongoMode is set to \"replicaSet\"\nreadPreference:\n  # Read preference mode, can be \"primary\", \"primaryPreferred\", \"secondary\", \"secondaryPreferred\", \"nearest\"\n  mode: primary\n  maxStaleness: 90s\n  # TagSets is an array of maps with priority based on order, empty map must be placed last for fallback tagSets\n  tagSets:\n    - datacenter: \"cn-east\"\n      rack: \"1\"\n      storage: \"ssd\"\n    - datacenter: \"cn-east\"\n      storage: \"ssd\"\n    - datacenter: \"cn-east\"\n    - {} # Empty map, indicates any node\n\n# The following configurations only take effect when mongoMode is set to \"replicaSet\"\nwriteConcern:\n  # Write node count or tag (int, \"majority\", or custom tag)\n  w: majority\n  # Whether to wait for journal confirmation\n  j: true\n  # Write timeout duration\n  wtimeout: 30s\n"
  },
  {
    "path": "config/notification.yml",
    "content": "groupCreated:\n  isSendMsg: true\n# Deprecated. Fixed as 1.\n  reliabilityLevel: 1\n# Deprecated. Fixed as false.\n  unreadCount: false\n# Configuration for offline push notifications.\n  offlinePush:\n    # Enables or disables offline push notifications.\n    enable: false\n    # Title for the notification when a group is created.\n    title: create group title\n    # Description for the notification.\n    desc: create group desc\n    # Additional information for the notification.\n    ext: create group ext\n\ngroupInfoSet:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupInfoSet title\n    desc: groupInfoSet desc\n    ext: groupInfoSet ext\n\n\njoinGroupApplication:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: true\n    title: joinGroupApplication title\n    desc: joinGroupApplication desc\n    ext: joinGroupApplication ext\n\nmemberQuit:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: memberQuit title\n    desc: memberQuit desc\n    ext: memberQuit ext\n\ngroupApplicationAccepted:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: true\n    title: groupApplicationAccepted title\n    desc: groupApplicationAccepted desc\n    ext: groupApplicationAccepted ext\n\ngroupApplicationRejected:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: true\n    title: groupApplicationRejected title\n    desc: groupApplicationRejected desc\n    ext: groupApplicationRejected ext\n\n\ngroupOwnerTransferred:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupOwnerTransferred title\n    desc: groupOwnerTransferred desc\n    ext: groupOwnerTransferred ext\n\nmemberKicked:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: memberKicked title\n    desc: memberKicked desc\n    ext: memberKicked ext\n\nmemberInvited:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: memberInvited title\n    desc: memberInvited desc\n    ext: memberInvited ext\n\nmemberEnter:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: memberEnter title\n    desc: memberEnter desc\n    ext: memberEnter ext\n\ngroupDismissed:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupDismissed title\n    desc: groupDismissed desc\n    ext: groupDismissed ext\n\ngroupMuted:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupMuted title\n    desc: groupMuted desc\n    ext: groupMuted ext\n\ngroupCancelMuted:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupCancelMuted title\n    desc: groupCancelMuted desc\n    ext: groupCancelMuted ext\n  defaultTips:\n    tips: group Cancel Muted\n\n\ngroupMemberMuted:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupMemberMuted title\n    desc: groupMemberMuted desc\n    ext: groupMemberMuted ext\n\ngroupMemberCancelMuted:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupMemberCancelMuted title\n    desc: groupMemberCancelMuted desc\n    ext: groupMemberCancelMuted ext\n\ngroupMemberInfoSet:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupMemberInfoSet title\n    desc: groupMemberInfoSet desc\n    ext: groupMemberInfoSet ext\n\ngroupInfoSetAnnouncement:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupInfoSetAnnouncement title\n    desc: groupInfoSetAnnouncement desc\n    ext: groupInfoSetAnnouncement ext\n\n\ngroupInfoSetName:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: groupInfoSetName title\n    desc: groupInfoSetName desc\n    ext: groupInfoSetName ext\n\n\n#############################friend#################################\nfriendApplicationAdded:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: true\n    title: Somebody applies to add you as a friend\n    desc: Somebody applies to add you as a friend\n    ext: Somebody applies to add you as a friend\n\nfriendApplicationApproved:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: true\n    title: Someone applies to add your friend application\n    desc: Someone applies to add your friend application\n    ext: Someone applies to add your friend application\n\nfriendApplicationRejected:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: true\n    title: Someone rejected your friend application\n    desc: Someone rejected your friend application\n    ext: Someone rejected your friend application\n\nfriendAdded:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: We have become friends\n    desc: We have become friends\n    ext: We have become friends\n\nfriendDeleted:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: deleted a friend\n    desc: deleted a friend\n    ext: deleted a friend\n\nfriendRemarkSet:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: Your friend's profile has been changed\n    desc: Your friend's profile has been changed\n    ext: Your friend's profile has been changed\n\nblackAdded:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: blocked a user\n    desc: blocked a user\n    ext: blocked a user\n\nblackDeleted:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: Remove a blocked user\n    desc: Remove a blocked user\n    ext: Remove a blocked user\n\nfriendInfoUpdated:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: friend info updated\n    desc: friend info updated\n    ext: friend info updated\n\n#####################user#########################\nuserInfoUpdated:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: userInfo updated\n    desc: userInfo updated\n    ext: userInfo updated\n\nuserStatusChanged:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: user status changed\n    desc: user status changed\n    ext: user status changed\n\n#####################conversation#########################\nconversationChanged:\n  isSendMsg: false\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: conversation changed\n    desc: conversation changed\n    ext: conversation changed\n\nconversationSetPrivate:\n  isSendMsg: true\n  reliabilityLevel: 1\n  unreadCount: false\n  offlinePush:\n    enable: false\n    title: burn after reading\n    desc: burn after reading\n    ext: burn after reading\n"
  },
  {
    "path": "config/openim-api.yml",
    "content": "api:\n  # Listening IP; 0.0.0.0 means both internal and external IPs are listened to, default is recommended\n  listenIP: 0.0.0.0\n  # Listening ports; if multiple are configured, multiple instances will be launched, must be consistent with the number of prometheus.ports\n  ports: [ 10002 ]\n  # API compression level; 0: default compression, 1: best compression, 2: best speed, -1: no compression\n  compressionLevel: 0\n\n\nprometheus:\n  # Whether to enable prometheus\n  enable: true\n  # autoSetPorts indicates whether to automatically set the ports\n  autoSetPorts: true\n  # Prometheus listening ports, must match the number of api.ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n  # This address can be accessed via a browser\n  grafanaURL:\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n"
  },
  {
    "path": "config/openim-crontask.yml",
    "content": "cronExecuteTime: 0 2 * * *\nretainChatRecords: 365\nfileExpireTime: 180\ndeleteObjectType: [\"msg-picture\",\"msg-file\", \"msg-voice\",\"msg-video\",\"msg-video-snapshot\",\"sdklog\"]"
  },
  {
    "path": "config/openim-msggateway.yml",
    "content": "rpc:\n  # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n  registerIP:\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nprometheus:\n  # Enable or disable Prometheus monitoring\n  enable: true\n  # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n# IP address that the RPC/WebSocket service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\nlistenIP: 0.0.0.0\n\nlongConnSvr:\n  # WebSocket listening ports, must match the number of rpc.ports\n  ports: [ 10001 ]\n  # Maximum number of WebSocket connections\n  websocketMaxConnNum: 100000\n  # Maximum length of the entire WebSocket message packet\n  websocketMaxMsgLen: 4096\n  # WebSocket connection handshake timeout in seconds\n  websocketTimeout: 10\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached"
  },
  {
    "path": "config/openim-msgtransfer.yml",
    "content": "prometheus:\n  # Enable or disable Prometheus monitoring\n  enable: true\n  # autoSetPorts indicates whether to automatically set the ports\n  autoSetPorts: true\n  # List of ports that Prometheus listens on; each port corresponds to an instance of monitoring. Ensure these are managed accordingly\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached"
  },
  {
    "path": "config/openim-push.yml",
    "content": "rpc:\n  # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n  registerIP:\n  # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n  listenIP: 0.0.0.0\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached\n\nprometheus:\n  # Enable or disable Prometheus monitoring\n  enable: false\n  # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nmaxConcurrentWorkers: 3\n#Use geTui for offline push notifications, or choose fcm or jpns; corresponding configuration settings must be specified.\nenable:\ngetui:\n  pushUrl: https://restapi.getui.com/v2/$appId\n  masterSecret:\n  appKey:\n  intent:\n  channelID:\n  channelName:\nfcm:\n  # Prioritize using file paths. If the file path is empty, use URL\n  filePath:   # File path is concatenated with the parameters passed in through - c(`mage` default pass in `config/`) and filePath.\n  authURL:   #  Must start with https or http.\njpush:\n  appKey:\n  masterSecret:\n  pushURL:\n  pushIntent:\n\n# iOS system push sound and badge count\niosPush:\n  pushSound: xxx\n  badgeCount: true\n  production: false\n\nfullUserCache: true\n"
  },
  {
    "path": "config/openim-rpc-auth.yml",
    "content": "rpc:\n  # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n  registerIP: \n  # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n  listenIP: 0.0.0.0\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nprometheus:\n  # Enable or disable Prometheus monitoring\n  enable: true\n  # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\ntokenPolicy:\n  # Token validity period, in days\n  expire: 90\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached"
  },
  {
    "path": "config/openim-rpc-conversation.yml",
    "content": "rpc:\n  # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n  registerIP: \n  # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n  listenIP: 0.0.0.0\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nprometheus:\n  # Enable or disable Prometheus monitoring\n  enable: true\n  # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached"
  },
  {
    "path": "config/openim-rpc-friend.yml",
    "content": "rpc:\n  # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n  registerIP: \n  # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n  listenIP: 0.0.0.0\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nprometheus:\n  # Enable or disable Prometheus monitoring\n  enable: true\n  # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached"
  },
  {
    "path": "config/openim-rpc-group.yml",
    "content": "rpc:\n  # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n  registerIP:\n  # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n  listenIP: 0.0.0.0\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nprometheus:\n  # Enable or disable Prometheus monitoring\n  enable: true\n  # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\n\nenableHistoryForNewMembers: true\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached"
  },
  {
    "path": "config/openim-rpc-msg.yml",
    "content": "rpc:\n  # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n  registerIP:\n  # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n  listenIP: 0.0.0.0\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nprometheus:\n  # Enable or disable Prometheus monitoring\n  enable: true\n  # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\n\n# Does sending messages require friend verification\nfriendVerify: false\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached"
  },
  {
    "path": "config/openim-rpc-third.yml",
    "content": "rpc:\n  # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n  registerIP: \n  # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n  listenIP: 0.0.0.0\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nprometheus:\n  # Enable or disable Prometheus monitoring\n  enable: true\n  # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached\n\nobject:\n  # Use MinIO as object storage, or set to \"cos\", \"oss\", \"kodo\", \"aws\", while also configuring the corresponding settings\n  enable: minio\n  cos:\n    bucketURL: https://temp-1252357374.cos.ap-chengdu.myqcloud.com\n    secretID: \n    secretKey: \n    sessionToken: \n    publicRead: false\n  oss:\n    endpoint: https://oss-cn-chengdu.aliyuncs.com\n    bucket: demo-9999999\n    bucketURL: https://demo-9999999.oss-cn-chengdu.aliyuncs.com\n    accessKeyID: \n    accessKeySecret: \n    sessionToken: \n    publicRead: false\n  kodo:\n    endpoint: https://s3.cn-south-1.qiniucs.com\n    bucket: testdemo12313\n    bucketURL: http://so2at6d05.hn-bkt.clouddn.com\n    accessKeyID:\n    accessKeySecret:\n    sessionToken: \n    publicRead: false\n  aws:\n    region: ap-southeast-2\n    bucket: testdemo832234\n    accessKeyID:\n    secretAccessKey:\n    sessionToken:\n    publicRead: false"
  },
  {
    "path": "config/openim-rpc-user.yml",
    "content": "rpc:\n  # API or other RPCs can access this RPC through this IP; if left blank, the internal network IP is obtained by default\n  registerIP: \n  # Listening IP; 0.0.0.0 means both internal and external IPs are listened to, if blank, the internal network IP is automatically obtained by default\n  listenIP: 0.0.0.0\n  # autoSetPorts indicates whether to automatically set the ports\n  # if you use in kubernetes, set it to false\n  autoSetPorts: true\n  # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nprometheus:\n  # Whether to enable prometheus\n  enable: true\n  # Prometheus listening ports, must be consistent with the number of rpc.ports\n  # It will only take effect when autoSetPorts is set to false.\n  ports:\n\nratelimiter:\n  # Whether to enable rate limiting\n  enable: false\n  # WindowSize defines time duration per window\n  window: 20s\n  # BucketNum defines bucket number for each window\n  bucket: 500\n  # CPU threshold; valid range 0–1000 (1000 = 100%)\n  cpuThreshold: 850\n\ncircuitBreaker:\n  enable: false\n  window: 5s            # Time window size (seconds)\n  bucket: 100            # Number of buckets\n  success: 0.6          # Success rate threshold (0.6 means 60%)\n  request: 500 # Request threshold; circuit breaker evaluation occurs when reached"
  },
  {
    "path": "config/prometheus.yml",
    "content": "# my global config\nglobal:\n  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\n  # scrape_timeout is set to the global default (10s).\n\n# Alertmanager configuration\nalerting:\n  alertmanagers:\n    - static_configs:\n        - targets: [127.0.0.1:19093]\n\n# Load rules once and periodically evaluate them according to the global evaluation_interval.\nrule_files:\n  - instance-down-rules.yml\n# - first_rules.yml\n# - second_rules.yml\n\n# A scrape configuration containing exactly one endpoint to scrape:\n# Here it's Prometheus itself.\nscrape_configs:\n  # The job name is added as a label \"job=job_name\" to any timeseries scraped from this config.\n  # Monitored information captured by prometheus\n\n  # prometheus fetches application services\n  - job_name: node_exporter\n    static_configs:\n      - targets: [ 127.0.0.1:19100 ]\n\n  - job_name: openimserver-openim-api\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/api\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12002 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-msggateway\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/msg_gateway\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12140 ]\n#        #      - targets: [ 127.0.0.1:12140, 127.0.0.1:12141, 127.0.0.1:12142, 127.0.0.1:12143, 127.0.0.1:12144, 127.0.0.1:12145, 127.0.0.1:12146, 127.0.0.1:12147, 127.0.0.1:12148, 127.0.0.1:12149, 127.0.0.1:12150, 127.0.0.1:12151, 127.0.0.1:12152, 127.0.0.1:12153, 127.0.0.1:12154, 127.0.0.1:12155 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-msgtransfer\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/msg_transfer\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12020, 127.0.0.1:12021, 127.0.0.1:12022, 127.0.0.1:12023, 127.0.0.1:12024, 127.0.0.1:12025, 127.0.0.1:12026, 127.0.0.1:12027 ]\n#        #      - targets: [ 127.0.0.1:12020, 127.0.0.1:12021, 127.0.0.1:12022, 127.0.0.1:12023, 127.0.0.1:12024, 127.0.0.1:12025, 127.0.0.1:12026, 127.0.0.1:12027, 127.0.0.1:12028, 127.0.0.1:12029, 127.0.0.1:12030, 127.0.0.1:12031, 127.0.0.1:12032, 127.0.0.1:12033, 127.0.0.1:12034, 127.0.0.1:12035 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-push\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/push\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12170, 127.0.0.1:12171, 127.0.0.1:12172, 127.0.0.1:12173, 127.0.0.1:12174, 127.0.0.1:12175, 127.0.0.1:12176, 127.0.0.1:12177 ]\n##      - targets: [ 127.0.0.1:12170, 127.0.0.1:12171, 127.0.0.1:12172, 127.0.0.1:12173, 127.0.0.1:12174, 127.0.0.1:12175, 127.0.0.1:12176, 127.0.0.1:12177, 127.0.0.1:12178, 127.0.0.1:12179, 127.0.0.1:12180,  127.0.0.1:12182, 127.0.0.1:12183, 127.0.0.1:12184, 127.0.0.1:12185, 127.0.0.1:12186 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-rpc-auth\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/auth\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12200 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-rpc-conversation\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/conversation\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12220 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-rpc-friend\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/friend\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12240 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-rpc-group\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/group\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12260 ]\n#        labels:\n#          namespace: default.\n\n  - job_name: openimserver-openim-rpc-msg\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/msg\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12280 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-rpc-third\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/third\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12300 ]\n#        labels:\n#          namespace: default\n\n  - job_name: openimserver-openim-rpc-user\n    http_sd_configs:\n      - url: \"http://127.0.0.1:10002/prometheus_discovery/user\"\n#    static_configs:\n#      - targets: [ 127.0.0.1:12320 ]\n#        labels:\n#          namespace: default"
  },
  {
    "path": "config/redis.yml",
    "content": "address: [localhost:16379]\nusername:\npassword: openIM123\n# redis Mode, including \"standalone\",\"cluster\",\"sentinel\"\nredisMode: \"standalone\"\ndb: 0\nmaxRetry: 10\npoolSize: 100\n# Sentinel configuration (only used when redisMode is \"sentinel\")\nsentinelMode:\n  masterName: \"redis-master\"\n  sentinelsAddrs: [\"127.0.0.1:26379\", \"127.0.0.1:26380\", \"127.0.0.1:26381\"]\n  routeByLatency: true\n  routeRandomly: true\n"
  },
  {
    "path": "config/share.yml",
    "content": "secret: openIM123\n\n# imAdminUser: Configuration for instant messaging system administrators\nimAdminUser:\n  # userIDs: List of administrator user IDs.\n  # Each entry here corresponds by index to the matching entry in the nicknames list below.\n  userIDs: [imAdmin]\n  # nicknames: List of administrator display names.\n  # Each entry here corresponds by index to the matching entry in the userIDs list above.\n  nicknames: [superAdmin]\n\n# 1: For Android, iOS, Windows, Mac, and web platforms, only one instance can be online at a time\nmultiLogin:\n  policy: 1\n  # max num of tokens in one end\n  maxNumOneEnd: 30\n\nrpcMaxBodySize:\n  requestMaxBodySize:  8388608\n  responseMaxBodySize: 8388608\n"
  },
  {
    "path": "config/webhooks.yml",
    "content": "url: http://127.0.0.1:10006/callbackExample\nbeforeSendSingleMsg:\n  enable: false\n  timeout: 5\n  failedContinue: true\n  # Only the contentType not in deniedTypes will send the callback.\n  # If not set, all contentType messages will through this filter.\n  deniedTypes: []\nbeforeUpdateUserInfoEx:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterUpdateUserInfoEx:\n  enable: false\n  timeout: 5\nafterSendSingleMsg:\n  enable: false\n  timeout: 5\n  # Only the recvIDs specified in attentionIds will send the callback\n  # if not set, all user messages will be callback\n  attentionIds: []\n  # See beforeSendSingleMsg comment.\n  deniedTypes: []\nbeforeSendGroupMsg:\n  enable: false\n  timeout: 5\n  failedContinue: true\n  # See beforeSendSingleMsg comment.\n  deniedTypes: []\nbeforeMsgModify:\n  enable: false\n  timeout: 5\n  failedContinue: true\n  # See beforeSendSingleMsg comment.\n  deniedTypes: []\nafterSendGroupMsg:\n  enable: false\n  timeout: 5\n  # Only the GroupIDs specified in attentionIds will send the callback\n  # if not set, all user messages will be callback\n  attentionIds: []\n  # See beforeSendSingleMsg comment.\n  deniedTypes: []\nafterMsgSaveDB:\n  enable: false\n  timeout: 5\nafterUserOnline:\n  enable: false\n  timeout: 5\nafterUserOffline:\n  enable: false\n  timeout: 5\nafterUserKickOff:\n  enable: false\n  timeout: 5\nbeforeOfflinePush:\n  enable: false\n  timeout: 5\n  failedContinue: true\nbeforeOnlinePush:\n  enable: false\n  timeout: 5\n  failedContinue: true\nbeforeGroupOnlinePush:\n  enable: false\n  timeout: 5\n  failedContinue: true\nbeforeAddFriend:\n  enable: false\n  timeout: 5\n  failedContinue: true\nbeforeUpdateUserInfo:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterUpdateUserInfo:\n  enable: false\n  timeout: 5\nbeforeCreateGroup:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterCreateGroup:\n  enable: false\n  timeout: 5\nbeforeMemberJoinGroup:\n  enable: false\n  timeout: 5\n  failedContinue: true\nbeforeSetGroupMemberInfo:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterSetGroupMemberInfo:\n  enable: false\n  timeout: 5\nafterQuitGroup:\n  enable: false\n  timeout: 5\nafterKickGroupMember:\n  enable: false\n  timeout: 5\nafterDismissGroup:\n  enable: false\n  timeout: 5\nbeforeApplyJoinGroup:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterGroupMsgRead:\n  enable: false\n  timeout: 5\nafterSingleMsgRead:\n  enable: false\n  timeout: 5\nbeforeUserRegister:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterUserRegister:\n  enable: false\n  timeout: 5\nafterTransferGroupOwner:\n  enable: false\n  timeout: 5\nbeforeSetFriendRemark:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterSetFriendRemark:\n  enable: false\n  timeout: 5\nafterGroupMsgRevoke:\n  enable: false\n  timeout: 5\nafterJoinGroup:\n  enable: false\n  timeout: 5\nbeforeInviteUserToGroup:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterSetGroupInfo:\n  enable: false\n  timeout: 5\nbeforeSetGroupInfo:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterSetGroupInfoEx:\n  enable: false\n  timeout: 5\nbeforeSetGroupInfoEx:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterRevokeMsg:\n  enable: false\n  timeout: 5\nbeforeAddBlack:\n  enable: false\n  timeout: 5\n  failedContinue:\nafterAddFriend:\n  enable: false\n  timeout: 5\nbeforeAddFriendAgree:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterAddFriendAgree:\n  enable: false\n  timeout: 5\nafterDeleteFriend:\n  enable: false\n  timeout: 5\nbeforeImportFriends:\n  enable: false\n  timeout: 5\n  failedContinue: true\nafterImportFriends:\n  enable: false\n  timeout: 5\nafterRemoveBlack:\n  enable: false\n  timeout: 5\nbeforeCreateSingleChatConversations:\n  enable: false\n  timeout: 5\n  failedContinue: false\nafterCreateSingleChatConversations:\n  enable: false\n  timeout: 5\n  failedContinue: false\nbeforeCreateGroupChatConversations:\n  enable: false\n  timeout: 5\n  failedContinue: false\nafterCreateGroupChatConversations:\n  enable: false\n  timeout: 5\n  failedContinue: false\n"
  },
  {
    "path": "deployments/Readme.md",
    "content": "# Kubernetes Deployment\n\n## Resource Requests\n\n- CPU: 2 cores\n- Memory: 4 GiB\n- Disk usage: 20 GiB (on Node)\n\n## Preconditions\n\nensure that you have already deployed the following components:\n\n- Redis\n- MongoDB\n- Kafka\n- MinIO\n\n## Origin Deploy\n\n### Enter the target dir\n\n`cd ./deployments/deploy/`\n\n### Deploy configs and dependencies\n\nUpate your configMap `openim-config.yml`. **You can check the official docs for more details.**\n\nIn `openim-config.yml`, you need modify the following configurations:\n\n**discovery.yml**\n\n- `kubernetes.namespace`: default is `default`, you can change it to your namespace.\n\n**mongodb.yml**\n\n- `address`: set to your already mongodb address or mongo Service name and port in your deployed.\n- `database`: set to your mongodb database name.(Need have a created database.)\n- `authSource`: set to your mongodb authSource. (authSource is specify the database name associated with the user's credentials, user need create in this database.)\n\n**kafka.yml**\n\n- `address`: set to your already kafka address or kafka Service name and port in your deployed.\n\n**redis.yml**\n\n- `address`: set to your already redis address or redis Service name and port in your deployed.\n\n**minio.yml**\n\n- `internalAddress`: set to your minio Service name and port in your deployed.\n- `externalAddress`: set to your already expose minio external address.\n\n### Set the secret\n\nA Secret is an object that contains a small amount of sensitive data. Such as password and secret. Secret is similar to ConfigMaps.\n\n#### Redis:\n\nUpdate the `redis-password` value in `redis-secret.yml` to your Redis password encoded in base64.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-redis-secret\ntype: Opaque\ndata:\n  redis-password: b3BlbklNMTIz # update to your redis password encoded in base64, if need empty, you can set to \"\"\n```\n\n#### Mongo:\n\nUpdate the `mongo_openim_username`, `mongo_openim_password` value in `mongo-secret.yml` to your Mongo username and password encoded in base64.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-mongo-secret\ntype: Opaque\ndata:\n  mongo_openim_username: b3BlbklN # update to your mongo username encoded in base64, if need empty, you can set to \"\" (this user credentials need in authSource database).\n  mongo_openim_password: b3BlbklNMTIz # update to your mongo password encoded in base64, if need empty, you can set to \"\"\n```\n\n#### Minio:\n\nUpdate the `minio-root-user` and `minio-root-password` value in `minio-secret.yml` to your MinIO accessKeyID and secretAccessKey encoded in base64.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-minio-secret\ntype: Opaque\ndata:\n  minio-root-user: cm9vdA== # update to your minio accessKeyID encoded in base64, if need empty, you can set to \"\"\n  minio-root-password: b3BlbklNMTIz # update to your minio secretAccessKey encoded in base64, if need empty, you can set to \"\"\n```\n\n#### Kafka:\n\nUpdate the `kafka-password` value in `kafka-secret.yml` to your Kafka password encoded in base64.\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-kafka-secret\ntype: Opaque\ndata:\n  kafka-password: b3BlbklNMTIz # update to your kafka password encoded in base64, if need empty, you can set to \"\"\n```\n\n### Apply the secret.\n\n```shell\nkubectl apply -f redis-secret.yml -f minio-secret.yml -f mongo-secret.yml -f kafka-secret.yml\n```\n\n### Apply all config\n\n`kubectl apply -f ./openim-config.yml`\n\n> Attation: If you use `default` namespace, you can excute `clusterRile.yml` to create a cluster role binding for default service account.\n>\n> Namespace is modify to `discovery.yml` in `openim-config.yml`, you can change `kubernetes.namespace` to your namespace.\n\n**Excute `clusterRole.yml`**\n\n`kubectl apply -f ./clusterRole.yml`\n\n### run all deployments and services\n\n> Note: Ensure that infrastructure services like MinIO, Redis, and Kafka are running before deploying the main applications.\n\n```bash\nkubectl apply \\\n  -f openim-api-deployment.yml \\\n  -f openim-api-service.yml \\\n  -f openim-crontask-deployment.yml \\\n  -f openim-rpc-user-deployment.yml \\\n  -f openim-rpc-user-service.yml \\\n  -f openim-msggateway-deployment.yml \\\n  -f openim-msggateway-service.yml \\\n  -f openim-push-deployment.yml \\\n  -f openim-push-service.yml \\\n  -f openim-msgtransfer-service.yml \\\n  -f openim-msgtransfer-deployment.yml \\\n  -f openim-rpc-conversation-deployment.yml \\\n  -f openim-rpc-conversation-service.yml \\\n  -f openim-rpc-auth-deployment.yml \\\n  -f openim-rpc-auth-service.yml \\\n  -f openim-rpc-group-deployment.yml \\\n  -f openim-rpc-group-service.yml \\\n  -f openim-rpc-friend-deployment.yml \\\n  -f openim-rpc-friend-service.yml \\\n  -f openim-rpc-msg-deployment.yml \\\n  -f openim-rpc-msg-service.yml \\\n  -f openim-rpc-third-deployment.yml \\\n  -f openim-rpc-third-service.yml\n```\n\n### Verification\n\nAfter deploying the services, verify that everything is running smoothly:\n\n```bash\n# Check the status of all pods\nkubectl get pods\n\n# Check the status of services\nkubectl get svc\n\n# Check the status of deployments\nkubectl get deployments\n\n# View all resources\nkubectl get all\n```\n\n### clean all\n\n`kubectl delete -f ./`\n\n### Notes:\n\n- If you use a specific namespace for your deployment, be sure to append the -n <namespace> flag to your kubectl commands.\n"
  },
  {
    "path": "deployments/deploy/clusterRole.yml",
    "content": "# ClusterRole.yaml\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: service-reader\nrules:\n  - apiGroups: [\"\"]\n    resources: [\"services\", \"endpoints\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n\n---\n# ClusterRoleBinding.yaml\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: default-service-reader-binding\nsubjects:\n  - kind: ServiceAccount\n    name: default\n    namespace: default\nroleRef:\n  kind: ClusterRole\n  name: service-reader\n  apiGroup: rbac.authorization.k8s.io\n"
  },
  {
    "path": "deployments/deploy/ingress.yml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: openim-ingress\n  annotations:\n    nginx.ingress.kubernetes.io/rewrite-target: /\nspec:\n  ingressClassName: openim-nginx\n  rules:\n    - http:\n        paths:\n          - path: /openim-api\n            pathType: Prefix\n            backend:\n              service:\n                name: openim-api-service\n                port:\n                  number: 10002\n          - path: /openim-msggateway\n            pathType: Prefix\n            backend:\n              service:\n                name: openim-msggateway-service\n                port:\n                  number: 10001\n"
  },
  {
    "path": "deployments/deploy/kafka-secret.yml",
    "content": "apiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-kafka-secret\ntype: Opaque\ndata:\n  kafka-password: \"\"\n"
  },
  {
    "path": "deployments/deploy/kafka-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: kafka-service\n  labels:\n    app: kafka\nspec:\n  ports:\n    - name: plaintext\n      port: 9092\n      targetPort: 9092\n    - name: controller\n      port: 9093\n      targetPort: 9093\n    - name: external\n      port: 19094\n      targetPort: 9094\n  selector:\n    app: kafka\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/kafka-statefulset.yml",
    "content": "apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: kafka-statefulset\n  labels:\n    app: kafka\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: kafka\n  serviceName: \"kafka-service\"\n  template:\n    metadata:\n      labels:\n        app: kafka\n    spec:\n      containers:\n        - name: kafka\n          image: bitnami/kafka:3.5.1\n          imagePullPolicy: IfNotPresent\n          resources:\n            limits:\n              memory: \"2Gi\"\n              cpu: \"1000m\"\n            requests:\n              memory: \"1Gi\"\n              cpu: \"500m\"\n          ports:\n            - containerPort: 9092 # PLAINTEXT\n            - containerPort: 9093 # CONTROLLER\n            - containerPort: 9094 # EXTERNAL\n          env:\n            - name: TZ\n              value: \"Asia/Shanghai\"\n            - name: KAFKA_CFG_NODE_ID\n              value: \"0\"\n            - name: KAFKA_CFG_PROCESS_ROLES\n              value: \"controller,broker\"\n            - name: KAFKA_CFG_CONTROLLER_QUORUM_VOTERS\n              value: \"0@kafka-service:9093\"\n            - name: KAFKA_CFG_LISTENERS\n              value: \"PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094\"\n            - name: KAFKA_CFG_ADVERTISED_LISTENERS\n              value: \"PLAINTEXT://kafka-service:9092,EXTERNAL://kafka-service:19094\"\n            - name: KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP\n              value: \"CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT\"\n            - name: KAFKA_CFG_CONTROLLER_LISTENER_NAMES\n              value: \"CONTROLLER\"\n            - name: KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE\n              value: \"true\"\n          volumeMounts:\n            - name: kafka-data\n              mountPath: /bitnami/kafka\n\n      volumes:\n        - name: kafka-data\n          persistentVolumeClaim:\n            claimName: kafka-pvc\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: kafka-pvc\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 10Gi\n"
  },
  {
    "path": "deployments/deploy/minio-secret.yml",
    "content": "apiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-minio-secret\ntype: Opaque\ndata:\n  minio-root-user: cm9vdA== # Base64 encoded \"root\"\n  minio-root-password: b3BlbklNMTIz # Base64 encoded \"openIM123\"\n"
  },
  {
    "path": "deployments/deploy/minio-service.yml",
    "content": "---\napiVersion: v1\nkind: Service\nmetadata:\n  name: minio-service\nspec:\n  selector:\n    app: minio\n  ports:\n    - name: minio\n      protocol: TCP\n      port: 10005 # External port for accessing MinIO service\n      targetPort: 9000 # Container port for MinIO service\n    - name: minio-console\n      protocol: TCP\n      port: 19090 # External port for accessing MinIO console\n      targetPort: 9090 # Container port for MinIO console\n  type: NodePort\n"
  },
  {
    "path": "deployments/deploy/minio-statefulset.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: minio\n  labels:\n    app: minio\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: minio\n  template:\n    metadata:\n      labels:\n        app: minio\n    spec:\n      containers:\n        - name: minio\n          image: minio/minio:RELEASE.2024-01-11T07-46-16Z\n          ports:\n            - containerPort: 9000 # MinIO service port\n            - containerPort: 9090 # MinIO console port\n          volumeMounts:\n            - name: minio-data\n              mountPath: /data\n            - name: minio-config\n              mountPath: /root/.minio\n          env:\n            - name: TZ\n              value: \"Asia/Shanghai\"\n            - name: MINIO_ROOT_USER\n              valueFrom:\n                secretKeyRef:\n                  name: openim-minio-secret\n                  key: minio-root-user\n            - name: MINIO_ROOT_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-minio-secret\n                  key: minio-root-password\n          command:\n            - \"/bin/sh\"\n            - \"-c\"\n            - |\n              mkdir -p /data && \\\n              minio server /data --console-address \":9090\"\n      volumes:\n        - name: minio-data\n          persistentVolumeClaim:\n            claimName: minio-pvc\n        - name: minio-config\n          persistentVolumeClaim:\n            claimName: minio-config-pvc\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: minio-pvc\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 10Gi\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: minio-config-pvc\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 2Gi\n\n\n"
  },
  {
    "path": "deployments/deploy/mongo-secret.yml",
    "content": "apiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-mongo-secret\ntype: Opaque\ndata:\n  mongo_openim_username: b3BlbklN # base64 for \"openIM\", this user credentials need in authSource database.\n  mongo_openim_password: b3BlbklNMTIz # base64 for \"openIM123\"\n"
  },
  {
    "path": "deployments/deploy/mongo-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: mongo-service\nspec:\n  selector:\n    app: mongo\n  ports:\n    - name: mongodb-port\n      protocol: TCP\n      port: 37017\n      targetPort: 27017\n  type: NodePort\n"
  },
  {
    "path": "deployments/deploy/mongo-statefulset.yml",
    "content": "apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: mongo-statefulset\nspec:\n  serviceName: \"mongo\"\n  replicas: 2\n  selector:\n    matchLabels:\n      app: mongo\n  template:\n    metadata:\n      labels:\n        app: mongo\n    spec:\n      containers:\n        - name: mongo\n          image: mongo:7.0\n          command: [\"/bin/bash\", \"-c\"]\n          args:\n            - >\n              docker-entrypoint.sh mongod --wiredTigerCacheSizeGB ${wiredTigerCacheSizeGB} --auth &\n              until mongosh -u ${MONGO_INITDB_ROOT_USERNAME} -p ${MONGO_INITDB_ROOT_PASSWORD} --authenticationDatabase admin --eval \"db.runCommand({ ping: 1 })\" &>/dev/null; do\n                echo \"Waiting for MongoDB to start...\";\n                sleep 1;\n              done &&\n              mongosh -u ${MONGO_INITDB_ROOT_USERNAME} -p ${MONGO_INITDB_ROOT_PASSWORD} --authenticationDatabase admin --eval \"\n              db = db.getSiblingDB(\\\"${MONGO_INITDB_DATABASE}\\\");\n              if (!db.getUser(\\\"${MONGO_OPENIM_USERNAME}\\\")) {\n                db.createUser({\n                  user: \\\"${MONGO_OPENIM_USERNAME}\\\",\n                  pwd: \\\"${MONGO_OPENIM_PASSWORD}\\\",\n                  roles: [{role: \\\"readWrite\\\", db: \\\"${MONGO_INITDB_DATABASE}\\\"}]\n                });\n                print(\\\"User created successfully: \\\");\n                print(\\\"Username: ${MONGO_OPENIM_USERNAME}\\\");\n                print(\\\"Password: ${MONGO_OPENIM_PASSWORD}\\\");\n                print(\\\"Database: ${MONGO_INITDB_DATABASE}\\\");\n              } else {\n                print(\\\"User already exists in database: ${MONGO_INITDB_DATABASE}, Username: ${MONGO_OPENIM_USERNAME}\\\");\n              }\n              \" &&\n              tail -f /dev/null\n          ports:\n            - containerPort: 27017\n          env:\n            - name: MONGO_INITDB_ROOT_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-init-secret\n                  key: mongo_initdb_root_username\n            - name: MONGO_INITDB_ROOT_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-init-secret\n                  key: mongo_initdb_root_password\n            - name: MONGO_INITDB_DATABASE\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-init-secret\n                  key: mongo_initdb_database\n            - name: MONGO_OPENIM_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-init-secret\n                  key: mongo_openim_username\n            - name: MONGO_OPENIM_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-init-secret\n                  key: mongo_openim_password\n            - name: TZ\n              value: \"Asia/Shanghai\"\n            - name: wiredTigerCacheSizeGB\n              value: \"1\"\n          volumeMounts:\n            - name: mongo-storage\n              mountPath: /data/db\n\n      volumes:\n        - name: mongo-storage\n          persistentVolumeClaim:\n            claimName: mongo-pvc\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: mongo-pvc\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 5Gi\n\n---\napiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-mongo-init-secret\ntype: Opaque\ndata:\n  mongo_initdb_root_username: cm9vdA== # base64 for \"root\"\n  mongo_initdb_root_password: b3BlbklNMTIz # base64 for \"openIM123\"\n  mongo_initdb_database: b3BlbmltX3Yz # base64 for \"openim_v3\"\n  mongo_openim_username: b3BlbklN # base64 for \"openIM\"\n  mongo_openim_password: b3BlbklNMTIz # base64 for \"openIM123\"\n"
  },
  {
    "path": "deployments/deploy/openim-api-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: openim-api\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: openim-api\n  template:\n    metadata:\n      labels:\n        app: openim-api\n    spec:\n      containers:\n        - name: openim-api-container\n          image: openim/openim-api:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_MONGODB_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_username\n            - name: IMENV_MONGODB_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_password\n\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10002\n            - containerPort: 12002\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-api-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: openim-api-service\nspec:\n  selector:\n    app: openim-api\n  ports:\n    - name: http-10002\n      protocol: TCP\n      port: 10002\n      targetPort: 10002\n    - name: prometheus-12002\n      protocol: TCP\n      port: 12002\n      targetPort: 12002\n  type: NodePort\n"
  },
  {
    "path": "deployments/deploy/openim-config.yml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: openim-config\ndata:\n  discovery.yml: |\n    enable: \"kubernetes\" # \"kubernetes\" or \"etcd\"\n    kubernetes:\n      namespace: default\n    etcd:\n      rootDirectory: openim\n      address: [ localhost:12379 ]\n      username: ''\n      password: ''\n\n    rpcService:\n      user: user-rpc-service\n      friend: friend-rpc-service\n      msg: msg-rpc-service\n      push: push-rpc-service\n      messageGateway: messagegateway-rpc-service\n      group: group-rpc-service\n      auth: auth-rpc-service\n      conversation: conversation-rpc-service\n      third: third-rpc-service\n\n  log.yml: |\n    # Log storage path, default is acceptable, change to a full path if modification is needed\n    storageLocation: ./logs/\n    # Log rotation period (in hours), default is acceptable\n    rotationTime: 24\n    # Number of log files to retain, default is acceptable\n    remainRotationCount: 2\n    # Log level settings: 3 for production environment; 6 for more verbose logging in debugging environments\n    remainLogLevel: 6\n    # Whether to output to standard output, default is acceptable\n    isStdout: true\n    # Whether to log in JSON format, default is acceptable\n    isJson: false\n    # output simplify log when KeyAndValues's value len is bigger than 50 in rpc method log\n    isSimplify: true\n\n  mongodb.yml: |\n    # URI for database connection, leave empty if using address and credential settings directly\n    uri: ''\n    # List of MongoDB server addresses\n    address: [ mongo-service:37017 ]\n    # Name of the database\n    database: openim_v3\n    # Username for database authentication\n    username: ''  # openIM\n    # Password for database authentication\n    password: '' # openIM123\n    # Authentication source for database authentication, if use root user, set it to admin\n    authSource: openim_v3\n    # Maximum number of connections in the connection pool\n    maxPoolSize: 100\n    # Maximum number of retry attempts for a failed database connection\n    maxRetry: 10\n\n  local-cache.yml: |\n    user:\n      topic: DELETE_CACHE_USER\n      slotNum: 100\n      slotSize: 2000\n      successExpire: 300\n      failedExpire: 5\n    group:\n      topic: DELETE_CACHE_GROUP\n      slotNum: 100\n      slotSize: 2000\n      successExpire: 300\n      failedExpire: 5\n    friend:\n      topic: DELETE_CACHE_FRIEND\n      slotNum: 100\n      slotSize: 2000\n      successExpire: 300\n      failedExpire: 5\n    conversation:\n      topic: DELETE_CACHE_CONVERSATION\n      slotNum: 100\n      slotSize: 2000\n      successExpire: 300\n      failedExpire: 5\n\n  openim-api.yml: |\n    api:\n      # Listening IP; 0.0.0.0 means both internal and external IPs are listened to, default is recommended\n      listenIP: 0.0.0.0\n      # Listening ports; if multiple are configured, multiple instances will be launched, must be consistent with the number of prometheus.ports\n      ports: [ 10002 ]\n      # API compression level; 0: default compression, 1: best compression, 2: best speed, -1: no compression\n      compressionLevel: 0\n\n    prometheus:\n      # Whether to enable prometheus\n      enable: true\n      # Prometheus listening ports, must match the number of api.ports\n      ports: [ 12002 ]\n      # This address can be accessed via a browser\n      grafanaURL: http://127.0.0.1:13000/\n\n  openim-rpc-user.yml: |\n    rpc:\n      # API or other RPCs can access this RPC through this IP; if left blank, the internal network IP is obtained by default\n      registerIP:\n      # Listening IP; 0.0.0.0 means both internal and external IPs are listened to, if blank, the internal network IP is automatically obtained by default\n      listenIP: 0.0.0.0\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      # It will only take effect when autoSetPorts is set to false.\n      ports: [ 10320 ]\n    prometheus:\n      # Whether to enable prometheus\n      enable: true\n      # Prometheus listening ports, must be consistent with the number of rpc.ports\n      ports: [ 12320 ]\n\n  openim-crontask.yml: |\n    cronExecuteTime: 0 2 * * *\n    retainChatRecords: 365\n    fileExpireTime: 180\n    deleteObjectType: [\"msg-picture\",\"msg-file\", \"msg-voice\",\"msg-video\",\"msg-video-snapshot\",\"sdklog\"]\n\n  openim-msggateway.yml: |\n    rpc:\n      # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n      registerIP:\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      # It will only take effect when autoSetPorts is set to false.\n      ports: [ 10140 ]\n\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n      ports: [ 12140 ]\n\n    # IP address that the RPC/WebSocket service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n    listenIP: 0.0.0.0\n\n    longConnSvr:\n      # WebSocket listening ports, must match the number of rpc.ports\n      ports: [ 10001 ]\n      # Maximum number of WebSocket connections\n      websocketMaxConnNum: 100000\n      # Maximum length of the entire WebSocket message packet\n      websocketMaxMsgLen: 4096\n      # WebSocket connection handshake timeout in seconds\n      websocketTimeout: 10\n\n  openim-msgtransfer.yml: |\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; each port corresponds to an instance of monitoring. Ensure these are managed accordingly\n      # Because four instances have been launched, four ports need to be specified\n      ports: [ 12020 ]\n\n  openim-push.yml: |\n    rpc:\n      # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n      registerIP:\n      # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n      listenIP: 0.0.0.0\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      # It will only take effect when autoSetPorts is set to false.\n      ports: [ 10170 ]\n\n\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n      ports: [ 12170 ]\n\n    maxConcurrentWorkers: 3\n    #Use geTui for offline push notifications, or choose fcm or jpns; corresponding configuration settings must be specified.\n    enable:\n    geTui:\n      pushUrl: https://restapi.getui.com/v2/$appId\n      masterSecret:\n      appKey:\n      intent:\n      channelID:\n      channelName:\n    fcm:\n      # Prioritize using file paths. If the file path is empty, use URL\n      filePath:   # File path is concatenated with the parameters passed in through - c(`mage` default pass in `config/`) and filePath.\n      authURL:   #  Must start with https or http.\n    jpush:\n      appKey:\n      masterSecret:\n      pushURL:\n      pushIntent:\n\n    # iOS system push sound and badge count\n    iosPush:\n      pushSound: xxx\n      badgeCount: true\n      production: false\n\n    fullUserCache: true\n\n  openim-rpc-auth.yml: |\n    rpc:\n      # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n      registerIP:\n      # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n      listenIP: 0.0.0.0\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      # It will only take effect when autoSetPorts is set to false.\n      ports: [ 10200 ]\n\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n      ports: [12200]\n\n    tokenPolicy:\n      # Token validity period, in days\n      expire: 90\n\n  openim-rpc-conversation.yml: |\n    rpc:\n      # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n      registerIP:\n      # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n      listenIP: 0.0.0.0\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      # It will only take effect when autoSetPorts is set to false.\n      ports: [ 10220 ]\n\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n      ports: [ 12200 ]\n\n    tokenPolicy:\n      # Token validity period, in days\n      expire: 90\n\n  openim-rpc-friend.yml: |\n    rpc:\n      # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n      registerIP:\n      # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n      listenIP: 0.0.0.0\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      # It will only take effect when autoSetPorts is set to false.\n      ports: [ 10240 ]\n\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n      ports: [ 12240 ]\n\n  openim-rpc-group.yml: |\n    rpc:\n      # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n      registerIP:\n      # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n      listenIP: 0.0.0.0\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      # It will only take effect when autoSetPorts is set to false.\n      ports: [ 10260 ]\n\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n      ports: [ 12260 ]\n\n    enableHistoryForNewMembers: true\n\n  openim-rpc-msg.yml: |\n    rpc:\n      # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n      registerIP:\n      # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n      listenIP: 0.0.0.0\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      ports: [ 10280 ]\n\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n      ports: [ 12280 ]\n\n\n    # Does sending messages require friend verification\n    friendVerify: false\n\n  openim-rpc-third.yml: |\n    rpc:\n      # The IP address where this RPC service registers itself; if left blank, it defaults to the internal network IP\n      registerIP:\n      # IP address that the RPC service listens on; setting to 0.0.0.0 listens on both internal and external IPs. If left blank, it automatically uses the internal network IP\n      listenIP: 0.0.0.0\n      # autoSetPorts indicates whether to automatically set the ports\n      # if you use in kubernetes, set it to false\n      autoSetPorts: false\n      # List of ports that the RPC service listens on; configuring multiple ports will launch multiple instances. These must match the number of configured prometheus ports\n      # It will only take effect when autoSetPorts is set to false.\n      ports: [ 10300 ]\n\n    prometheus:\n      # Enable or disable Prometheus monitoring\n      enable: true\n      # List of ports that Prometheus listens on; these must match the number of rpc.ports to ensure correct monitoring setup\n      ports: [ 12300 ]\n\n\n    object:\n      # Use MinIO as object storage, or set to \"cos\", \"oss\", \"kodo\", \"aws\", while also configuring the corresponding settings\n      enable: minio\n      cos:\n        bucketURL: https://temp-1252357374.cos.ap-chengdu.myqcloud.com\n        secretID:\n        secretKey:\n        sessionToken:\n        publicRead: false\n      oss:\n        endpoint: https://oss-cn-chengdu.aliyuncs.com\n        bucket: demo-9999999\n        bucketURL: https://demo-9999999.oss-cn-chengdu.aliyuncs.com\n        accessKeyID:\n        accessKeySecret:\n        sessionToken:\n        publicRead: false\n      kodo:\n        endpoint: http://s3.cn-south-1.qiniucs.com\n        bucket: kodo-bucket-test\n        bucketURL: http://kodo-bucket-test-oetobfb.qiniudns.com\n        accessKeyID:\n        accessKeySecret:\n        sessionToken:\n        publicRead: false\n      aws:\n        region: ap-southeast-2\n        bucket: testdemo832234\n        accessKeyID:\n        secretAccessKey:\n        sessionToken:\n        publicRead: false\n\n  share.yml: |\n    secret: openIM123\n\n    imAdminUserID: [\"imAdmin\"]\n\n    # 1: For Android, iOS, Windows, Mac, and web platforms, only one instance can be online at a time\n    multiLogin:\n      policy: 1\n      maxNumOneEnd: 30\n\n  kafka.yml: |\n    # Username for authentication\n    username: ''\n    # Password for authentication\n    password: ''\n    # Producer acknowledgment settings\n    producerAck:\n    # Compression type to use (e.g., none, gzip, snappy)\n    compressType: none\n    # List of Kafka broker addresses\n    address: [ \"kafka-service:19094\" ]\n    # Kafka topic for Redis integration\n    toRedisTopic: toRedis\n    # Kafka topic for MongoDB integration\n    toMongoTopic: toMongo\n    # Kafka topic for push notifications\n    toPushTopic: toPush\n    # Kafka topic for offline push notifications\n    toOfflinePushTopic: toOfflinePush\n    # Consumer group ID for Redis topic\n    toRedisGroupID: redis\n    # Consumer group ID for MongoDB topic\n    toMongoGroupID: mongo\n    # Consumer group ID for push notifications topic\n    toPushGroupID: push\n    # Consumer group ID for offline push notifications topic\n    toOfflinePushGroupID: offlinePush\n    # TLS (Transport Layer Security) configuration\n    tls:\n      # Enable or disable TLS\n      enableTLS: false\n      # CA certificate file path\n      caCrt:\n      # Client certificate file path\n      clientCrt:\n      # Client key file path\n      clientKey:\n      # Client key password\n      clientKeyPwd:\n      # Whether to skip TLS verification (not recommended for production)\n      insecureSkipVerify: false\n\n  redis.yml: |\n    address: [ \"redis-service:16379\" ]\n    username:\n    password:  # openIM123\n    clusterMode: false\n    db: 0\n    maxRetry: 10\n    poolSize: 100\n\n  minio.yml: |\n    # Name of the bucket in MinIO\n    bucket: openim\n    # Access key ID for MinIO authentication\n    accessKeyID: root   \n    # Secret access key for MinIO authentication\n    secretAccessKey:    # openIM123\n    # Session token for MinIO authentication (optional)\n    sessionToken:\n    # Internal address of the MinIO server\n    internalAddress: minio-service:10005\n    # External address of the MinIO server, accessible from outside. Supports both HTTP and HTTPS using a domain name\n    externalAddress: http://minio-service:10005\n    # Flag to enable or disable public read access to the bucket\n    publicRead: \"false\"\n\n  notification.yml: |\n    groupCreated:\n      isSendMsg: true\n    # Reliability level of the message sending.\n    # Set to 1 to send only when online, 2 for guaranteed delivery.\n      reliabilityLevel: 1\n    # This setting is effective only when 'isSendMsg' is true.\n    # It controls whether to count unread messages.\n      unreadCount: false\n    # Configuration for offline push notifications.\n      offlinePush:\n        # Enables or disables offline push notifications.\n        enable: false\n        # Title for the notification when a group is created.\n        title: create group title\n        # Description for the notification.\n        desc: create group desc\n        # Additional information for the notification.\n        ext: create group ext\n\n    groupInfoSet:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupInfoSet title\n        desc: groupInfoSet desc\n        ext: groupInfoSet ext\n\n    joinGroupApplication:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: joinGroupApplication title\n        desc: joinGroupApplication desc\n        ext: joinGroupApplication ext\n\n    memberQuit:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: memberQuit title\n        desc: memberQuit desc\n        ext: memberQuit ext\n\n    groupApplicationAccepted:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupApplicationAccepted title\n        desc: groupApplicationAccepted desc\n        ext: groupApplicationAccepted ext\n\n    groupApplicationRejected:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupApplicationRejected title\n        desc: groupApplicationRejected desc\n        ext: groupApplicationRejected ext\n\n    groupOwnerTransferred:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupOwnerTransferred title\n        desc: groupOwnerTransferred desc\n        ext: groupOwnerTransferred ext\n\n    memberKicked:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: memberKicked title\n        desc: memberKicked desc\n        ext: memberKicked ext\n\n    memberInvited:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: memberInvited title\n        desc: memberInvited desc\n        ext: memberInvited ext\n\n    memberEnter:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: memberEnter title\n        desc: memberEnter desc\n        ext: memberEnter ext\n\n    groupDismissed:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupDismissed title\n        desc: groupDismissed desc\n        ext: groupDismissed ext\n\n    groupMuted:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupMuted title\n        desc: groupMuted desc\n        ext: groupMuted ext\n\n    groupCancelMuted:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupCancelMuted title\n        desc: groupCancelMuted desc\n        ext: groupCancelMuted ext\n      defaultTips:\n        tips: group Cancel Muted\n\n    groupMemberMuted:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupMemberMuted title\n        desc: groupMemberMuted desc\n        ext: groupMemberMuted ext\n\n    groupMemberCancelMuted:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupMemberCancelMuted title\n        desc: groupMemberCancelMuted desc\n        ext: groupMemberCancelMuted ext\n\n    groupMemberInfoSet:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupMemberInfoSet title\n        desc: groupMemberInfoSet desc\n        ext: groupMemberInfoSet ext\n\n    groupInfoSetAnnouncement:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupInfoSetAnnouncement title\n        desc: groupInfoSetAnnouncement desc\n        ext: groupInfoSetAnnouncement ext\n\n    groupInfoSetName:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: groupInfoSetName title\n        desc: groupInfoSetName desc\n        ext: groupInfoSetName ext\n\n    #############################friend#################################\n    friendApplicationAdded:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: Somebody applies to add you as a friend\n        desc: Somebody applies to add you as a friend\n        ext: Somebody applies to add you as a friend\n\n    friendApplicationApproved:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: Someone applies to add your friend application\n        desc: Someone applies to add your friend application\n        ext: Someone applies to add your friend application\n\n    friendApplicationRejected:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: Someone rejected your friend application\n        desc: Someone rejected your friend application\n        ext: Someone rejected your friend application\n\n    friendAdded:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: We have become friends\n        desc: We have become friends\n        ext: We have become friends\n\n    friendDeleted:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: deleted a friend\n        desc: deleted a friend\n        ext: deleted a friend\n\n    friendRemarkSet:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: Your friend's profile has been changed\n        desc: Your friend's profile has been changed\n        ext: Your friend's profile has been changed\n\n    blackAdded:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: blocked a user\n        desc: blocked a user\n        ext: blocked a user\n\n    blackDeleted:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: Remove a blocked user\n        desc: Remove a blocked user\n        ext: Remove a blocked user\n\n    friendInfoUpdated:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: friend info updated\n        desc: friend info updated\n        ext: friend info updated\n\n    #####################user#########################\n    userInfoUpdated:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: userInfo updated\n        desc: userInfo updated\n        ext: userInfo updated\n\n    userStatusChanged:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: false\n        title: user status changed\n        desc: user status changed\n        ext: user status changed\n\n    #####################conversation#########################\n    conversationChanged:\n      isSendMsg: false\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: conversation changed\n        desc: conversation changed\n        ext: conversation changed\n\n    conversationSetPrivate:\n      isSendMsg: true\n      reliabilityLevel: 1\n      unreadCount: false\n      offlinePush:\n        enable: true\n        title: burn after reading\n        desc: burn after reading\n        ext: burn after reading\n\n  webhooks.yml: |\n    url: http://127.0.0.1:10006/callbackExample\n    beforeSendSingleMsg:\n      enable: false\n      timeout: 5\n      failedContinue: true\n      # Only the contentType in allowedTypes will send the callback.\n      # Supports two formats: a single type or a range. The range is defined by the lower and upper bounds connected with a hyphen (\"-\").\n      # e.g. allowedTypes: [1, 100, 200-500, 600-700] means that only contentType within the range\n      # {1, 100} ∪ [200, 500] ∪ [600, 700] will be allowed through the filter.\n      # If not set, all contentType messages will through this filter.\n      allowedTypes: []\n      # Only the contentType not in deniedTypes will send the callback.\n      # Supports two formats, same as allowedTypes.\n      # If not set, all contentType messages will through this filter.\n      deniedTypes: []\n    beforeUpdateUserInfoEx:\n      enable:  false\n      timeout: 5\n      failedContinue: true\n    afterUpdateUserInfoEx:\n      enable:  false\n      timeout: 5\n    afterSendSingleMsg:\n      enable: false\n      timeout: 5\n      # Only the senID/recvID specified in attentionIds will send the callback\n      # if not set, all user messages will be callback\n      attentionIds: []\n      # See beforeSendSingleMsg comment.\n      allowedTypes: []\n      deniedTypes: []\n    beforeSendGroupMsg:\n      enable: false\n      timeout: 5\n      failedContinue: true\n      # See beforeSendSingleMsg comment.\n      allowedTypes: []\n      deniedTypes: []\n    beforeMsgModify:\n      enable: false\n      timeout: 5\n      failedContinue: true\n      # See beforeSendSingleMsg comment.\n      allowedTypes: []\n      deniedTypes: []\n    afterSendGroupMsg:\n      enable: false\n      timeout: 5\n      # See beforeSendSingleMsg comment.\n      allowedTypes: []\n      deniedTypes: []\n    afterUserOnline:\n      enable: false\n      timeout: 5\n    afterUserOffline:\n      enable: false\n      timeout: 5\n    afterUserKickOff:\n      enable: false\n      timeout: 5\n    beforeOfflinePush:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    beforeOnlinePush:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    beforeGroupOnlinePush:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    beforeAddFriend:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    beforeUpdateUserInfo:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterUpdateUserInfo:\n      enable: false\n      timeout: 5\n    beforeCreateGroup:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterCreateGroup:\n      enable: false\n      timeout: 5\n    beforeMemberJoinGroup:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    beforeSetGroupMemberInfo:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterSetGroupMemberInfo:\n      enable: false\n      timeout: 5\n    afterQuitGroup:\n      enable: false\n      timeout: 5\n    afterKickGroupMember:\n      enable: false\n      timeout: 5\n    afterDismissGroup:\n      enable: false\n      timeout: 5\n    beforeApplyJoinGroup:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterGroupMsgRead:\n      enable: false\n      timeout: 5\n    afterSingleMsgRead:\n      enable: false\n      timeout: 5\n    beforeUserRegister:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterUserRegister:\n      enable: false\n      timeout: 5\n    afterTransferGroupOwner:\n      enable: false\n      timeout: 5\n    beforeSetFriendRemark:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterSetFriendRemark:\n      enable: false\n      timeout: 5\n    afterGroupMsgRevoke:\n      enable: false\n      timeout: 5\n    afterJoinGroup:\n      enable: false\n      timeout: 5\n    beforeInviteUserToGroup:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterSetGroupInfo:\n      enable: false\n      timeout: 5\n    beforeSetGroupInfo:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterSetGroupInfoEx:\n      enable: false\n      timeout: 5\n    beforeSetGroupInfoEx:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterRevokeMsg:\n      enable: false\n      timeout: 5\n    beforeAddBlack:\n      enable: false\n      timeout: 5\n      failedContinue:\n    afterAddFriend:\n      enable: false\n      timeout: 5\n    beforeAddFriendAgree:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterAddFriendAgree:\n      enable: false\n      timeout: 5\n    afterDeleteFriend:\n      enable: false\n      timeout: 5\n    beforeImportFriends:\n      enable: false\n      timeout: 5\n      failedContinue: true\n    afterImportFriends:\n      enable: false\n      timeout: 5\n    afterRemoveBlack:\n      enable: false\n      timeout: 5\n\n  prometheus.yml: |\n    # my global config\n    global:\n      scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n      evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\n      # scrape_timeout is set to the global default (10s).\n\n    # Alertmanager configuration\n    alerting:\n      alertmanagers:\n        - static_configs:\n            - targets: [internal_ip:19093]\n\n    # Load rules once and periodically evaluate them according to the global evaluation_interval.\n    rule_files:\n      - instance-down-rules.yml\n    # - first_rules.yml\n    # - second_rules.yml\n\n    # A scrape configuration containing exactly one endpoint to scrape:\n    # Here it's Prometheus itself.\n    scrape_configs:\n      # The job name is added as a label \"job=job_name\" to any timeseries scraped from this config.\n      # Monitored information captured by prometheus\n\n      # prometheus fetches application services\n      - job_name: node_exporter\n        static_configs:\n          - targets: [ internal_ip:20500 ]\n      - job_name: openimserver-openim-api\n        static_configs:\n          - targets: [ internal_ip:12002 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-msggateway\n        static_configs:\n          - targets: [ internal_ip:12140 ]\n    #      - targets: [ internal_ip:12140, internal_ip:12141, internal_ip:12142, internal_ip:12143, internal_ip:12144, internal_ip:12145, internal_ip:12146, internal_ip:12147, internal_ip:12148, internal_ip:12149, internal_ip:12150, internal_ip:12151, internal_ip:12152, internal_ip:12153, internal_ip:12154, internal_ip:12155 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-msgtransfer\n        static_configs:\n          - targets: [ internal_ip:12020, internal_ip:12021, internal_ip:12022, internal_ip:12023, internal_ip:12024, internal_ip:12025, internal_ip:12026, internal_ip:12027 ]\n    #      - targets: [ internal_ip:12020, internal_ip:12021, internal_ip:12022, internal_ip:12023, internal_ip:12024, internal_ip:12025, internal_ip:12026, internal_ip:12027, internal_ip:12028, internal_ip:12029, internal_ip:12030, internal_ip:12031, internal_ip:12032, internal_ip:12033, internal_ip:12034, internal_ip:12035 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-push\n        static_configs:\n          - targets: [ internal_ip:12170, internal_ip:12171, internal_ip:12172, internal_ip:12173, internal_ip:12174, internal_ip:12175, internal_ip:12176, internal_ip:12177 ]\n    #      - targets: [ internal_ip:12170, internal_ip:12171, internal_ip:12172, internal_ip:12173, internal_ip:12174, internal_ip:12175, internal_ip:12176, internal_ip:12177, internal_ip:12178, internal_ip:12179, internal_ip:12180,  internal_ip:12182, internal_ip:12183, internal_ip:12184, internal_ip:12185, internal_ip:12186 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-rpc-auth\n        static_configs:\n          - targets: [ internal_ip:12200 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-rpc-conversation\n        static_configs:\n          - targets: [ internal_ip:12220 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-rpc-friend\n        static_configs:\n          - targets: [ internal_ip:12240 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-rpc-group\n        static_configs:\n          - targets: [ internal_ip:12260 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-rpc-msg\n        static_configs:\n          - targets: [ internal_ip:12280 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-rpc-third\n        static_configs:\n          - targets: [ internal_ip:12300 ]\n            labels:\n              namespace: default\n      - job_name: openimserver-openim-rpc-user\n        static_configs:\n          - targets: [ internal_ip:12320 ]\n            labels:\n              namespace: default\n"
  },
  {
    "path": "deployments/deploy/openim-crontask-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: openim-crontask\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: crontask\n  template:\n    metadata:\n      labels:\n        app: crontask\n    spec:\n      containers:\n        - name: crontask-container\n          image: openim/openim-crontask:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-msggateway-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: messagegateway-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: messagegateway-rpc-server\n  template:\n    metadata:\n      labels:\n        app: messagegateway-rpc-server\n    spec:\n      containers:\n        - name: openim-msggateway-container\n          image: openim/openim-msggateway:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10140\n            - containerPort: 12001\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-msggateway-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: messagegateway-rpc-service\nspec:\n  selector:\n    app: messagegateway-rpc-server\n  ports:\n    - name: longConnServer-10001\n      protocol: TCP\n      port: 10001\n      targetPort: 10001\n    - name: grpc-10140\n      protocol: TCP\n      port: 10140\n      targetPort: 10140\n    - name: prometheus-12001\n      protocol: TCP\n      port: 12001\n      targetPort: 12001\n  type: NodePort\n"
  },
  {
    "path": "deployments/deploy/openim-msgtransfer-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: openim-msgtransfer-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: openim-msgtransfer-server\n  template:\n    metadata:\n      labels:\n        app: openim-msgtransfer-server\n    spec:\n      containers:\n        - name: openim-msgtransfer-container\n          image: openim/openim-msgtransfer:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_MONGODB_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_username\n            - name: IMENV_MONGODB_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_password\n            - name: IMENV_KAFKA_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-kafka-secret\n                  key: kafka-password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 12020\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-msgtransfer-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: openim-msgtransfer-service\nspec:\n  selector:\n    app: openim-msgtransfer-server\n  ports:\n    - name: prometheus-12020\n      protocol: TCP\n      port: 12020\n      targetPort: 12020\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/openim-push-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: push-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: push-rpc-server\n  template:\n    metadata:\n      labels:\n        app: push-rpc-server\n    spec:\n      containers:\n        - name: push-rpc-server-container\n          image: openim/openim-push:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_KAFKA_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-kafka-secret\n                  key: kafka-password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10170\n            - containerPort: 12170\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-push-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: push-rpc-service\nspec:\n  selector:\n    app: push-rpc-server\n  ports:\n    - name: http-10170\n      protocol: TCP\n      port: 10170\n      targetPort: 10170\n    - name: prometheus-12170\n      protocol: TCP\n      port: 12170\n      targetPort: 12170\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-auth-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: auth-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: auth-rpc-server\n  template:\n    metadata:\n      labels:\n        app: auth-rpc-server\n    spec:\n      containers:\n        - name: auth-rpc-server-container\n          image: openim/openim-rpc-auth:v3.8.3\n          imagePullPolicy: Never\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10200\n            - containerPort: 12200\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-auth-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: auth-rpc-service\nspec:\n  selector:\n    app: auth-rpc-server\n  ports:\n    - name: http-10200\n      protocol: TCP\n      port: 10200\n      targetPort: 10200\n    - name: prometheus-12200\n      protocol: TCP\n      port: 12200\n      targetPort: 12200\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-conversation-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: conversation-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: conversation-rpc-server\n  template:\n    metadata:\n      labels:\n        app: conversation-rpc-server\n    spec:\n      containers:\n        - name: conversation-rpc-server-container\n          image: openim/openim-rpc-conversation:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_MONGODB_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_username\n            - name: IMENV_MONGODB_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10220\n            - containerPort: 12220\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-conversation-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: conversation-rpc-service\nspec:\n  selector:\n    app: conversation-rpc-server\n  ports:\n    - name: http-10220\n      protocol: TCP\n      port: 10220\n      targetPort: 10220\n    - name: prometheus-12220\n      protocol: TCP\n      port: 12220\n      targetPort: 12220\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-friend-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: friend-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: friend-rpc-server\n  template:\n    metadata:\n      labels:\n        app: friend-rpc-server\n    spec:\n      containers:\n        - name: friend-rpc-server-container\n          image: openim/openim-rpc-friend:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_MONGODB_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_username\n            - name: IMENV_MONGODB_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10240\n            - containerPort: 12240\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-friend-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: friend-rpc-service\nspec:\n  selector:\n    app: friend-rpc-server\n  ports:\n    - name: http-10240\n      protocol: TCP\n      port: 10240\n      targetPort: 10240\n    - name: prometheus-12240\n      protocol: TCP\n      port: 12240\n      targetPort: 12240\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-group-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: group-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: group-rpc-server\n  template:\n    metadata:\n      labels:\n        app: group-rpc-server\n    spec:\n      containers:\n        - name: group-rpc-server-container\n          image: openim/openim-rpc-group:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_MONGODB_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_username\n            - name: IMENV_MONGODB_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10260\n            - containerPort: 12260\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-group-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: group-rpc-service\nspec:\n  selector:\n    app: group-rpc-server\n  ports:\n    - name: http-10260\n      protocol: TCP\n      port: 10260\n      targetPort: 10260\n    - name: prometheus-12260\n      protocol: TCP\n      port: 12260\n      targetPort: 12260\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-msg-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: msg-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: msg-rpc-server\n  template:\n    metadata:\n      labels:\n        app: msg-rpc-server\n    spec:\n      containers:\n        - name: msg-rpc-server-container\n          image: openim/openim-rpc-msg:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_MONGODB_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_username\n            - name: IMENV_MONGODB_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_password\n            - name: IMENV_KAFKA_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-kafka-secret\n                  key: kafka-password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10280\n            - containerPort: 12280\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-msg-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: msg-rpc-service\nspec:\n  selector:\n    app: msg-rpc-server\n  ports:\n    - name: http-10280\n      protocol: TCP\n      port: 10280\n      targetPort: 10280\n    - name: prometheus-12280\n      protocol: TCP\n      port: 12280\n      targetPort: 12280\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-third-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: third-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: third-rpc-server\n  template:\n    metadata:\n      labels:\n        app: third-rpc-server\n    spec:\n      containers:\n        - name: third-rpc-server-container\n          image: openim/openim-rpc-third:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_MINIO_ACCESSKEYID\n              valueFrom:\n                secretKeyRef:\n                  name: openim-minio-secret\n                  key: minio-root-user\n            - name: IMENV_MINIO_SECRETACCESSKEY\n              valueFrom:\n                secretKeyRef:\n                  name: openim-minio-secret\n                  key: minio-root-password\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_MONGODB_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_username\n            - name: IMENV_MONGODB_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10300\n            - containerPort: 12300\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-third-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: third-rpc-service\nspec:\n  selector:\n    app: third-rpc-server\n  ports:\n    - name: http-10300\n      protocol: TCP\n      port: 10300\n      targetPort: 10300\n    - name: prometheus-12300\n      protocol: TCP\n      port: 12300\n      targetPort: 12300\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-user-deployment.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: user-rpc-server\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: user-rpc-server\n  template:\n    metadata:\n      labels:\n        app: user-rpc-server\n    spec:\n      containers:\n        - name: user-rpc-server-container\n          image: openim/openim-rpc-user:v3.8.3\n          env:\n            - name: CONFIG_PATH\n              value: \"/config\"\n            - name: IMENV_REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-redis-secret\n                  key: redis-password\n            - name: IMENV_MONGODB_USERNAME\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_username\n            - name: IMENV_MONGODB_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-mongo-secret\n                  key: mongo_openim_password\n            - name: IMENV_KAFKA_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: openim-kafka-secret\n                  key: kafka-password\n          volumeMounts:\n            - name: openim-config\n              mountPath: \"/config\"\n              readOnly: true\n          ports:\n            - containerPort: 10320\n            - containerPort: 12320\n      volumes:\n        - name: openim-config\n          configMap:\n            name: openim-config\n"
  },
  {
    "path": "deployments/deploy/openim-rpc-user-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: user-rpc-service\nspec:\n  selector:\n    app: user-rpc-server\n  ports:\n    - name: http-10320\n      protocol: TCP\n      port: 10320\n      targetPort: 10320\n    - name: prometheus-12320\n      protocol: TCP\n      port: 12320\n      targetPort: 12320\n  type: ClusterIP\n"
  },
  {
    "path": "deployments/deploy/redis-secret.yml",
    "content": "apiVersion: v1\nkind: Secret\nmetadata:\n  name: openim-redis-secret\ntype: Opaque\ndata:\n  redis-password: b3BlbklNMTIz # \"openIM123\" in base64\n"
  },
  {
    "path": "deployments/deploy/redis-service.yml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: redis-service\n  labels:\n    app: redis\nspec:\n  type: ClusterIP\n  selector:\n    app: redis\n  ports:\n    - name: redis-port\n      protocol: TCP\n      port: 16379\n      targetPort: 6379\n"
  },
  {
    "path": "deployments/deploy/redis-statefulset.yml",
    "content": "apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: redis-statefulset\nspec:\n  serviceName: \"redis\"\n  replicas: 2\n  selector:\n    matchLabels:\n      app: redis\n  template:\n    metadata:\n      labels:\n        app: redis\n    spec:\n      containers:\n        - name: redis\n          image: redis:7.0.0\n          ports:\n            - containerPort: 6379\n          env:\n            - name: TZ\n              value: \"Asia/Shanghai\"\n            - name: REDIS_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: redis-secret\n                  key: redis-password\n          volumeMounts:\n            - name: redis-data\n              mountPath: /data\n          command:\n            [\n              \"/bin/sh\",\n              \"-c\",\n              'redis-server  --requirepass \"$REDIS_PASSWORD\" --appendonly yes',\n            ]\n      volumes:\n        - name: redis-config-volume\n          configMap:\n            name: openim-config\n        - name: redis-data\n          persistentVolumeClaim:\n            claimName: redis-pvc\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: redis-pvc\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 5Gi\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "networks:\n  openim:\n    driver: bridge\n\nservices:\n  mongodb:\n    image: \"${MONGO_IMAGE}\"\n    ports:\n      - \"37017:27017\"\n    container_name: mongo\n    command: >\n      bash -c '\n      docker-entrypoint.sh mongod --wiredTigerCacheSizeGB $$wiredTigerCacheSizeGB --auth &\n      until mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD --authenticationDatabase admin --eval \"db.runCommand({ ping: 1 })\" &>/dev/null; do\n        echo \"Waiting for MongoDB to start...\"\n        sleep 1\n      done &&\n      mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD --authenticationDatabase admin --eval \"\n      db = db.getSiblingDB(\\\"$$MONGO_INITDB_DATABASE\\\");\n      if (!db.getUser(\\\"$$MONGO_OPENIM_USERNAME\\\")) {\n        db.createUser({\n          user: \\\"$$MONGO_OPENIM_USERNAME\\\",\n          pwd: \\\"$$MONGO_OPENIM_PASSWORD\\\",\n          roles: [{role: \\\"readWrite\\\", db: \\\"$$MONGO_INITDB_DATABASE\\\"}]\n        });\n        print(\\\"User created successfully: \\\");\n        print(\\\"Username: $$MONGO_OPENIM_USERNAME\\\");\n        print(\\\"Password: $$MONGO_OPENIM_PASSWORD\\\");\n        print(\\\"Database: $$MONGO_INITDB_DATABASE\\\");\n      } else {\n        print(\\\"User already exists in database: $$MONGO_INITDB_DATABASE, Username: $$MONGO_OPENIM_USERNAME\\\");\n      }\n      \" &&\n      tail -f /dev/null\n      '\n    volumes:\n      - \"${DATA_DIR}/components/mongodb/data/db:/data/db\"\n      - \"${DATA_DIR}/components/mongodb/data/logs:/data/logs\"\n      - \"${DATA_DIR}/components/mongodb/data/conf:/etc/mongo\"\n      - \"${MONGO_BACKUP_DIR}:/data/backup\"\n    environment:\n      - TZ=Asia/Shanghai\n      - wiredTigerCacheSizeGB=1\n      - MONGO_INITDB_ROOT_USERNAME=root\n      - MONGO_INITDB_ROOT_PASSWORD=openIM123\n      - MONGO_INITDB_DATABASE=openim_v3\n      - MONGO_OPENIM_USERNAME=openIM\n      - MONGO_OPENIM_PASSWORD=openIM123\n    restart: always\n    networks:\n      - openim\n\n  redis:\n    image: \"${REDIS_IMAGE}\"\n    container_name: redis\n    ports:\n      - \"16379:6379\"\n    volumes:\n      - \"${DATA_DIR}/components/redis/data:/data\"\n      - \"${DATA_DIR}/components/redis/config/redis.conf:/usr/local/redis/config/redis.conf\"\n    environment:\n      TZ: Asia/Shanghai\n    restart: always\n    sysctls:\n      net.core.somaxconn: 1024\n    command: >\n      redis-server\n      --requirepass openIM123\n      --appendonly yes\n      --aof-use-rdb-preamble yes\n      --save \"\"\n    networks:\n      - openim\n\n  etcd:\n    image: \"${ETCD_IMAGE}\"\n    container_name: etcd\n    ports:\n      - \"12379:2379\"\n      - \"12380:2380\"\n    environment:\n      - ETCD_NAME=s1\n      - ETCD_DATA_DIR=/etcd-data\n      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379\n      - ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379\n      - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380\n      - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://0.0.0.0:2380\n      - ETCD_INITIAL_CLUSTER=s1=http://0.0.0.0:2380\n      - ETCD_INITIAL_CLUSTER_TOKEN=tkn\n      - ETCD_INITIAL_CLUSTER_STATE=new\n      - ALLOW_NONE_AUTHENTICATION=no\n\n      ## Optional: Enable etcd authentication by setting the following credentials\n      # - ETCD_ROOT_USER=root\n      # - ETCD_ROOT_PASSWORD=openIM123\n      # - ETCD_USERNAME=openIM\n      # - ETCD_PASSWORD=openIM123\n    volumes:\n      - \"${DATA_DIR}/components/etcd:/etcd-data\"\n    command: >\n      /bin/sh -c '\n        etcd &\n        export ETCDCTL_API=3\n        echo \"Waiting for etcd to become healthy...\"\n        until etcdctl --endpoints=http://127.0.0.1:2379 endpoint health &>/dev/null; do\n          echo \"Waiting for ETCD to start...\"\n          sleep 1\n        done\n\n        echo \"etcd is healthy.\"\n\n        if [ -n \"$${ETCD_ROOT_USER}\" ] && [ -n \"$${ETCD_ROOT_PASSWORD}\" ] && [ -n \"$${ETCD_USERNAME}\" ] && [ -n \"$${ETCD_PASSWORD}\" ]; then\n          echo \"Authentication credentials provided. Setting up authentication...\"\n\n        echo \"Checking authentication status...\"\n        if ! etcdctl --endpoints=http://127.0.0.1:2379 auth status | grep -q \"Authentication Status: true\"; then\n          echo \"Authentication is disabled. Creating users and enabling...\"\n          \n          # Create users and setup permissions\n          etcdctl --endpoints=http://127.0.0.1:2379 user add $${ETCD_ROOT_USER} --new-user-password=$${ETCD_ROOT_PASSWORD} || true\n          etcdctl --endpoints=http://127.0.0.1:2379 user add $${ETCD_USERNAME} --new-user-password=$${ETCD_PASSWORD} || true\n          \n          etcdctl --endpoints=http://127.0.0.1:2379 role add openim-role || true\n          etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission openim-role --prefix=true readwrite / || true\n          etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission openim-role --prefix=true readwrite \"\" || true\n          etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_USERNAME} openim-role || true\n          \n          etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_ROOT_USER} $${ETCD_USERNAME} root || true\n          \n          echo \"Enabling authentication...\"\n          etcdctl --endpoints=http://127.0.0.1:2379 auth enable\n          echo \"Authentication enabled successfully\"\n        else\n          echo \"Authentication is already enabled. Checking OpenIM user...\"\n          \n          # Check if openIM user exists and can perform operations\n          if ! etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} put /test/auth \"auth-check\" &>/dev/null; then\n            echo \"OpenIM user test failed. Recreating user with root credentials...\"\n            \n            # Try to create/update the openIM user using root credentials\n            etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} user add $${ETCD_USERNAME} --new-user-password=$${ETCD_PASSWORD} --no-password-file || true\n            etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role add openim-role || true\n            etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role grant-permission openim-role --prefix=true readwrite / || true\n            etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role grant-permission openim-role --prefix=true readwrite \"\" || true\n            etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} user grant-role $${ETCD_USERNAME} openim-role || true\n            etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_ROOT_USER} $${ETCD_USERNAME} root || true\n            \n            echo \"OpenIM user recreated with required permissions\"\n          else\n            echo \"OpenIM user exists and has correct permissions\"\n            etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} del /test/auth &>/dev/null\n          fi\n        fi\n        echo \"Testing authentication with OpenIM user...\"\n        if etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} put /test/auth \"auth-works\"; then\n          echo \"Authentication working properly\"\n          etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} del /test/auth\n        else\n          echo \"WARNING: Authentication test failed\"\n          fi\n        else\n          echo \"No authentication credentials provided. Running in no-auth mode.\"\n          echo \"To enable authentication, set ETCD_ROOT_USER, ETCD_ROOT_PASSWORD, ETCD_USERNAME, and ETCD_PASSWORD environment variables.\"\n        fi\n        \n        tail -f /dev/null\n      '\n    restart: always\n    networks:\n      - openim\n\n  kafka:\n    image: \"${KAFKA_IMAGE}\"\n    container_name: kafka\n    user: root\n    restart: always\n    ports:\n      - \"19094:9094\"\n    volumes:\n      - \"${DATA_DIR}/components/kafka:/bitnami/kafka\"\n    environment:\n      #KAFKA_HEAP_OPTS: \"-Xms128m -Xmx256m\"\n      TZ: Asia/Shanghai\n      # Unique identifier for the Kafka node (required in controller mode)\n      KAFKA_CFG_NODE_ID: 0\n      # Defines the roles this Kafka node plays: broker, controller, or both\n      KAFKA_CFG_PROCESS_ROLES: controller,broker\n      # Specifies which nodes are controller nodes for quorum voting.\n      # The syntax follows the KRaft mode (no ZooKeeper): node.id@host:port\n      # The controller listener endpoint here is kafka:9093\n      KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093\n      # Specifies which listener is used for controller-to-controller communication\n      KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER\n      # Default number of partitions for new topics\n      KAFKA_NUM_PARTITIONS: 8\n      # Whether to enable automatic topic creation\n      KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: \"true\"\n      # Kafka internal listeners; Kafka supports multiple ports with different protocols\n      # Each port is used for a specific purpose: INTERNAL for internal broker communication,\n      # CONTROLLER for controller communication, EXTERNAL for external client connections.\n      # These logical listener names are mapped to actual protocols via KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP\n      # In short, Kafka is listening on three logical ports: 9092 for internal communication,\n      # 9093 for controller traffic, and 9094 for external access.\n      KAFKA_CFG_LISTENERS: \"INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094\"\n      # Addresses advertised to clients. INTERNAL://kafka:9092 uses the internal Docker service name 'kafka',\n      # so other containers can access Kafka via kafka:9092.\n      # EXTERNAL://localhost:19094 is the address external clients (e.g., in the LAN) should use to connect.\n      # If Kafka is deployed on a different machine than IM, 'localhost' should be replaced with the LAN IP.\n      KAFKA_CFG_ADVERTISED_LISTENERS: \"INTERNAL://kafka:9092,EXTERNAL://localhost:19094\"\n      # Maps logical listener names to actual protocols.\n      # Supported protocols include: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL\n      KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: \"CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT\"\n      # Defines which listener is used for inter-broker communication within the Kafka cluster\n      KAFKA_CFG_INTER_BROKER_LISTENER_NAME: \"INTERNAL\"\n\n      # Authentication configuration variables - comment out to disable auth\n      # KAFKA_USERNAME: \"openIM\"\n      # KAFKA_PASSWORD: \"openIM123\"\n    command: >\n      /bin/sh -c '\n        if [ -n \"$${KAFKA_USERNAME}\" ] && [ -n \"$${KAFKA_PASSWORD}\" ]; then\n          echo \"=== Kafka SASL Authentication ENABLED ===\"\n          echo \"Username: $${KAFKA_USERNAME}\"\n          \n          # Set environment variables for SASL authentication\n          export KAFKA_CFG_LISTENERS=\"SASL_PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094\"\n          export KAFKA_CFG_ADVERTISED_LISTENERS=\"SASL_PLAINTEXT://kafka:9092,EXTERNAL://localhost:19094\"\n          export KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=\"CONTROLLER:PLAINTEXT,EXTERNAL:SASL_PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT\"\n          export KAFKA_CFG_SASL_ENABLED_MECHANISMS=\"PLAIN\"\n          export KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL=\"PLAIN\"\n          export KAFKA_CFG_INTER_BROKER_LISTENER_NAME=\"SASL_PLAINTEXT\"\n          export KAFKA_CLIENT_USERS=\"$${KAFKA_USERNAME}\"\n          export KAFKA_CLIENT_PASSWORDS=\"$${KAFKA_PASSWORD}\"\n        fi\n        \n        # Start Kafka with the configured environment\n        exec /opt/bitnami/scripts/kafka/entrypoint.sh /opt/bitnami/scripts/kafka/run.sh\n      '\n    networks:\n      - openim\n\n  minio:\n    image: \"${MINIO_IMAGE}\"\n    ports:\n      - \"10005:9000\"\n      - \"19090:9090\"\n    container_name: minio\n    volumes:\n      - \"${DATA_DIR}/components/mnt/data:/data\"\n      - \"${DATA_DIR}/components/mnt/config:/root/.minio\"\n    environment:\n      TZ: Asia/Shanghai\n      MINIO_ROOT_USER: root\n      MINIO_ROOT_PASSWORD: openIM123\n    restart: always\n    command: minio server /data --console-address ':9090'\n    networks:\n      - openim\n\n  openim-web-front:\n    image: ${OPENIM_WEB_FRONT_IMAGE}\n    container_name: openim-web-front\n    restart: always\n    ports:\n      - \"11001:80\"\n    networks:\n      - openim\n\n  # openim-admin-front:\n  #   image: ${OPENIM_ADMIN_FRONT_IMAGE}\n  #   container_name: openim-admin-front\n  #   restart: always\n  #   ports:\n  #     - \"11002:80\"\n  #   networks:\n  #     - openim\n\n  prometheus:\n    image: ${PROMETHEUS_IMAGE}\n    container_name: prometheus\n    restart: always\n    user: root\n    profiles:\n      - m\n    volumes:\n      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml\n      - ./config/instance-down-rules.yml:/etc/prometheus/instance-down-rules.yml\n      - ${DATA_DIR}/components/prometheus/data:/prometheus\n    command:\n      - \"--config.file=/etc/prometheus/prometheus.yml\"\n      - \"--storage.tsdb.path=/prometheus\"\n      - \"--web.listen-address=:${PROMETHEUS_PORT}\"\n    network_mode: host\n\n  alertmanager:\n    image: ${ALERTMANAGER_IMAGE}\n    container_name: alertmanager\n    restart: always\n    profiles:\n      - m\n    volumes:\n      - ./config/alertmanager.yml:/etc/alertmanager/alertmanager.yml\n      - ./config/email.tmpl:/etc/alertmanager/email.tmpl\n    command:\n      - \"--config.file=/etc/alertmanager/alertmanager.yml\"\n      - \"--web.listen-address=:${ALERTMANAGER_PORT}\"\n    network_mode: host\n\n  grafana:\n    image: ${GRAFANA_IMAGE}\n    container_name: grafana\n    user: root\n    restart: always\n    profiles:\n      - m\n    environment:\n      - GF_SECURITY_ALLOW_EMBEDDING=true\n      - GF_SESSION_COOKIE_SAMESITE=none\n      - GF_SESSION_COOKIE_SECURE=true\n      - GF_AUTH_ANONYMOUS_ENABLED=true\n      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin\n      - GF_SERVER_HTTP_PORT=${GRAFANA_PORT}\n    volumes:\n      - ${DATA_DIR:-./}/components/grafana:/var/lib/grafana\n    network_mode: host\n\n  node-exporter:\n    image: ${NODE_EXPORTER_IMAGE}\n    container_name: node-exporter\n    restart: always\n    profiles:\n      - m\n    volumes:\n      - /proc:/host/proc:ro\n      - /sys:/host/sys:ro\n      - /:/rootfs:ro\n    command:\n      - \"--path.procfs=/host/proc\"\n      - \"--path.sysfs=/host/sys\"\n      - \"--path.rootfs=/rootfs\"\n      - \"--web.listen-address=:19100\"\n    network_mode: host\n"
  },
  {
    "path": "docs/.generated_docs",
    "content": "docs/.generated_docs\n\ndocs/guide/en-US/cmd/openim/openim.md\ndocs/guide/en-US/cmd/openim/openim_color.md\ndocs/guide/en-US/cmd/openim/openim_completion.md\ndocs/guide/en-US/cmd/openim/openim_info.md\ndocs/guide/en-US/cmd/openim/openim_jwt.md\ndocs/guide/en-US/cmd/openim/openim_jwt_show.md\ndocs/guide/en-US/cmd/openim/openim_jwt_sign.md\ndocs/guide/en-US/cmd/openim/openim_jwt_verify.md\ndocs/guide/en-US/cmd/openim/openim_new.md\ndocs/guide/en-US/cmd/openim/openim_options.md\ndocs/guide/en-US/cmd/openim/openim_policy.md\ndocs/guide/en-US/cmd/openim/openim_policy_create.md\ndocs/guide/en-US/cmd/openim/openim_policy_delete.md\ndocs/guide/en-US/cmd/openim/openim_policy_get.md\ndocs/guide/en-US/cmd/openim/openim_policy_list.md\ndocs/guide/en-US/cmd/openim/openim_policy_update.md\ndocs/guide/en-US/cmd/openim/openim_secret.md\ndocs/guide/en-US/cmd/openim/openim_secret_create.md\ndocs/guide/en-US/cmd/openim/openim_secret_delete.md\ndocs/guide/en-US/cmd/openim/openim_secret_get.md\ndocs/guide/en-US/cmd/openim/openim_secret_list.md\ndocs/guide/en-US/cmd/openim/openim_secret_update.md\ndocs/guide/en-US/cmd/openim/openim_set.md\ndocs/guide/en-US/cmd/openim/openim-rpc-user.md\ndocs/guide/en-US/cmd/openim/openim-rpc-user_create.md\ndocs/guide/en-US/cmd/openim/openim-rpc-user_delete.md\ndocs/guide/en-US/cmd/openim/openim-rpc-user_get.md\ndocs/guide/en-US/cmd/openim/openim-rpc-user_list.md\ndocs/guide/en-US/cmd/openim/openim-rpc-user_update.md\ndocs/guide/en-US/cmd/openim/openim_validate.md\ndocs/guide/en-US/cmd/openim/openim_version.md\ndocs/guide/en-US/yaml/openim/config.yaml\ndocs/guide/en-US/yaml/openim/openim_color.yaml\ndocs/guide/en-US/yaml/openim/openim_completion.yaml\ndocs/guide/en-US/yaml/openim/openim_info.yaml\ndocs/guide/en-US/yaml/openim/openim_jwt.yaml\ndocs/guide/en-US/yaml/openim/openim_new.yaml\ndocs/guide/en-US/yaml/openim/openim_options.yaml\ndocs/guide/en-US/yaml/openim/openim_policy.yaml\ndocs/guide/en-US/yaml/openim/openim_secret.yaml\ndocs/guide/en-US/yaml/openim/openim_set.yaml\ndocs/guide/en-US/yaml/openim/openim-rpc-user.yaml\ndocs/guide/en-US/yaml/openim/openim_validate.yaml\ndocs/guide/en-US/yaml/openim/openim_version.yaml\ndocs/man/man1/openim-color.1\ndocs/man/man1/openim-completion.1\ndocs/man/man1/openim-info.1\ndocs/man/man1/openim-jwt-show.1\ndocs/man/man1/openim-jwt-sign.1\ndocs/man/man1/openim-jwt-verify.1\ndocs/man/man1/openim-jwt.1\ndocs/man/man1/openim-new.1\ndocs/man/man1/openim-options.1\ndocs/man/man1/openim-policy-create.1\ndocs/man/man1/openim-policy-delete.1\ndocs/man/man1/openim-policy-get.1\ndocs/man/man1/openim-policy-list.1\ndocs/man/man1/openim-policy-update.1\ndocs/man/man1/openim-policy.1\ndocs/man/man1/openim-secret-create.1\ndocs/man/man1/openim-secret-delete.1\ndocs/man/man1/openim-secret-get.1\ndocs/man/man1/openim-secret-list.1\ndocs/man/man1/openim-secret-update.1\ndocs/man/man1/openim-secret.1\ndocs/man/man1/openim-set.1\ndocs/man/man1/openim-user-create.1\ndocs/man/man1/openim-user-delete.1\ndocs/man/man1/openim-user-get.1\ndocs/man/man1/openim-user-list.1\ndocs/man/man1/openim-user-update.1\ndocs/man/man1/openim-user.1\ndocs/man/man1/openim-validate.1\ndocs/man/man1/openim-version.1\ndocs/man/man1/openim.1\n"
  },
  {
    "path": "docs/CODEOWNERS",
    "content": "# CODEOWNERS file\n# This file is used to specify the individuals who are required to review changes in this repository.\n\n* @Bloomingg @FGadvancer @skiffer-git @withchao"
  },
  {
    "path": "docs/README.md",
    "content": "# OpenIM Server Docs\n\nWelcome to the OpenIM Documentation hub! This center provides a comprehensive range of guides and manuals designed to help you get the most out of your OpenIM experience.\n\n## Table of Contents\n\n1. [Contrib](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib) - Guidance on contributing and configurations for developers\n2. [Conversions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib) - Coding conventions, logging policies, and other transformation tools\n\n\n## Contrib\n\nThis section offers developers a detailed guide on how to contribute code, set up their environment, and follow the associated processes.\n\n- [Code Conventions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/code-conventions.md) - Rules and conventions for writing code in OpenIM.\n- [Development Guide](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/development.md) - A guide on how to carry out development within OpenIM.\n- [Git Cherry Pick](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/gitcherry-pick.md) - Guidelines on cherry-picking operations.\n- [Git Workflow](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/git-workflow.md) - The git workflow in OpenIM.\n- [Initialization Configurations](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/init-config.md) - Guidance on setting up and initializing OpenIM.\n- [Docker Installation](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/install-docker.md) - How to install Docker on your machine.\n- [Linux Development Environment](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/linux-development.md) - Guide to set up the development environment on Linux.\n- [Local Actions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/local-actions.md) - Guidelines on how to carry out certain common actions locally.\n- [Offline Deployment](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/offline-deployment.md) - Methods of deploying OpenIM offline.\n- [Protoc Tools](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/protoc-tools.md) - Guide on using protoc tools.\n- [Go Tools](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/util-go.md) - Tools and libraries in OpenIM for Go.\n- [Makefile Tools](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/util-makefile.md) - Best practices and tools for Makefile.\n- [Script Tools](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/util-scripts.md) - Best practices and tools for scripts.\n\n## Conversions\n\nThis section introduces various conventions and policies within OpenIM, encompassing code, logs, versions, and more.\n\n- [API Conversions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/api.md) - Guidelines and methods for API conversions.\n- [Logging Policy](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/bash-log.md) - Logging policies and conventions in OpenIM.\n- [CI/CD Actions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/cicd-actions.md) - Procedures and conventions for CI/CD.\n- [Commit Conventions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/commit.md) - Conventions for code commits in OpenIM.\n- [Directory Conventions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/directory.md) - Directory structure and conventions within OpenIM.\n- [Error Codes](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/error-code.md) - List and descriptions of error codes.\n- [Go Code Conversions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/go-code.md) - Conventions and conversions for Go code.\n- [Docker Image Strategy](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/images.md) - Management strategies for OpenIM Docker images, spanning multiple architectures and image repositories.\n- [Logging Conventions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/logging.md) - Further detailed conventions on logging.\n- [Version Conventions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/version.md) - Naming and management strategies for OpenIM versions.\n\n\n## For Developers, Contributors, and Community Maintainers\n\n### Developers & Contributors\n\nIf you're a developer or someone keen on contributing:\n\n- Familiarize yourself with our [Code Conventions](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/code-conventions.md) and [Git Workflow](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/git-workflow.md) to ensure smooth contributions.\n- Dive into the [Development Guide](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/development.md) to get a hang of the development practices in OpenIM.\n\n### Community Maintainers\n\nAs a community maintainer:\n\n- Ensure that contributions align with the standards outlined in our documentation.\n- Regularly review the [Logging Policy](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/bash-log.md) and [Error Codes](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/error-code.md) to stay updated.\n\n## For Users\n\nUsers should pay particular attention to:\n\n- [Docker Installation](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/install-docker.md) - Necessary if you're planning to use Docker images of OpenIM.\n- [Docker Image Strategy](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/images.md) - To understand the different images available and how to choose the right one for your architecture."
  },
  {
    "path": "docs/contrib/README.md",
    "content": "# Contrib Documentation Index\n\n## 📚 General Information\n- [📄 README](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/README.md) - General introduction to the contribution documentation.\n- [📑 Development Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md) - Guidelines for setting up a development environment.\n\n## 🛠 Setup and Installation\n- [🌍 Environment Setup](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md) - Instructions on setting up the development environment.\n- [🐳 Docker Installation Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md) - Steps to install Docker for container management.\n- [🔧 OpenIM Linux System Installation](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md) - Guide for installing OpenIM on a Linux system.\n\n## 💻 Development Practices\n- [👨‍💻 Code Conventions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md) - Coding standards to follow for consistency.\n- [📐 Directory Structure](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md) - Explanation of the repository's directory layout.\n- [🔀 Git Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md) - The workflow for using Git in this project (note the file extension error).\n- [💾 GitHub Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md) - Workflow guidelines for GitHub.\n\n## 🧪 Testing and Deployment\n- [⚙️ CI/CD Actions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md) - Continuous integration and deployment configurations.\n- [🚀 Offline Deployment](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md) - How to deploy the application offline.\n\n## 🔧 Utilities and Tools\n- [📦 Protoc Tools](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md) - Protobuf compiler-related utilities.\n- [🔨 Utility Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md) - Go utilities and helper functions.\n- [🛠 Makefile Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md) - Makefile scripts for automation.\n- [📜 Script Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md) - Utility scripts for development.\n\n## 📋 Standards and Conventions\n- [🚦 Commit Guidelines](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md) - Standards for writing commit messages.\n- [✅ Testing Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md) - Guidelines and conventions for writing tests.\n- [📈 Versioning](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md) - Version management for the project.\n\n## 🖼 Additional Resources\n- [🌐 API Reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - Detailed API documentation.\n- [📚 Go Code Standards](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md) - Go programming language standards.\n- [🖼 Image Guidelines](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md) - Guidelines for image assets.\n\n## 🐛 Troubleshooting\n- [🔍 Error Code Reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md) - List of error codes and their meanings.\n- [🐚 Bash Logging](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) - Logging standards for bash scripts.\n- [📈 Logging Conventions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md) - Conventions for application logging.\n- [🛠 Local Actions Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md) - How to perform local actions for troubleshooting.\n"
  },
  {
    "path": "docs/contrib/api.md",
    "content": "## Interface Standards\n\nOur project, OpenIM, adheres to the [OpenAPI 3.0](https://spec.openapis.org/oas/latest.html) interface standards.\n\n> Chinese translation: [OpenAPI Specification Chinese Translation](https://fishead.gitbook.io/openapi-specification-zhcn-translation/3.0.0.zhcn)"
  },
  {
    "path": "docs/contrib/bash-log.md",
    "content": "## OpenIM Logging System: Design and Usage\n\n**PATH:** `scripts/lib/logging.sh`\n\n\n\n### Introduction\n\nOpenIM, an intricate project, requires a robust logging mechanism to diagnose issues, maintain system health, and provide insights. A custom-built logging system embedded within OpenIM ensures consistent and structured logs. Let's delve into the design of this logging system and understand its various functions and their usage scenarios.\n\n### Design Overview\n\n1. **Initialization**: The system begins by determining the verbosity level through the `OPENIM_VERBOSE` variable. If it's not set, a default value of 5 is assigned. This verbosity level dictates the depth of the log details.\n2. **Log File Setup**: Logs are stored in the directory specified by `OPENIM_OUTPUT`. If this variable isn't explicitly set, it defaults to the `_output` directory relative to the script location. Each log file is named based on the date to facilitate easy identification.\n3. **Logging Function**: The `echo_log()` function plays a pivotal role by writing messages to both the console (stdout) and the log file.\n4. **Logging to a file**: The `echo_log()` function writes to the log file by appending the message to the file. It also adds a timestamp to the message. path: `_output/logs/*`, Enable logging by default. Set to false to disable. If you wish to turn off output to log files set `export ENABLE_LOGGING=flase`. \n\n### Key Functions & Their Usages\n\n1. **Error Handling**:\n   - `openim::log::errexit()`: Activated when a command exits with an error. It prints a call tree showing the sequence of functions leading to the error and then calls `openim::log::error_exit()` with relevant information.\n   - `openim::log::install_errexit()`: Sets up the trap for catching errors and ensures that the error handler (`errexit`) gets propagated to various script constructs like functions, expansions, and subshells.\n2. **Logging Levels**:\n   - `openim::log::error()`: Logs error messages with a timestamp. The log message starts with '!!!' to indicate its severity.\n   - `openim::log::info()`: Provides informational messages. The display of these messages is governed by the verbosity level (`OPENIM_VERBOSE`).\n   - `openim::log::progress()`: Designed for logging progress messages or creating progress bars.\n   - `openim::log::status()`: Logs status messages with a timestamp, prefixing each entry with '+++' for easy identification.\n   - `openim::log::success()`: Highlights successful operations with a bright green prefix. It's ideal for visually signifying operations that completed successfully.\n3. **Exit and Stack Trace**:\n   - `openim::log::error_exit()`: Logs an error message, dumps the call stack, and exits the script with a specified exit code.\n   - `openim::log::stack()`: Prints out a stack trace, showing the call hierarchy leading to the point where this function was invoked.\n4. **Usage Information**:\n   - `openim::log::usage() & openim::log::usage_from_stdin()`: Both functions provide a mechanism to display usage instructions. The former accepts arguments directly, while the latter reads them from stdin.\n5. **Test Function**:\n   - `openim::log::test_log()`: This function is a test suite to verify that all logging functions are operating as expected.\n\n### Usage Scenario\n\nImagine a situation where an OpenIM operation fails, and you need to ascertain the cause. With the logging system in place, you can:\n\n- Check the log file for the specific day to find error messages with the '!!!' prefix.\n- View the call tree and stack trace to trace back the sequence of operations leading to the failure.\n- Use the verbosity level to filter out unnecessary details and focus on the crux of the issue.\n\nThis systematic and structured approach greatly simplifies the debugging process, making system maintenance more efficient.\n\n### Conclusion\n\nOpenIM's logging system is a testament to the importance of structured and detailed logging in complex projects. By using this logging mechanism, developers and system administrators can streamline troubleshooting and ensure the seamless operation of the OpenIM project."
  },
  {
    "path": "docs/contrib/cicd-actions.md",
    "content": "# Continuous Integration and Automation\n\nEvery change on the OpenIM repository, either made through a pull request or direct push, triggers the continuous integration pipelines defined within the same repository. Needless to say, all the OpenIM contributions can be merged until all the checks pass (AKA having green builds).\n\n- [Continuous Integration and Automation](#continuous-integration-and-automation)\n  - [CI Platforms](#ci-platforms)\n    - [GitHub Actions](#github-actions)\n  - [Running locally](#running-locally)\n\n## CI Platforms\n\nCurrently, there are two different platforms involved in running the CI processes:\n\n- GitHub actions\n- Drone pipelines on CNCF infrastructure\n\n### GitHub Actions\n\nAll the existing GitHub Actions are defined as YAML files under the `.github/workflows` directory. These can be grouped into:\n\n- **PR Checks**. These actions run all the required validations upon PR creation and update. Covering the DCO compliance check, `x86_64` test batteries (unit, integration, smoke), and code coverage.\n- **Repository automation**. Currently, it only covers issues and epic grooming.\n\nEverything runs on GitHub's provided runners; thus, the tests are limited to run in `x86_64` architectures.\n\n\n## Running locally\n\nA contributor should verify their changes locally to speed up the pull request process. Fortunately, all the CI steps can be on local environments, except for the publishing ones, through either of the following methods:\n\n**User Makefile:**\n```bash\nroot@PS2023EVRHNCXG:~/workspaces/openim/Open-IM-Server# make help 😊\n\nUsage: make <TARGETS> <OPTIONS> ...\n\nTargets:\n\nall                          Run tidy, gen, add-copyright, format, lint, cover, build 🚀\nbuild                        Build binaries by default 🛠️\nmultiarch                    Build binaries for multiple platforms. See option PLATFORMS. 🌍\ntidy                         tidy go.mod ✨\nvendor                       vendor go.mod 📦\nstyle                        code style -> fmt,vet,lint 💅\nfmt                          Run go fmt against code. ✨\nvet                          Run go vet against code. ✅\nlint                         Check syntax and styling of go sources. ✔️\nformat                       Gofmt (reformat) package sources (exclude vendor dir if existed). 🔄\ntest                         Run unit test. 🧪\ncover                        Run unit test and get test coverage. 📊\nupdates                      Check for updates to go.mod dependencies 🆕\nimports                      task to automatically handle import packages in Go files using goimports tool 📥\nclean                        Remove all files that are created by building. 🗑️\nimage                        Build docker images for host arch. 🐳\nimage.multiarch              Build docker images for multiple platforms. See option PLATFORMS. 🌍🐳\npush                         Build docker images for host arch and push images to registry. 📤🐳\npush.multiarch               Build docker images for multiple platforms and push images to registry. 🌍📤🐳\ntools                        Install dependent tools. 🧰\ngen                          Generate all necessary files. 🧩\nswagger                      Generate swagger document. 📖\nserve-swagger                Serve swagger spec and docs. 🚀📚\nverify-copyright             Verify the license headers for all files. ✅\nadd-copyright                Add copyright ensure source code files have license headers. 📄\nrelease                      release the project 🎉\nhelp                         Show this help info. ℹ️\nhelp-all                     Show all help details info. ℹ️📚\n\nOptions:\n\nDEBUG            Whether or not to generate debug symbols. Default is 0. ❓\n\nBINS             Binaries to build. Default is all binaries under cmd. 🛠️\nThis option is available when using: make {build}(.multiarch) 🧰\nExample: make build BINS=\"openim-api openim_cms_api\".\n\nPLATFORMS        Platform to build for. Default is linux_arm64 and linux_amd64. 🌍\nThis option is available when using: make {build}.multiarch 🌍\nExample: make multiarch PLATFORMS=\"linux_s390x linux_mips64\nlinux_mips64le darwin_amd64 windows_amd64 linux_amd64 linux_arm64\".\n\nV                Set to 1 enable verbose build. Default is 0. 📝\n```\n\n\nHow to Use Makefile to Help Contributors Build Projects Quickly 😊\n\nThe `make help` command is a handy tool that provides useful information on how to utilize the Makefile effectively. By running this command, contributors will gain insights into various targets and options available for building projects swiftly.\n\nHere's a breakdown of the targets and options provided by the Makefile:\n\n**Targets 😃**\n\n1. `all`: This target runs multiple tasks like `tidy`, `gen`, `add-copyright`, `format`, `lint`, `cover`, and `build`. It ensures comprehensive project building.\n2. `build`: The primary target that compiles binaries by default. It is particularly useful for creating the necessary executable files.\n3. `multiarch`: A target that builds binaries for multiple platforms. Contributors can specify the desired platforms using the `PLATFORMS` option.\n4. `tidy`: This target cleans up the `go.mod` file, ensuring its consistency.\n5. `vendor`: A target that updates the project dependencies based on the `go.mod` file.\n6. `style`: Checks the code style using tools like `fmt`, `vet`, and `lint`. It ensures a consistent coding style throughout the project.\n7. `fmt`: Formats the code using the `go fmt` command, ensuring proper indentation and formatting.\n8. `vet`: Runs the `go vet` command to identify common errors in the code.\n9. `lint`: Validates the syntax and styling of Go source files using a linter.\n10. `format`: Reformats the package sources using `gofmt`. It excludes the vendor directory if it exists.\n11. `test`: Executes unit tests to ensure the functionality and stability of the code.\n12. `cover`: Performs unit tests and calculates the test coverage of the code.\n13. `updates`: Checks for updates to the project's dependencies specified in the `go.mod` file.\n14. `imports`: Automatically handles import packages in Go files using the `goimports` tool.\n15. `clean`: Removes all files generated during the build process, effectively cleaning up the project directory.\n16. `image`: Builds Docker images for the host architecture.\n17. `image.multiarch`: Similar to the `image` target, but it builds Docker images for multiple platforms. Contributors can specify the desired platforms using the `PLATFORMS` option.\n18. `push`: Builds Docker images for the host architecture and pushes them to a registry.\n19. `push.multiarch`: Builds Docker images for multiple platforms and pushes them to a registry. Contributors can specify the desired platforms using the `PLATFORMS` option.\n20. `tools`: Installs the necessary tools or dependencies required by the project.\n21. `gen`: Generates all the required files automatically.\n22. `swagger`: Generates the swagger document for the project.\n23. `serve-swagger`: Serves the swagger specification and documentation.\n24. `verify-copyright`: Verifies the license headers for all project files.\n25. `add-copyright`: Adds copyright headers to the source code files.\n26. `release`: Releases the project, presumably for distribution.\n27. `help`: Displays information about available targets and options.\n28. `help-all`: Shows detailed information about all available targets and options.\n\n**Options 😄**\n\n1. `DEBUG`: A boolean option that determines whether or not to generate debug symbols. The default value is 0 (false).\n2. `BINS`: Specifies the binaries to build. By default, it builds all binaries under the `cmd` directory. Contributors can provide a list of specific binaries using this option.\n3. `PLATFORMS`: Specifies the platforms to build for. The default platforms are `linux_arm64` and `linux_amd64`. Contributors can specify multiple platforms by providing a space-separated list of platform names.\n4. `V`: A boolean option that enables verbose build output when set to 1 (true). The default value is 0 (false).\n\nWith these targets and options in place, contributors can efficiently build projects using the Makefile. Happy coding! 🚀😊\n"
  },
  {
    "path": "docs/contrib/code-conventions.md",
    "content": "# Code conventions\n\n- [Code conventions](#code-conventions)\n  - [POSIX shell](#posix-shell)\n  - [Go](#go)\n  - [OpenIM Naming Conventions Guide](#openim-naming-conventions-guide)\n    - [1. General File Naming](#1-general-file-naming)\n    - [2. Special File Types](#2-special-file-types)\n      - [a. Script and Markdown Files](#a-script-and-markdown-files)\n      - [b. Uppercase Markdown Documentation](#b-uppercase-markdown-documentation)\n    - [3. Directory Naming](#3-directory-naming)\n    - [4. Configuration Files](#4-configuration-files)\n    - [Best Practices](#best-practices)\n  - [Directory and File Conventions](#directory-and-file-conventions)\n  - [Testing conventions](#testing-conventions)\n\n## POSIX shell\n\n- [Style guide](https://google.github.io/styleguide/shell.xml)\n\n## Go\n\n- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)\n- [Effective Go](https://golang.org/doc/effective_go.html)\n- Know and avoid [Go landmines](https://gist.github.com/lavalamp/4bd23295a9f32706a48f)\n- Comment your code.\n  - [Go's commenting conventions](http://blog.golang.org/godoc-documenting-go-code)\n  - If reviewers ask questions about why the code is the way it is, that's a sign that comments might be helpful.\n- Command-line flags should use dashes, not underscores\n- Naming\n  - Please consider package name when selecting an interface name, and avoid redundancy. For example, `storage.Interface` is better than `storage.StorageInterface`.\n  - Do not use uppercase characters, underscores, or dashes in package names.\n  - Please consider parent directory name when choosing a package name. For example, `pkg/controllers/autoscaler/foo.go` should say `package autoscaler` not `package autoscalercontroller`.\n    - Unless there's a good reason, the `package foo` line should match the name of the directory in which the `.go` file exists.\n    - Importers can use a different name if they need to disambiguate.Ⓜ️\n\n## OpenIM Naming Conventions Guide\n\nWelcome to the OpenIM Naming Conventions Guide. This document outlines the best practices and standardized naming conventions that our project follows to maintain clarity, consistency, and alignment with industry standards, specifically taking cues from the Google Naming Conventions.\n\n### 1. General File Naming\n\nFiles within the OpenIM project should adhere to the following rules:\n\n+ Both hyphens (`-`) and underscores (`_`) are acceptable in file names.\n+ Underscores (`_`) are preferred for general files to enhance readability and compatibility.\n+ For example: `data_processor.py`, `user_profile_generator.go`\n\n### 2. Special File Types\n\n#### a. Script and Markdown Files\n\n+ Bash scripts and Markdown files should use hyphens (`-`) to facilitate better searchability and compatibility in web browsers.\n+ For example: `deploy-script.sh`, `project-overview.md`\n\n#### b. Uppercase Markdown Documentation\n\n+ Markdown files with uppercase names, such as `README`, may include underscores (`_`) to separate words if necessary.\n+ For example: `README_SETUP.md`, `CONTRIBUTING_GUIDELINES.md`\n\n### 3. Directory Naming\n\n+ Directories must use hyphens (`-`) exclusively to maintain a clean and organized file structure.\n+ For example: `image-assets`, `user-data`\n\n### 4. Configuration Files\n\n+ Configuration files, including but not limited to `.yaml` files, should use hyphens (`-`).\n+ For example: `app-config.yaml`, `logging-config.yaml`\n\n### Best Practices\n\n+ Keep names concise but descriptive enough to convey the file's purpose or contents at a glance.\n+ Avoid using spaces in names; use hyphens or underscores instead to improve compatibility across different operating systems and environments.\n+ Stick to lowercase naming where possible for consistency and to prevent issues with case-sensitive systems.\n+ Include version numbers or dates in file names if the file is subject to updates, following the format: `project-plan-v1.2.md` or `backup-2023-03-15.sql`.\n\n## Directory and File Conventions\n\n- Avoid generic utility packages. Instead of naming a package \"util\", choose a name that clearly describes its purpose. For instance, functions related to waiting operations are contained within the `wait` package, which includes methods like `Poll`, fully named as `wait.Poll`.\n- All filenames, script files, configuration files, and directories should be in lowercase and use dashes (`-`) as separators.\n- For Go language files, filenames should be in lowercase and use underscores (`_`).\n- Package names should match their directory names to ensure consistency. For example, within the `openim-api` directory, the Go file should be named `openim-api.go`, following the convention of using dashes for directory names and aligning package names with directory names.\n\n\n## Testing conventions\n\nPlease refer to [TESTING.md](https://github.com/openimsdk/open-im-server/tree/main/test/readme) document.\n"
  },
  {
    "path": "docs/contrib/commit.md",
    "content": "## Commit Standards\n\nOur project, OpenIM, follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) standards.\n\n> Chinese translation: [Conventional Commits: A Specification Making Commit Logs More Human and Machine-friendly](https://tool.lu/en_US/article/2ac/preview)\n\nIn addition to adhering to these standards, we encourage all contributors to the OpenIM project to ensure that their commit messages are clear and descriptive. This helps in maintaining a clean and meaningful project history. Each commit message should succinctly describe the changes made and, where necessary, the reasoning behind those changes.\n\nTo facilitate a streamlined process, we also recommend using appropriate commit type based on Conventional Commits guidelines such as `fix:` for bug fixes, `feat:` for new features, and so forth. Understanding and using these conventions helps in generating automatic release notes, making versioning easier, and improving overall readability of commit history."
  },
  {
    "path": "docs/contrib/development.md",
    "content": "# Development Guide\n\nSince OpenIM is written in Go, it is fair to assume that the Go tools are all one needs to contribute to this project. Unfortunately, there is a point where this no longer holds true when required to test or build local changes. This document elaborates on the required tooling for OpenIM development.\n\n- [Development Guide](#development-guide)\n  - [Non-Linux environment prerequisites](#non-linux-environment-prerequisites)\n    - [Windows Setup](#windows-setup)\n    - [macOS Setup](#macos-setup)\n  - [Installing Required Software](#installing-required-software)\n    - [Go](#go)\n    - [Docker](#docker)\n    - [Vagrant](#vagrant)\n  - [Dependency management](#dependency-management)\n\n## Non-Linux environment prerequisites\n\nAll the test and build scripts within this repository were created to be run on GNU Linux development environments. Due to this, it is suggested to use the virtual machine defined on this repository's [Vagrantfile](https://developer.hashicorp.com/vagrant/docs/vagrantfile) to use them.\n\nEither way, if one still wants to build and test OpenIM on non-Linux environments, specific setups are to be followed.\n\n### Windows Setup\n\nTo build OpenIM on Windows is only possible for versions that support Windows Subsystem for Linux (WSL). If the development environment in question has Windows 10, Version 2004, Build 19041 or higher, [follow these instructions to install WSL2](https://docs.microsoft.com/en-us/windows/wsl/install-win10); otherwise, use a Linux Virtual machine instead.\n\n### macOS Setup\n\nThe shell scripts in charge of the build and test processes rely on GNU utils (i.e. `sed`), [which slightly differ on macOS](https://unix.stackexchange.com/a/79357), meaning that one must make some adjustments before using them.\n\nFirst, install the GNU utils:\n\n```sh\nbrew install coreutils findutils gawk gnu-sed gnu-tar grep make\n```\n\nThen update the shell init script (i.e. `.bashrc`) to prepend the GNU Utils to the `$PATH` variable\n\n```sh\nGNUBINS=\"$(find /usr/local/opt -type d -follow -name gnubin -print)\"\n\nfor bindir in ${GNUBINS[@]}; do\n  PATH=$bindir:$PATH\ndone\n\nexport PATH\n```\n\n## Installing Required Software\n\n### Go\n\nIt is well known that OpenIM is written in [Go](http://golang.org). Please follow the [Go Getting Started guide](https://golang.org/doc/install) to install and set up the Go tools used to compile and run the test batteries.\n\n|     OpenIM     | requires Go |\n|----------------|-------------|\n| 2.24 - 3.00    |    1.15 +   |\n|     3.30 +     |    1.18 +   |\n\n### Docker\n\nOpenIM build and test processes development require Docker to run certain steps. [Follow the Docker website instructions to install Docker](https://docs.docker.com/get-docker/) in the development environment.\n\n### Vagrant\n\nAs described in the [Testing documentation](https://github.com/openimsdk/open-im-server/tree/main/test/readme), all the smoke tests are run in virtual machines managed by Vagrant.  To install Vagrant in the development environment, [follow the instructions from the Hashicorp website](https://www.vagrantup.com/downloads), alongside any of the following hypervisors:\n\n- [VirtualBox](https://www.virtualbox.org/)\n- [libvirt](https://libvirt.org/) and the [vagrant-libvirt plugin](https://github.com/vagrant-libvirt/vagrant-libvirt#installation)\n\n\n## Dependency management\n\nOpenIM uses [go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.\n"
  },
  {
    "path": "docs/contrib/directory.md",
    "content": "## Catalog Service Interface Specification\n\n+ [https://github.com/kubecub/go-project-layout](https://github.com/kubecub/go-project-layout)"
  },
  {
    "path": "docs/contrib/environment.md",
    "content": "# OpenIM ENVIRONMENT CONFIGURATION\n\n<!-- vscode-markdown-toc -->\n* 1. [OpenIM Deployment Guide](#OpenIMDeploymentGuide)\n\t* 1.1. [Deployment Strategies](#DeploymentStrategies)\n\t* 1.2. [Source Code Deployment](#SourceCodeDeployment)\n\t* 1.3. [Docker Compose Deployment](#DockerComposeDeployment)\n\t* 1.4. [Environment Variable Configuration](#EnvironmentVariableConfiguration)\n\t\t* 1.4.1. [Recommended using environment variables](#Recommendedusingenvironmentvariables)\n\t\t* 1.4.2. [Additional Configuration](#AdditionalConfiguration)\n\t\t* 1.4.3. [Security Considerations](#SecurityConsiderations)\n\t\t* 1.4.4. [Data Management](#DataManagement)\n\t\t* 1.4.5. [Monitoring and Logging](#MonitoringandLogging)\n\t\t* 1.4.6. [Troubleshooting](#Troubleshooting)\n\t\t* 1.4.7. [Conclusion](#Conclusion)\n\t\t* 1.4.8. [Additional Resources](#AdditionalResources)\n* 2. [Further Configuration](#FurtherConfiguration)\n\t* 2.1. [Image Registry Configuration](#ImageRegistryConfiguration)\n\t* 2.2. [OpenIM Docker Network Configuration](#OpenIMDockerNetworkConfiguration)\n\t* 2.3. [OpenIM Configuration](#OpenIMConfiguration)\n\t* 2.4. [OpenIM Chat Configuration](#OpenIMChatConfiguration)\n\t* 2.5. [Zookeeper Configuration](#ZookeeperConfiguration)\n\t* 2.6. [MySQL Configuration](#MySQLConfiguration)\n\t* 2.7. [MongoDB Configuration](#MongoDBConfiguration)\n\t* 2.8. [Tencent Cloud COS Configuration](#TencentCloudCOSConfiguration)\n\t* 2.9. [Alibaba Cloud OSS Configuration](#AlibabaCloudOSSConfiguration)\n\t* 2.10. [Redis Configuration](#RedisConfiguration)\n\t* 2.11. [Kafka Configuration](#KafkaConfiguration)\n\t* 2.12. [OpenIM Web Configuration](#OpenIMWebConfiguration)\n\t* 2.13. [RPC Configuration](#RPCConfiguration)\n\t* 2.14. [Prometheus Configuration](#PrometheusConfiguration)\n\t* 2.15. [Grafana Configuration](#GrafanaConfiguration)\n\t* 2.16. [RPC Port Configuration Variables](#RPCPortConfigurationVariables)\n\t* 2.17. [RPC Register Name Configuration](#RPCRegisterNameConfiguration)\n\t* 2.18. [Log Configuration](#LogConfiguration)\n\t* 2.19. [Additional Configuration Variables](#AdditionalConfigurationVariables)\n\t* 2.20. [Prometheus Configuration](#PrometheusConfiguration-1)\n\t\t* 2.20.1. [General Configuration](#GeneralConfiguration)\n\t\t* 2.20.2. [Service-Specific Prometheus Ports](#Service-SpecificPrometheusPorts)\n\t* 2.21. [Qiniu Cloud Kodo Configuration](#QiniuCloudKODOConfiguration)\n\n## 0. <a name='TableofContents'></a>OpenIM Config File\n\nEnsuring that OpenIM operates smoothly requires clear direction on the configuration file's location. Here's a detailed step-by-step guide on how to provide this essential path to OpenIM:\n\n1. **Using the Command-line Argument**:\n\n   + **For Configuration Path**: When initializing OpenIM, you can specify the path to the configuration file directly using the `-c` or `--config_folder_path` option.\n\n     ```bash\n     ❯ _output/bin/platforms/linux/amd64/openim-api --config_folder_path=\"/your/config/folder/path\"\n     ```\n\n   + **For Port Specification**: Similarly, if you wish to designate a particular port, utilize the `-p` option followed by the desired port number.\n\n     ```bash\n     ❯ _output/bin/platforms/linux/amd64/openim-api -p 1234\n     ```\n\n     Note: If the port is not specified here, OpenIM will fetch it from the configuration file. Setting the port via environment variables isn't supported. We recommend consolidating settings in the configuration file for a more consistent and streamlined setup.\n\n2. **Leveraging the Environment Variable**:\n\n   You have the flexibility to determine OpenIM's configuration path by setting an `OPENIMCONFIG` environment variable. This method provides a seamless way to instruct OpenIM without command-line parameters every time.\n\n   ```bash\n   export OPENIMCONFIG=\"/path/to/your/config\"\n   ```\n\n3. **Relying on the Default Path**:\n\n   In scenarios where neither command-line arguments nor environment variables are provided, OpenIM will intuitively revert to the `config/` directory to locate its configuration.\n\n\n\n##  1. <a name='OpenIMDeploymentGuide'></a>OpenIM Deployment Guide\n\nWelcome to the OpenIM Deployment Guide! OpenIM offers a versatile and robust instant messaging server, and deploying it can be achieved through various methods. This guide will walk you through the primary deployment strategies, ensuring you can set up OpenIM in a way that best suits your needs.\n\n###  1.1. <a name='DeploymentStrategies'></a>Deployment Strategies\n\nOpenIM provides multiple deployment methods, each tailored to different use cases and technical preferences:\n\n1. **[Source Code Deployment Guide](https://doc.rentsoft.cn/guides/gettingStarted/imSourceCodeDeployment)**\n2. **[Docker Deployment Guide](https://doc.rentsoft.cn/guides/gettingStarted/dockerCompose)**\n3. **[Kubernetes Deployment Guide](https://github.com/openimsdk/open-im-server/tree/main/deployments)**\n\nWhile the first two methods will be our main focus, it's worth noting that the third method, Kubernetes deployment, is also viable and can be rendered via the `environment.sh` script variables.\n\n###  1.2. <a name='SourceCodeDeployment'></a>Source Code Deployment\n\nIn the source code deployment method, the configuration generation process involves executing `make init`, which fundamentally runs the script `./scripts/init-config.sh`. This script utilizes variables defined in the [`environment.sh`](https://github.com/openimsdk/open-im-server/blob/main/scripts/install/environment.sh) script to render the [`config.yaml`](https://github.com/openimsdk/open-im-server/blob/main/deployments/templates/config.yaml) template file, subsequently generating the [`config.yaml`](https://github.com/openimsdk/open-im-server/blob/main/config/config.yaml) configuration file.\n\n###  1.3. <a name='DockerComposeDeployment'></a>Docker Compose Deployment\n\nDocker deployment offers a slightly more intricate template. Within the [openim-server](https://github.com/openimsdk/openim-docker/tree/main/openim-server) directory, multiple subdirectories correspond to various versions, each aligning with `openim-chat` as illustrated below:\n\n| openim-server                                                | openim-chat                                                  |\n| ------------------------------------------------------------ | ------------------------------------------------------------ |\n| [main](https://github.com/openimsdk/openim-docker/tree/main/openim-server/main) | [main](https://github.com/openimsdk/openim-docker/tree/main/openim-chat/main) |\n| [release-v3.2](https://github.com/openimsdk/openim-docker/tree/main/openim-server/release-v3.3) | [release-v3.2](https://github.com/openimsdk/openim-docker/tree/main/openim-chat/release-v1.3) |\n| [release-v3.2](https://github.com/openimsdk/openim-docker/tree/main/openim-server/release-v3.2) | [release-v3.2](https://github.com/openimsdk/openim-docker/tree/main/openim-chat/release-v1.2) |\n\nConfiguration file modifications can be made by specifying corresponding environment variables, for instance:\n\n```bash\nexport CHAT_IMAGE_VERSION=\"main\"\nexport SERVER_IMAGE_VERSION=\"main\"\n```\n\nThese variables are stored within the [`environment.sh`](https://github.com/OpenIMSDK/open-im-server/blob/main/scripts/install/environment.sh) configuration:\n\n```bash\nreadonly CHAT_IMAGE_VERSION=${CHAT_IMAGE_VERSION:-'main'}\nreadonly SERVER_IMAGE_VERSION=${SERVER_IMAGE_VERSION:-'main'}\n```\n> [!IMPORTANT]\n> Can learn to read our mirror version strategy: https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/images.md\n\n\nSetting a variable, e.g., `export CHAT_IMAGE_VERSION=\"release-v1.3\"`, will prioritize `CHAT_IMAGE_VERSION=\"release-v1.3\"` as the variable value. Ultimately, the chosen image version is determined, and rendering is achieved through `make init` (or `./scripts/init-config.sh`).\n\n> Note: Direct modifications to the `config.yaml` file are also permissible without utilizing `make init`.\n\n###  1.4. <a name='EnvironmentVariableConfiguration'></a>Environment Variable Configuration\n\nFor convenience, configuration through modifying environment variables is recommended:\n\n####  1.4.1. <a name='Recommendedusingenvironmentvariables'></a>Recommended using environment variables\n\n+ PASSWORD\n\n  + **Description**: Password for mongodb, redis, and minio.\n  + **Default**: `openIM123`\n  + Notes:\n    + Minimum password length: 8 characters.\n    + Special characters are not allowed.\n\n  ```bash\n  export PASSWORD=\"openIM123\"\n  ```\n\n+ OPENIM_USER\n\n  + **Description**: Username for redis, and minio.\n  + **Default**: `root`\n\n  ```bash\n  export OPENIM_USER=\"root\"\n  ```\n\n> mongo is `openIM`, use `export MONGO_OPENIM_USERNAME=\"openIM\"` to modify\n\n+ OPENIM_IP\n\n  + **Description**: API address.\n  + **Note**: If the server has an external IP, it will be automatically obtained. For internal networks, set this variable to the IP serving internally.\n\n  ```bash\n  export OPENIM_IP=\"ip\"\n  ```\n\n+ DATA_DIR\n\n  + **Description**: Data mount directory for components.\n  + **Default**: `/data/openim`\n\n  ```bash\n  export DATA_DIR=\"/data/openim\"\n  ```\n\n####  1.4.2. <a name='AdditionalConfiguration'></a>Additional Configuration\n\n##### MinIO Access and Secret Key\n\nTo secure your MinIO server, you should set up an access key and secret key. These credentials are used to authenticate requests to your MinIO server.\n\n```bash\nexport MINIO_ACCESS_KEY=\"YourAccessKey\"\nexport MINIO_SECRET_KEY=\"YourSecretKey\"\n```\n\n##### MinIO Browser\n\nMinIO comes with an embedded web-based object browser. You can control the availability of the MinIO browser by setting the `MINIO_BROWSER` environment variable.\n\n```bash\nexport MINIO_BROWSER=\"on\"\n```\n\n####  1.4.3. <a name='SecurityConsiderations'></a>Security Considerations\n\n##### TLS/SSL Configuration\n\nFor secure communication, it's recommended to enable TLS/SSL for your MinIO server. You can do this by providing the path to the SSL certificate and key files.\n\n```bash\nexport MINIO_CERTS_DIR=\"/path/to/certs/directory\"\n```\n\n####  1.4.4. <a name='DataManagement'></a>Data Management\n\n##### Data Retention Policy\n\nYou may want to set up a data retention policy to automatically delete objects after a specified period.\n\n```bash\nexport MINIO_RETENTION_DAYS=\"30\"\n```\n\n####  1.4.5. <a name='MonitoringandLogging'></a>Monitoring and Logging\n\n##### [Audit Logging](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/environment.md#audit-logging)\n\nEnable audit logging to keep track of access and changes to your data.\n\n```bash\nexport MINIO_AUDIT=\"on\"\n```\n\n####  1.4.6. <a name='Troubleshooting'></a>Troubleshooting\n\n##### Debug Mode\n\nIn case of issues, you may enable debug mode to get more detailed logs to assist in troubleshooting.\n\n```bash\nexport MINIO_DEBUG=\"on\"\n```\n\n####  1.4.7. <a name='Conclusion'></a>Conclusion\n\nWith the environment variables configured as per your requirements, your MinIO server should be ready to securely store and manage your object data. Ensure to verify the setup and monitor the logs for any unusual activities or errors. Regularly update the MinIO server and review your configuration to adapt to any changes or improvements in the MinIO system.\n\n####  1.4.8. <a name='AdditionalResources'></a>Additional Resources\n\n+ [MinIO Client Quickstart Guide](https://docs.min.io/docs/minio-client-quickstart-guide)\n+ [MinIO Admin Complete Guide](https://docs.min.io/docs/minio-admin-complete-guide)\n+ [MinIO Docker Quickstart Guide](https://docs.min.io/docs/minio-docker-quickstart-guide)\n\nFeel free to explore the MinIO documentation for more advanced configurations and usage scenarios.\n\n\n\n##  2. <a name='FurtherConfiguration'></a>Further Configuration\n\n###  2.1. <a name='ImageRegistryConfiguration'></a>Image Registry Configuration\n\n**Description**: The image registry configuration allows users to select an image address for use. The default is set to use GITHUB images, but users can opt for Docker Hub or Ali Cloud, especially beneficial for Chinese users due to its local proximity.\n\n| Parameter        | Default Value         | Description                                                  |\n| ---------------- | --------------------- | ------------------------------------------------------------ |\n| `IMAGE_REGISTRY` | `\"ghcr.io/openimsdk\"` | The registry from which Docker images will be pulled. Other options include `\"openim\"` and `\"registry.cn-hangzhou.aliyuncs.com/openimsdk\"`. |\n\n###  2.2. <a name='OpenIMDockerNetworkConfiguration'></a>OpenIM Docker Network Configuration\n\n**Description**: This section configures the Docker network subnet and generates IP addresses for various services within the defined subnet.\n\n| Parameter                   | Example Value     | Description                                                  |\n| --------------------------- | ----------------- | ------------------------------------------------------------ |\n| `DOCKER_BRIDGE_SUBNET`      | `'172.28.0.0/16'` | The subnet for the Docker network.                           |\n| `DOCKER_BRIDGE_GATEWAY`     | Generated IP      | The gateway IP address within the Docker subnet.             |\n| `[SERVICE]_NETWORK_ADDRESS` | Generated IP      | The network IP address for a specific service (e.g., MYSQL, MONGO, REDIS, etc.) within the Docker subnet. |\n\n###  2.3. <a name='OpenIMConfiguration'></a>OpenIM Configuration\n\n**Description**: OpenIM configuration involves setting up directories for data, installation, configuration, and logs. It also involves configuring the OpenIM server address and ports for WebSocket and API.\n\n| Parameter               | Default Value            | Description                               |\n| ----------------------- | ------------------------ | ----------------------------------------- |\n| `OPENIM_DATA_DIR`       | `\"/data/openim\"`         | Directory for OpenIM data.                |\n| `OPENIM_INSTALL_DIR`    | `\"/opt/openim\"`          | Directory where OpenIM is installed.      |\n| `OPENIM_CONFIG_DIR`     | `\"/etc/openim\"`          | Directory for OpenIM configuration files. |\n| `OPENIM_LOG_DIR`        | `\"/var/log/openim\"`      | Directory for OpenIM logs.                |\n| `OPENIM_SERVER_ADDRESS` | Docker Bridge Gateway IP | OpenIM server address.                    |\n| `OPENIM_WS_PORT`        | `'10001'`                | Port for OpenIM WebSocket.                |\n| `API_OPENIM_PORT`       | `'10002'`                | Port for OpenIM API.                      |\n\n###  2.4. <a name='OpenIMChatConfiguration'></a>OpenIM Chat Configuration\n\n**Description**: Configuration for OpenIM chat, including data directory, server address, and ports for API and chat functionalities.\n\n| Parameter               | Example Value              | Description                     |\n| ----------------------- | -------------------------- | ------------------------------- |\n| `OPENIM_CHAT_DATA_DIR`  | `\"./openim-chat/[BRANCH]\"` | Directory for OpenIM chat data. |\n| `OPENIM_CHAT_ADDRESS`   | Docker Bridge Gateway IP   | OpenIM chat service address.    |\n| `OPENIM_CHAT_API_PORT`  | `\"10008\"`                  | Port for OpenIM chat API.       |\n| `OPENIM_ADMIN_API_PORT` | `\"10009\"`                  | Port for OpenIM Admin API.      |\n| `OPENIM_ADMIN_PORT`     | `\"30200\"`                  | Port for OpenIM chat Admin.     |\n| `OPENIM_CHAT_PORT`      | `\"30300\"`                  | Port for OpenIM chat.           |\n\n###  2.5. <a name='ZookeeperConfiguration'></a>Zookeeper Configuration\n\n**Description**: Configuration for Zookeeper, including schema, port, address, and credentials.\n\n| Parameter            | Example Value            | Description             |\n| -------------------- | ------------------------ | ----------------------- |\n| `ZOOKEEPER_SCHEMA`   | `\"openim\"`               | Schema for Zookeeper.   |\n| `ZOOKEEPER_PORT`     | `\"12181\"`                | Port for Zookeeper.     |\n| `ZOOKEEPER_ADDRESS`  | Docker Bridge Gateway IP | Address for Zookeeper.  |\n| `ZOOKEEPER_USERNAME` | `\"\"`                     | Username for Zookeeper. |\n| `ZOOKEEPER_PASSWORD` | `\"\"`                     | Password for Zookeeper. |\n\n###  2.7. <a name='MongoDBConfiguration'></a>MongoDB Configuration\n\nThis section involves setting up MongoDB, including its port, address, and credentials.\n\n\n| Parameter      | Example Value  | Description             |\n| -------------- | -------------- | ----------------------- |\n| MONGO_PORT     | \"27017\"        | Port used by MongoDB.   |\n| MONGO_ADDRESS  | [Generated IP] | IP address for MongoDB. |\n| MONGO_USERNAME | [User Defined] | Admin Username for MongoDB.   |\n| MONGO_PASSWORD | [User Defined] | Admin Password for MongoDB.   |\n| MONGO_OPENIM_USERNAME | [User Defined] | OpenIM Username for MongoDB.   |\n| MONGO_OPENIM_PASSWORD | [User Defined] | OpenIM Password for MongoDB.   |\n\n###  2.8. <a name='TencentCloudCOSConfiguration'></a>Tencent Cloud COS Configuration\n\nThis section involves setting up Tencent Cloud COS, including its bucket URL and credentials.\n\n| Parameter         | Example Value                                                | Description                          |\n| ----------------- | ------------------------------------------------------------ | ------------------------------------ |\n| COS_BUCKET_URL    | \"[https://temp-1252357374.cos.ap-chengdu.myqcloud.com](https://temp-1252357374.cos.ap-chengdu.myqcloud.com/)\" | Tencent Cloud COS bucket URL.        |\n| COS_SECRET_ID     | [User Defined]                                               | Secret ID for Tencent Cloud COS.     |\n| COS_SECRET_KEY    | [User Defined]                                               | Secret key for Tencent Cloud COS.    |\n| COS_SESSION_TOKEN | [User Defined]                                               | Session token for Tencent Cloud COS. |\n| COS_PUBLIC_READ   | \"false\"                                                      | Public read access.                  |\n\n###  2.9. <a name='AlibabaCloudOSSConfiguration'></a>Alibaba Cloud OSS Configuration\n\nThis section involves setting up Alibaba Cloud OSS, including its endpoint, bucket name, and credentials.\n\n| Parameter             | Example Value                                                | Description                              |\n| --------------------- | ------------------------------------------------------------ | ---------------------------------------- |\n| OSS_ENDPOINT          | \"[https://oss-cn-chengdu.aliyuncs.com](https://oss-cn-chengdu.aliyuncs.com/)\" | Endpoint URL for Alibaba Cloud OSS.      |\n| OSS_BUCKET            | \"demo-9999999\"                                               | Bucket name for Alibaba Cloud OSS.       |\n| OSS_BUCKET_URL        | \"[https://demo-9999999.oss-cn-chengdu.aliyuncs.com](https://demo-9999999.oss-cn-chengdu.aliyuncs.com/)\" | Bucket URL for Alibaba Cloud OSS.        |\n| OSS_ACCESS_KEY_ID     | [User Defined]                                               | Access key ID for Alibaba Cloud OSS.     |\n| OSS_ACCESS_KEY_SECRET | [User Defined]                                               | Access key secret for Alibaba Cloud OSS. |\n| OSS_SESSION_TOKEN     | [User Defined]                                               | Session token for Alibaba Cloud OSS.     |\n| OSS_PUBLIC_READ       | \"false\"                                                      | Public read access.                      |\n\n###  2.10. <a name='RedisConfiguration'></a>Redis Configuration\n\nThis section involves setting up Redis, including its port, address, and credentials.\n\n| Parameter      | Example Value              | Description           |\n| -------------- | -------------------------- | --------------------- |\n| REDIS_PORT     | \"16379\"                    | Port used by Redis.   |\n| REDIS_ADDRESS  | \"${DOCKER_BRIDGE_GATEWAY}\" | IP address for Redis. |\n| REDIS_USERNAME | [User Defined]             | Username for Redis.   |\n| REDIS_PASSWORD | \"${PASSWORD}\"              | Password for Redis.   |\n\n###  2.11. <a name='KafkaConfiguration'></a>Kafka Configuration\n\nThis section involves setting up Kafka, including its port, address, credentials, and topics.\n\n| Parameter                    | Example Value              | Description                         |\n| ---------------------------- | -------------------------- | ----------------------------------- |\n| KAFKA_USERNAME               | [User Defined]             | Username for Kafka.                 |\n| KAFKA_PASSWORD               | [User Defined]             | Password for Kafka.                 |\n| KAFKA_PORT                   | \"19094\"                    | Port used by Kafka.                 |\n| KAFKA_ADDRESS                | \"${DOCKER_BRIDGE_GATEWAY}\" | IP address for Kafka.               |\n| KAFKA_LATESTMSG_REDIS_TOPIC  | \"latestMsgToRedis\"         | Topic for latest message to Redis.  |\n| KAFKA_OFFLINEMSG_MONGO_TOPIC | \"offlineMsgToMongoMysql\"   | Topic for offline message to Mongo. |\n| KAFKA_MSG_PUSH_TOPIC         | \"msgToPush\"                | Topic for message to push.          |\n| KAFKA_CONSUMERGROUPID_REDIS  | \"redis\"                    | Consumer group ID to Redis.         |\n| KAFKA_CONSUMERGROUPID_MONGO  | \"mongo\"                    | Consumer group ID to Mongo.         |\n| KAFKA_CONSUMERGROUPID_MYSQL  | \"mysql\"                    | Consumer group ID to MySQL.         |\n| KAFKA_CONSUMERGROUPID_PUSH   | \"push\"                     | Consumer group ID to push.          |\n\nNote: Ensure to replace placeholder values (like [User Defined], `${DOCKER_BRIDGE_GATEWAY}`, and `${PASSWORD}`) with actual values before deploying the configuration.\n\n\n\n###  2.12. <a name='OpenIMWebConfiguration'></a>OpenIM Web Configuration\n\nThis section involves setting up OpenIM Web, including its port, address, and dist path.\n\n| Parameter            | Example Value              | Description               |\n| -------------------- | -------------------------- | ------------------------- |\n| OPENIM_WEB_PORT      | \"11001\"                    | Port used by OpenIM Web.  |\n| OPENIM_WEB_ADDRESS   | \"${DOCKER_BRIDGE_GATEWAY}\" | Address for OpenIM Web.   |\n| OPENIM_WEB_DIST_PATH | \"/app/dist\"                | Dist path for OpenIM Web. |\n\n###  2.13. <a name='RPCConfiguration'></a>RPC Configuration\n\nConfiguration for RPC, including the register and listen IP.\n\n| Parameter       | Example Value  | Description          |\n| --------------- | -------------- | -------------------- |\n| RPC_REGISTER_IP | [User Defined] | Register IP for RPC. |\n| RPC_LISTEN_IP   | \"0.0.0.0\"      | Listen IP for RPC.   |\n\n###  2.14. <a name='PrometheusConfiguration'></a>Prometheus Configuration\n\nSetting up Prometheus, including its port and address.\n\n| Parameter          | Example Value              | Description              |\n| ------------------ | -------------------------- | ------------------------ |\n| PROMETHEUS_PORT    | \"19090\"                    | Port used by Prometheus. |\n| PROMETHEUS_ADDRESS | \"${DOCKER_BRIDGE_GATEWAY}\" | Address for Prometheus.  |\n\n###  2.15. <a name='GrafanaConfiguration'></a>Grafana Configuration\n\nConfiguration for Grafana, including its port and address.\n\n| Parameter       | Example Value              | Description           |\n| --------------- | -------------------------- | --------------------- |\n| GRAFANA_PORT    | \"13000\"                     | Port used by Grafana. |\n| GRAFANA_ADDRESS | \"${DOCKER_BRIDGE_GATEWAY}\" | Address for Grafana.  |\n\n###  2.16. <a name='RPCPortConfigurationVariables'></a>RPC Port Configuration Variables\n\nConfiguration for various RPC ports. Note: For launching multiple programs, just fill in multiple ports separated by commas. Try not to have spaces.\n\n| Parameter                   | Example Value | Description                         |\n| --------------------------- | ------------- | ----------------------------------- |\n| OPENIM_USER_PORT            | '10110'       | OpenIM User Service Port.           |\n| OPENIM_FRIEND_PORT          | '10120'       | OpenIM Friend Service Port.         |\n| OPENIM_MESSAGE_PORT         | '10130'       | OpenIM Message Service Port.        |\n| OPENIM_MESSAGE_GATEWAY_PORT | '10140'       | OpenIM Message Gateway Service Port |\n| OPENIM_GROUP_PORT           | '10150'       | OpenIM Group Service Port.          |\n| OPENIM_AUTH_PORT            | '10160'       | OpenIM Authorization Service Port.  |\n| OPENIM_PUSH_PORT            | '10170'       | OpenIM Push Service Port.           |\n| OPENIM_CONVERSATION_PORT    | '10180'       | OpenIM Conversation Service Port.   |\n| OPENIM_THIRD_PORT           | '10190'       | OpenIM Third-Party Service Port.    |\n\n###  2.17. <a name='RPCRegisterNameConfiguration'></a>RPC Register Name Configuration\n\nThis section involves setting up the RPC Register Names for various OpenIM services.\n\n| Parameter                   | Example Value    | Description                         |\n| --------------------------- | ---------------- | ----------------------------------- |\n| OPENIM_USER_NAME            | \"User\"           | OpenIM User Service Name            |\n| OPENIM_FRIEND_NAME          | \"Friend\"         | OpenIM Friend Service Name          |\n| OPENIM_MSG_NAME             | \"Msg\"            | OpenIM Message Service Name         |\n| OPENIM_PUSH_NAME            | \"Push\"           | OpenIM Push Service Name            |\n| OPENIM_MESSAGE_GATEWAY_NAME | \"MessageGateway\" | OpenIM Message Gateway Service Name |\n| OPENIM_GROUP_NAME           | \"Group\"          | OpenIM Group Service Name           |\n| OPENIM_AUTH_NAME            | \"Auth\"           | OpenIM Authorization Service Name   |\n| OPENIM_CONVERSATION_NAME    | \"Conversation\"   | OpenIM Conversation Service Name    |\n| OPENIM_THIRD_NAME           | \"Third\"          | OpenIM Third-Party Service Name     |\n\n###  2.18. <a name='LogConfiguration'></a>Log Configuration\n\nThis section involves configuring the log settings, including storage location, rotation time, and log level.\n\n| Parameter                 | Example Value            | Description                       |\n| ------------------------- | ------------------------ | --------------------------------- |\n| LOG_STORAGE_LOCATION      | \"${OPENIM_ROOT}/_output/logs/\" | Location for storing logs         |\n| LOG_ROTATION_TIME         | \"24\"                     | Log rotation time (in hours)      |\n| LOG_REMAIN_ROTATION_COUNT | \"2\"                      | Number of log rotations to retain |\n| LOG_REMAIN_LOG_LEVEL      | \"6\"                      | Log level to retain               |\n| LOG_IS_STDOUT             | \"false\"                  | Output log to standard output     |\n| LOG_IS_JSON               | \"false\"                  | Log in JSON format                |\n| LOG_WITH_STACK            | \"false\"                  | Include stack info in logs        |\n\n###  2.19. <a name='AdditionalConfigurationVariables'></a>Additional Configuration Variables\n\nThis section involves setting up additional configuration variables for Websocket, Push Notifications, and Chat.\n\n| Parameter               | Example Value     | Description                      |\n|-------------------------|-------------------|----------------------------------|\n| WEBSOCKET_MAX_CONN_NUM  | \"100000\"          | Maximum Websocket connections    |\n| WEBSOCKET_MAX_MSG_LEN   | \"4096\"            | Maximum Websocket message length |\n| WEBSOCKET_TIMEOUT       | \"10\"              | Websocket timeout                |\n| PUSH_ENABLE             | \"getui\"           | Push notification enable status  |\n| GETUI_PUSH_URL          | [Generated URL]   | GeTui Push Notification URL      |\n| GETUI_MASTER_SECRET     | [User Defined]    | GeTui Master Secret              |\n| GETUI_APP_KEY           | [User Defined]    | GeTui Application Key            |\n| GETUI_INTENT            | [User Defined]    | GeTui Push Intent                |\n| GETUI_CHANNEL_ID        | [User Defined]    | GeTui Channel ID                 |\n| GETUI_CHANNEL_NAME      | [User Defined]    | GeTui Channel Name               |\n| FCM_SERVICE_ACCOUNT     | \"x.json\"          | FCM Service Account              |\n| JPUSH_APP_KEY            | [User Defined]    | JPUSH Application Key             |\n| JPUSH_MASTER_SECRET      | [User Defined]    | JPUSH Master Secret               |\n| JPUSH_PUSH_URL           | [User Defined]    | JPUSH Push Notification URL       |\n| JPUSH_PUSH_INTENT        | [User Defined]    | JPUSH Push Intent                 |\n| IM_ADMIN_USERID         | \"imAdmin\"         | IM Administrator ID              |\n| IM_ADMIN_NAME           | \"imAdmin\"         | IM Administrator Nickname        |\n| MULTILOGIN_POLICY       | \"1\"               | Multi-login Policy               |\n| CHAT_PERSISTENCE_MYSQL  | \"true\"            | Chat Persistence in MySQL        |\n| MSG_CACHE_TIMEOUT       | \"86400\"           | Message Cache Timeout            |\n| GROUP_MSG_READ_RECEIPT  | \"true\"            | Group Message Read Receipt Enable |\n| SINGLE_MSG_READ_RECEIPT | \"true\"            | Single Message Read Receipt Enable |\n| RETAIN_CHAT_RECORDS     | \"365\"             | Retain Chat Records (in days)    |\n| CHAT_RECORDS_CLEAR_TIME | [Cron Expression] | Chat Records Clear Time          |\n| MSG_DESTRUCT_TIME       | [Cron Expression] | Message Destruct Time            |\n| SECRET                  | \"${PASSWORD}\"     | Secret Key                       |\n| TOKEN_EXPIRE            | \"90\"              | Token Expiry Time                |\n| FRIEND_VERIFY           | \"false\"           | Friend Verification Enable       |\n| IOS_PUSH_SOUND          | \"xxx\"             | iOS                              |\n| CALLBACK_ENABLE         | \"false\"            | Enable callback                  | \n| CALLBACK_TIMEOUT        | \"5\"               | Maximum timeout for callback call |\n| CALLBACK_FAILED_CONTINUE| \"true\"            | fails to continue to the next step |\n###  2.20. <a name='PrometheusConfiguration-1'></a>Prometheus Configuration\n\nThis section involves configuring Prometheus, including enabling/disabling it and setting up ports for various services.\n\n####  2.20.1. <a name='GeneralConfiguration'></a>General Configuration\n\n| Parameter           | Example Value | Description                   |\n| ------------------- | ------------- | ----------------------------- |\n| `PROMETHEUS_ENABLE` | \"false\"       | Whether to enable Prometheus. |\n\n####  2.20.2. <a name='Service-SpecificPrometheusPorts'></a>Service-Specific Prometheus Ports\n\n| Service                  | Parameter                | Default Port Value           | Description                                        |\n| ------------------------ | ------------------------ | ---------------------------- | -------------------------------------------------- |\n| User Service             | `USER_PROM_PORT`         | '20110'                      | Prometheus port for the User service.              |\n| Friend Service           | `FRIEND_PROM_PORT`       | '20120'                      | Prometheus port for the Friend service.            |\n| Message Service          | `MESSAGE_PROM_PORT`      | '20130'                      | Prometheus port for the Message service.           |\n| Message Gateway          | `MSG_GATEWAY_PROM_PORT`  | '20140'                      | Prometheus port for the Message Gateway.           |\n| Group Service            | `GROUP_PROM_PORT`        | '20150'                      | Prometheus port for the Group service.             |\n| Auth Service             | `AUTH_PROM_PORT`         | '20160'                      | Prometheus port for the Auth service.              |\n| Push Service             | `PUSH_PROM_PORT`         | '20170'                      | Prometheus port for the Push service.              |\n| Conversation Service     | `CONVERSATION_PROM_PORT` | '20230'                      | Prometheus port for the Conversation service.      |\n| RTC Service              | `RTC_PROM_PORT`          | '21300'                      | Prometheus port for the RTC service.               |\n| Third Service            | `THIRD_PROM_PORT`        | '21301'                      | Prometheus port for the Third service.             |\n| Message Transfer Service | `MSG_TRANSFER_PROM_PORT` | '21400, 21401, 21402, 21403' | Prometheus ports for the Message Transfer service. |\n\n\n###  2.21. <a name='QiniuCloudKODOConfiguration'></a>Qiniu Cloud Kodo Configuration\n\nThis section involves setting up Qiniu Cloud Kodo, including its endpoint, bucket name, and credentials.\n\n| Parameter             | Example Value                                                | Description                              |\n| --------------------- | ------------------------------------------------------------ | ---------------------------------------- |\n| KODO_ENDPOINT          | \"[http://s3.cn-east-1.qiniucs.com](http://s3.cn-east-1.qiniucs.com)\" | Endpoint URL for Qiniu Cloud Kodo.      |\n| KODO_BUCKET            | \"demo-9999999\"                                               | Bucket name for Qiniu Cloud Kodo.       |\n| KODO_BUCKET_URL        | \"[http://your.domain.com](http://your.domain.com)\" | Bucket URL for Qiniu Cloud Kodo.        |\n| KODO_ACCESS_KEY_ID     | [User Defined]                                               | Access key ID for Qiniu Cloud Kodo.     |\n| KODO_ACCESS_KEY_SECRET | [User Defined]                                               | Access key secret for Qiniu Cloud Kodo. |\n| KODO_SESSION_TOKEN     | [User Defined]                                               | Session token for Qiniu Cloud Kodo.     |\n| KODO_PUBLIC_READ       | \"false\"                                                      | Public read access.                      |\n"
  },
  {
    "path": "docs/contrib/error-code.md",
    "content": "## Error Code Standards\n\nError codes are one of the important means for users to locate and solve problems. When an application encounters an exception, users can quickly locate and resolve the problem based on the error code and the description and solution of the error code in the documentation.\n\n### Error Code Naming Standards\n\n- Follow CamelCase notation;\n- Error codes are divided into two levels. For example, `InvalidParameter.BindError`, separated by a `.`. The first-level error code is platform-level, and the second-level error code is resource-level, which can be customized according to the scenario;\n- The second-level error code can only use English letters or numbers ([a-zA-Z0-9]), and should use standard English word spelling, standard abbreviations, RFC term abbreviations, etc.;\n- The error code should avoid multiple definitions of the same semantics, for example: `InvalidParameter.ErrorBind`, `InvalidParameter.BindError`.\n\n### First-Level Common Error Codes\n\n| Error Code       | Error Description                                            | Error Type |\n| ---------------- | ------------------------------------------------------------ | ---------- |\n| InternalError    | Internal error                                               | 1          |\n| InvalidParameter | Parameter error (including errors in parameter type, format, value, etc.) | 0          |\n| AuthFailure      | Authentication / Authorization error                         | 0          |\n| ResourceNotFound | Resource does not exist                                      | 0          |\n| FailedOperation  | Operation failed                                             | 2          |\n\n> Error Type: 0 represents the client, 1 represents the server, 2 represents both the client / server."
  },
  {
    "path": "docs/contrib/git-workflow.md",
    "content": "# Git workflows\n\nThis document is an overview of OpenIM git workflow. It includes conventions, tips, and how to maintain good repository hygiene.\n\n- [Git workflows](#git-workflows)\n  - [Branching model](#branching-model)\n    - [Branch naming conventions](#branch-naming-conventions)\n    - [Backport policy](#backport-policy)\n  - [Git operations](#git-operations)\n    - [Setting up](#setting-up)\n    - [Branching out](#branching-out)\n    - [Keeping local branches in sync](#keeping-local-branches-in-sync)\n    - [Pushing changes](#pushing-changes)\n\n## Branching model\n\nOpenIM project uses the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) as its branching model, where most of the changes come from repositories forks instead of branches within the same one.\n\n### Branch naming conventions\n\nEvery forked repository works independently, meaning that any contributor can create branches with the name they see fit. However, it is worth noting that OpenIM mirrors [OpenIM version skew policy](https://github.com/openimsdk/open-im-server/releases) by maintaining release branches for the most recent three minor releases. The only exception is that the main branch mirrors the latest OpenIM release (3.10) instead of using a `release-` prefixed one.\n\n```text\nmain          -------------------------------------------. (OpenIM 3.10)\nrelease-3.0.0            \\---------------|---------------. (OpenIM 3.00)\nrelease-2.4.0                            \\---------------. (OpenIM 2.40)\n```\n\n\n### Backport policy\n\nAll new work happens on the main branch, which means that for most cases, one should branch out from there and create the pull request against it. If the change involves adding a feature or patching OpenIM, the maintainers will backport it into the supported release branches.\n\n## Git operations\n\nThere are everyday tasks related to git that every contributor needs to perform, and this section elaborates on them.\n\n### Setting up\n\nCreating a OpenIM fork, cloning it, and setting its upstream remote can be summarized on:\n\n1. Visit <https://github.com/openimsdk/open-im-server>\n2. Click the `Fork` button (top right) to establish a cloud-based fork\n3. Clone fork to local storage\n4. Add to your fork OpenIM remote as upstream\n\nOnce cloned, in code it would look this way:\n\n```sh\n## Clone fork to local storage\nexport user=\"your github profile name\"\ngit clone https://github.com/$user/OpenIM.git\n# or: git clone git@github.com:$user/OpenIM.git\n\n## Add OpenIM as upstream to your fork\ncd OpenIM \ngit remote add upstream https://github.com/openimsdk/open-im-server.git\n# or: git remote add upstream git@github.com:openimsdk/open-im-server.git\n\n## Ensure to never push to upstream directly\ngit remote set-url --push upstream no_push\n\n## Confirm that your remotes make sense:\ngit remote -v\n```\n\n### Branching out\n\nEvery time one wants to work on a new OpenIM feature, we do:\n\n1. Get local main branch up to date\n2. Create a new branch from the main one (i.e.: myfeature branch )\n\nIn code it would look this way:\n\n```sh\n## Get local main up to date\n# Assuming the OpenIM clone is the current working directory\ngit fetch upstream\ngit checkout main\ngit rebase upstream/main\n\n## Create a new branch from main\ngit checkout -b myfeature\n```\n\n### Keeping local branches in sync\n\nEither when branching out from main or a release one, keep in mind it is worth checking if any change has been pushed upstream by doing:\n\n```sh\ngit fetch upstream\ngit rebase upstream/main\n```\n\nIt is suggested to `fetch` then `rebase` instead of `pull` since the latter does a merge, which leaves merge commits. For this, one can consider changing the local repository configuration by doing `git config branch.autoSetupRebase always` to change the behavior of `git pull`, or another non-merge option such as `git pull --rebase`.\n\n### Pushing changes\n\nFor commit messages and signatures please refer to the [CONTRIBUTING.md](../../CONTRIBUTING.md) document.\n\nNobody should push directly to upstream, even if one has such contributor access; instead, prefer [Github's pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) mechanism to contribute back into OpenIM. For expectations and guidelines about pull requests, consult the [CONTRIBUTING.md](../../CONTRIBUTING.md) document.\n"
  },
  {
    "path": "docs/contrib/gitcherry-pick.md",
    "content": "# Git Cherry-Pick Guide\n\n- Git Cherry-Pick Guide\n  - [Introduction](#introduction)\n  - [What is git cherry-pick?](#what-is-git-cherry-pick)\n  - [Using git cherry-pick](#using-git-cherry-pick)\n  - [Applying Multiple Commits](#applying-multiple-commits)\n  - [Configurations](#configurations)\n  - [Handling Conflicts](#handling-conflicts)\n  - [Applying Commits from Another Repository](#applying-commits-from-another-repository)\n\n## Introduction\n\nAuthor: @cubxxw\n\nAs OpenIM has progressively embarked on a standardized path, I've had the honor of initiating a significant project, `git cherry-pick`. While some may see it as merely a naming convention in the Go language, it represents more. It's a thoughtful design within the OpenIM project, my very first conscious design, and a first in laying out an extensive collaboration process and copyright management with goals of establishing a top-tier community standard.\n\n## What is git cherry-pick?\n\nIn multi-branch repositories, transferring commits from one branch to another is common. You can either merge all changes from one branch (using `git merge`) or selectively apply certain commits. This selective application of commits is where `git cherry-pick` comes into play.\n\nOur collaboration strategy with GitHub necessitates maintenance of multiple `release-v*` branches alongside the `main` branch. To manage this, we mainly develop on the `main` branch and selectively merge into `release-v*` branches. This ensures the `main` branch stays current while the `release-v*` branches remain stable.\n\nEnsuring this strategy's success extends beyond just documentation; it hinges on well-engineered solutions and automation tools, like Makefile, powerful CI/CD processes, and even Prow.\n\n## Prerequisites\n\n- [Contributor License Agreement](https://github.com/openim-sigs/cla) is considered implicit for all code within cherry pick pull requests, **unless there is a large conflict**.\n- A pull request merged against the `main` branch.\n- The release branch exists (example: [`release-1.18`](https://github.com/openimsdk/open-im-server/tree/release-v3.1))\n- The normal git and GitHub configured shell environment for pushing to your openim-server `origin` fork on GitHub and making a pull request against a configured remote `upstream` that tracks `https://github.com/openimsdk/open-im-server.git`, including `GITHUB_USER`.\n- Have GitHub CLI (`gh`) installed following [installation instructions](https://github.com/cli/cli#installation).\n- A github personal access token which has permissions \"repo\" and \"read:org\". Permissions are required for [gh auth login](https://cli.github.com/manual/gh_auth_login) and not used for anything unrelated to cherry-pick creation process (creating a branch and initiating PR).\n\n## What Kind of PRs are Good for Cherry Picks\n\nCompared to the normal main branch's merge volume across time, the release branches see one or two orders of magnitude less PRs. This is because there is an order or two of magnitude higher scrutiny. Again, the emphasis is on critical bug fixes, e.g.,\n\n- Loss of data\n- Memory corruption\n- Panic, crash, hang\n- Security\n\nA bugfix for a functional issue (not a data loss or security issue) that only affects an alpha feature does not qualify as a critical bug fix.\n\nIf you are proposing a cherry pick and it is not a clear and obvious critical bug fix, please reconsider. If upon reflection you wish to continue, bolster your case by supplementing your PR with e.g.,\n\n- A GitHub issue detailing the problem\n- Scope of the change\n- Risks of adding a change\n- Risks of associated regression\n- Testing performed, test cases added\n- Key stakeholder SIG reviewers/approvers attesting to their confidence in the change being a required backport\n\nIf the change is in cloud provider-specific platform code (which is in the process of being moved out of core openim-server), describe the customer impact, how the issue escaped initial testing, remediation taken to prevent similar future escapes, and why the change cannot be carried in your downstream fork of the openim-server project branches.\n\nIt is critical that our full community is actively engaged on enhancements in the project. If a released feature was not enabled on a particular provider's platform, this is a community miss that needs to be resolved in the `main` branch for subsequent releases. Such enabling will not be backported to the patch release branches.\n\n## Initiate a Cherry Pick\n\n### Before you begin\n\n- Plan to initiate a cherry-pick against *every* supported release branch. If you decide to skip some release branch, explain your decision in a comment to the PR being cherry-picked.\n- Initiate cherry-picks in order, from newest to oldest supported release branches. For example, if 3.1 is the newest supported release branch, then, before cherry-picking to 2.25, make sure the cherry-pick PR already exists for in 2.26 and 3.1. This helps to prevent regressions as a result of an upgrade to the next release.\n\n### Steps\n\n- Run the [cherry pick script](https://github.com/openimsdk/open-im-server/tree/main/scripts/cherry-pick.sh)\n\n  This example applies a main branch PR #98765 to the remote branch `upstream/release-v3.1`:\n\n  ```\n  scripts/cherry-pick.sh upstream/release-v3.1 98765\n  ```\n\n  - Be aware the cherry pick script assumes you have a git remote called `upstream` that points at the openim-server github org.\n\n    Please see our [recommended Git workflow](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/github-workflow.md#workflow).\n\n  - You will need to run the cherry pick script separately for each patch release you want to cherry pick to. Cherry picks should be applied to all [active](https://github.com/openimsdk/open-im-server/releases) release branches where the fix is applicable.\n\n  - If `GITHUB_TOKEN` is not set you will be asked for your github password: provide the github [personal access token](https://github.com/settings/tokens) rather than your actual github password. If you can securely set the environment variable `GITHUB_TOKEN` to your personal access token then you can avoid an interactive prompt. Refer [mislav/hub#2655 (comment)](https://github.com/mislav/hub/issues/2655#issuecomment-735836048)\n\n- Your cherry pick PR will immediately get the `do-not-merge/cherry-pick-not-approved` label.\n\n\n## Cherry Pick Review\n\nAs with any other PR, code OWNERS review (`/lgtm`) and approve (`/approve`) on cherry pick PRs as they deem appropriate.\n\nThe same release note requirements apply as normal pull requests, except the release note stanza will auto-populate from the main branch pull request from which the cherry pick originated.\n\n\n## Using git cherry-pick\n\n`git cherry-pick` applies specified commits from one branch to another.\n\n```bash\n$ git cherry-pick <commitHash>\n```\n\nAs an example, consider a repository with `main` and `release-v3.1` branches. To apply commit `f` from the `release-v3.1` branch to the `main` branch:\n\n```\n# Switch to main branch\n$ git checkout main\n\n# Perform cherry-pick\n$ git cherry-pick f\n```\n\nYou can also use a branch name instead of a commit hash to cherry-pick the latest commit from that branch.\n\n```bash\n$ git cherry-pick release-v3.1\n```\n\n## Applying Multiple Commits\n\nTo apply multiple commits simultaneously:\n\n```bash\n$ git cherry-pick <HashA> <HashB>\n```\n\nTo apply a range of consecutive commits:\n\n```bash\n$ git cherry-pick <HashA>..<HashB>\n```\n\n## Configurations\n\nHere are some commonly used configurations for `git cherry-pick`:\n\n- **`-e`, `--edit`**: Open an external editor to modify the commit message.\n- **`-n`, `--no-commit`**: Update the working directory and staging area without creating a new commit.\n- **`-x`**: Append a reference in the commit message for tracking the source of the cherry-picked commit.\n- **`-s`, `--signoff`**: Add a sign-off message at the end of the commit indicating who performed the cherry-pick.\n- **`-m parent-number`, `--mainline parent-number`**: When the original commit is a merge of two branches, specify which parent branch's changes should be used.\n\n## Handling Conflicts\n\nIf conflicts arise during the cherry-pick:\n\n- **`--continue`**: After resolving conflicts, stage the changes with `git add .` and then continue the cherry-pick process.\n- **`--abort`**: Abandon the cherry-pick and revert to the previous state.\n- **`--quit`**: Exit the cherry-pick without reverting to the previous state.\n\n## Applying Commits from Another Repository\n\nYou can also cherry-pick commits from another repository:\n\n1. Add the external repository as a remote:\n\n   ```\n   $ git remote add target git://gitUrl\n   ```\n\n2. Fetch the commits from the remote:\n\n   ```\n   $ git fetch target\n   ```\n\n3. Identify the commit hash you wish to cherry-pick:\n\n   ```\n   $ git log target/main\n   ```\n\n4. Perform the cherry-pick:\n\n   ```\n   $ git cherry-pick <commitHash>\n   ```"
  },
  {
    "path": "docs/contrib/github-workflow.md",
    "content": "---\ntitle: \"GitHub Workflow\"\nweight: 6\ndescription: |\n  This document is an overview of the GitHub workflow used by the\n  open-im-server project. It includes tips and suggestions on keeping your\n  local environment in sync with upstream and how to maintain good\n  commit hygiene.\n---\n\n\n## 1. Fork in the cloud\n\n1. Visit https://github.com/openimsdk/open-im-server\n2. Click `Fork` button (top right) to establish a cloud-based fork.\n\n## 2. Clone fork to local storage\n\nPer Go's [workspace instructions][go-workspace], place open-im-server' code on your\n`GOPATH` using the following cloning procedure.\n\n[go-workspace]: https://golang.org/doc/code.html#Workspaces\n\nIn your shell, define a local working directory as `working_dir`. If your `GOPATH` has multiple paths, pick\njust one and use it instead of `$GOPATH`. You must follow exactly this pattern,\nneither `$GOPATH/src/github.com/${your github profile name}/`\nnor any other pattern will work.\n\n```sh\nexport working_dir=\"$(go env GOPATH)/src/github.com/openimsdk\"\n```\n\nIf you already do Go development on github, the `github.com/openimsdk` directory\nwill be a sibling to your existing `github.com` directory.\n\nSet `user` to match your github profile name:\n\n```sh\nexport user=<your github profile name>\n```\n\nBoth `$working_dir` and `$user` are mentioned in the figure above.\n\nCreate your clone:\n\n```sh\nmkdir -p $working_dir\ncd $working_dir\ngit clone https://github.com/$user/open-im-server.git\n# or: git clone git@github.com:$user/open-im-server.git\n\ncd $working_dir/open-im-server\ngit remote add upstream https://github.com/openimsdk/open-im-server.git\n# or: git remote add upstream git@github.com:openimsdk/open-im-server.git\n\n# Never push to upstream master\ngit remote set-url --push upstream no_push\n\n# Confirm that your remotes make sense:\ngit remote -v\n```\n\n## 3. Create a Working Branch\n\nGet your local master up to date. Note that depending on which repository you are working from,\nthe default branch may be called \"main\" instead of \"master\".\n\n```sh\ncd $working_dir/open-im-server\ngit fetch upstream\ngit checkout master\ngit rebase upstream/master\n```\n\nCreate your new branch.\n\n```sh\ngit checkout -b myfeature\n```\n\nYou may now edit files on the `myfeature` branch.\n\n### Building open-im-server\n\nThis workflow is process-specific. For quick-start build instructions for [openimsdk/open-im-server](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/util-makefile.md)\n\n## 4. Keep your branch in sync\n\nYou will need to periodically fetch changes from the `upstream`\nrepository to keep your working branch in sync. Note that depending on which repository you are working from,\nthe default branch may be called 'main' instead of 'master'.\n\nMake sure your local repository is on your working branch and run the\nfollowing commands to keep it in sync:\n\n```sh\ngit fetch upstream\ngit rebase upstream/master\n```\n\nPlease don't use `git pull` instead of the above `fetch` and\n`rebase`. Since `git pull` executes a merge, it creates merge commits. These make the commit history messy\nand violate the principle that commits ought to be individually understandable\nand useful (see below). \n\nYou might also consider changing your `.git/config` file via\n`git config branch.autoSetupRebase always` to change the behavior of `git pull`, or another non-merge option such as `git pull --rebase`.\n\n## 5. Commit Your Changes\n\nYou will probably want to regularly commit your changes. It is likely that you will go back and edit,\nbuild, and test multiple times. After a few cycles of this, you might\n[amend your previous commit](https://www.w3schools.com/git/git_amend.asp).\n\n```sh\ngit commit\n```\n\n## 6. Push to GitHub\n\nWhen your changes are ready for review, push your working branch to\nyour fork on GitHub.\n\n```sh\ngit push -f <your_remote_name> myfeature\n```\n\n## 7. Create a Pull Request\n\n1. Visit your fork at `https://github.com/<user>/open-im-server`\n2. Click the **Compare & Pull Request** button next to your `myfeature` branch.\n3. Check out the pull request process for more details and\n   advice.\n\n_If you have upstream write access_, please refrain from using the GitHub UI for\ncreating PRs, because GitHub will create the PR branch inside the main\nrepository rather than inside your fork.\n\n### Get a code review\n\nOnce your pull request has been opened it will be assigned to one or more\nreviewers.  Those reviewers will do a thorough code review, looking for\ncorrectness, bugs, opportunities for improvement, documentation and comments,\nand style.\n\nCommit changes made in response to review comments to the same branch on your\nfork.\n\nVery small PRs are easy to review.  Very large PRs are very difficult to review.\n\n### Squash commits\n\nAfter a review, prepare your PR for merging by squashing your commits.\n\nAll commits left on your branch after a review should represent meaningful milestones or units of work. Use commits to add clarity to the development and review process.\n\nBefore merging a PR, squash the following kinds of commits:\n\n- Fixes/review feedback\n- Typos\n- Merges and rebases\n- Work in progress\n\nAim to have every commit in a PR compile and pass tests independently if you can, but it's not a requirement. In particular, `merge` commits must be removed, as they will not pass tests.\n\nTo squash your commits, perform an [interactive rebase](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History):\n\n1. Check your git branch:\n\n  ```\n  git status\n  ```\n\n  The output should be similar to this:\n\n  ```\n  On branch your-contribution\n  Your branch is up to date with 'origin/your-contribution'.\n  ```\n\n2. Start an interactive rebase using a specific commit hash, or count backwards from your last commit using `HEAD~<n>`, where `<n>` represents the number of commits to include in the rebase.\n\n  ```\n  git rebase -i HEAD~3\n  ```\n\n  The output should be similar to this:\n\n  ```\n  pick 2ebe926 Original commit\n  pick 31f33e9 Address feedback\n  pick b0315fe Second unit of work\n\n  # Rebase 7c34fc9..b0315ff onto 7c34fc9 (3 commands)\n  #\n  # Commands:\n  # p, pick <commit> = use commit\n  # r, reword <commit> = use commit, but edit the commit message\n  # e, edit <commit> = use commit, but stop for amending\n  # s, squash <commit> = use commit, but meld into previous commit\n  # f, fixup <commit> = like \"squash\", but discard this commit's log message\n\n  ...\n\n  ```\n\n3. Use a command line text editor to change the word `pick` to `squash` for the commits you want to squash, then save your changes and continue the rebase:\n\n  ```\n  pick 2ebe926 Original commit\n  squash 31f33e9 Address feedback\n  pick b0315fe Second unit of work\n\n  ...\n\n  ```\n\n  The output after saving changes should look similar to this:\n\n  ```\n  [detached HEAD 61fdded] Second unit of work\n   Date: Thu Mar 5 19:01:32 2020 +0100\n   2 files changed, 15 insertions(+), 1 deletion(-)\n\n   ...\n\n  Successfully rebased and updated refs/heads/master.\n  ```\n4. Force push your changes to your remote branch:\n\n  ```\n  git push --force\n  ```\n\nFor mass automated fixups such as automated doc formatting, use one or more\ncommits for the changes to tooling and a final commit to apply the fixup en\nmasse. This makes reviews easier.\n\nAn alternative to this manual squashing process is to use the Prow and Tide based automation that is configured in GitHub: adding a comment to your PR with `/label tide/merge-method-squash` will trigger the automation so that GitHub squash your commits onto the target branch once the PR is approved. Using this approach simplifies things for those less familiar with Git, but there are situations in where it's better to squash locally; reviewers will have this in mind and can ask for manual squashing to be done.\n\nBy squashing locally, you control the commit message(s) for your work, and can separate a large PR into logically separate changes.\nFor example: you have a pull request that is code complete and has 24 commits. You rebase this against the same merge base, simplifying the change to two commits. Each of those two commits represents a single logical change and each commit message summarizes what changes. Reviewers see that the set of changes are now understandable, and approve your PR.\n\n## Merging a commit\n\nOnce you've received review and approval, your commits are squashed, your PR is ready for merging.\n\nMerging happens automatically after both a Reviewer and Approver have approved the PR. If you haven't squashed your commits, they may ask you to do so before approving a PR.\n\n## Reverting a commit\n\nIn case you wish to revert a commit, use the following instructions.\n\n_If you have upstream write access_, please refrain from using the\n`Revert` button in the GitHub UI for creating the PR, because GitHub\nwill create the PR branch inside the main repository rather than inside your fork.\n\n- Create a branch and sync it with upstream. Note that depending on which repository you are working from, the default branch may be called 'main' instead of 'master'.\n  ```sh\n  # create a branch\n  git checkout -b myrevert\n\n  # sync the branch with upstream\n  git fetch upstream\n  git rebase upstream/master\n  ```\n- If the commit you wish to revert is a *merge commit*, use this command:\n  ```sh\n  # SHA is the hash of the merge commit you wish to revert\n  git revert -m 1 <SHA>\n  ```\n  If it is a *single commit*, use this command:\n  ```sh\n  # SHA is the hash of the single commit you wish to revert\n  git revert <SHA>\n  ```\n\n- This will create a new commit reverting the changes. Push this new commit to your remote.\n  ```sh\n  git push <your_remote_name> myrevert\n  ```\n\n- Finally, [create a Pull Request](#7-create-a-pull-request) using this branch."
  },
  {
    "path": "docs/contrib/go-code.md",
    "content": "## OpenIM development specification\nWe have very high standards for code style and specification, and we want our products to be polished and perfect\n\n## 1. Code style\n\n### 1.1 Code format\n\n- Code must be formatted with `gofmt`.\n- Leave spaces between operators and operands.\n- It is recommended that a line of code does not exceed 120 characters. If the part exceeds, please use an appropriate line break method. But there are also some exception scenarios, such as import lines, code automatically generated by tools, and struct fields with tags.\n- The file length cannot exceed 800 lines.\n- Function length cannot exceed 80 lines.\n- import specification\n- All code must be formatted with `goimports` (it is recommended to set the code Go code editor to: run `goimports` on save).\n- Do not use relative paths to import packages, such as `import ../util/net`.\n- Import aliases must be used when the package name does not match the last directory name of the import path, or when multiple identical package names conflict.\n\n```go\n// bad\n\"github.com/dgrijalva/jwt-go/v4\"\n\n//good\njwt \"github.com/dgrijalva/jwt-go/v4\"\n```\n- Imported packages are suggested to be grouped, and anonymous package references use a new group, and anonymous package references are explained.\n\n```go\nimport (\n  // go standard package\n  \"fmt\"\n  \n  // third party package\n  \"github.com/jinzhu/gorm\"\n  \"github.com/spf13/cobra\"\n  \"github.com/spf13/viper\"\n  \n  // Anonymous packages are grouped separately, and anonymous package references are explained\n  // import mysql driver\n  _ \"github.com/jinzhu/gorm/dialects/mysql\"\n  \n  // inner package\n)\n```\n\n### 1.2 Declaration, initialization and definition\n\nWhen multiple variables need to be used in a function, the `var` declaration can be used at the beginning of the function. Declaration outside the function must use `var`, do not use `:=`, it is easy to step on the scope of the variable.\n\n```go\nvar (\n  Width int\n  Height int\n)\n```\n\n- When initializing a structure reference, please use `&T{}` instead of `new(T)` to make it consistent with structure initialization.\n\n```go\n  // bad\n  sptr := new(T)\n  sptr.Name = \"bar\"\n  \n  // good\n  sptr := &T{Name: \"bar\"}\n```\n\n- The struct declaration and initialization format takes multiple lines and is defined as follows.\n\n```go\n  type User struct{\n       Username string\n       Email string\n  }\n\n  user := User{\n  Username: \"belm\",\n  Email: \"nosbelm@qq.com\",\n}\n```\n\n- Similar declarations are grouped together, and the same applies to constant, variable, and type declarations.\n\n```go\n// bad\nimport \"a\"\nimport \"b\"\n\n//good\nimport (\n   \"a\"\n   \"b\"\n)\n```\n\n- Specify container capacity where possible to pre-allocate memory for the container, for example:\n\n```go\nv := make(map[int]string, 4)\nv := make([]string, 0, 4)\n```\n\n- At the top level, use the standard var keyword. Do not specify a type unless it is different from the type of the expression.\n\n```go\n// bad\nvar s string = F()\n\nfunc F() string { return \"A\" }\n\n// good\nvar s = F()\n// Since F already explicitly returns a string type, we don't need to explicitly specify the type of _s\n// still of that type\n\nfunc F() string { return \"A\" }\n```\n\n- This example emphasizes using PascalCase for exported constants and camelCase for unexported ones, avoiding all caps and underscores.\n\n```go\n// bad\nconst (\n    MAX_COUNT = 100\n    timeout = 30\n)\n\n// good\nconst (\n    MaxCount = 100  // Exported constants should use PascalCase.\n    defaultTimeout = 30  // Unexported constants should use camelCase.\n)\n```\n\n- Grouping related constants enhances organization and readability, especially when there are multiple constants related to a particular feature or configuration.\n\n```go\n// bad\nconst apiVersion = \"v1\"\nconst retryInterval = 5\n\n// good\nconst (\n    ApiVersion    = \"v1\"  // Group related constants together for better organization.\n    RetryInterval = 5\n)\n```\n\n- The \"good\" practice utilizes iota for a clear, concise, and auto-incrementing way to define enumerations, reducing the potential for errors and improving maintainability.\n\n```go\n// bad\nconst (\n    StatusActive   = 0\n    StatusInactive = 1\n    StatusUnknown  = 2\n)\n\n// good\nconst (\n    StatusActive = iota  // Use iota for simple and efficient constant enumerations.\n    StatusInactive\n    StatusUnknown\n)\n```\n\n- Specifying types explicitly improves clarity, especially when the purpose or type of a constant might not be immediately obvious. Additionally, adding comments to exported constants or those whose purpose isn't clear from the name alone can greatly aid in understanding the code.\n\n```go\n// bad\nconst serverAddress = \"localhost:8080\"\nconst debugMode = 1  // Is this supposed to be a boolean or an int?\n\n// good\nconst ServerAddress string = \"localhost:8080\"  // Specify type for clarity.\n// DebugMode indicates if the application should run in debug mode (true for debug mode).\nconst DebugMode bool = true\n```\n\n- By defining a contextKey type and making userIDKey of this type, you avoid potential collisions with other context keys. This approach leverages Go's type system to provide compile-time checks against misuse.\n\n```go\n// bad\nconst userIDKey = \"userID\"\n\n// In this example, userIDKey is a string type, which can lead to conflicts or accidental misuse because string keys are prone to typos and collisions in a global namespace.\n\n\n// good\ntype contextKey string\n\nconst userIDKey contextKey = \"userID\"\n```\n\n\n- Embedded types (such as mutexes) should be at the top of the field list within the struct, and there must be a blank line separating embedded fields from regular fields.\n\n```go\n// bad\ntype Client struct {\n   version int\n   http.Client\n}\n\n//good\ntype Client struct {\n   http.Client\n\n   version int\n}\n```\n\n### 1.3 Error Handling\n\n- `error` is returned as the value of the function, `error` must be handled, or the return value assigned to explicitly ignore. For `defer xx.Close()`, there is no need to explicitly handle it.\n\n```go\nfunc load() error {\n// normal code\n}\n\n// bad\nload()\n\n//good\n  _ = load()\n```\n\n- When `error` is returned as the value of a function and there are multiple return values, `error` must be the last parameter.\n\n```go\n// bad\nfunc load() (error, int) {\n// normal code\n}\n\n//good\nfunc load() (int, error) {\n// normal code\n}\n```\n\n- Perform error handling as early as possible and return as early as possible to reduce nesting.\n\n```go\n// bad\nif err != nil {\n// error code\n} else {\n// normal code\n}\n\n//good\nif err != nil {\n// error handling\nreturn err\n}\n// normal code\n```\n\n- If you need to use the result of the function call outside if, you should use the following method.\n\n```go\n// bad\nif v, err := foo(); err != nil {\n// error handling\n}\n\n// good\nv, err := foo()\nif err != nil {\n// error handling\n}\n```\n\n- Errors should be judged independently, not combined with other logic.\n\n```go\n// bad\nv, err := foo()\nif err != nil || v == nil {\n  // error handling\n  return err\n}\n\n//good\nv, err := foo()\nif err != nil {\n  // error handling\n  return err\n}\n\nif v == nil {\n  // error handling\n  return errors. New(\"invalid value v\")\n}\n```\n\n- If the return value needs to be initialized, use the following method.\n\n```go\nv, err := f()\nif err != nil {\n  // error handling\n  return // or continue.\n}\n```\n\n- Bug description suggestions\n- Error descriptions start with a lowercase letter and do not end with punctuation, for example:\n\n```go\n// bad\nerrors.New(\"Redis connection failed\")\nerrors.New(\"redis connection failed.\")\n\n// good\nerrors.New(\"redis connection failed\")\n```\n\n- Tell users what they can do, not what they can't.\n- When declaring a requirement, use must instead of should. For example, `must be greater than 0, must match regex '[a-z]+'`.\n- When declaring that a format is incorrect, use must not. For example, `must not contain`.\n- Use may not when declaring an action. For example, `may not be specified when otherField is empty, only name may be specified`.\n- When quoting a literal string value, indicate the literal in single quotes. For example, `ust not contain '..'`.\n- When referencing another field name, specify that name in backticks. For example, must be greater than `request`.\n- When specifying unequal, use words instead of symbols. For example, `must be less than 256, must be greater than or equal to 0 (do not use larger than, bigger than, more than, higher than)`.\n- When specifying ranges of numbers, use inclusive ranges whenever possible.\n- Go 1.13 or above is recommended, and the error generation method is `fmt.Errorf(\"module xxx: %w\", err)`.\n\n### 1.4 Panic Processing\n\nThe use of `panic` should be carefully controlled in Go applications to ensure program stability and predictable error handling. Following are revised guidelines emphasizing the restriction on using `panic` and promoting alternative strategies for error handling and program termination.\n\n- **Prohibited in Business Logic:** Using `panic` within business logic processing is strictly prohibited. Business logic should handle errors gracefully and use error returns to propagate issues up the call stack.\n\n- **Restricted Use in Main Package:** In the main package, the use of `panic` should be reserved for situations where the program is entirely inoperable, such as failure to open essential files, inability to connect to the database, or other critical startup issues. Even in these scenarios, prefer using structured error handling to terminate the program.\n\n- **Prohibition on Exportable Interfaces:** Exportable interfaces must not invoke `panic`. They should handle errors gracefully and return errors as part of their contract.\n\n- **Prefer Errors Over Panic:** It is recommended to use error returns instead of panic to convey errors within a package. This approach promotes error handling that integrates smoothly with Go's error handling idioms.\n\n#### Alternative to Panic: Structured Program Termination\n\nTo enforce these guidelines, consider implementing structured functions to terminate the program gracefully in the face of unrecoverable errors, while providing clear error messages. Here are two recommended functions:\n\n```go\n// ExitWithError logs an error message and exits the program with a non-zero status.\nfunc ExitWithError(err error) {\n\tprogName := filepath.Base(os.Args[0])\n\tfmt.Fprintf(os.Stderr, \"%s exit -1: %+v\\n\", progName, err)\n\tos.Exit(-1)\n}\n\n// SIGTERMExit logs a warning message when the program receives a SIGTERM signal and exits with status 0.\nfunc SIGTERMExit() {\n\tprogName := filepath.Base(os.Args[0])\n\tfmt.Fprintf(os.Stderr, \"Warning %s receive process terminal SIGTERM exit 0\\n\", progName)\n}\n```\n\n#### Example Usage:\n\n```go\nimport (\n\t_ \"net/webhook/pprof\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\tutil \"github.com/openimsdk/open-im-server/v3/pkg/util/genutil\"\n)\n\nfunc main() {\n\tapiCmd := cmd.NewApiCmd()\n\tapiCmd.AddPortFlag()\n\tapiCmd.AddPrometheusPortFlag()\n\tif err := apiCmd.Execute(); err != nil {\n\t\tutil.ExitWithError(err)\n\t}\n}\n```\n\nIn this example, `ExitWithError` is used to terminate the program when an unrecoverable error occurs, providing a clear error message to stderr and exiting with a non-zero status. This approach ensures that critical errors are logged and the program exits in a controlled manner, facilitating troubleshooting and maintaining the stability of the application.\n\n\n### 1.5 Unit Tests\n\n- The unit test filename naming convention is `example_test.go`.\n- Write a test case for every important exportable function.\n- Because the functions in the unit test file are not external, the exportable structures, functions, etc. can be uncommented.\n- If `func (b *Bar) Foo` exists, the single test function can be `func TestBar_Foo`.\n\n### 1.6 Type assertion failure handling\n\n- A single return value from a type assertion will panic for an incorrect type. Always use the \"comma ok\" idiom.\n\n```go\n// bad\nt := n.(int)\n\n//good\nt, ok := n.(int)\nif !ok {\n// error handling\n}\n```\n\n## 2. Naming convention\n\nThe naming convention is a very important part of the code specification. A uniform, short, and precise naming convention can greatly improve the readability of the code and avoid unnecessary bugs.\n\n### 2.1 Package Naming\n\n- The package name must be consistent with the directory name, try to use a meaningful and short package name, and do not conflict with the standard library.\n- Package names are all lowercase, without uppercase or underscores, and use multi-level directories to divide the hierarchy.\n- Item names can connect multiple words with dashes.\n- Do not use plurals for the package name and the directory name where the package is located, for example, `net/url` instead of `net/urls`.\n- Don't use broad, meaningless package names like common, util, shared or lib.\n- The package name should be simple and clear, such as net, time, log.\n\n\n### 2.2 Function Naming Conventions\n\nFunction names should adhere to the following guidelines, inspired by OpenIM’s standards and Google’s Go Style Guide:\n\n- Use camel case for function names. Start with an uppercase letter for public functions (`MixedCaps`) and a lowercase letter for private functions (`mixedCaps`).\n- Exceptions to this rule include code automatically generated by tools (e.g., `xxxx.pb.go`) and test functions that use underscores for clarity (e.g., `TestMyFunction_WhatIsBeingTested`).\n\n### 2.3 File and Directory Naming Practices\n\nTo maintain consistency and readability across the OpenIM project, observe the following naming practices:\n\n**File Names:**\n- Use underscores (`_`) as the default separator in filenames, keeping them short and descriptive.\n- Both hyphens (`-`) and underscores (`_`) are allowed, but underscores are preferred for general use.\n\n**Script and Markdown Files:**\n- Prefer hyphens (`-`) for shell scripts and Markdown (`.md`) files to enhance searchability and web compatibility.\n\n**Directories:**\n- Name directories with hyphens (`-`) exclusively to separate words, ensuring consistency and readability.\n\nRemember to keep filenames lowercase and use meaningful, concise identifiers to facilitate better organization and navigation within the project.\n\n\n\n### 2.4 Structure Naming\n\n- The camel case is adopted, and the first letter is uppercase or lowercase according to the access control, such as `MixedCaps` or `mixedCaps`.\n- Struct names should not be verbs, but should be nouns, such as `Node`, `NodeSpec`.\n- Avoid using meaningless structure names such as Data and Info.\n- The declaration and initialization of the structure should take multiple lines, for example:\n\n```go\n// User multi-line declaration\ntype User struct {\n     name string\n     Email string\n}\n\n// multi-line initialization\nu := User{\n     UserName: \"belm\",\n     Email: \"nosbelm@qq.com\",\n}\n```\n\n### 2.5 Interface Naming\n\n- The interface naming rules are basically consistent with the structure naming rules:\n- Interface names of individual functions suffixed with \"er\"\" (e.g. Reader, Writer) can sometimes lead to broken English, but that's okay.\n- The interface name of the two functions is named after the two function names, eg ReadWriter.\n- An interface name for more than three functions, similar to a structure name.\n\nFor example:\n\n```go\n// Seeking to an offset before the start of the file is an error.\n// Seeking to any positive offset is legal, but the behavior of subsequent\n// I/O operations on the underlying object are implementation-dependent.\ntype Seeker interface {\n  Seek(offset int64, whence int) (int64, error)\n}\n\n// ReadWriter is the interface that groups the basic Read and Write methods.\ntype ReadWriter interface {\n  reader\n  Writer\n}\n```\n\n### 2.6 Variable Naming\n\n- Variable names must follow camel case, and the initial letter is uppercase or lowercase according to the access control decision.\n- In relatively simple (few objects, highly targeted) environments, some names can be abbreviated from full words to single letters, for example:\n- user can be abbreviated as u;\n- userID can be abbreviated as uid.\n- When using proper nouns, the following rules need to be followed:\n- If the variable is private and the proper noun is the first word, use lowercase, such as apiClient.\n- In other cases, the original wording of the noun should be used, such as APIClient, repoID, UserID.\n\nSome common nouns are listed below.\n\n```go\n// A GonicMapper that contains a list of common initialisms taken from golang/lint\nvar LintGonicMapper = GonicMapper{\n     \"API\": true,\n     \"ASCII\": true,\n     \"CPU\": true,\n     \"CSS\": true,\n     \"DNS\": true,\n     \"EOF\": true,\n     \"GUID\": true,\n     \"HTML\": true,\n     \"HTTP\": true,\n     \"HTTPS\": true,\n     \"ID\": true,\n     \"IP\": true,\n     \"JSON\": true,\n     \"LHS\": true,\n     \"QPS\": true,\n     \"RAM\": true,\n     \"RHS\": true,\n     \"RPC\": true,\n     \"SLA\": true,\n     \"SMTP\": true,\n     \"SSH\": true,\n     \"TLS\": true,\n     \"TTL\": true,\n     \"UI\": true,\n     \"UID\": true,\n     \"UUID\": true,\n     \"URI\": true,\n     \"URL\": true,\n     \"UTF8\": true,\n     \"VM\": true,\n     \"XML\": true,\n     \"XSRF\": true,\n     \"XSS\": true,\n}\n```\n\n- If the variable type is bool, the name should start with Has, Is, Can or Allow, for example:\n\n```go\nvar hasConflict bool\nvar isExist bool\nvar canManage bool\nvar allowGitHook bool\n```\n\n- Local variables should be as short as possible, for example, use buf to refer to buffer, and use i to refer to index.\n- The code automatically generated by the code generation tool can exclude this rule (such as the Id in `xxx.pb.go`)\n\n### 2.7 Constant Naming\n\nIn Go, constants play a critical role in defining values that do not change throughout the execution of a program. Adhering to best practices in naming constants can significantly improve the readability and maintainability of your code. Here are some guidelines for constant naming:\n\n- **Camel Case Naming:** The name of a constant must follow the camel case notation. The initial letter should be uppercase or lowercase based on the access control requirements. Uppercase indicates that the constant is exported (visible outside the package), while lowercase indicates package-private visibility (visible only within its own package).\n\n- **Enumeration Type Constants:** For constants that represent a set of enumerated values, it's recommended to define a corresponding type first. This approach not only enhances type safety but also improves code readability by clearly indicating the purpose of the enumeration.\n\n**Example:**\n\n```go\n// Code defines an error code type.\ntype Code int\n\n// Internal errors.\nconst (\n     // ErrUnknown - 0: An unknown error occurred.\n     ErrUnknown Code = iota\n     // ErrFatal - 1: A fatal error occurred.\n     ErrFatal\n)\n```\n\nIn the example above, `Code` is defined as a new type based on `int`. The enumerated constants `ErrUnknown` and `ErrFatal` are then defined with explicit comments to indicate their purpose and values. This pattern is particularly useful for grouping related constants and providing additional context.\n\n### Global Variables and Constants Across Packages\n\n- **Use Constants for Global Variables:** When defining variables that are intended to be accessed across packages, prefer using constants to ensure immutability. This practice avoids unintended modifications to the value, which can lead to unpredictable behavior or hard-to-track bugs.\n\n- **Lowercase for Package-Private Usage:** If a global variable or constant is intended for use only within its own package, it should start with a lowercase letter. This clearly signals its limited scope of visibility, adhering to Go's access control mechanism based on naming conventions.\n\n**Guideline:**\n\n- For global constants that need to be accessed across packages, declare them with an uppercase initial letter. This makes them exported, adhering to Go's visibility rules.\n- For constants used within the same package, start their names with a lowercase letter to limit their scope to the package.\n\n**Example:**\n\n```go\npackage config\n\n// MaxConnections - the maximum number of allowed connections. Visible across packages.\nconst MaxConnections int = 100\n\n// minIdleTime - the minimum idle time before a connection is considered stale. Only visible within the config package.\nconst minIdleTime int = 30\n```\n\nIn this example, `MaxConnections` is a global constant meant to be accessed across packages, hence it starts with an uppercase letter. On the other hand, `minIdleTime` is intended for use only within the `config` package, so it starts with a lowercase letter.\n\nFollowing these guidelines ensures that your Go code is more readable, maintainable, and consistent with Go's design philosophy and access control mechanisms.\n\n### 2.8 Error naming\n\n- The Error type should be written in the form of FooError.\n\n```go\ntype ExitError struct {\n// ....\n}\n```\n\n- The Error variable is written in the form of ErrFoo.\n\n```go\nvar ErrFormat = errors. New(\"unknown format\")\n```\n\nFor non-standard Err naming, CICD will report an error\n\n\n### 2.9 Handling Errors Properly\n\nIn Go, proper error handling is crucial for creating reliable and maintainable applications. It's important to ensure that errors are not ignored or discarded, as this can lead to unpredictable behavior and difficult-to-debug issues. Here are the guidelines and examples regarding the proper handling of errors.\n\n#### Guideline: Do Not Discard Errors\n\n- **Mandatory Error Propagation:** When calling a function that returns an error, the calling function must handle or propagate the error, instead of ignoring it. This approach ensures that errors are not silently ignored, allowing higher-level logic to make informed decisions about error handling.\n\n#### Incorrect Example: Discarding an Error\n\n```go\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n)\n\nfunc ReadFileContent(filename string) string {\n\tcontent, _ := ioutil.ReadFile(filename) // Incorrect: Error is ignored\n\treturn string(content)\n}\n\nfunc main() {\n\tcontent := ReadFileContent(\"example.txt\")\n\tlog.Println(content)\n}\n```\n\nIn this incorrect example, the error returned by `ioutil.ReadFile` is ignored. This can lead to situations where the program continues execution even if the file doesn't exist or cannot be accessed, potentially causing more cryptic errors downstream.\n\n#### Correct Example: Propagating an Error\n\n```go\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n)\n\n// ReadFileContent attempts to read and return the content of the specified file.\n// It returns an error if reading fails.\nfunc ReadFileContent(filename string) (string, error) {\n\tcontent, err := ioutil.ReadFile(filename)\n\tif err != nil {\n\t\t// Correct: Propagate the error\n\t\treturn \"\", err\n\t}\n\treturn string(content), nil\n}\n\nfunc main() {\n\tcontent, err := ReadFileContent(\"example.txt\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to read file: %v\", err)\n\t}\n\tlog.Println(content)\n}\n```\n\nIn the correct example, the error returned by `ioutil.ReadFile` is propagated back to the caller. The `main` function then checks the error and terminates the program with an appropriate error message if an error occurred. This approach ensures that errors are handled appropriately, and the program does not proceed with invalid state.\n\n### Best Practices for Error Handling\n\n1. **Always check the error returned by a function.** Do not ignore it.\n2. **Propagate errors up the call stack unless they can be handled gracefully at the current level.**\n3. **Provide context for errors when propagating them, making it easier to trace the source of the error.** This can be achieved using `fmt.Errorf` with the `%w` verb or dedicated wrapping functions provided by some error handling packages.\n4. **Log the error at the point where it is handled or makes the program to terminate, to provide insight into the failure.**\n\nBy following these guidelines, you ensure that your Go applications handle errors in a consistent and effective manner, improving their reliability and maintainability.\n\n\n### 2.10 Using Context with IO or Inter-Process Communication (IPC)\n\nIn Go, `context.Context` is a powerful construct for managing deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes. It is particularly important in I/O operations or inter-process communication (IPC), where operations might need to be cancelled or timed out.\n\n#### Guideline: Use Context for IO and IPC\n\n- **Mandatory Use of Context:** When performing I/O operations or inter-process communication, it's crucial to use `context.Context` to manage the lifecycle of these operations. This includes setting deadlines, handling cancellation signals, and passing request-scoped values.\n\n#### Incorrect Example: Ignoring Context in an HTTP Call\n\n```go\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"log\"\n)\n\n// FetchData makes an HTTP GET request to the specified URL and returns the response body.\n// This function does not use context, making it impossible to cancel the request or set a deadline.\nfunc FetchData(url string) (string, error) {\n\tresp, err := http.Get(url) // Incorrect: Ignoring context\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(body), nil\n}\n\nfunc main() {\n\tdata, err := FetchData(\"http://example.com\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to fetch data: %v\", err)\n\t}\n\tlog.Println(data)\n}\n```\n\nIn this incorrect example, the `FetchData` function makes an HTTP GET request without using a `context`. This approach does not allow the request to be cancelled or a timeout to be set, potentially leading to resources being wasted if the server takes too long to respond or if the operation needs to be aborted for any reason.\n\n#### Correct Example: Using Context in an HTTP Call\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"log\"\n\t\"time\"\n)\n\n// FetchDataWithContext makes an HTTP GET request to the specified URL using the provided context.\n// This allows the request to be cancelled or timed out according to the context's deadline.\nfunc FetchDataWithContext(ctx context.Context, url string) (string, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(body), nil\n}\n\nfunc main() {\n\t// Create a context with a 5-second timeout\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tdata, err := FetchDataWithContext(ctx, \"http://example.com\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to fetch data: %v\", err)\n\t}\n\tlog.Println(data)\n}\n```\n\nIn the correct example, `FetchDataWithContext` uses a context to make the HTTP GET request. This allows the operation to be cancelled or subjected to a timeout, as dictated by the context passed to it. The `context.WithTimeout` function is used in `main` to create a context that cancels the request if it takes longer than 5 seconds, demonstrating a practical use of context to manage operation lifecycle.\n\n### Best Practices for Using Context\n\n1. **Pass context as the first parameter of a function**, following the convention `func(ctx context.Context, ...)`.\n2. **Never ignore the context** provided to you in functions that support it. Always use it in your I/O or IPC operations.\n3. **Avoid storing context in a struct**. Contexts are meant to be passed around within the call stack, not stored.\n4. **Use context's cancellation and deadline features** to control the lifecycle of blocking operations, especially in network I/O and IPC scenarios.\n5. **Propagate context down the call stack** to any function that supports it, ensuring that your application can respond to cancellation signals and deadlines effectively.\n\nBy adhering to these guidelines and examples, you can ensure that your Go applications handle I/O and IPC operations more reliably and efficiently, with proper support for cancellation, timeouts, and request-scoped values.\n\n\n## 3. Comment specification\n\n- Each exportable name must have a comment, which briefly introduces the exported variables, functions, structures, interfaces, etc.\n- All single-line comments are used, and multi-line comments are prohibited.\n- Same as the code specification, single-line comments should not be too long, and no more than 120 characters are allowed. If it exceeds, please use a new line to display, and try to keep the format elegant.\n- A comment must be a complete sentence, starting with the content to be commented and ending with a period, `the format is // name description.`. For example:\n\n```go\n// bad\n// logs the flags in the flagset.\nfunc PrintFlags(flags *pflag. FlagSet) {\n// normal code\n}\n\n//good\n// PrintFlags logs the flags in the flagset.\nfunc PrintFlags(flags *pflag. FlagSet) {\n// normal code\n}\n```\n\n- All commented out code should be deleted before submitting code review, otherwise, it should explain why it is not deleted, and give follow-up processing suggestions.\n\n- Multiple comments can be separated by blank lines, as follows:\n\n```go\n// Package superman implements methods for saving the world.\n//\n// Experience has shown that a small number of procedures can prove\n// helpful when attempting to save the world.\npackage superman\n```\n\n### 3.1 Package Notes\n\n- Each package has one and only one package-level annotation.\n- Package comments are uniformly commented with // in the format of `// Package <package name> package description`, for example:\n\n```go\n// Package genericclioptions contains flags which can be added to you command, bound, completed, and produce\n// useful helper functions.\npackage genericclioptions\n```\n\n### 3.2 Variable/Constant Comments\n\n- Each variable/constant that can be exported must have a comment description, `the format is // variable name variable description`, for example:\n\n```go\n// ErrSigningMethod defines invalid signing method error.\nvar ErrSigningMethod = errors. New(\"Invalid signing method\")\n```\n\n- When there is a large block of constant or variable definition, you can comment a general description in front, and then comment the definition of the constant in detail before or at the end of each line of constant, for example:\n```go\n// Code must start with 1xxxxx.\nconst (\n     // ErrSuccess - 200: OK.\n     ErrSuccess int = iota + 100001\n\n     // ErrUnknown - 500: Internal server error.\n     ErrUnknown\n\n     // ErrBind - 400: Error occurred while binding the request body to the struct.\n     ErrBind\n\n     // ErrValidation - 400: Validation failed.\n     ErrValidation\n)\n```\n### 3.3 Structure Annotation\n\n- Each structure or interface that needs to be exported must have a comment description, the format is `// structure name structure description.`.\n- The name of the exportable member variable in the structure, if the meaning is not clear, a comment must be given and placed before the member variable or at the end of the same line. For example:\n\n```go\n// User represents a user restful resource. It is also used as gorm model.\ntype User struct {\n     // Standard object's metadata.\n     metav1.ObjectMeta `json:\"metadata,omitempty\"`\n\n     Nickname string `json:\"nickname\" gorm:\"column:nickname\"`\n     Password string `json:\"password\" gorm:\"column:password\"`\n     Email string `json:\"email\" gorm:\"column:email\"`\n     Phone string `json:\"phone\" gorm:\"column:phone\"`\n     IsAdmin int `json:\"isAdmin,omitempty\" gorm:\"column:isAdmin\"`\n}\n```\n\n### 3.4 Method Notes\n\nEach function or method that needs to be exported must have a comment, the format is // function name function description., for examplelike:\n\n```go\n// BeforeUpdate run before update database record.\nfunc (p *Policy) BeforeUpdate() (err error) {\n// normal code\n  return nil\n}\n```\n\n### 3.5 Type annotations\n\n- Each type definition and type alias that needs to be exported must have a comment description, the format is `// type name type description.`, for example:\n\n```go\n// Code defines an error code type.\ntype Code int\n```\n\n## 4. Type\n\n### 4.1 Strings\n\n- Empty string judgment.\n\n```go\n// bad\nif s == \"\" {\n     // normal code\n}\n\n//good\nif len(s) == 0 {\n     // normal code\n}\n```\n\n- `[]byte`/`string` equality comparison.\n\n```go\n// bad\nvar s1 []byte\nvar s2 []byte\n...\nbytes.Equal(s1, s2) == 0\nbytes.Equal(s1, s2) != 0\n\n//good\nvar s1 []byte\nvar s2 []byte\n...\nbytes. Compare(s1, s2) == 0\nbytes. Compare(s1, s2) != 0\n```\n\n- Complex strings use raw strings to avoid character escaping.\n\n```go\n// bad\nregexp.MustCompile(\"\\\\.\")\n\n//good\nregexp.MustCompile(`\\.`)\n```\n\n### 4.2 Slicing\n\n- Empty slice judgment.\n\n```go\n// bad\nif len(slice) = 0 {\n     // normal code\n}\n\n//good\nif slice != nil && len(slice) == 0 {\n     // normal code\n}\n```\n\nThe above judgment also applies to map and channel.\n\n- Declare a slice.\n\n```go\n// bad\ns := []string{}\ns := make([]string, 0)\n\n//good\nvar s[]string\n```\n\n- slice copy.\n\n```go\n// bad\nvar b1, b2 []byte\nfor i, v := range b1 {\n    b2[i] = v\n}\nfor i := range b1 {\n    b2[i] = b1[i]\n}\n\n//good\ncopy(b2, b1)\n```\n\n- slice added.\n\n```go\n// bad\nvar a, b []int\nfor _, v := range a {\n     b = append(b, v)\n}\n\n//good\nvar a, b []int\nb = append(b, a...)\n```\n\n### 4.3 Structure\n\n- struct initialization.\n\nThe struct is initialized in multi-line format.\n\n```go\ntype user struct {\nId int64\nname string\n}\n\nu1 := user{100, \"Colin\"}\n\nu2 := user{\n     Id: 200,\n     Name: \"Lex\",\n}\n```\n\n## 5. Control Structure\n\n### 5.1 if\n\n- if accepts the initialization statement, the convention is to create local variables in the following way.\n\n```go\nif err := loadConfig(); err != nil {\n// error handling\nreturn err\n}\n```\n\n- if For variables of bool type, true and false judgments should be made directly.\n\n```go\nvar isAllow bool\nif isAllow {\n// normal code\n}\n```\n\n### 5.2 for\n\n- Create local variables using short declarations.\n\n```go\nsum := 0\nfor i := 0; i < 10; i++ {\n     sum += 1\n}\n```\n\n- Don't use defer in for loop, defer will only be executed when the function exits.\n\n```go\n// bad\nfor file := range files {\n    fd, err := os. Open(file)\n    if err != nil {\n    return err\n}\ndefer fd. Close()\n// normal code\n}\n\n//good\nfor file := range files {\n    func() {\n        fd, err := os. Open(file)\n        if err != nil {\n        return err\n    }\n    defer fd. Close()\n    // normal code\n    }()\n}\n```\n\n### 5.3 range\n\n- If only the first item (key) is needed, discard the second.\n\n```go\nfor keyIndex := range keys {\n// normal code\n}\n```\n\n- If only the second item is required, underline the first item.\n\n```go\nsum := 0\nfor _, value := range array {\n     sum += value\n}\n```\n\n### 5.4 switch\n\n- must have default.\n\n```go\nswitch os := runtime.GOOS; os {\n     case \"linux\":\n         fmt.Println(\"Linux.\")\n     case \"darwin\":\n         fmt.Println(\"OS X.\")\n     default:\n         fmt.Printf(\"%s.\\n\", os)\n}\n```\n\n### 5.5 goto\n- Business code prohibits the use of goto.\n- Try not to use frameworks or other low-level source code.\n\n## 6. Functions\n\n- Incoming variables and return variables start with a lowercase letter.\n- The number of function parameters cannot exceed 5.\n- Function grouping and ordering\n- Functions should be sorted in rough calling order.\n- Functions in the same file should be grouped by receiver.\n- Try to use value transfer instead of pointer transfer.\n- The incoming parameters are map, slice, chan, interface, do not pass pointers.\n\n### 6.1 Function parameters\n\n- If the function returns two or three arguments of the same type, or if the meaning of the result is not clear from the context, use named returns, otherwise it is not recommended to use named returns, for example:\n\n```go\nfunc coordinate() (x, y float64, err error) {\n// normal code\n}\n```\n- Both incoming and returned variables start with a lowercase letter.\n- Try to pass by value instead of pointer.\n- The number of parameters cannot exceed 5.\n- Multiple return values can return up to three, and if there are more than three, please use struct.\n\n### 6.2 defer\n\n- When resources are created, resources should be released immediately after defer (defer can be used boldly, the performance of defer is greatly improved in Go1.14 version, and the performance loss of defer can be ignored even in performance-sensitive businesses).\n- First judge whether there is an error, and then defer to release resources, for example:\n\n```go\nrep, err := http. Get(url)\nif err != nil {\n     return err\n}\n\ndefer resp.Body.Close()\n```\n\n### 6.3 Method Receiver\n\n- It is recommended to use the lowercase of the first English letter of the class name as the name of the receiver.\n- Don't use a single character in the name of the receiver when the function exceeds 20 lines.\n- The name of the receiver cannot use confusing names such as me, this, and self.\n\n### 6.4 Nesting\n- The nesting depth cannot exceed 4 levels.\n\n### 6.5 Variable Naming\n- The variable declaration should be placed before the first use of the variable as far as possible, following the principle of proximity.\n- If the magic number appears more than twice, it is forbidden to use it and use a constant instead, for example:\n\n```go\n// PI...\nconst Price = 3.14\n\nfunc getAppleCost(n float64) float64 {\nreturn Price * n\n}\n\nfunc getOrangeCost(n float64) float64 {\nreturn Price * n\n}\n```\n\n## 7. GOPATH setting specification\n- After Go 1.11, the GOPATH rule has been weakened. Existing code (many libraries must have been created before 1.11) must conform to this rule. It is recommended to keep the GOPATH rule to facilitate code maintenance.\n- Only one GOPATH is recommended, multiple GOPATHs are not recommended. If multiple GOPATHs are used, the bin directory where compilation takes effect is under the first GOPATH.\n\n## 8. Dependency Management\n\n- Go 1.11 and above must use Go Modules.\n- When using Go Modules as a dependency management project, it is not recommended to submit the vendor directory.\n- When using Go Modules as a dependency management project, the go.sum file must be submitted.\n\n### 9. Best Practices\n\n- Minimize the use of global variables, but pass parameters, so that each function is \"stateless\". This reduces coupling and facilitates division of labor and unit testing.\n- Verify interface compliance at compile time, for example:\n\n```go\ntype LogHandler struct {\n   h http.Handler\n   log *zap. Logger\n}\nvar_http.Handler = LogHandler{}\n```\n\n- When the server processes a request, it should create a context, save the relevant information of the request (such as requestID), and pass it in the function call chain.\n\n### 9.1 Performance\n- string represents an immutable string variable, modifying string is a relatively heavy operation, and basically needs to re-apply for memory. Therefore, if there is no special need, use []byte more when you need to modify.\n- Prefer strconv over fmt.\n\n### 9.2 Precautions\n\n- append Be careful about automatically allocating memory, append may return a newly allocated address.\n- If you want to directly modify the value of the map, the value can only be a pointer, otherwise the original value must be overwritten.\n- map needs to be locked during concurrency.\n- The conversion of interface{} cannot be checked during compilation, it can only be checked at runtime, be careful to cause panic.\n\n## 10 Golang CI Lint\n\n- Golang CI Lint is a fast Go linters runner. It runs linters in parallel, uses caching, and works well with all environments, including CI.\n\n**In local development, you can use the following command to install Golang CI Lint: **\n\n```bash\nmake lint\n```\n\n**In CI/CD, Check the Github Actions status code below after you submit the code directly**\n\n[![OpenIM golangci-lint](https://github.com/openimsdk/open-im-server/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/openimsdk/open-im-server/actions/workflows/golangci-lint.yml)\n\ngolangci lint can select the types of tools, refer to the official documentation: [https://golangci-lint.run/usage/linters/](https://golangci-lint.run/usage/linters/)\n\nThe types of comments we currently use include: [https://github.com/openimsdk/open-im-server/blob/main/.golangci.yml](https://github.com/openimsdk/open-im-server/blob/main/.golangci.yml) the `linters.enable` field in the file.\n\ne.g:\n```yaml\nlinters:\n  # please, do not use `enable-all`: it's deprecated and will be removed soon.\n  # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint\n  # enable-all: true\n  disable-all: true\n  enable:\n    - typecheck     # Basic type checking\n    - gofmt         # Format check\n    - govet         # Go's standard linting tool\n    - gosimple      # Suggestions for simplifying code\n    - misspell      # Spelling mistakes\n    - staticcheck   # Static analysis\n    - unused        # Checks for unused code\n    - goimports     # Checks if imports are correctly sorted and formatted\n    - godot         # Checks for comment punctuation\n    - bodyclose     # Ensures HTTP response body is closed\n    - errcheck      # Checks for missed error returns\n  fast: true\n```\n\nAdd that Chinese comments are not allowed in go code, please write a complete golangci lint specification on the basis of the above.\n\n\n### 10.1 Configuration Document\n\nThis configuration document is designed to configure the operational parameters of OpenIM (a hypothetical or specific code analysis tool), customize output formats, and provide detailed settings for specific code checkers (linters). Below is a summary of the document drafted based on the provided configuration information.\n\n#### 10.1 Runtime Options\n\n- **Concurrency** (`concurrency`): Default to use the available CPU count, can be manually set to 4 for parallel analysis.\n- **Timeout** (`timeout`): Timeout duration for analysis operations, default is 1 minute, set here to 5 minutes.\n- **Issue Exit Code** (`issues-exit-code`): Exit code defaults to 1 if at least one issue is found.\n- **Test Files** (`tests`): Whether to include test files, defaults to true.\n- **Build Tags** (`build-tags`): Specify build tags used by all linters, defaults to an empty list. Example adds `mytag`.\n- **Skip Directories** (`skip-dirs`): Configure which directories' issues are not reported, defaults to empty, but some default directories are independently skipped.\n- **Skip Files** (`skip-files`): Specify files where issues should not be reported, supports regular expressions.\n\n#### 10.2 Output Configuration\n\n- **Format** (`format`): Set output format, default is \"colored-line-number\".\n- **Print Issued Lines** (`print-issued-lines`): Whether to print the lines where issues occur, defaults to true.\n- **Print Linter Name** (`print-linter-name`): Whether to print the linter name at the end of issue text, defaults to true.\n- **Uniqueness Filter** (`uniq-by-line`): Whether to make issue outputs unique per line, defaults to true.\n- **Path Prefix** (`path-prefix`): Prefix to add to output file references, defaults to no prefix.\n- **Sort Results** (`sort-results`): Sort results by file path, line number, and column number.\n\n#### 10.3 Linters Settings\n\nIn the configuration file, the `linters-settings` section allows detailed configuration of individual linters. Below are examples of specific linters settings and their purposes:\n\n- **bidichk**: Used to check bidirectional text characters, ensuring correct display direction of text, especially when dealing with mixed left-to-right (LTR) and right-to-left (RTL) text.\n  \n- **dogsled**: Monitors excessive use of blank identifiers (`_`) in assignment operations, which may obscure data processing errors or unclear logic.\n  \n- **dupl**: Identifies duplicate code blocks, helping developers avoid code redundancy. The `threshold` parameter in settings allows adjustment of code similarity threshold triggering warnings.\n  \n- **errcheck**: Checks for unhandled errors. In Go, error handling is achieved by checking function return values. This linter helps ensure all errors are properly handled.\n  \n- **exhaustive**: Checks if `switch` statements include all possible values of an enum type, ensuring exhaustiveness of code. This helps avoid forgetting to handle certain cases.\n\n#### 10.4 Example: `errcheck`\n\n**Incorrect Code Example**:\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n)\n\nfunc main() {\n    f, _ := os.Open(\"filename.ext\")\n    defer f.Close()\n}\n```\n\n**Issue**: In the above code, the error return value of `os.Open` function is explicitly ignored. This is a common mistake as it may lead to unhandled errors and hard-to-trace bugs.\n\n**Correct Form**:\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n)\n\nfunc main() {\n    f, err := os.Open(\"filename.ext\")\n    if err != nil {\n        fmt.Printf(\"error opening file: %v\\n\", err)\n        return\n    }\n    defer f.Close()\n}\n```\n\nIn the correct form, by checking the error (`err`) returned by `os.Open`, we gracefully handle error cases rather than simply ignoring them.\n\n#### 10.5 Example: `gofmt`\n\n**Incorrect Code Example**:\n```go\npackage main\nimport \"fmt\"\nfunc main() {\nfmt.Println(\"Hello, world!\")\n}\n```\n\n**Issue**: This code snippet doesn't follow Go's standard formatting rules, for example, incorrect indentation of `fmt.Println`.\n\n**Correct Form**:\n```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n    fmt.Println(\"Hello, world!\")\n}\n```\n\nUsing `gofmt` tool can automatically fix such formatting issues, ensuring the code adheres to the coding standards of the Go community.\n\n#### 10.6 Example: `unused`\n\n**Incorrect Code Example**:\n```go\npackage main\n\nfunc helper() {}\n\nfunc main() {}\n```\n\n**Issue**: The `helper` function is defined but not called anywhere, indicating potential redundant code or missing functionality implementation.\n\n**Correct Form**:\n```go\npackage main\n\n// If the helper function is indeed needed, ensure it's used properly.\nfunc helper() {\n    // Implement the function's functionality or ensure it's called elsewhere\n}\n\nfunc main() {\n    helper()\n}\n```\n\nTo improve the section on Linters settings in the document, we'll expand with more detailed explanations and reinforce understanding through examples.\n\n#### 10.7 Example: `dogsled`\n\n**Incorrect Code Example**:\n```go\nfunc getValues() (int, int, int) {\n    return 1, 2, 3\n}\n\nfunc main() {\n    _, _, val := getValues()\n    fmt.Println(val) // Only interested in the third return value\n}\n```\n\n**Explanation**: In the above code, we use two blank identifiers to ignore the first two return values. Excessive use of blank identifiers can make code reading difficult.\n\n**Improved Code**:\nConsider refactoring the function or the usage of return values to reduce the need for blank identifiers or explicitly comment why ignoring certain values is safe.\n\n#### 10.8: `exhaustive`\n\n**Incorrect Code Example**:\n```go\ntype Fruit int\n\nconst (\n    Apple Fruit = iota\n    Banana\n    Orange\n)\n\nfunc getFruitName(f Fruit) string {\n    switch f {\n    case Apple:\n        return \"Apple\"\n    case Banana:\n        return \"Banana\"\n    // Missing handling for Orange\n    }\n    return \"Unknown\"\n}\n```\n\n**Explanation**: In this code, the `switch` statement doesn't cover all possible values of the `Fruit` type; the case for `Orange` is missing.\n\n**Improved Code**:\n```go\nfunc getFruitName(f Fruit) string {\n    switch f {\n    case Apple:\n        return \"Apple\"\n    case Banana:\n        return \"Banana\"\n    case Orange:\n        return \"Orange\"\n    }\n    return \"Unknown\"\n}\n```\n\nBy adding the missing `case`, we ensure the `switch` statement is exhaustive, handling every possible enum value.\n\n#### 10.9 Optimization of Configuration Files and Application of Code Analysis Tools\n\nThrough these examples, we demonstrate how to improve code quality by identifying and fixing common coding issues. OpenIM's configuration files allow developers to customize linters' behavior according to project requirements, ensuring code compliance with predefined quality standards and style guidelines.\n\nBy employing these tools and configuration strategies, teams can reduce the number of bugs, enhance code maintainability, and facilitate efficient collaboration during code review processes.\n"
  },
  {
    "path": "docs/contrib/go-code1.md",
    "content": "## OpenIM development specification\nWe have very high standards for code style and specification, and we want our products to be polished and perfect\n\n## 1. Code style\n\n### 1.1 Code format\n\n- Code must be formatted with `gofmt`.\n- Leave spaces between operators and operands.\n- It is recommended that a line of code does not exceed 120 characters. If the part exceeds, please use an appropriate line break method. But there are also some exception scenarios, such as import lines, code automatically generated by tools, and struct fields with tags.\n- The file length cannot exceed 800 lines.\n- Function length cannot exceed 80 lines.\n- import specification\n- All code must be formatted with `goimports` (it is recommended to set the code Go code editor to: run `goimports` on save).\n- Do not use relative paths to import packages, such as `import ../util/net`.\n- Import aliases must be used when the package name does not match the last directory name of the import path, or when multiple identical package names conflict.\n\n```go\n// bad\n\"github.com/dgrijalva/jwt-go/v4\"\n\n//good\njwt \"github.com/dgrijalva/jwt-go/v4\"\n```\n- Imported packages are suggested to be grouped, and anonymous package references use a new group, and anonymous package references are explained.\n\n```go\nimport (\n  // go standard package\n  \"fmt\"\n  \n  // third party package\n  \"github.com/jinzhu/gorm\"\n  \"github.com/spf13/cobra\"\n  \"github.com/spf13/viper\"\n  \n  // Anonymous packages are grouped separately, and anonymous package references are explained\n  // import mysql driver\n  _ \"github.com/jinzhu/gorm/dialects/mysql\"\n  \n  // inner package\n)\n```\n\n### 1.2 Declaration, initialization and definition\n\nWhen multiple variables need to be used in a function, the `var` declaration can be used at the beginning of the function. Declaration outside the function must use `var`, do not use `:=`, it is easy to step on the scope of the variable.\n\n```go\nvar (\n  Width int\n  Height int\n)\n```\n\n- When initializing a structure reference, please use `&T{}` instead of `new(T)` to make it consistent with structure initialization.\n\n```go\n  // bad\n  sptr := new(T)\n  sptr.Name = \"bar\"\n  \n  // good\n  sptr := &T{Name: \"bar\"}\n```\n\n- The struct declaration and initialization format takes multiple lines and is defined as follows.\n\n```go\n  type User struct{\n       Username string\n       Email string\n  }\n\n  user := User{\n  Username: \"belm\",\n  Email: \"nosbelm@qq.com\",\n}\n```\n\n- Similar declarations are grouped together, and the same applies to constant, variable, and type declarations.\n\n```go\n// bad\nimport \"a\"\nimport \"b\"\n\n//good\nimport (\n   \"a\"\n   \"b\"\n)\n```\n\n- Specify container capacity where possible to pre-allocate memory for the container, for example:\n\n```go\nv := make(map[int]string, 4)\nv := make([]string, 0, 4)\n```\n\n- At the top level, use the standard var keyword. Do not specify a type unless it is different from the type of the expression.\n\n```go\n// bad\nvar s string = F()\n\nfunc F() string { return \"A\" }\n\n// good\nvar s = F()\n// Since F already explicitly returns a string type, we don't need to explicitly specify the type of _s\n// still of that type\n\nfunc F() string { return \"A\" }\n```\n\n- This example emphasizes using PascalCase for exported constants and camelCase for unexported ones, avoiding all caps and underscores.\n\n```go\n// bad\nconst (\n    MAX_COUNT = 100\n    timeout = 30\n)\n\n// good\nconst (\n    MaxCount = 100  // Exported constants should use PascalCase.\n    defaultTimeout = 30  // Unexported constants should use camelCase.\n)\n```\n\n- Grouping related constants enhances organization and readability, especially when there are multiple constants related to a particular feature or configuration.\n\n```go\n// bad\nconst apiVersion = \"v1\"\nconst retryInterval = 5\n\n// good\nconst (\n    ApiVersion    = \"v1\"  // Group related constants together for better organization.\n    RetryInterval = 5\n)\n```\n\n- The \"good\" practice utilizes iota for a clear, concise, and auto-incrementing way to define enumerations, reducing the potential for errors and improving maintainability.\n\n```go\n// bad\nconst (\n    StatusActive   = 0\n    StatusInactive = 1\n    StatusUnknown  = 2\n)\n\n// good\nconst (\n    StatusActive = iota  // Use iota for simple and efficient constant enumerations.\n    StatusInactive\n    StatusUnknown\n)\n```\n\n- Specifying types explicitly improves clarity, especially when the purpose or type of a constant might not be immediately obvious. Additionally, adding comments to exported constants or those whose purpose isn't clear from the name alone can greatly aid in understanding the code.\n\n```go\n// bad\nconst serverAddress = \"localhost:8080\"\nconst debugMode = 1  // Is this supposed to be a boolean or an int?\n\n// good\nconst ServerAddress string = \"localhost:8080\"  // Specify type for clarity.\n// DebugMode indicates if the application should run in debug mode (true for debug mode).\nconst DebugMode bool = true\n```\n\n- By defining a contextKey type and making userIDKey of this type, you avoid potential collisions with other context keys. This approach leverages Go's type system to provide compile-time checks against misuse.\n\n```go\n// bad\nconst userIDKey = \"userID\"\n\n// In this example, userIDKey is a string type, which can lead to conflicts or accidental misuse because string keys are prone to typos and collisions in a global namespace.\n\n\n// good\ntype contextKey string\n\nconst userIDKey contextKey = \"userID\"\n```\n\n\n- Embedded types (such as mutexes) should be at the top of the field list within the struct, and there must be a blank line separating embedded fields from regular fields.\n\n```go\n// bad\ntype Client struct {\n   version int\n   http.Client\n}\n\n//good\ntype Client struct {\n   http.Client\n\n   version int\n}\n```\n\n\n### 1.5 Unit Tests\n\n- The unit test filename naming convention is `example_test.go`.\n- Write a test case for every important exportable function.\n- Because the functions in the unit test file are not external, the exportable structures, functions, etc. can be uncommented.\n- If `func (b *Bar) Foo` exists, the single test function can be `func TestBar_Foo`.\n\n## 2. Naming convention\n\nThe naming convention is a very important part of the code specification. A uniform, short, and precise naming convention can greatly improve the readability of the code and avoid unnecessary bugs.\n\n### 2.1 Package Naming\n\n- The package name must be consistent with the directory name, try to use a meaningful and short package name, and do not conflict with the standard library.\n- Package names are all lowercase, without uppercase or underscores, and use multi-level directories to divide the hierarchy.\n- Item names can connect multiple words with dashes.\n- Do not use plurals for the package name and the directory name where the package is located, for example, `net/url` instead of `net/urls`.\n- Don't use broad, meaningless package names like common, util, shared or lib.\n- The package name should be simple and clear, such as net, time, log.\n\n\n### 2.2 Function Naming Conventions\n\nFunction names should adhere to the following guidelines, inspired by OpenIM’s standards and Google’s Go Style Guide:\n\n- Use camel case for function names. Start with an uppercase letter for public functions (`MixedCaps`) and a lowercase letter for private functions (`mixedCaps`).\n- Exceptions to this rule include code automatically generated by tools (e.g., `xxxx.pb.go`) and test functions that use underscores for clarity (e.g., `TestMyFunction_WhatIsBeingTested`).\n\n### 2.3 File and Directory Naming Practices\n\nTo maintain consistency and readability across the OpenIM project, observe the following naming practices:\n\n**File Names:**\n- Use underscores (`_`) as the default separator in filenames, keeping them short and descriptive.\n- Both hyphens (`-`) and underscores (`_`) are allowed, but underscores are preferred for general use.\n\n**Script and Markdown Files:**\n- Prefer hyphens (`-`) for shell scripts and Markdown (`.md`) files to enhance searchability and web compatibility.\n\n**Directories:**\n- Name directories with hyphens (`-`) exclusively to separate words, ensuring consistency and readability.\n\nRemember to keep filenames lowercase and use meaningful, concise identifiers to facilitate better organization and navigation within the project.\n\n### 2.4 Structure Naming\n\n- The camel case is adopted, and the first letter is uppercase or lowercase according to the access control, such as `MixedCaps` or `mixedCaps`.\n- Struct names should not be verbs, but should be nouns, such as `Node`, `NodeSpec`.\n- Avoid using meaningless structure names such as Data and Info.\n- The declaration and initialization of the structure should take multiple lines, for example:\n\n```go\n// User multi-line declaration\ntype User struct {\n     name string\n     Email string\n}\n\n// multi-line initialization\nu := User{\n     UserName: \"belm\",\n     Email: \"nosbelm@qq.com\",\n}\n```\n\n### 2.5 Interface Naming\n\n- The interface naming rules are basically consistent with the structure naming rules:\n- Interface names of individual functions suffixed with \"er\"\" (e.g. Reader, Writer) can sometimes lead to broken English, but that's okay.\n- The interface name of the two functions is named after the two function names, eg ReadWriter.\n- An interface name for more than three functions, similar to a structure name.\n\nFor example:\n\n```go\n// Seeking to an offset before the start of the file is an error.\n// Seeking to any positive offset is legal, but the behavior of subsequent\n// I/O operations on the underlying object are implementation-dependent.\ntype Seeker interface {\n  Seek(offset int64, whence int) (int64, error)\n}\n\n// ReadWriter is the interface that groups the basic Read and Write methods.\ntype ReadWriter interface {\n  reader\n  Writer\n}\n```\n\n### 2.6 Variable Naming\n\n- Variable names must follow camel case, and the initial letter is uppercase or lowercase according to the access control decision.\n- In relatively simple (few objects, highly targeted) environments, some names can be abbreviated from full words to single letters, for example:\n- user can be abbreviated as u;\n- userID can be abbreviated as uid.\n- When using proper nouns, the following rules need to be followed:\n- If the variable is private and the proper noun is the first word, use lowercase, such as apiClient.\n- In other cases, the original wording of the noun should be used, such as APIClient, repoID, UserID.\n\nSome common nouns are listed below.\n\n```go\n// A GonicMapper that contains a list of common initialisms taken from golang/lint\nvar LintGonicMapper = GonicMapper{\n     \"API\": true,\n     \"ASCII\": true,\n     \"CPU\": true,\n     \"CSS\": true,\n     \"DNS\": true,\n     \"EOF\": true,\n     \"GUID\": true,\n     \"HTML\": true,\n     \"HTTP\": true,\n     \"HTTPS\": true,\n     \"ID\": true,\n     \"IP\": true,\n     \"JSON\": true,\n     \"LHS\": true,\n     \"QPS\": true,\n     \"RAM\": true,\n     \"RHS\": true,\n     \"RPC\": true,\n     \"SLA\": true,\n     \"SMTP\": true,\n     \"SSH\": true,\n     \"TLS\": true,\n     \"TTL\": true,\n     \"UI\": true,\n     \"UID\": true,\n     \"UUID\": true,\n     \"URI\": true,\n     \"URL\": true,\n     \"UTF8\": true,\n     \"VM\": true,\n     \"XML\": true,\n     \"XSRF\": true,\n     \"XSS\": true,\n}\n```\n\n- If the variable type is bool, the name should start with Has, Is, Can or Allow, for example:\n\n```go\nvar hasConflict bool\nvar isExist bool\nvar canManage bool\nvar allowGitHook bool\n```\n\n- Local variables should be as short as possible, for example, use buf to refer to buffer, and use i to refer to index.\n- The code automatically generated by the code generation tool can exclude this rule (such as the Id in `xxx.pb.go`)\n\n### 2.7 Constant Naming\n\nIn Go, constants play a critical role in defining values that do not change throughout the execution of a program. Adhering to best practices in naming constants can significantly improve the readability and maintainability of your code. Here are some guidelines for constant naming:\n\n- **Camel Case Naming:** The name of a constant must follow the camel case notation. The initial letter should be uppercase or lowercase based on the access control requirements. Uppercase indicates that the constant is exported (visible outside the package), while lowercase indicates package-private visibility (visible only within its own package).\n\n- **Enumeration Type Constants:** For constants that represent a set of enumerated values, it's recommended to define a corresponding type first. This approach not only enhances type safety but also improves code readability by clearly indicating the purpose of the enumeration.\n\n**Example:**\n\n```go\n// Code defines an error code type.\ntype Code int\n\n// Internal errors.\nconst (\n     // ErrUnknown - 0: An unknown error occurred.\n     ErrUnknown Code = iota\n     // ErrFatal - 1: A fatal error occurred.\n     ErrFatal\n)\n```\n\nIn the example above, `Code` is defined as a new type based on `int`. The enumerated constants `ErrUnknown` and `ErrFatal` are then defined with explicit comments to indicate their purpose and values. This pattern is particularly useful for grouping related constants and providing additional context.\n\n### Global Variables and Constants Across Packages\n\n- **Use Constants for Global Variables:** When defining variables that are intended to be accessed across packages, prefer using constants to ensure immutability. This practice avoids unintended modifications to the value, which can lead to unpredictable behavior or hard-to-track bugs.\n\n- **Lowercase for Package-Private Usage:** If a global variable or constant is intended for use only within its own package, it should start with a lowercase letter. This clearly signals its limited scope of visibility, adhering to Go's access control mechanism based on naming conventions.\n\n**Guideline:**\n\n- For global constants that need to be accessed across packages, declare them with an uppercase initial letter. This makes them exported, adhering to Go's visibility rules.\n- For constants used within the same package, start their names with a lowercase letter to limit their scope to the package.\n\n**Example:**\n\n```go\npackage config\n\n// MaxConnections - the maximum number of allowed connections. Visible across packages.\nconst MaxConnections int = 100\n\n// minIdleTime - the minimum idle time before a connection is considered stale. Only visible within the config package.\nconst minIdleTime int = 30\n```\n\nIn this example, `MaxConnections` is a global constant meant to be accessed across packages, hence it starts with an uppercase letter. On the other hand, `minIdleTime` is intended for use only within the `config` package, so it starts with a lowercase letter.\n\nFollowing these guidelines ensures that your Go code is more readable, maintainable, and consistent with Go's design philosophy and access control mechanisms.\n\n\n\n\n### 2.10 Using Context with IO or Inter-Process Communication (IPC)\n\nIn Go, `context.Context` is a powerful construct for managing deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes. It is particularly important in I/O operations or inter-process communication (IPC), where operations might need to be cancelled or timed out.\n\n#### Guideline: Use Context for IO and IPC\n\n- **Mandatory Use of Context:** When performing I/O operations or inter-process communication, it's crucial to use `context.Context` to manage the lifecycle of these operations. This includes setting deadlines, handling cancellation signals, and passing request-scoped values.\n\n#### Incorrect Example: Ignoring Context in an HTTP Call\n\n```go\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"log\"\n)\n\n// FetchData makes an HTTP GET request to the specified URL and returns the response body.\n// This function does not use context, making it impossible to cancel the request or set a deadline.\nfunc FetchData(url string) (string, error) {\n\tresp, err := http.Get(url) // Incorrect: Ignoring context\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(body), nil\n}\n\nfunc main() {\n\tdata, err := FetchData(\"http://example.com\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to fetch data: %v\", err)\n\t}\n\tlog.Println(data)\n}\n```\n\nIn this incorrect example, the `FetchData` function makes an HTTP GET request without using a `context`. This approach does not allow the request to be cancelled or a timeout to be set, potentially leading to resources being wasted if the server takes too long to respond or if the operation needs to be aborted for any reason.\n\n#### Correct Example: Using Context in an HTTP Call\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"log\"\n\t\"time\"\n)\n\n// FetchDataWithContext makes an HTTP GET request to the specified URL using the provided context.\n// This allows the request to be cancelled or timed out according to the context's deadline.\nfunc FetchDataWithContext(ctx context.Context, url string) (string, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(body), nil\n}\n\nfunc main() {\n\t// Create a context with a 5-second timeout\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tdata, err := FetchDataWithContext(ctx, \"http://example.com\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to fetch data: %v\", err)\n\t}\n\tlog.Println(data)\n}\n```\n\nIn the correct example, `FetchDataWithContext` uses a context to make the HTTP GET request. This allows the operation to be cancelled or subjected to a timeout, as dictated by the context passed to it. The `context.WithTimeout` function is used in `main` to create a context that cancels the request if it takes longer than 5 seconds, demonstrating a practical use of context to manage operation lifecycle.\n\n### Best Practices for Using Context\n\n1. **Pass context as the first parameter of a function**, following the convention `func(ctx context.Context, ...)`.\n2. **Never ignore the context** provided to you in functions that support it. Always use it in your I/O or IPC operations.\n3. **Avoid storing context in a struct**. Contexts are meant to be passed around within the call stack, not stored.\n4. **Use context's cancellation and deadline features** to control the lifecycle of blocking operations, especially in network I/O and IPC scenarios.\n5. **Propagate context down the call stack** to any function that supports it, ensuring that your application can respond to cancellation signals and deadlines effectively.\n\nBy adhering to these guidelines and examples, you can ensure that your Go applications handle I/O and IPC operations more reliably and efficiently, with proper support for cancellation, timeouts, and request-scoped values.\n\n\n\n## 3. 日志规范\n\n启动时正常日志，打印流程日志，如链接mongo成功，注意不要打印密码等敏感信息。\n\n启动时以及运行中异常终止日志，如果需要终止程序，调用ExitWithError\n\n运行时日志打印，对于错误日志，使用日志库打印，仅在最上层调用打印；对于debug日志，可以随意打印；对于关键日志打印 info；\n\n## 5.异常及错误处理\n\n任何情况禁止使用panic\n\n错误需要wrap，并带上message和key value，用户排查问题；错误wrap仅一次，及函数本身出现的错误，或者调用项目之外的函数产生的错误。\n\n用errs.New()替代errors.New()\n\n\n\n\n\n### 1.4 Panic Processing\n\nThe use of `panic` should be carefully controlled in Go applications to ensure program stability and predictable error handling. Following are revised guidelines emphasizing the restriction on using `panic` and promoting alternative strategies for error handling and program termination.\n\n- **Prohibited in Business Logic:** Using `panic` within business logic processing is strictly prohibited. Business logic should handle errors gracefully and use error returns to propagate issues up the call stack.\n\n- **Restricted Use in Main Package:** In the main package, the use of `panic` should be reserved for situations where the program is entirely inoperable, such as failure to open essential files, inability to connect to the database, or other critical startup issues. Even in these scenarios, prefer using structured error handling to terminate the program.\n\n- **Prohibition on Exportable Interfaces:** Exportable interfaces must not invoke `panic`. They should handle errors gracefully and return errors as part of their contract.\n\n- **Prefer Errors Over Panic:** It is recommended to use error returns instead of panic to convey errors within a package. This approach promotes error handling that integrates smoothly with Go's error handling idioms.\n\n#### Alternative to Panic: Structured Program Termination\n\nTo enforce these guidelines, consider implementing structured functions to terminate the program gracefully in the face of unrecoverable errors, while providing clear error messages. Here are two recommended functions:\n\n```go\n// ExitWithError logs an error message and exits the program with a non-zero status.\nfunc ExitWithError(err error) {\n\tprogName := filepath.Base(os.Args[0])\n\tfmt.Fprintf(os.Stderr, \"%s exit -1: %+v\\n\", progName, err)\n\tos.Exit(-1)\n}\n\n// SIGTERMExit logs a warning message when the program receives a SIGTERM signal and exits with status 0.\nfunc SIGTERMExit() {\n\tprogName := filepath.Base(os.Args[0])\n\tfmt.Fprintf(os.Stderr, \"Warning %s receive process terminal SIGTERM exit 0\\n\", progName)\n}\n```\n\n#### Example Usage:\n\n```go\nimport (\n\t_ \"net/webhook/pprof\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n\tutil \"github.com/openimsdk/open-im-server/v3/pkg/util/genutil\"\n)\n\nfunc main() {\n\tapiCmd := cmd.NewApiCmd()\n\tapiCmd.AddPortFlag()\n\tapiCmd.AddPrometheusPortFlag()\n\tif err := apiCmd.Execute(); err != nil {\n\t\tutil.ExitWithError(err)\n\t}\n}\n```\n\nIn this example, `ExitWithError` is used to terminate the program when an unrecoverable error occurs, providing a clear error message to stderr and exiting with a non-zero status. This approach ensures that critical errors are logged and the program exits in a controlled manner, facilitating troubleshooting and maintaining the stability of the application.\n\n\n\n### 1.3 Error Handling\n\n- `error` is returned as the value of the function, `error` must be handled, or the return value assigned to explicitly ignore. For `defer xx.Close()`, there is no need to explicitly handle it.\n\n```go\nfunc load() error {\n// normal code\n}\n\n// bad\nload()\n\n//good\n  _ = load()\n```\n\n- When `error` is returned as the value of a function and there are multiple return values, `error` must be the last parameter.\n\n```go\n// bad\nfunc load() (error, int) {\n// normal code\n}\n\n//good\nfunc load() (int, error) {\n// normal code\n}\n```\n\n- Perform error handling as early as possible and return as early as possible to reduce nesting.\n\n```go\n// bad\nif err != nil {\n// error code\n} else {\n// normal code\n}\n\n//good\nif err != nil {\n// error handling\nreturn err\n}\n// normal code\n```\n\n- If you need to use the result of the function call outside if, you should use the following method.\n\n```go\n// bad\nif v, err := foo(); err != nil {\n// error handling\n}\n\n// good\nv, err := foo()\nif err != nil {\n// error handling\n}\n```\n\n- Errors should be judged independently, not combined with other logic.\n\n```go\n// bad\nv, err := foo()\nif err != nil || v == nil {\n  // error handling\n  return err\n}\n\n//good\nv, err := foo()\nif err != nil {\n  // error handling\n  return err\n}\n\nif v == nil {\n  // error handling\n  return errors. New(\"invalid value v\")\n}\n```\n\n- If the return value needs to be initialized, use the following method.\n\n```go\nv, err := f()\nif err != nil {\n  // error handling\n  return // or continue.\n}\n```\n\n- Bug description suggestions\n- Error descriptions start with a lowercase letter and do not end with punctuation, for example:\n\n```go\n// bad\nerrors.New(\"Redis connection failed\")\nerrors.New(\"redis connection failed.\")\n\n// good\nerrors.New(\"redis connection failed\")\n```\n\n- Tell users what they can do, not what they can't.\n- When declaring a requirement, use must instead of should. For example, `must be greater than 0, must match regex '[a-z]+'`.\n- When declaring that a format is incorrect, use must not. For example, `must not contain`.\n- Use may not when declaring an action. For example, `may not be specified when otherField is empty, only name may be specified`.\n- When quoting a literal string value, indicate the literal in single quotes. For example, `ust not contain '..'`.\n- When referencing another field name, specify that name in backticks. For example, must be greater than `request`.\n- When specifying unequal, use words instead of symbols. For example, `must be less than 256, must be greater than or equal to 0 (do not use larger than, bigger than, more than, higher than)`.\n- When specifying ranges of numbers, use inclusive ranges whenever possible.\n- Go 1.13 or above is recommended, and the error generation method is `fmt.Errorf(\"module xxx: %w\", err)`.\n\n### 1.6 Type assertion failure handling\n\n- A single return value from a type assertion will panic for an incorrect type. Always use the \"comma ok\" idiom.\n\n```go\n// bad\nt := n.(int)\n\n//good\nt, ok := n.(int)\nif !ok {\n// error handling\n}\n```\n\n\n\n### 2.8 Error naming\n\n- The Error type should be written in the form of FooError.\n\n```go\ntype ExitError struct {\n// ....\n}\n```\n\n- The Error variable is written in the form of ErrFoo.\n\n```go\nvar ErrFormat = errors. New(\"unknown format\")\n```\n\nFor non-standard Err naming, CICD will report an error\n\n\n### 2.9 Handling Errors Properly\n\nIn Go, proper error handling is crucial for creating reliable and maintainable applications. It's important to ensure that errors are not ignored or discarded, as this can lead to unpredictable behavior and difficult-to-debug issues. Here are the guidelines and examples regarding the proper handling of errors.\n\n#### Guideline: Do Not Discard Errors\n\n- **Mandatory Error Propagation:** When calling a function that returns an error, the calling function must handle or propagate the error, instead of ignoring it. This approach ensures that errors are not silently ignored, allowing higher-level logic to make informed decisions about error handling.\n\n#### Incorrect Example: Discarding an Error\n\n```go\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n)\n\nfunc ReadFileContent(filename string) string {\n\tcontent, _ := ioutil.ReadFile(filename) // Incorrect: Error is ignored\n\treturn string(content)\n}\n\nfunc main() {\n\tcontent := ReadFileContent(\"example.txt\")\n\tlog.Println(content)\n}\n```\n\nIn this incorrect example, the error returned by `ioutil.ReadFile` is ignored. This can lead to situations where the program continues execution even if the file doesn't exist or cannot be accessed, potentially causing more cryptic errors downstream.\n\n#### Correct Example: Propagating an Error\n\n```go\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n)\n\n// ReadFileContent attempts to read and return the content of the specified file.\n// It returns an error if reading fails.\nfunc ReadFileContent(filename string) (string, error) {\n\tcontent, err := ioutil.ReadFile(filename)\n\tif err != nil {\n\t\t// Correct: Propagate the error\n\t\treturn \"\", err\n\t}\n\treturn string(content), nil\n}\n\nfunc main() {\n\tcontent, err := ReadFileContent(\"example.txt\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to read file: %v\", err)\n\t}\n\tlog.Println(content)\n}\n```\n\nIn the correct example, the error returned by `ioutil.ReadFile` is propagated back to the caller. The `main` function then checks the error and terminates the program with an appropriate error message if an error occurred. This approach ensures that errors are handled appropriately, and the program does not proceed with invalid state.\n\n### Best Practices for Error Handling\n\n1. **Always check the error returned by a function.** Do not ignore it.\n2. **Propagate errors up the call stack unless they can be handled gracefully at the current level.**\n3. **Provide context for errors when propagating them, making it easier to trace the source of the error.** This can be achieved using `fmt.Errorf` with the `%w` verb or dedicated wrapping functions provided by some error handling packages.\n4. **Log the error at the point where it is handled or makes the program to terminate, to provide insight into the failure.**\n\nBy following these guidelines, you ensure that your Go applications handle errors in a consistent and effective manner, improving their reliability and maintainability.\n\n\n\n\n\n\n\n\n\n\n\n\n\n## Suggestions\n\n\n\n## 3. Comment specification\n\n- Each exportable name must have a comment, which briefly introduces the exported variables, functions, structures, interfaces, etc.\n- All single-line comments are used, and multi-line comments are prohibited.\n- Same as the code specification, single-line comments should not be too long, and no more than 120 characters are allowed. If it exceeds, please use a new line to display, and try to keep the format elegant.\n- A comment must be a complete sentence, starting with the content to be commented and ending with a period, `the format is // name description.`. For example:\n\n```go\n// bad\n// logs the flags in the flagset.\nfunc PrintFlags(flags *pflag. FlagSet) {\n// normal code\n}\n\n//good\n// PrintFlags logs the flags in the flagset.\nfunc PrintFlags(flags *pflag. FlagSet) {\n// normal code\n}\n```\n\n- All commented out code should be deleted before submitting code review, otherwise, it should explain why it is not deleted, and give follow-up processing suggestions.\n\n- Multiple comments can be separated by blank lines, as follows:\n\n```go\n// Package superman implements methods for saving the world.\n//\n// Experience has shown that a small number of procedures can prove\n// helpful when attempting to save the world.\npackage superman\n```\n\n### 3.1 Package Notes\n\n- Each package has one and only one package-level annotation.\n- Package comments are uniformly commented with // in the format of `// Package <package name> package description`, for example:\n\n```go\n// Package genericclioptions contains flags which can be added to you command, bound, completed, and produce\n// useful helper functions.\npackage genericclioptions\n```\n\n### 3.2 Variable/Constant Comments\n\n- Each variable/constant that can be exported must have a comment description, `the format is // variable name variable description`, for example:\n\n```go\n// ErrSigningMethod defines invalid signing method error.\nvar ErrSigningMethod = errors. New(\"Invalid signing method\")\n```\n\n- When there is a large block of constant or variable definition, you can comment a general description in front, and then comment the definition of the constant in detail before or at the end of each line of constant, for example:\n\n```go\n// Code must start with 1xxxxx.\nconst (\n     // ErrSuccess - 200: OK.\n     ErrSuccess int = iota + 100001\n\n     // ErrUnknown - 500: Internal server error.\n     ErrUnknown\n\n     // ErrBind - 400: Error occurred while binding the request body to the struct.\n     ErrBind\n\n     // ErrValidation - 400: Validation failed.\n     ErrValidation\n)\n```\n\n### 3.3 Structure Annotation\n\n- Each structure or interface that needs to be exported must have a comment description, the format is `// structure name structure description.`.\n- The name of the exportable member variable in the structure, if the meaning is not clear, a comment must be given and placed before the member variable or at the end of the same line. For example:\n\n```go\n// User represents a user restful resource. It is also used as gorm model.\ntype User struct {\n     // Standard object's metadata.\n     metav1.ObjectMeta `json:\"metadata,omitempty\"`\n\n     Nickname string `json:\"nickname\" gorm:\"column:nickname\"`\n     Password string `json:\"password\" gorm:\"column:password\"`\n     Email string `json:\"email\" gorm:\"column:email\"`\n     Phone string `json:\"phone\" gorm:\"column:phone\"`\n     IsAdmin int `json:\"isAdmin,omitempty\" gorm:\"column:isAdmin\"`\n}\n```\n\n### 3.4 Method Notes\n\nEach function or method that needs to be exported must have a comment, the format is // function name function description., for examplelike:\n\n```go\n// BeforeUpdate run before update database record.\nfunc (p *Policy) BeforeUpdate() (err error) {\n// normal code\n  return nil\n}\n```\n\n### 3.5 Type annotations\n\n- Each type definition and type alias that needs to be exported must have a comment description, the format is `// type name type description.`, for example:\n\n```go\n// Code defines an error code type.\ntype Code int\n```\n\n## 4. Type\n\n### 4.1 Strings\n\n- Empty string judgment.\n\n```go\n// bad\nif s == \"\" {\n     // normal code\n}\n\n//good\nif len(s) == 0 {\n     // normal code\n}\n```\n\n- `[]byte`/`string` equality comparison.\n\n```go\n// bad\nvar s1 []byte\nvar s2 []byte\n...\nbytes.Equal(s1, s2) == 0\nbytes.Equal(s1, s2) != 0\n\n//good\nvar s1 []byte\nvar s2 []byte\n...\nbytes. Compare(s1, s2) == 0\nbytes. Compare(s1, s2) != 0\n```\n\n- Complex strings use raw strings to avoid character escaping.\n\n```go\n// bad\nregexp.MustCompile(\"\\\\.\")\n\n//good\nregexp.MustCompile(`\\.`)\n```\n\n### 4.2 Slicing\n\n- Empty slice judgment.\n\n```go\n// bad\nif len(slice) = 0 {\n     // normal code\n}\n\n//good\nif slice != nil && len(slice) == 0 {\n     // normal code\n}\n```\n\nThe above judgment also applies to map and channel.\n\n- Declare a slice.\n\n```go\n// bad\ns := []string{}\ns := make([]string, 0)\n\n//good\nvar s[]string\n```\n\n- slice copy.\n\n```go\n// bad\nvar b1, b2 []byte\nfor i, v := range b1 {\n    b2[i] = v\n}\nfor i := range b1 {\n    b2[i] = b1[i]\n}\n\n//good\ncopy(b2, b1)\n```\n\n- slice added.\n\n```go\n// bad\nvar a, b []int\nfor _, v := range a {\n     b = append(b, v)\n}\n\n//good\nvar a, b []int\nb = append(b, a...)\n```\n\n### 4.3 Structure\n\n- struct initialization.\n\nThe struct is initialized in multi-line format.\n\n```go\ntype user struct {\nId int64\nname string\n}\n\nu1 := user{100, \"Colin\"}\n\nu2 := user{\n     Id: 200,\n     Name: \"Lex\",\n}\n```\n\n- \n\n\n\n## 5. Control Structure\n\n### 5.1 if\n\n- if accepts the initialization statement, the convention is to create local variables in the following way.\n\n```go\nif err := loadConfig(); err != nil {\n// error handling\nreturn err\n}\n```\n\n- if For variables of bool type, true and false judgments should be made directly.\n\n```go\nvar isAllow bool\nif isAllow {\n// normal code\n}\n```\n\n### 5.2 for\n\n- Create local variables using short declarations.\n\n```go\nsum := 0\nfor i := 0; i < 10; i++ {\n     sum += 1\n}\n```\n\n- Don't use defer in for loop, defer will only be executed when the function exits.\n\n```go\n// bad\nfor file := range files {\n    fd, err := os. Open(file)\n    if err != nil {\n    return err\n}\ndefer fd. Close()\n// normal code\n}\n\n//good\nfor file := range files {\n    func() {\n        fd, err := os. Open(file)\n        if err != nil {\n        return err\n    }\n    defer fd. Close()\n    // normal code\n    }()\n}\n```\n\n### 5.3 range\n\n- If only the first item (key) is needed, discard the second.\n\n```go\nfor keyIndex := range keys {\n// normal code\n}\n```\n\n- If only the second item is required, underline the first item.\n\n```go\nsum := 0\nfor _, value := range array {\n     sum += value\n}\n```\n\n### 5.4 switch\n\n- must have default.\n\n```go\nswitch os := runtime.GOOS; os {\n     case \"linux\":\n         fmt.Println(\"Linux.\")\n     case \"darwin\":\n         fmt.Println(\"OS X.\")\n     default:\n         fmt.Printf(\"%s.\\n\", os)\n}\n```\n\n### 5.5 goto\n\n- Business code prohibits the use of goto.\n- Try not to use frameworks or other low-level source code.\n\n## 6. Functions\n\n- Incoming variables and return variables start with a lowercase letter.\n- The number of function parameters cannot exceed 5.\n- Function grouping and ordering\n- Functions should be sorted in rough calling order.\n- Functions in the same file should be grouped by receiver.\n- Try to use value transfer instead of pointer transfer.\n- The incoming parameters are map, slice, chan, interface, do not pass pointers.\n\n### 6.1 Function parameters\n\n- If the function returns two or three arguments of the same type, or if the meaning of the result is not clear from the context, use named returns, otherwise it is not recommended to use named returns, for example:\n\n```go\nfunc coordinate() (x, y float64, err error) {\n// normal code\n}\n```\n\n- Both incoming and returned variables start with a lowercase letter.\n- Try to pass by value instead of pointer.\n- The number of parameters cannot exceed 5.\n- Multiple return values can return up to three, and if there are more than three, please use struct.\n\n### 6.2 defer\n\n- When resources are created, resources should be released immediately after defer (defer can be used boldly, the performance of defer is greatly improved in Go1.14 version, and the performance loss of defer can be ignored even in performance-sensitive businesses).\n- First judge whether there is an error, and then defer to release resources, for example:\n\n```go\nrep, err := http. Get(url)\nif err != nil {\n     return err\n}\n\ndefer resp.Body.Close()\n```\n\n### 6.3 Method Receiver\n\n- It is recommended to use the lowercase of the first English letter of the class name as the name of the receiver.\n- Don't use a single character in the name of the receiver when the function exceeds 20 lines.\n- The name of the receiver cannot use confusing names such as me, this, and self.\n\n### 6.4 Nesting\n\n- The nesting depth cannot exceed 4 levels.\n\n### 6.5 Variable Naming\n\n- The variable declaration should be placed before the first use of the variable as far as possible, following the principle of proximity.\n- If the magic number appears more than twice, it is forbidden to use it and use a constant instead, for example:\n\n```go\n// PI...\nconst Price = 3.14\n\nfunc getAppleCost(n float64) float64 {\nreturn Price * n\n}\n\nfunc getOrangeCost(n float64) float64 {\nreturn Price * n\n}\n```\n\n## 7. GOPATH setting specification\n\n- After Go 1.11, the GOPATH rule has been weakened. Existing code (many libraries must have been created before 1.11) must conform to this rule. It is recommended to keep the GOPATH rule to facilitate code maintenance.\n- Only one GOPATH is recommended, multiple GOPATHs are not recommended. If multiple GOPATHs are used, the bin directory where compilation takes effect is under the first GOPATH.\n\n\n\n## 8. Dependency Management\n\n- Go 1.11 and above must use Go Modules.\n- When using Go Modules as a dependency management project, it is not recommended to submit the vendor directory.\n- When using Go Modules as a dependency management project, the go.sum file must be submitted.\n\n### 9. Best Practices\n\n- Minimize the use of global variables, but pass parameters, so that each function is \"stateless\". This reduces coupling and facilitates division of labor and unit testing.\n- Verify interface compliance at compile time, for example:\n\n```go\ntype LogHandler struct {\n   h http.Handler\n   log *zap. Logger\n}\nvar_http.Handler = LogHandler{}\n```\n\n- When the server processes a request, it should create a context, save the relevant information of the request (such as requestID), and pass it in the function call chain.\n\n### 9.1 Performance\n\n- string represents an immutable string variable, modifying string is a relatively heavy operation, and basically needs to re-apply for memory. Therefore, if there is no special need, use []byte more when you need to modify.\n- Prefer strconv over fmt.\n\n### 9.2 Precautions\n\n- append Be careful about automatically allocating memory, append may return a newly allocated address.\n- If you want to directly modify the value of the map, the value can only be a pointer, otherwise the original value must be overwritten.\n- map needs to be locked during concurrency.\n- The conversion of interface{} cannot be checked during compilation, it can only be checked at runtime, be careful to cause panic.\n\n\n\n\n\n## 10 Golang CI Lint\n\n- Golang CI Lint is a fast Go linters runner. It runs linters in parallel, uses caching, and works well with all environments, including CI.\n\n**In local development, you can use the following command to install Golang CI Lint: **\n\n```bash\nmake lint\n```\n\n**In CI/CD, Check the Github Actions status code below after you submit the code directly**\n\n[![OpenIM golangci-lint](https://github.com/openimsdk/open-im-server/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/openimsdk/open-im-server/actions/workflows/golangci-lint.yml)\n\ngolangci lint can select the types of tools, refer to the official documentation: [https://golangci-lint.run/usage/linters/](https://golangci-lint.run/usage/linters/)\n\nThe types of comments we currently use include: [https://github.com/openimsdk/open-im-server/blob/main/.golangci.yml](https://github.com/openimsdk/open-im-server/blob/main/.golangci.yml) the `linters.enable` field in the file.\n\ne.g:\n\n```yaml\nlinters:\n  # please, do not use `enable-all`: it's deprecated and will be removed soon.\n  # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint\n  # enable-all: true\n  disable-all: true\n  enable:\n    - typecheck     # Basic type checking\n    - gofmt         # Format check\n    - govet         # Go's standard linting tool\n    - gosimple      # Suggestions for simplifying code\n    - misspell      # Spelling mistakes\n    - staticcheck   # Static analysis\n    - unused        # Checks for unused code\n    - goimports     # Checks if imports are correctly sorted and formatted\n    - godot         # Checks for comment punctuation\n    - bodyclose     # Ensures HTTP response body is closed\n    - errcheck      # Checks for missed error returns\n  fast: true\n```\n\nAdd that Chinese comments are not allowed in go code, please write a complete golangci lint specification on the basis of the above.\n\n\n### 10.1 Configuration Document\n\nThis configuration document is designed to configure the operational parameters of OpenIM (a hypothetical or specific code analysis tool), customize output formats, and provide detailed settings for specific code checkers (linters). Below is a summary of the document drafted based on the provided configuration information.\n\n#### 10.1 Runtime Options\n\n- **Concurrency** (`concurrency`): Default to use the available CPU count, can be manually set to 4 for parallel analysis.\n- **Timeout** (`timeout`): Timeout duration for analysis operations, default is 1 minute, set here to 5 minutes.\n- **Issue Exit Code** (`issues-exit-code`): Exit code defaults to 1 if at least one issue is found.\n- **Test Files** (`tests`): Whether to include test files, defaults to true.\n- **Build Tags** (`build-tags`): Specify build tags used by all linters, defaults to an empty list. Example adds `mytag`.\n- **Skip Directories** (`skip-dirs`): Configure which directories' issues are not reported, defaults to empty, but some default directories are independently skipped.\n- **Skip Files** (`skip-files`): Specify files where issues should not be reported, supports regular expressions.\n\n#### 10.2 Output Configuration\n\n- **Format** (`format`): Set output format, default is \"colored-line-number\".\n- **Print Issued Lines** (`print-issued-lines`): Whether to print the lines where issues occur, defaults to true.\n- **Print Linter Name** (`print-linter-name`): Whether to print the linter name at the end of issue text, defaults to true.\n- **Uniqueness Filter** (`uniq-by-line`): Whether to make issue outputs unique per line, defaults to true.\n- **Path Prefix** (`path-prefix`): Prefix to add to output file references, defaults to no prefix.\n- **Sort Results** (`sort-results`): Sort results by file path, line number, and column number.\n\n#### 10.3 Linters Settings\n\nIn the configuration file, the `linters-settings` section allows detailed configuration of individual linters. Below are examples of specific linters settings and their purposes:\n\n- **bidichk**: Used to check bidirectional text characters, ensuring correct display direction of text, especially when dealing with mixed left-to-right (LTR) and right-to-left (RTL) text.\n\n- **dogsled**: Monitors excessive use of blank identifiers (`_`) in assignment operations, which may obscure data processing errors or unclear logic.\n\n- **dupl**: Identifies duplicate code blocks, helping developers avoid code redundancy. The `threshold` parameter in settings allows adjustment of code similarity threshold triggering warnings.\n\n- **errcheck**: Checks for unhandled errors. In Go, error handling is achieved by checking function return values. This linter helps ensure all errors are properly handled.\n\n- **exhaustive**: Checks if `switch` statements include all possible values of an enum type, ensuring exhaustiveness of code. This helps avoid forgetting to handle certain cases.\n\n#### 10.4 Example: `errcheck`\n\n**Incorrect Code Example**:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n)\n\nfunc main() {\n    f, _ := os.Open(\"filename.ext\")\n    defer f.Close()\n}\n```\n\n**Issue**: In the above code, the error return value of `os.Open` function is explicitly ignored. This is a common mistake as it may lead to unhandled errors and hard-to-trace bugs.\n\n**Correct Form**:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n)\n\nfunc main() {\n    f, err := os.Open(\"filename.ext\")\n    if err != nil {\n        fmt.Printf(\"error opening file: %v\\n\", err)\n        return\n    }\n    defer f.Close()\n}\n```\n\nIn the correct form, by checking the error (`err`) returned by `os.Open`, we gracefully handle error cases rather than simply ignoring them.\n\n#### 10.5 Example: `gofmt`\n\n**Incorrect Code Example**:\n\n```go\npackage main\nimport \"fmt\"\nfunc main() {\nfmt.Println(\"Hello, world!\")\n}\n```\n\n**Issue**: This code snippet doesn't follow Go's standard formatting rules, for example, incorrect indentation of `fmt.Println`.\n\n**Correct Form**:\n\n```go\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n    fmt.Println(\"Hello, world!\")\n}\n```\n\nUsing `gofmt` tool can automatically fix such formatting issues, ensuring the code adheres to the coding standards of the Go community.\n\n#### 10.6 Example: `unused`\n\n**Incorrect Code Example**:\n\n```go\npackage main\n\nfunc helper() {}\n\nfunc main() {}\n```\n\n**Issue**: The `helper` function is defined but not called anywhere, indicating potential redundant code or missing functionality implementation.\n\n**Correct Form**:\n\n```go\npackage main\n\n// If the helper function is indeed needed, ensure it's used properly.\nfunc helper() {\n    // Implement the function's functionality or ensure it's called elsewhere\n}\n\nfunc main() {\n    helper()\n}\n```\n\nTo improve the section on Linters settings in the document, we'll expand with more detailed explanations and reinforce understanding through examples.\n\n#### 10.7 Example: `dogsled`\n\n**Incorrect Code Example**:\n\n```go\nfunc getValues() (int, int, int) {\n    return 1, 2, 3\n}\n\nfunc main() {\n    _, _, val := getValues()\n    fmt.Println(val) // Only interested in the third return value\n}\n```\n\n**Explanation**: In the above code, we use two blank identifiers to ignore the first two return values. Excessive use of blank identifiers can make code reading difficult.\n\n**Improved Code**:\nConsider refactoring the function or the usage of return values to reduce the need for blank identifiers or explicitly comment why ignoring certain values is safe.\n\n#### 10.8: `exhaustive`\n\n**Incorrect Code Example**:\n\n```go\ntype Fruit int\n\nconst (\n    Apple Fruit = iota\n    Banana\n    Orange\n)\n\nfunc getFruitName(f Fruit) string {\n    switch f {\n    case Apple:\n        return \"Apple\"\n    case Banana:\n        return \"Banana\"\n    // Missing handling for Orange\n    }\n    return \"Unknown\"\n}\n```\n\n**Explanation**: In this code, the `switch` statement doesn't cover all possible values of the `Fruit` type; the case for `Orange` is missing.\n\n**Improved Code**:\n\n```go\nfunc getFruitName(f Fruit) string {\n    switch f {\n    case Apple:\n        return \"Apple\"\n    case Banana:\n        return \"Banana\"\n    case Orange:\n        return \"Orange\"\n    }\n    return \"Unknown\"\n}\n```\n\nBy adding the missing `case`, we ensure the `switch` statement is exhaustive, handling every possible enum value.\n\n#### 10.9 Optimization of Configuration Files and Application of Code Analysis Tools\n\nThrough these examples, we demonstrate how to improve code quality by identifying and fixing common coding issues. OpenIM's configuration files allow developers to customize linters' behavior according to project requirements, ensuring code compliance with predefined quality standards and style guidelines.\n\nBy employing these tools and configuration strategies, teams can reduce the number of bugs, enhance code maintainability, and facilitate efficient collaboration during code review processes.\n\n\n\n\n\n\n\n"
  },
  {
    "path": "docs/contrib/go-doc.md",
    "content": "# Go Language Documentation for OpenIM\n\nIn the realm of software development, especially within Go language projects, documentation plays a crucial role in ensuring code maintainability and ease of use. Properly written and accurate documentation is not only essential for understanding and utilizing software effectively but also needs to be easy to write and maintain. This principle is at the heart of OpenIM's approach to supporting commands and generating documentation.\n\n## Supported Commands in OpenIM\n\nOpenIM leverages Go language's documentation standards to facilitate clear and maintainable code documentation. Below are some of the key commands used in OpenIM for documentation purposes:\n\n### `go doc` Command\n\nThe `go doc` command is used to print documentation for Go language entities such as variables, constants, functions, structures, and interfaces. This command allows specifying the identifier of the program entity to tailor the output. Examples of `go doc` command usage include:\n\n- `go doc sync.WaitGroup.Add` prints the documentation for a specific method of a type in a package.\n- `go doc -u -all sync.WaitGroup` displays all program entities, including unexported ones, for a specified type.\n- `go doc -u sync` outputs all program entities for a specified package, focusing on exported ones without detailed comments.\n\n### `godoc` Command\n\nFor environments lacking internet access, the `godoc` command serves to view the Go language standard library and project dependency library documentation in a web format. Notably, post-Go 1.12 versions, `godoc` is not part of the Go compiler suite. It can be installed using:\n\n```shell\ngo get -u -v golang.org/x/tools/cmd/godoc\n```\n\nThe `godoc` command, once running, hosts a local web server (by default on port 6060) to facilitate documentation browsing at http://127.0.0.1:6060. It generates documentation based on the GOROOT and GOPATH directories, showcasing both the project's own documentation and that of third-party packages installed via `go get`.\n\n### Custom Documentation Generation Commands in OpenIM\n\nOpenIM includes a suite of commands aimed at initializing, generating, and maintaining project documentation and associated files. Some notable commands are:\n\n- `gen.init`: Initializes the OpenIM server project.\n- `gen.docgo`: Generates missing `doc.go` files for Go packages, crucial for package-level documentation.\n- `gen.errcode.doc`: Generates markdown documentation for OpenIM error codes.\n- `gen.ca`: Generates CA files for all certificates, enhancing security documentation.\n\nThese commands underscore the project's commitment to thorough and accessible documentation, supporting both developers and users alike.\n\n## Writing Your Own Documentation\n\nWhen creating documentation for Go projects, including OpenIM, it's important to follow certain practices:\n\n1. **Commenting**: Use single-line (`//`) and block (`/* */`) comments to provide detailed documentation within the code. Block comments are especially useful for package documentation, which should immediately precede the package statement without any intervening blank lines.\n\n2. **Overview Section**: To create an overview section in the documentation, place a block comment directly before the package statement. This section should succinctly describe the package's purpose and functionality.\n\n3. **Detailed Descriptions**: Comments placed before functions, structures, or variables will be used to generate detailed descriptions in the documentation. Follow the same commenting rules as for the overview section.\n\n4. **Examples**: Include example functions prefixed with `Example` to demonstrate usage. Output from these examples can be documented at the end of the function, starting with `// Output:` followed by the expected result.\n\nThrough adherence to these documentation practices, OpenIM ensures that its codebase remains accessible, maintainable, and easy to use for developers and users alike."
  },
  {
    "path": "docs/contrib/images.md",
    "content": "# OpenIM Image Management Strategy and Pulling Guide\n\nOpenIM is an efficient, stable, and scalable instant messaging framework that provides convenient deployment methods through Docker images. OpenIM manages multiple image sources, hosted respectively on GitHub (ghcr), Alibaba Cloud, and Docker Hub. This document is aimed at detailing the image management strategy of OpenIM and providing the steps for pulling these images.\n\n\n## Image Management Strategy\n\nOpenIM's versions correspond to GitHub's tag versions. Each time we release a new version and tag it on GitHub, an automated process is triggered that pushes the new Docker image version to the following three platforms:\n\n1. **GitHub (ghcr.io):** We use GitHub Container Registry (ghcr.io) to host OpenIM's Docker images. This allows us to better integrate with the GitHub source code repository, providing better version control and continuous integration/deployment (CI/CD) features. You can view all GitHub images [here](https://github.com/orgs/OpenIMSDK/packages).\n2. **Alibaba Cloud (registry.cn-hangzhou.aliyuncs.com):** For users in Mainland China, we also host OpenIM's Docker images on Alibaba Cloud to provide faster pull speeds. You can view all Alibaba Cloud images on this [page](https://cr.console.aliyun.com/cn-hangzhou/instances/repositories) of Alibaba Cloud Image Service (note that you need to log in to your Alibaba Cloud account first).\n3. **Docker Hub (docker.io):** Docker Hub is the most commonly used Docker image hosting platform, and we also host OpenIM's images there to facilitate developers worldwide. You can view all Docker Hub images on the [OpenIM's Docker Hub page](https://hub.docker.com/r/openim).\n\n## Base images design\n\n+ [https://github.com/openim-sigs/openim-base-image](https://github.com/openim-sigs/openim-base-image)\n\n## OpenIM Image Design and Usage Guide\n\nOpenIM offers a comprehensive and flexible system of Docker images, available across multiple repositories. We actively maintain these images across different platforms, namely GitHub's ghcr.io, Alibaba Cloud, and Docker Hub. However, we highly recommend ghcr.io for deployment.\n\n### Available Versions\n\nWe provide multiple versions of our images to meet different project requirements. Here's a quick overview of what you can expect:\n\n1. `main`: This image corresponds to the latest version of the main branch in OpenIM. It is updated frequently, making it perfect for users who want to stay at the cutting edge of our features.\n2. `release-v3.*`: This is the image that corresponds to the latest version of OpenIM's stable release branch. It's ideal for users who prefer a balance between new features and stability.\n3. `v3.*.*`: These images are specific to each tag in OpenIM. They are preserved in their original state and are never overwritten. These are the go-to images for users who need a specific, unchanging version of OpenIM.\n4. The image versions adhere to Semantic Versioning 2.0.0 strategy. Taking the `openim-server` image as an example, available at [openim-server container package](https://github.com/openimsdk/open-im-server/pkgs/container/openim-server): upon tagging with v3.5.0, the CI automatically releases the following tags - `openim-server:3`, `openim-server:3.5`, `openim-server:3.5.0`, `openim-server:v3.5.0`, `openim-server:latest`, and `sha-e0244d9`. It's important to note that only `sha-e0244d9` is absolutely unique, whereas `openim-server:v3.5.0` and `openim-server:3.5.0` maintain a degree of uniqueness.\n\n### Multi-Architecture Images\n\nIn order to cater to a wider range of needs, some of our images are provided with multiple architectures under `OS / Arch`. These images offer greater compatibility across different operating systems and hardware architectures, ensuring that OpenIM can be deployed virtually anywhere.\n\n**Example:**\n\n+ [https://github.com/OpenIMSDK/chat/pkgs/container/openim-chat/113925695?tag=v1.1.0](https://github.com/OpenIMSDK/chat/pkgs/container/openim-chat/113925695?tag=v1.1.0)\n\n\n## Methods and Steps for Pulling Images\n\nWhen pulling OpenIM's Docker images, you can choose the most suitable source based on your geographic location and network conditions. Here are the steps to pull OpenIM images from each source:\n\n### Select image\n\n1. Choose the image repository platform you prefer. As previously mentioned, we recommend [OpenIM ghcr.io](https://github.com/orgs/OpenIMSDK/packages).\n\n2. Choose the image name and image version that suits your needs. Refer to the description above for more details.\n\n\n### Install image\n\n1. First, make sure Docker is installed on your machine. If not, you can refer to the [Docker official documentation](https://docs.docker.com/get-docker/) for installation.\n\n2. Open the terminal and run the following commands to pull the images:\n\n   For OpenIM Server:\n\n   - Pull from GitHub:\n\n     ```bash\n     docker pull ghcr.io/openimsdk/openim-server:latest\n     ```\n\n   - Pull from Alibaba Cloud:\n\n     ```bash\n     docker pull registry.cn-hangzhou.aliyuncs.com/openimsdk/openim-server:latest\n     ```\n\n   - Pull from Docker Hub:\n\n     ```bash\n     docker pull docker.io/openim/openim-server:latest\n     ```\n\n   For OpenIM Chat:\n\n   - Pull from GitHub:\n\n     ```bash\n     docker pull ghcr.io/openimsdk/openim-chat:latest\n     ```\n\n   - Pull from Alibaba Cloud:\n\n     ```bash\n     docker pull registry.cn-hangzhou.aliyuncs.com/openimsdk/openim-chat:latest\n     ```\n\n   - Pull from Docker Hub:\n\n     ```bash\n     docker pull docker.io/openim/openim-chat:latest\n     ```\n\n3. Run the `docker images` command to confirm that the image has been successfully pulled.\n\n### Accelerating Deployment for Users in China with Aliyun Mirror or Alternative Image Addresses\n\nFor users in China looking to speed up the deployment process of OpenIM, leveraging a mirror image address is a highly recommended practice. After executing the `make init` command, a `.env` file is generated, which you'll need to edit to configure the image registry source. This configuration is crucial for optimizing download speeds and ensuring a smoother setup process.\n\nWithin the generated `.env` file, you'll find a section dedicated to choosing the image address. It includes options for GitHub (`ghcr.io/openimsdk`), Docker Hub (`openim`), and Ali Cloud (`registry.cn-hangzhou.aliyuncs.com/openimsdk`). To achieve the best performance within China, it is advised to use the Aliyun image address. \n\nTo do this, you need to comment out the current `IMAGE_REGISTRY` setting and uncomment the Aliyun option. Here is how you can adjust it for Aliyun:\n\n```bash\n# Choose the image address: GitHub (ghcr.io/openimsdk), Docker Hub (openim), \n# or Ali Cloud (registry.cn-hangzhou.aliyuncs.com/openimsdk).\n# Uncomment one of the following three options. Aliyun is recommended for users in China.\n# IMAGE_REGISTRY=\"ghcr.io/openimsdk\"\n# IMAGE_REGISTRY=\"openim\"\nIMAGE_REGISTRY=\"registry.cn-hangzhou.aliyuncs.com/openimsdk\"\n```\n\nThis change directs the deployment process to fetch the required images from the Aliyun registry, significantly improving download and installation speeds due to the geographical and network advantages within China. If, for any reason, you prefer not to use Aliyun or encounter issues, consider switching to another mirror address listed in the `.env` file by following the same uncommenting process. This flexibility ensures that users can select the most suitable image source for their specific situation, leading to a more efficient deployment of OpenIM.\n"
  },
  {
    "path": "docs/contrib/init-config.md",
    "content": "# Init OpenIM Config\n\n- [Init OpenIM Config](#init-openim-config)\n  - [Start](#start)\n  - [Define Automated Configuration](#define-automated-configuration)\n  - [Define Configuration Variables](#define-configuration-variables)\n    - [Bash Parsing Features](#bash-parsing-features)\n  - [Reasons and Advantages of the Design](#reasons-and-advantages-of-the-design)\n\n\n##  Start\n\nWith the increasing complexity of software engineering, effective configuration management has become more and more important. Yaml and other configuration files provide the necessary parameters and guidance for systems, but they also impose additional management overhead for developers. This article explores how to automate and optimize configuration management, thereby improving efficiency and reducing the chances of errors.\n\nFirst, obtain the OpenIM code through the contributor documentation and initialize it following the steps below.\n\n## Define Automated Configuration\n\nWe no longer strongly recommend modifying the same configuration file. If you have a new configuration file related to your business, we suggest generating and managing it through automation.\n\nIn the `scripts/init_config.sh` file, we defined some template files. These templates will be automatically generated to the corresponding directories when executing `make init`.\n\n```\n# Defines an associative array where the keys are the template files and the values are the corresponding output files.\ndeclare -A TEMPLATES=(\n  [\"${OPENIM_ROOT}/scripts/template/config-tmpl/env.template\"]=\"${OPENIM_OUTPUT_SUBPATH}/bin/.env\"\n  [\"${OPENIM_ROOT}/scripts/template/config-tmpl/config.yaml\"]=\"${OPENIM_OUTPUT_SUBPATH}/bin/config.yaml\"\n)\n```\n\nIf you have your new mapping files, you can implement them by appending them to the array.\n\nLastly, run:\n\n```\n./scripts/init_config.sh\n```\n\n## Define Configuration Variables\n\nIn the `scripts/install/environment.sh` file, we defined many reusable variables for automation convenience.\n\nIn the provided example, the def function is a core element. This function not only provides a concise way to define variables but also offers default value options for each variable. This way, even if a specific variable is not explicitly set in an environment or scenario, it can still have an expected value.\n\n```\nfunction def() {\n    local var_name=\"$1\"\n    local default_value=\"$2\"\n    eval \"readonly $var_name=\\${$var_name:-$default_value}\"\n}\n```\n\n### Bash Parsing Features\n\nSince bash is a parsing script language, it executes commands in the order they appear in the script. This characteristic means we can define commonly used or core variables at the beginning of the script and then reuse or modify them later on.\n\nFor instance, we can initially set a universal password and reuse this password in subsequent variable definitions.\n\n```\n# Set a consistent password for easy memory\ndef \"PASSWORD\" \"openIM123\"\n\n# Linux system user for openim\ndef \"LINUX_USERNAME\" \"openim\"\ndef \"LINUX_PASSWORD\" \"${PASSWORD}\"\n```\n\n## Reasons and Advantages of the Design\n\n1. **Simplify Configuration Management**: Through automation scripts, we can avoid manual operations and configurations, thus reducing tedious repetitive tasks.\n2. **Reduce Errors**: Manually editing yaml or other configuration files can lead to formatting mistakes or typographical errors. Automating with scripts can lower the risk of such errors.\n3. **Enhanced Readability**: Using the `def` function and other bash scripting techniques, we can establish a clear, easy-to-read, and maintainable configuration system.\n4. **Improved Reusability**: As demonstrated above, we can reuse variables and functions in different parts of the script, reducing redundant code and increasing overall consistency.\n5. **Flexible Default Value Mechanism**: By providing default values for variables, we can ensure configurations are complete and consistent across various scenarios, while also offering customization options for advanced users."
  },
  {
    "path": "docs/contrib/install-docker.md",
    "content": "<!-- vscode-markdown-toc -->\n\n<!-- vscode-markdown-toc-config\n\tnumbering=true\n\tautoSave=true\n\t/vscode-markdown-toc-config -->\n<!-- /vscode-markdown-toc -->\n# Install Docker\n\n\nThe installation command is as follows:\n\n```bash\n$ curl -fsSL https://get.docker.com | bash -s docker --mirror aliyun\n``\n\n## 2.2 Start Docker\n\n```bash\n$ systemctl start docker\n```\n\n## 2.3 Test Docker\n\n```bash\n$ docker run hello-world\n```\n\n## 2.4 Configure Docker Acceleration\n\n```bash\n$ mkdir -p /etc/docker\n$ tee /etc/docker/daemon.json <<-'EOF'\n{\n  \"registry-mirrors\": [\"https://registry.docker-cn.com\"]\n}\nEOF\n$ systemctl daemon-reload\n$ systemctl restart docker\n```\n\n## 2.5 Install Docker Compose\n\n```bash\n$ sudo curl -L \"https://github.com/docker/compose/releases/download/latest/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\n$ sudo chmod +x /usr/local/bin/docker-compose\n```\n\n## 2.6 Test Docker Compose\n\n```bash\n$ docker-compose --version\n```\n"
  },
  {
    "path": "docs/contrib/install-openim-linux-system.md",
    "content": "# OpenIM System: Setup and Usage Guide\n\n<!-- vscode-markdown-toc -->\n* 1. [1. Introduction](#Introduction)\n* 2. [2. Prerequisites (Requires root permissions)](#PrerequisitesRequiresrootpermissions)\n* 3. [3. Create `openim-api` systemd unit template file](#Createopenim-apisystemdunittemplatefile)\n* 4. [4. Copy systemd unit template file to systemd config directory (Requires root permissions)](#CopysystemdunittemplatefiletosystemdconfigdirectoryRequiresrootpermissions)\n* 5. [5. Start systemd service](#Startsystemdservice)\n\n\n##  0. <a name='Introduction'></a>0. Introduction\n\nSystemd is the default service management form for the latest Linux distributions, replacing the original init.\n\nThe OpenIM system is a comprehensive suite of services tailored to address a wide variety of messaging needs. This guide will walk you through the steps of setting up the OpenIM system services and provide insights into its usage.\n\n**Prerequisites:**\n\n+ A Linux server with necessary privileges.\n+ Ensure you have `systemctl` installed and running.\n\n\n##  1. <a name='Deployment'></a>1. Deployment\n\n1. **Retrieve the Installation Script**:\n\n   Begin by obtaining the OpenIM installation script which will be utilized to deploy the entire OpenIM system.\n\n2. **Install OpenIM**:\n\n   To install all the components of OpenIM, run:\n\n   ```bash\n   ./scripts/install/install.sh -i  \n   ```\n\n   or\n\n   ```bash\n   ./scripts/install/install.sh --install  \n   ```\n\n   This will initiate the installation process for all OpenIM components.\n\n3. **Check the Status**:\n\n   Post installation, it is good practice to verify if all the services are running as expected:\n\n   ```bash\n   systemctl status openim.target\n   ```\n\n   This will list the status of all related services of OpenIM.\n\n**Maintenance & Management:**\n\n1. **Checking Individual Service Status**:\n\n   You can monitor the status of individual services with the following command:\n\n   ```bash\n   systemctl status <service-name>\n   ```\n\n   For instance:\n\n   ```bash\n   systemctl status openim-api.service\n   ``\n\n2. **Starting and Stopping Services**:\n\n   If you wish to start or stop any specific service, you can do so with `systemctl start` or `systemctl stop` followed by the service name:\n\n   ```bash\n   systemctl start openim-api.service\n   systemctl stop openim-api.service\n   ```\n\n3. **Uninstalling OpenIM**:\n\n   In case you wish to remove the OpenIM components from your server, utilize:\n\n   ```bash\n   ./scripts/install/install.sh -u\n   ```\n\n   or\n\n   ```bash\n   ./scripts/install/install.sh --uninstall\n   ```\n\n   Ensure you take a backup of any important data before executing the uninstall command.\n\n4. **Logs & Troubleshooting**:\n\n   Logs play a pivotal role in understanding the system's operation and troubleshooting any issues. OpenIM logs can typically be found in the directory specified during installation, usually `${OPENIM_LOG_DIR}`.\n\n   Always refer to the logs when troubleshooting. Look for any error messages or warnings that might give insights into the issue at hand.\n\n\n**Note:**\n\n+ `openim-api.service`: Manages the main API gateways for OpenIM communication.\n+ `openim-crontask.service`: Manages scheduled tasks and jobs.\n+ `openim-msggateway.service`: Takes care of message gateway operations.\n+ `openim-msgtransfer.service`: Handles message transfer functionalities.\n+ `openim-push.service`: Responsible for push notification services.\n+ `openim-rpc-auth.service`: Manages RPC (Remote Procedure Call) for authentication.\n+ `openim-rpc-conversation.service`: Manages RPC for conversations.\n+ `openim-rpc-friend.service`: Handles RPC for friend-related operations.\n+ `openim-rpc-group.service`: Manages group-related RPC operations.\n+ `openim-rpc-msg.service`: Takes care of message RPCs.\n+ `openim-rpc-third.service`: Deals with third-party integrations using RPC.\n+ `openim-rpc-user.service`: Manages user-related RPC operations.\n+ `openim.target`: A target that bundles all the above services for collective operations.\n\n\n**Viewing Logs with `journalctl`:**\n\n`systemctl` services usually log their output to the systemd journal, which you can access using the `journalctl` command.\n\n1. **View Logs for a Specific Service**:\n\n   To view the logs for a particular service, you can use:\n\n   ```bash\n   journalctl -u <service-name>\n   ```\n\n   For example, to see the logs for the `openim-api.service`, you would use:\n\n   ```bash\n   journalctl -u openim-api.service\n   ```\n\n2. **Filtering Logs**:\n\n   + By Time\n\n     : If you wish to see logs since a specific time:\n\n     ```bash\n     journalctl -u openim-api.service --since \"2023-10-28 12:00:00\"\n     ```\n\n   + Most Recent Logs\n\n     : To view the most recent logs, you can combine \n`tail` functionality with `journalctl`:\n\n     ```bash\n     journalctl -u openim-api.service -n 100\n     ```\n\n3. **Continuous Monitoring of Logs**:\n\n   To see new log messages in real-time, you can use the `-f` flag, which mimics the behavior of `tail -f`:\n\n   ```bash\n   journalctl -u openim-api.service -f\n   ```\n\n### Continued Maintenance:\n\n1. **Regularly Check Service Status**:\n\n   It's good practice to routinely verify that all services are active and running. This can be done with:\n\n   ```bash\n   systemctl status openim-api.service openim-push.service openim-rpc-group.service openim-crontask.service openim-rpc-auth.service openim-rpc-msg.service openim-msggateway.service openim-rpc-conversation.service openim-rpc-third.service openim-msgtransfer.service openim-rpc-friend.service openim-rpc-user.service\n   ```\n\n2. **Update Services**:\n\n   Periodically, there might be updates or patches to the OpenIM system or its components. Make sure you keep the system updated. After updating any service, always reload the daemon and restart the service:\n\n   ```bash\n   systemctl daemon-reload\n   systemctl restart openim-api.service\n   ```\n\n3. **Backup Important Data**:\n\n   Regularly backup any configuration files, user data, and other essential data. This ensures that you can restore the system to a working state in case of failures.\n\n### Important `systemctl` and Logging Commands to Learn:\n\n1. **Start/Stop/Restart Services**:\n\n   ```bash\n   systemctl start <service-name>\n   systemctl stop <service-name>\n   systemctl restart <service-name>\n   ```\n\n2. **Enable/Disable Services**:\n\n   If you want a service to start automatically at boot:\n\n   ```bash\n   systemctl enable <service-name>\n   ```\n\n   To prevent it from starting at boot:\n\n   ```bash\n   systemctl disable <service-name>\n   ```\n\n3. **Check Failed Services**:\n\n   To quickly check if any service has failed:\n\n   ```bash\n   systemctl --failed\n   ```\n\n4. **Log Rotation**:\n\n   `journalctl` logs can grow large. To clear all archived journal entries, use:\n\n   ```bash\n   journalctl --vacuum-time=1d\n   ```\n\n\n**Advanced requirements:**\n\n- Convenient service runtime log recording for problem analysis\n- Service management logs\n- Option to restart upon abnormal exit\n\nThe daemon does not meet these advanced requirements.\n\n`nohup` only logs the service's runtime outputs and errors.\n\nOnly systemd can fulfill all of the above requirements.\n\n> The default logs are enhanced with timestamps, usernames, service names, PIDs, etc., making them user-friendly. You can view logs of abnormal service exits. Advanced customization is possible through the configuration files in `/lib/systemd/system/`.\n\nIn short, systemd is the current mainstream way to manage backend services on Linux, so I've abandoned `nohup` in my new versions of bash scripts, opting instead for systemd.\n\n##  2. <a name='PrerequisitesRequiresrootpermissions'></a>Prerequisites (Requires root permissions)\n\n1. Configure `environment.sh` based on the comments.\n2. Create a data directory:\n\n```bash\nmkdir -p ${OPENIM_DATA_DIR}/{openim-api,openim-crontask}\n```\n\n3. Create a bin directory and copy `openim-api` and `openim-crontask` executable files:\n\n```bash\nsource ./environment.sh\nmkdir -p ${OPENIM_INSTALL_DIR}/bin\ncp openim-api openim-crontask ${OPENIM_INSTALL_DIR}/bin\n```\n\n4. Copy the configuration files of `openim-api` and `openim-crontask` to the `${OPENIM_CONFIG_DIR}` directory:\n\n```bash\nmkdir -p ${OPENIM_CONFIG_DIR}\ncp openim-api.yaml openim-crontask.yaml ${OPENIM_CONFIG_DIR}\n```\n\n##  3. <a name='Createopenim-apisystemdunittemplatefile'></a> Create `openim-api` systemd unit template file\n\nFor each OpenIM service, we will create a systemd unit template. Follow the steps below for each service:\n\nRun the following shell script to generate the `openim-api.service.template`:\n\n```bash\nsource ./environment.sh\ncat > openim-api.service.template <<EOF\n[Unit]\nDescription=OpenIM Server API\nDocumentation=https://github.com/oepnimsdk/open-im-server/blob/master/init/README.md\n\n[Service]\nWorkingDirectory=${OPENIM_DATA_DIR}/openim-api\nExecStart=${OPENIM_INSTALL_DIR}/bin/openim-api --config=${OPENIM_CONFIG_DIR}/openim-api.yaml\nRestart=always\nRestartSec=5\nStartLimitInterval=0\n\n[Install]\nWantedBy=multi-user.target\nEOF\n```\n\nFollowing the above style, create the respective template files or generate them in bulk:\n\nFirst, make sure you've sourced the environment variables:\n\n```bash\nsource ./environment.sh\n```\n\nUse the shell script to generate the systemd unit template for each service:\n\n```bash\ndeclare -a services=(\n\"openim-api\"\n... [other services]\n)\n\nfor service in \"${services[@]}\"\ndo\n   cat > $service.service.template <<EOF\n[Unit]\nDescription=OpenIM Server - $service\nDocumentation=https://github.com/oepnimsdk/open-im-server/blob/master/init/README.md\n\n[Service]\nWorkingDirectory=${OPENIM_DATA_DIR}/$service\nExecStart=${OPENIM_INSTALL_DIR}/bin/$service --config=${OPENIM_CONFIG_DIR}/$service.yaml\nRestart=always\nRestartSec=5\nStartLimitInterval=0\n\n[Install]\nWantedBy=multi-user.target\nEOF\ndone\n```\n\n##  4. <a name='CopysystemdunittemplatefiletosystemdconfigdirectoryRequiresrootpermissions'></a>Copy systemd unit template file to systemd config directory (Requires root permissions)\n\nEnsure you have root permissions to perform this operation:\n\n```bash\nfor service in \"${services[@]}\"\ndo\n   sudo cp $service.service.template /etc/systemd/system/$service.service\ndone\n...\n```\n\n##  5. <a name='Startsystemdservice'></a>Start systemd service\n\nTo start the OpenIM services:\n\n```bash\nfor service in \"${services[@]}\"\ndo\n   sudo systemctl daemon-reload \n   sudo systemctl enable $service \n   sudo systemctl restart $service\ndone\n```\n"
  },
  {
    "path": "docs/contrib/kafka.md",
    "content": "# OpenIM Kafka Guide\n\nThis document aims to provide a set of concise guidelines to help you quickly install and use Kafka through Docker Compose.\n\n## Installing Kafka\n\nWith the Docker Compose script provided by OpenIM, you can easily install Kafka. Use the following command to start Kafka:\n\n```bash\ndocker compose up -d\n```\n\nAfter executing this command, Kafka will be installed and started. You can confirm the Kafka container is running with the following command:\n\n```bash\ndocker ps | grep kafka\n```\n\nThe output of this command, as shown below, displays the status information of the Kafka container:\n\n```\nbe416b5a0851   bitnami/kafka:3.5.1 \"/opt/bitnami/script…\"   3 days ago   Up 2 days   9092/tcp, 0.0.0.0:19094->9094/tcp, :::19094->9094/tcp   kafka\n```\n\n### References\n\n- Official Docker installation documentation: [Click here](http://events.jianshu.io/p/b60afa35303a)\n- Detailed installation guide: [Tutorial on Towards Data Science](https://towardsdatascience.com/how-to-install-apache-kafka-using-docker-the-easy-way-4ceb00817d8b)\n\n## Using Kafka\n\n### Entering the Kafka Container\n\nTo execute Kafka commands, you first need to enter the Kafka container. Use the following command:\n\n```bash\ndocker exec -it kafka bash\n```\n\n### Kafka Command Tools\n\nInside the Kafka container, you can use various command-line tools to manage Kafka. These tools include but are not limited to:\n\n- `kafka-topics.sh`: For creating, deleting, listing, or altering topics.\n- `kafka-console-producer.sh`: Allows sending messages to a specified topic from the command line.\n- `kafka-console-consumer.sh`: Allows reading messages from the command line, with the ability to specify topics.\n- `kafka-consumer-groups.sh`: For managing consumer group information.\n\n### Kafka Client Tool Installation\n\nFor easier Kafka management, you can install Kafka client tools. If you installed Kafka through OpenIM's Docker Compose, you can install the Kafka client tools with the following command:\n\n```bash\nmake install.kafkactl\n```\n\n### Automatic Topic Creation\n\nWhen installing Kafka through OpenIM's Docker Compose method, OpenIM automatically creates the following topics:\n\n- `latestMsgToRedis`\n- `msgToPush`\n- `offlineMsgToMongoMysql`\n\nThese topics are created using the `scripts/create-topic.sh` script. The script waits for Kafka to be ready before executing the commands to create topics:\n\n```bash\n# Wait for Kafka to be ready\nuntil /opt/bitnami/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092; do\n  echo \"Waiting for Kafka to be ready...\"\n  sleep 2\ndone\n\n# Create topics\n/opt/bitnami/kafka/bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 8 --topic latestMsgToRedis\n/opt/bitnami/kafka/bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 8 --topic msgToPush\n/opt/bitnami/kafka/bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 8 --topic offlineMsgToMongoMysql\n\necho \"Topics created.\"\n```\n\nThe optimized and expanded documentation further details some basic commands for operations inside the Kafka container, as well as basic commands for managing Kafka using `kafkactl`. Here is a more detailed guide.\n\n\n## Basic Commands in the Kafka Container\n\n### Listing Topics\n\nTo list all existing topics, you can use the following command:\n\n```bash\nkafka-topics.sh --list --bootstrap-server localhost:9092\n```\n\n### Creating a New Topic\n\nWhen creating a new topic, you can specify the number of partitions and the replication factor. Here is the command to create a new topic:\n\n```bash\nkafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic your_topic_name\n```\n\n### Producing Messages\n\nTo send messages to a specific topic, you can use the producer command. The following command prompts you to enter messages, which are sent to the specified topic with each press of the Enter key:\n\n```bash\nkafka-console-producer.sh --broker-list localhost:9092 --topic your_topic_name\n```\n\n### Consuming Messages\n\nTo read messages from a specific topic, you can use the consumer command. The following command reads new messages from the specified topic and outputs them on the console:\n\n```bash\nkafka-console-consumer.sh --bootstrap-server localhost:9092 --topic your_topic_name --from-beginning\n```\n\nThe `\n\n--from-beginning` parameter reads messages from the beginning of the topic. If this parameter is omitted, only new messages will be read.\n\n\n## Basic Commands Using `kafkactl`\n\n`kafkactl` is a command-line tool for managing and operating Kafka clusters. It offers a more modern way to interact with Kafka.\n\n### Listing Topics\n\nTo list all topics, you can use:\n\n```bash\nkafkactl get topics\n```\n\n### Creating a New Topic\n\nTo create a new topic with `kafkactl`, use:\n\n```bash\nkafkactl create topic your_topic_name --partitions 1 --replication-factor 1\n```\n\n### Producing Messages\n\nTo send messages to a topic, you can use:\n\n```bash\nkafkactl produce your_topic_name --value \"your message\"\n```\n\nHere, `\"your message\"` is the content of the message you want to send.\n\n### Consuming Messages\n\nTo consume messages from a topic, use:\n\n```bash\nkafkactl consume your_topic_name --from-beginning\n```\n\nAgain, the `--from-beginning` parameter will start consuming messages from the beginning of the topic. If you do not wish to start from the beginning, you can omit this parameter."
  },
  {
    "path": "docs/contrib/linux-development.md",
    "content": "# Ubuntu 22.04 OpenIM Project Development Guide\n\n## TOC\n- [Ubuntu 22.04 OpenIM Project Development Guide](#ubuntu-2204-openim-project-development-guide)\n  - [TOC](#toc)\n  - [1. Setting Up Ubuntu Server](#1-setting-up-ubuntu-server)\n  - [1.1 Create `openim` Standard User](#11-create-openim-standard-user)\n  - [1.2 Setting up the `openim` User's Shell Environment](#12-setting-up-the-openim-users-shell-environment)\n  - [1.3 Installing Dependencies](#13-installing-dependencies)\n\n## 1. Setting Up Ubuntu Server\n\nYou can use tools like PuTTY or other SSH clients to log in to your Ubuntu server. Once logged in, a few fundamental configurations are required, such as creating a standard user, adding to sudoers, and setting up the `$HOME/.bashrc` file. The steps are as follows:\n\n## 1.1 Create `openim` Standard User\n\n1. Log in to the Ubuntu system as the `root` user and create a standard user.\n\nGenerally, a project will involve multiple developers. Instead of provisioning a server for every developer, many organizations share a single development machine among developers. To simulate this real-world scenario, we'll use a standard user for development. To create the `openim` user:\n\n```\n# adduser openim # Create the openim user, which developers will use for login and development.\n# passwd openim # Set the login password for openim.\n```\n\nWorking with a non-root user ensures the system's safety and is a good practice. It's recommended to avoid using the root user as much as possible during everyday development.\n\n1. Add to sudoers.\n\nOften, even standard users need root privileges. Instead of frequently asking the system administrator for the root password, you can add the standard user to the sudoers. This allows them to temporarily gain root access using the sudo command. To add the `openim` user to sudoers:\n\n```\n\n# sed -i '/^root.*ALL=(ALL:ALL).*ALL/a\\openim\\tALL=(ALL) \\tALL' /etc/sudoers\n```\n\n## 1.2 Setting up the `openim` User's Shell Environment\n\n1. Log into the Ubuntu system.\n\nAssuming we're using the **openim** user, log in using PuTTY or other SSH clients.\n\n1. Configure the `$HOME/.bashrc` file.\n\nThe first step after logging into a new server is to configure the `$HOME/.bashrc` file. It makes the Linux shell more user-friendly by setting environment variables like `LANG` and `PS1`. Here's how the configuration would look:\n\n```\n# .bashrc\n\n# User specific aliases and functions\n\nalias rm='rm -i'\nalias cp='cp -i'\nalias mv='mv -i'\n\n# Source global definitions\nif [ -f /etc/bashrc ]; then\n    . /etc/bashrc\nfi\n\nif [ ! -d $HOME/workspace ]; then\n    mkdir -p $HOME/workspace\nfi\n\n# User specific environment\nexport LANG=\"en_US.UTF-8\" \nexport PS1='[\\u@dev \\W]\\$ '\nexport WORKSPACE=\"$HOME/workspace\"\nexport PATH=$HOME/bin:$PATH\n\ncd $WORKSPACE\n```\n\nAfter updating `$HOME/.bashrc`, run the `bash` command to reload the configurations into the current shell.\n\n## 1.3 Installing Dependencies\n\nThe OpenIM project on Ubuntu may have various dependencies. Some are direct, and others are indirect. Installing these in advance prevents issues like missing packages or compile-time errors later on.\n\n1. Install dependencies.\n\nYou can use the `apt` command to install the required tools on Ubuntu:\n\n```\n$ sudo apt-get update \n$ sudo apt-get install build-essential autoconf automake cmake perl libcurl4-gnutls-dev libtool gcc g++ glibc-doc-reference zlib1g-dev git-lfs telnet lrzsz jq libexpat1-dev libssl-dev\n$ sudo apt install libcurl4-openssl-dev\n```\n\n1. Install Git.\n\nA higher version of Git ensures compatibility with certain commands like `git fetch --unshallow`. To install a recent version:\n\n```\n$ cd /tmp\n$ wget --no-check-certificate https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.36.1.tar.gz\n$ tar -xvzf git-2.36.1.tar.gz\n$ cd git-2.36.1/\n$ ./configure\n$ make\n$ sudo make install\n$ git --version          \n```\n\nThen, add Git's binary directory to the `PATH`:\n\n```\n\n$ echo 'export PATH=/usr/local/libexec/git-core:$PATH' >> $HOME/.bashrc\n```\n\n1. Configure Git.\n\nTo set up Git:\n\n```\n$ git config --global user.name \"Your Name\"\n$ git config --global user.email \"your_email@example.com\"\n$ git config --global credential.helper store\n$ git config --global core.longpaths true\n```\n\nOther Git configurations include:\n\n```\n\n$ git config --global core.quotepath off\n```\n\nAnd for handling larger files:\n\n```\n\n$ git lfs install --skip-repo\n```\n\nBy following the steps in this guide, your Ubuntu 22.04 server should now be set up and ready for OpenIM project development."
  },
  {
    "path": "docs/contrib/local-actions.md",
    "content": "# act\n\nRun your [GitHub Actions](https://developer.github.com/actions/) locally! Why would you want to do this? Two reasons:\n\n- **Fast Feedback** - Rather than having to commit/push every time you want to test out the changes you are making to your `.github/workflows/` files (or for any changes to embedded GitHub actions), you can use `act` to run the actions locally. The [environment variables](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables) and [filesystem](https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners#filesystems-on-github-hosted-runners) are all configured to match what GitHub provides.\n- **Local Task Runner** - I love [make](https://en.wikipedia.org/wiki/Make_(software)). However, I also hate repeating myself. With `act`, you can use the GitHub Actions defined in your `.github/workflows/` to replace your `Makefile`!\n\n## install act\n\n+ [https://github.com/nektos/act](https://github.com/nektos/act)\n\n```bash\ncurl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash\n···"
  },
  {
    "path": "docs/contrib/logging.md",
    "content": "# OpenIM Logging and Error Handling Documentation\n\n## Script Logging Documentation Link\n\nIf you wish to view the script's logging documentation, you can click on this link: [Logging Documentation](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/bash-log.md).\n\nBelow is the documentation for logging and error handling in the OpenIM Go project.\n\nTo create a standard set of documentation that is quick to read and easy to understand, we will highlight key information about the `Logger` interface and the `CodeError` interface. This includes the purpose of each interface, key methods, and their use cases. This will help developers quickly grasp how to effectively use logging and error handling within the project.\n\n## Logging (`Logger` Interface)\n\n### Purpose\nThe `Logger` interface aims to provide the OpenIM project with a unified and flexible logging mechanism, supporting structured logging formats for efficient log management and analysis.\n\n### Key Methods\n\n- **Debug, Info, Warn, Error**  \n  Log messages of different levels to suit various logging needs and scenarios. These methods accept a context (`context.Context`), a message (`string`), and key-value pairs (`...interface{}`), allowing the log to carry rich context information.\n\n- **WithValues**  \n  Append key-value pair information to log messages, returning a new `Logger` instance. This helps in adding consistent context information.\n\n- **WithName**  \n  Set the name of the logger, which helps in identifying the source of the logs.\n\n- **WithCallDepth**  \n  Adjust the call stack depth to accurately identify the source of the log message.\n\n### Use Cases\n\n- Developers should choose the appropriate logging level (such as `Debug`, `Info`, `Warn`, `Error`) based on the importance of the information when logging.\n- Use `WithValues` and `WithName` to add richer context information to logs, facilitating subsequent tracking and analysis.\n\n## Error Handling (`CodeError` Interface)\n\n### Purpose\nThe `CodeError` interface is designed to provide a unified mechanism for error handling and wrapping, making error information more detailed and manageable.\n\n### Key Methods\n\n- **Code**  \n  Return the error code to distinguish between different types of errors.\n\n- **Msg**  \n  Return the error message description to display to the user.\n\n- **Detail**  \n  Return detailed information about the error for further debugging by developers.\n\n- **WithDetail**  \n  Add detailed information to the error, returning a new `CodeError` instance.\n\n- **Is**  \n  Determine whether the current error matches a specified error, supporting a flexible error comparison mechanism.\n\n- **Wrap**  \n  Wrap another error with additional message description, facilitating the tracing of the error's cause.\n\n### Use Cases\n\n- When defining errors with specific codes and messages, use error types that implement the `CodeError` interface.\n- Use `WithDetail` to add additional context information to errors for more accurate problem localization.\n- Use the `Is` method to judge the type of error for conditional branching.\n- Use the `Wrap` method to wrap underlying errors while adding more contextual descriptions.\n\n## Logging Standards and Code Examples\n\nIn the OpenIM project, we use the unified logging package `github.com/openimsdk/tools/log` for logging to achieve efficient log management and analysis. This logging package supports structured logging formats, making it easier for developers to handle log information.\n\n### Logger Interface and Implementation\n\nThe logger interface is defined as follows:\n\n```go\ntype Logger interface {\n    Debug(ctx context.Context, msg string, keysAndValues ...interface{})\n    Info(ctx context.Context, msg string, keysAndValues ...interface{})\n    Warn(ctx context.Context, msg string, err error, keysAndValues ...interface{})\n    Error(ctx context.Context, msg string, err error, keysAndValues ...interface{})\n    WithValues(keysAndValues ...interface{}) Logger\n    WithName(name string) Logger\n    WithCallDepth(depth int) Logger\n}\n```\n\nExample code: Using the `Logger` interface to log at the info level.\n\n```go\nfunc main() {\n\tlogger := log.NewLogger().WithName(\"MyService\")\n\tctx := context.Background()\n\tlogger.Info(ctx, \"Service started\", \"port\", \"8080\")\n}\n```\n\n## Error Handling and Code Examples\n\nWe use the `github.com/openimsdk/tools/errs` package for unified error handling and wrapping.\n\n### CodeError Interface and Implementation\n\nThe error interface is defined as follows:\n\n```go\ntype CodeError interface {\n    Code() int\n    Msg() string\n    Detail() string\n    WithDetail(detail string) CodeError\n    Is(err error, loose ...bool) bool\n    Wrap(msg ...string) error\n    error\n}\n```\n\nExample code: Creating and using the `CodeError` interface to handle errors.\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nfunc main() {\n\terr := errs.New(404, \"Resource not found\")\n\terr = err.WithDetail(\"\n\nMore details\")\n\tif e, ok := err.(errs.CodeError); ok {\n\t\tfmt.Println(e.Code(), e.Msg(), e.Detail())\n\t}\n}\n```\n\n### Detailed Logging Standards and Code Examples\n\n1. **Print key information at startup**  \n   It is crucial to print entry parameters and key process information at program startup. This helps understand the startup state and configuration of the program.\n\n   **Code Example**:\n   ```go\n   package main\n\n   import (\n       \"fmt\"\n       \"os\"\n   )\n\n   func main() {\n       fmt.Println(\"Program startup, version: 1.0.0\")\n       fmt.Printf(\"Connecting to database: %s\\n\", os.Getenv(\"DATABASE_URL\"))\n   }\n   ```\n\n2. **Use `tools/log` and `fmt` for logging**  \n   Logging should be done using a specialized logging library for unified management and formatted log output.\n\n   **Code Example**: Logging an info level message with `tools/log`.\n   ```go\n   package main\n\n   import (\n       \"context\"\n       \"github.com/openimsdk/tools/log\"\n   )\n\n   func main() {\n       ctx := context.Background()\n       log.Info(ctx, \"Application started successfully\")\n   }\n   ```\n\n3. **Use standard error output for startup failures or critical information**  \n   Critical error messages or program startup failures should be indicated to the user through standard error output.\n\n   **Code Example**:\n   ```go\n   package main\n\n   import (\n       \"fmt\"\n       \"os\"\n   )\n\n   func checkEnvironment() bool {\n       return os.Getenv(\"REQUIRED_ENV\") != \"\"\n   }\n\n   func main() {\n       if !checkEnvironment() {\n           fmt.Fprintln(os.Stderr, \"Missing required environment variable\")\n           os.Exit(1)\n       }\n   }\n   ```\n   \n   We encapsulate it into separate tools, which can output error information through the `tools/log` package.\n\n   ```go\n    package main\n\n    import (\n        util \"github.com/openimsdk/open-im-server/v3/pkg/util/genutil\"\n    )\n\n    func main() {\n        if err := apiCmd.Execute(); err != nil {\n            util.ExitWithError(err)\n        }\n    }\n    ```\n\n4. **Use `tools/log` package for runtime logging**  \n   This ensures consistency and control over logging.\n\n   **Code Example**: Same as the above example using `tools/log`. When `tools/log` is not initialized, consider using `fmt` for standard output.\n\n5. **Error logs should be printed by the top-level caller**  \n   This is to avoid duplicate logging of errors, typically errors are caught and logged at the application's outermost level.\n\n   **Code Example**:\n   ```go\n   package main\n\n   import (\n       \"github.com/openimsdk/tools/log\"\n       \"context\"\n   )\n\n   func doSomething() error {\n       // An error occurs here\n       return errs.Wrap(errors.New(\"An error occurred\"))\n   }\n\n   func controller() error {\n       err := doSomething()\n       if err != nil {\n           return err\n       }\n       return nil\n   }\n\n   func main() {\n       err := controller()\n       if err != nil {\n           log.Error(context.Background(), \"Operation failed\", err)\n       }\n   }\n   ```\n\n6. **Handling logs for API RPC calls and non-RPC applications**\n\n   For API RPC calls using gRPC, logs at the information level are printed by middleware on the gRPC server side, reducing the need to manually log in each RPC method. For non-RPC applications, it's recommended to manually log key execution paths to track the application's execution flow.\n\n    **gRPC Server-Side Logging Middleware:**\n\n    In gRPC, `UnaryInterceptor` and `StreamInterceptor` can intercept Unary and Stream type RPC calls, respectively. Here's an example of how to implement a simple Unary RPC logging middleware:\n\n    ```go\n    package main\n\n    import (\n        \"context\"\n        \"google.golang.org/grpc\"\n        \"google.golang.org/grpc/codes\"\n        \"google.golang.org/grpc/status\"\n        \"log\"\n        \"time\"\n    )\n\n    // unaryServerInterceptor returns a new unary server interceptor that logs each request.\n    func unaryServerInterceptor() grpc.UnaryServerInterceptor {\n        return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {\n            // Record the start time of the request\n            start := time.Now()\n            // Call the actual RPC method\n            resp, err = handler(ctx, req)\n            // After the request ends, log the duration and other information\n            log.Printf(\"Request method: %s, duration: %s, error status: %v\", info.FullMethod, time.Since(start), status.Code(err))\n            return resp, err\n        }\n    }\n\n    func main() {\n        // Create a gRPC server and add the middleware\n        s := grpc.NewServer\n\n(grpc.UnaryInterceptor(unaryServerInterceptor()))\n        // Register your service\n\n        // Start the gRPC server\n        log.Println(\"Starting gRPC server...\")\n        // ...\n    }\n    ```\n\n    **Logging for Non-RPC Applications:**\n\n    For non-RPC applications, the key is to log at appropriate places in the code to maintain an execution trace. Here's a simple example showing how to log when handling a task:\n\n    ```go\n    package main\n\n    import (\n        \"log\"\n    )\n\n    func processTask(taskID string) {\n        // Log the start of task processing\n        log.Printf(\"Starting task processing: %s\", taskID)\n        // Suppose this is where the task is processed\n\n        // Log after the task is completed\n        log.Printf(\"Task processing completed: %s\", taskID)\n    }\n\n    func main() {\n        // Example task ID\n        taskID := \"task123\"\n        processTask(taskID)\n    }\n    ```\n\n    In both scenarios, appropriate logging can help developers and operators monitor the health of the system, trace the source of issues, and quickly locate and resolve problems. For gRPC logging, using middleware can effectively centralize log management and control. For non-RPC applications, ensuring logs are placed at critical execution points can help understand the program's operational flow and state changes.\n\n### When to Wrap Errors?\n\n1. **Wrap errors generated within the function**  \n   When an error occurs within a function, use `errs.Wrap` to add context information to the original error.\n\n   **Code Example**:\n   ```go\n   func doSomething() error {\n       // Suppose an error occurs here\n       err, _ := someFunc()\n       if err != nil {\n         return errs.WrapMsg(err, \"doSomething failed\")\n       }\n   }\n   ```\n\n   It just works if the package is wrong:\n\n   ```go\n      func doSomething() error {\n       // Suppose an error occurs here\n       err, _ := someFunc()\n       if err != nil {\n         return errs.Wrap(err)\n       }\n   }\n   ```\n\n2. **Wrap errors from system calls or other packages**  \n   When calling external libraries or system functions that return errors, also add context information to wrap the error.\n\n   **Code Example**:\n   ```go\n   func readConfig(file string) error {\n       _, err := os.ReadFile(file)\n       if err != nil {\n           return errs.Wrap(err, \"Failed to read config file\")\n       }\n       return nil\n   }\n   ```\n\n3. **No need to re-wrap errors for internal module calls**  \n\n   If an error has been appropriately wrapped with sufficient context information in an internal module call, there's no need to wrap it again.\n\n    **Code Example**:\n    ```go\n    func doSomething() error {\n        err := doAnotherThing()\n        if err != nil {\n            return err\n        }\n        return nil\n    }\n    ```\n\n4. **Ensure comprehensive wrapping of errors with detailed messages**\n   When wrapping errors, ensure to provide ample context information to make the error more understandable and easier to debug.\n\n   **Code Example**:\n   ```go\n   func connectDatabase() error {\n       err := db.Connect(config.DatabaseURL)\n       if err != nil {\n           return errs.Wrap(err, fmt.Sprintf(\"Failed to connect to database, URL: %s\", config.DatabaseURL))\n       }\n       return nil\n   }\n   ```\n\n\n### About WrapMsg Use\n\n```go\n// \t\"github.com/openimsdk/tools/errs\"\nfunc WrapMsg(err error, msg string, kv ...any) error {\n\tif len(kv) == 0 {\n\t\tif len(msg) == 0 {\n\t\t\treturn errors.WithStack(err)\n\t\t} else {\n\t\t\treturn errors.WithMessage(err, msg)\n\t\t}\n\t}\n\tvar buf bytes.Buffer\n\tif len(msg) > 0 {\n\t\tbuf.WriteString(msg)\n\t\tbuf.WriteString(\" \")\n\t}\n\tfor i := 0; i < len(kv); i += 2 {\n\t\tif i > 0 {\n\t\t\tbuf.WriteString(\", \")\n\t\t}\n\t\tbuf.WriteString(toString(kv[i]))\n\t\tbuf.WriteString(\"=\")\n\t\tbuf.WriteString(toString(kv[i+1]))\n\t}\n\treturn errors.WithMessage(err, buf.String())\n}\n```\n\n1. **Function Signature**:\n   - `err error`: The original error object.\n   - `msg string`: The message text to append to the error.\n   - `kv ...any`: A variable number of parameters used to pass key-value pairs. `any` was introduced in Go 1.18 and is equivalent to `interface{}`, meaning any type.\n\n2. **Logic**:\n   - If there are no key-value pairs (`kv` is empty):\n     - If `msg` is also empty, use `errors.WithStack(err)` to return the original error with the call stack appended.\n     - If `msg` is not empty, use `errors.WithMessage(err, msg)` to append the message text to the original error.\n   - If there are key-value pairs, the function constructs a string containing the message text and all key-value pairs. The key-value pairs are added in the format `\"key=value\"`, separated by commas. If a message text is provided, it is added first, followed by a space.\n\n3. **Key-Value Pair Formatting**:\n   - A loop iterates over all the key-value pairs, processing one pair at a time.\n   - The `toString` function (although not provided in the code, we can assume it converts any type to a string) is used to convert both keys and values to strings, and they are added to a `bytes.Buffer` in the format `\"key=value\"`.\n\n4. **Result**:\n   - Use `errors.WithMessage(err, buf.String())` to append the constructed message text to the original error, and return this new error object.\n\nNext, let's demonstrate several ways to use the `WrapMsg` function:\n\n**Example 1: No Additional Information**\n\n```go\n// \"github.com/openimsdk/tools/errs\"\nerr := errors.New(\"original error\")\nwrappedErr := errs.WrapMsg(err, \"\")\n// wrappedErr will contain the original error and its call stack\n```\n\n**Example 2: Message Text Only**\n\n```go\n// \"github.com/openimsdk/tools/errs\"\nerr := errors.New(\"original error\")\nwrappedErr := errs.WrapMsg(err, \"additional error information\")\n// wrappedErr will contain the original error, call stack, and \"additional error information\"\n```\n\n**Example 3: Message Text and Key-Value Pairs**\n\n```go\n// \"github.com/openimsdk/tools/errs\"\nerr := errors.New(\"original error\")\nwrappedErr := errs.WrapMsg(err, \"problem occurred\", \"code\", 404, \"url\", \"webhook://example.com\")\n// wrappedErr will contain the original error, call stack, and \"problem occurred code=404, url=http://example.com\"\n```\n\n**Example 4: Key-Value Pairs Only**\n\n```go\n// \"github.com/openimsdk/tools/errs\"\nerr := errors.New(\"original error\")\nwrappedErr := errs.WrapMsg(err, \"\", \"user\", \"john_doe\", \"action\", \"login\")\n// wrappedErr will contain the original error, call stack, and \"user=john_doe, action=login\"\n```\n\n> [!TIP] WThese examples demonstrate how the `errs.WrapMsg` function can flexibly handle error messages and context data, helping developers to more effectively track and debug their programs.\n\n\n### Example 5: Dynamic Key-Value Pairs from Context\nSuppose we have some runtime context variables, such as a user ID and the type of operation being performed, and we want to include these variables in the error message. This can help with later debugging and identifying the specific environment of the issue.\n\n```go\n// Define some context variables\nuserID := \"user123\"\noperation := \"update profile\"\nerrorCode := 500\nrequestURL := \"webhook://example.com/updateProfile\"\n\n// Create a new error\nerr := errors.New(\"original error\")\n\n// Wrap the error, including dynamic key-value pairs from the context\nwrappedErr := errs.WrapMsg(err, \"operation failed\", \"user\", userID, \"action\", operation, \"code\", errorCode, \"url\", requestURL)\n// wrappedErr will contain the original error, call stack, and \"operation failed user=user123, action=update profile, code=500, url=http://example.com/updateProfile\"\n```\n\n> [!TIP]In this example, the `WrapMsg` function accepts not just a static error message and additional information, but also dynamic key-value pairs generated from the code's execution context, such as the user ID, operation type, error code, and the URL of the request. Including this contextual information in the error message makes it easier for developers to understand and resolve the issue."
  },
  {
    "path": "docs/contrib/mac-developer-deployment-guide.md",
    "content": "# Mac Developer Deployment Guide for OpenIM\n\n## Introduction\n\nThis guide aims to assist Mac-based developers in contributing effectively to OpenIM. It covers the setup of a development environment tailored for Mac, including the use of GitHub for development workflow and `devcontainer` for a consistent development experience.\n\nBefore contributing to OpenIM through issues and pull requests, make sure you are familiar with GitHub and the [pull request workflow](https://docs.github.com/en/get-started/quickstart/github-flow).\n\n## Prerequisites\n\n### System Requirements\n\n- macOS (latest stable version recommended)\n- Internet connection\n- Administrator access\n\n### Knowledge Requirements\n\n- Basic understanding of Git and GitHub\n- Familiarity with Docker and containerization\n- Experience with Go programming language\n\n## Setting up the Development Environment\n\n### Installing Homebrew\n\nHomebrew is an essential package manager for macOS. Install it using:\n\n```sh\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n```\n\n### Installing and Configuring Git\n\n1. Install Git:\n\n   ```sh\n   brew install git\n   ```\n\n2. Configure Git with your user details:\n\n   ```sh\n   git config --global user.name \"Your Name\"\n   git config --global user.email \"your.email@example.com\"\n   ```\n\n### Setting Up the Devcontainer\n\n`Devcontainers` provide a Docker-based isolated development environment. \n\nRead [README.md](https://github.com/openimsdk/open-im-server/tree/main/.devcontainer) in the `.devcontainer` directory of the project to learn more about the devcontainer.\n\nTo set it up:\n\n1. Install Docker Desktop for Mac from [Docker Hub](https://docs.docker.com/desktop/install/mac-install/).\n2. Install Visual Studio Code and the Remote - Containers extension.\n3. Open the cloned OpenIM repository in VS Code.\n4. VS Code will prompt to reopen the project in a container. Accept this to set up the environment automatically.\n\n### Installing Go and Dependencies\n\nUse Homebrew to install Go:\n\n```sh\nbrew install go\n```\n\nEnsure the version of Go is compatible with the version required by OpenIM (refer to the main documentation for version requirements).\n\n### Additional Tools\n\nInstall other required tools like Docker, Vagrant, and necessary GNU utils as described in the main documentation.\n\n## Mac Deployment openim-chat and openim-server\n\nTo integrate the Chinese document into an English document for Linux deployment, we will first translate the content and then adapt it to suit the Linux environment. Here's how the translated and adapted content might look:\n\n### Ensure a Clean Environment\n\n- It's recommended to execute in a new directory.\n- Run `ps -ef | grep openim` to ensure no OpenIM processes are running.\n- Run `ps -ef | grep chat` to check for absence of chat-related processes.\n- Execute `docker ps` to verify there are no related containers running.\n\n### Source Code Deployment\n\n#### Deploying openim-server\n\nSource code deployment is slightly more complex because Docker's networking on Linux differs from Mac.\n\n```bash\ngit clone https://github.com/openimsdk/open-im-server\ncd open-im-server\n\nexport OPENIM_IP=\"Your IP\" # If it's a cloud server, setting might not be needed\nmake init # Generates configuration files\n```\n\nBefore deploying openim-server, modify the Kafka logic in the docker-compose.yml file. Replace:\n\n```yaml\n- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://${DOCKER_BRIDGE_GATEWAY:-172.28.0.1}:${KAFKA_PORT:-19094}\n```\n\nWith:\n\n```yaml\n- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://127.0.0.1:${KAFKA_PORT:-19094}\n```\n\nThen start the service:\n\n```bash\ndocker compose up -d\n```\n\nBefore starting the openim-server source, set `config/config.yaml` by replacing all instances of `172.28.0.1` with `127.0.0.1`:\n\n```bash\nvim config/config.yaml -c \"%s/172\\.28\\.0\\.1/127.0.0.1/g\" -c \"wq\"\n```\n\nThen start openim-server:\n\n```bash\nmake start\n```\n\nTo check the startup:\n\n```bash\nmake check\n```\n\n<aside>\n🚧 To avoid mishaps, it's best to wait five minutes before running `make check` again.\n\n</aside>\n\n#### Deploying openim-chat\n\nThere are several ways to deploy openim-chat, either by source code or using Docker.\n\nNavigate back to the parent directory:\n\n```bash\ncd ..\n```\n\nFirst, let's look at deploying chat from source:\n\n```bash\ngit clone https://github.com/openimsdk/chat\ncd chat\nmake init # Generates configuration files\n```\n\nIf openim-chat has not deployed MySQL, you will need to deploy it. Note that the official Docker Hub for MySQL does not support architectures like ARM, so you can use the newer version of the open-source edition:\n\n```bash\ndocker run -d \\\n  --name mysql \\\n  -p 13306:3306 \\\n  -p 3306:33060 \\\n  -v \"$(pwd)/components/mysql/data:/var/lib/mysql\" \\\n  -v \"/etc/localtime:/etc/localtime\" \\\n  -e MYSQL_ROOT_PASSWORD=\"openIM123\" \\\n  --restart always \\\n  mariadb:10.6\n```\n\nBefore starting the source code of openim-chat, set `config/config.yaml` by replacing all instances of `172.28.0.1` with `127.0.0.1`:\n\n```bash\nvim config/config.yaml -c \"%s/172\\.28\\.0\\.1/127.0.0.1/g\" -c \"wq\"\n```\n\nThen start openim-chat from source:\n\n```bash\nmake start\n```\n\nTo check, ensure the following four processes start successfully:\n\n```bash\nmake check \n```\n\n### Docker Deployment\n\nRefer to https://github.com/openimsdk/openim-docker for Docker deployment instructions, which can be followed similarly on Linux.\n\n```bash\ngit clone https://github.com/openimsdk/openim-docker\ncd openim-docker\nexport OPENIM_IP=\"Your IP\"\nmake init\ndocker compose up -d \ndocker compose logs -f openim-server\ndocker compose logs -f openim-chat\n```\n\n## GitHub Development Workflow\n\n### Creating a New Branch\n\nFor new features or fixes, create a new branch:\n\n```sh\ngit checkout -b feat/your-feature-name\n```\n\n### Making Changes and Committing\n\n1. Make your changes in the code.\n2. Stage your changes:\n\n   ```sh\n   git add .\n   ```\n\n3. Commit with a meaningful message:\n\n   ```sh\n   git commit -m \"Add a brief description of your changes\"\n   ```\n\n### Pushing Changes and Creating Pull Requests\n\n1. Push your branch to GitHub:\n\n   ```sh\n   git push origin feat/your-feature-name\n   ```\n\n2. Go to your fork on GitHub and create a pull request to the main OpenIM repository.\n\n### Keeping Your Fork Updated\n\nRegularly sync your fork with the main repository:\n\n```sh\ngit fetch upstream\ngit checkout main\ngit rebase upstream/main\n```\n\nMore read: [https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)\n\n## Testing and Quality Assurance\n\nRun tests as described in the OpenIM documentation to ensure your changes do not break existing functionality.\n\n## Conclusion\n\nThis guide provides a comprehensive overview for Mac developers to set up and contribute to OpenIM. By following these steps, you can ensure a smooth and efficient development experience. Happy coding!"
  },
  {
    "path": "docs/contrib/offline-deployment.md",
    "content": "# OpenIM Offline Deployment Design\n\n## 1. Base Images\n\nBelow are the base images and their versions you'll need:\n\n- [ ] bitnami/kafka:3.5.1\n- [ ] redis:7.0.0\n- [ ] mongo:6.0.2\n- [ ] bitnami/zookeeper:3.8\n- [ ] minio/minio:RELEASE.2024-01-11T07-46-16Z\n\n> [!IMPORTANT]\n> It is important to note that OpenIM removed mysql components from versions v3.5.0 (release-v3.5) and above, so mysql can be deployed without this requirement or above\n\n**If you need to install more IM components or monitoring products：**\n\nOpenIM:\n\n> [!TIP]\n> If you need to install more IM components or monitoring products [images.md](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/images.md)\n\n- [ ] ghcr.io/openimsdk/openim-web:<version-name>\n- [ ] ghcr.io/openimsdk/openim-admin:<version-name>\n- [ ] ghcr.io/openimsdk/openim-chat:<version-name>\n- [ ] ghcr.io/openimsdk/openim-server:<version-name>\n\n\nMonitoring:\n\n- [ ] prom/prometheus：v2.48.1\n- [ ] prom/alertmanager：v0.23.0\n- [ ] grafana/grafana：10.2.2\n- [ ] bitnami/node-exporter：1.7.0\n\n\nUse the following commands to pull these base images:\n\n```bash\ndocker pull bitnami/kafka:3.5.1\ndocker pull redis:7.0.0\ndocker pull mongo:6.0.2\ndocker pull mariadb:10.6\ndocker pull bitnami/zookeeper:3.8\ndocker pull minio/minio:2024-01-11T07-46-16Z\n```\n\nIf you need to install more IM components or monitoring products:\n\n```bash\ndocker pull prom/prometheus:v2.48.1\ndocker pull prom/alertmanager:v0.23.0\ndocker pull grafana/grafana:10.2.2\ndocker pull bitnami/node-exporter:1.7.0\n```\n\n## 2. OpenIM Images\n\n**For detailed understanding of version management and storage of OpenIM and Chat**: [version.md](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/version.md)\n\n### OpenIM Image\n\n- Get image version info: [images.md](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/images.md)\n- Depending on the required version, execute the following command:\n\n```bash\ndocker pull ghcr.io/openimsdk/openim-server:<version-name>\n```\n\n### Chat Image\n\n- Execute the following command to pull the image:\n\n```bash\ndocker pull ghcr.io/openimsdk/openim-chat:<version-name>\n```\n\n### Web Image\n\n- Execute the following command to pull the image:\n\n```bash\ndocker pull ghcr.io/openimsdk/openim-web:<version-name>\n```\n\n### Admin Image\n\n- Execute the following command to pull the image:\n\n```bash\ndocker pull ghcr.io/openimsdk/openim-admin:<version-name>\n```\n\n\n## 3. Image Storage Selection\n\n**Repositories**:\n\n- Alibaba Cloud: `registry.cn-hangzhou.aliyuncs.com/openimsdk/openim-server`\n- Docker Hub: `openim/openim-server`\n\n**Version Selection**:\n\n- Stable: e.g. release-v3.2 (or 3.1, 3.3)\n- Latest: latest\n- Latest of main: main\n\n## 4. Version Selection\n\nYou can select from the following versions:\n\n- Stable: e.g. release-v3.2\n- Latest: latest\n- Latest from main branch: main\n\n## 5. Offline Deployment Steps\n\n1. **Pull images**: Execute the above `docker pull` commands to pull all required images locally.\n2. **Save images**:\n\n```bash\ndocker save -o <tar-file-name>.tar <image-name>\n```\n\nIf you want to save all the images, use the following command:\n\n```bash\ndocker save -o <tar-file-name>.tar $(docker images -q)\n```\n\n3. **Fetch code**: Clone the repository:\n\n```bash\ngit clone https://github.com/openimsdk/openim-docker.git\n```\n\nOr download the code from [Releases](https://github.com/openimsdk/openim-docker/releases/).\n\n> Because of the difference between win and linux newlines, please do not clone the repository with win and then synchronize scp to linux.\n\n4. **Transfer files**: Use `scp` to transfer all images and code to the intranet server.\n\n```bash\nscp <tar-file-name>.tar user@remote-ip:/path/on/remote/server\n```\n\nOr choose other transfer methods such as a hard drive.\n\n5. **Import images**: On the intranet server:\n\n```bash\ndocker load -i <tar-file-name>.tar\n```\n\nImport directly with shortcut commands:\n\n```bash\nfor i in `ls ./`;do docker load -i $i;done\n```\n\n6. **Deploy**: Navigate to the `openim-docker` repository directory and follow the [README guide](https://github.com/openimsdk/openim-docker) for deployment.\n\n7. **Deploy using docker compose**:\n\n```bash\nexport OPENIM_IP=\"your ip\" # Set Ip\nmake init # Init config\ndocker compose up -d # Deployment\ndocker compose ps # Verify\n```\n\n> **Note**: If you're using a version of Docker prior to 20, make sure you've installed `docker-compose`.\n\n## 6. Reference Links\n\n- [openimsdk Issue #432](https://github.com/openimsdk/open-im-server/issues/432)\n- [Notion Link](https://nsddd.notion.site/435ee747c0bc44048da9300a2d745ad3?pvs=25)\n- [openimsdk Issue #474](https://github.com/openimsdk/open-im-server/issues/474)\n"
  },
  {
    "path": "docs/contrib/prometheus-grafana.md",
    "content": "# Deployment and Design of OpenIM's Management Backend and Monitoring\n\n<!-- vscode-markdown-toc -->\n* 1. [Source Code & Docker](#SourceCodeDocker)\n\t* 1.1. [Deployment](#Deployment)\n\t* 1.2. [Configuration](#Configuration)\n\t* 1.3. [Monitoring Running in Docker Guide](#MonitoringRunninginDockerGuide)\n\t\t* 1.3.1. [Introduction](#Introduction)\n\t\t* 1.3.2. [Prerequisites](#Prerequisites)\n\t\t* 1.3.3. [Step 1: Clone the Repository](#Step1:ClonetheRepository)\n\t\t* 1.3.4. [Step 2: Start Docker Compose](#Step2:StartDockerCompose)\n\t\t* 1.3.5. [Step 3: Use the OpenIM Web Interface](#Step3:UsetheOpenIMWebInterface)\n\t\t* 1.3.6. [Running Effect](#RunningEffect)\n\t\t* 1.3.7. [Step 4: Access the Admin Panel](#Step4:AccesstheAdminPanel)\n\t\t* 1.3.8. [Step 5: Access the Monitoring Interface](#Step5:AccesstheMonitoringInterface)\n\t\t* 1.3.9. [Next Steps](#NextSteps)\n\t\t* 1.3.10. [Troubleshooting](#Troubleshooting)\n* 2. [Kubernetes](#Kubernetes)\n\t* 2.1. [Middleware Monitoring](#MiddlewareMonitoring)\n\t* 2.2. [Custom OpenIM Metrics](#CustomOpenIMMetrics)\n\t* 2.3. [Node Exporter](#NodeExporter)\n* 3. [Setting Up and Configuring AlertManager Using Environment Variables and `make init`](#SettingUpandConfiguringAlertManagerUsingEnvironmentVariablesandmakeinit)\n\t* 3.1. [Introduction](#Introduction-1)\n\t* 3.2. [Prerequisites](#Prerequisites-1)\n\t* 3.3. [Configuration Steps](#ConfigurationSteps)\n\t\t* 3.3.1. [Exporting Environment Variables](#ExportingEnvironmentVariables)\n\t\t* 3.3.2. [Initializing AlertManager](#InitializingAlertManager)\n\t\t* 3.3.3. [Key Configuration Fields](#KeyConfigurationFields)\n\t\t* 3.3.4. [Configuring SMTP Authentication Password](#ConfiguringSMTPAuthenticationPassword)\n\t\t* 3.3.5. [Useful Links for Common Email Servers](#UsefulLinksforCommonEmailServers)\n\t* 3.4. [Conclusion](#Conclusion)\n\n<!-- vscode-markdown-toc-config\n\tnumbering=true\n\tautoSave=true\n\t/vscode-markdown-toc-config -->\n<!-- /vscode-markdown-toc -->\n\nOpenIM offers various flexible deployment options to suit different environments and requirements. Here is a simplified and optimized description of these deployment options:\n\n1. Source Code Deployment:\n   + **Regular Source Code Deployment**: Deployment using the `nohup` method. This is a basic deployment method suitable for development and testing environments. For details, refer to the [Regular Source Code Deployment Guide](https://docs.openim.io/).\n   + **Production-Level Deployment**: Deployment using the `system` method, more suitable for production environments. This method provides higher stability and reliability. For details, refer to the [Production-Level Deployment Guide](https://docs.openim.io/guides/gettingStarted/install-openim-linux-system).\n2. Cluster Deployment:\n   + **Kubernetes Deployment**: Provides two deployment methods, including deployment through Helm and sealos. This is suitable for environments that require high availability and scalability. Specific methods can be found in the [Kubernetes Deployment Guide](https://docs.openim.io/guides/gettingStarted/k8s-deployment).\n3. Docker Deployment:\n   + **Regular Docker Deployment**: Suitable for quick deployments and small projects. For detailed information, refer to the [Docker Deployment Guide](https://docs.openim.io/guides/gettingStarted/dockerCompose).\n   + **Docker Compose Deployment**: Provides more convenient service management and configuration, suitable for complex multi-container applications.\n\nNext, we will introduce the specific steps, monitoring, and management backend configuration for each of these deployment methods, as well as usage tips to help you choose the most suitable deployment option according to your needs.\n\n##  1. <a name='SourceCodeDocker'></a>Source Code & Docker\n\n###  1.1. <a name='Deployment'></a>Deployment\n\nOpenIM deploys openim-server and openim-chat from source code, while other components are deployed via Docker.\n\nFor Docker deployment, you can deploy all components with a single command using the [openimsdk/openim-docker](https://github.com/openimsdk/openim-docker) repository. The deployment configuration can be found in the [environment.sh](https://github.com/openimsdk/open-im-server/blob/main/scripts/install/environment.sh) document, which provides information on how to learn and familiarize yourself with various environment variables.\n\nFor Prometheus, it is not enabled by default. To enable it, set the environment variable before executing `make init`:\n\n```bash\nexport PROMETHEUS_ENABLE=true   # Default is false\n```\n\nThen, execute:\n\n```bash\nmake init\ndocker compose up -d\n```\n\n###  1.2. <a name='Configuration'></a>Configuration\n\nTo configure Prometheus data sources in Grafana, follow these steps:\n\n1. **Log in to Grafana**: First, open your web browser and access the Grafana URL. If you haven't changed the port, the address is typically [http://localhost:13000](http://localhost:13000/).\n\n2. **Log in with default credentials**: Grafana's default username and password are both `admin`. You will be prompted to change the password on your first login.\n\n3. **Access Data Sources Settings**:\n\n   + In the left menu of Grafana, look for and click the \"gear\" icon representing \"Configuration.\"\n   + In the configuration menu, select \"Data Sources.\"\n\n4. **Add a New Data Source**:\n\n   + On the Data Sources page, click the \"Add data source\" button.\n   + In the list, find and select \"Prometheus.\"\n\n   ![image-20231114175117374](http://sm.nsddd.top/sm202311141751692.png)\n\n   Click `Add New connection` to add more data sources, such as Loki (responsible for log storage and query processing).\n\n5. **Configure the Prometheus Data Source**:\n\n   + On the configuration page, fill in the details of the Prometheus server. This typically includes the URL of the Prometheus service (e.g., if Prometheus is running on the same machine as OpenIM, the URL might be `http://172.28.0.1:19090`, with the address matching the `DOCKER_BRIDGE_GATEWAY` variable address). OpenIM and the components are linked via a gateway. The default port used by OpenIM is `19090`.\n   + Adjust other settings as needed, such as authentication and TLS settings.\n\n   ![image-20231114180351923](http://sm.nsddd.top/sm202311141803076.png)\n\n6. **Save and Test**:\n\n   + After completing the configuration, click the \"Save & Test\" button to ensure that Grafana can successfully connect to Prometheus.\n\n**Importing Dashboards in Grafana**\n\nImporting Grafana Dashboards is a straightforward process and is applicable to OpenIM Server application services and Node Exporter. Here are detailed steps and necessary considerations:\n\n**Key Metrics Overview and Deployment Steps**\n\nTo monitor OpenIM in Grafana, you need to focus on three categories of key metrics, each with its specific deployment and configuration steps:\n\n**OpenIM Metrics (`prometheus-dashboard.yaml`)**:\n\n- **Configuration File Path**: Find this at `config/prometheus-dashboard.yaml`.\n- **Enabling Monitoring**: Activate Prometheus monitoring by setting the environment variable: `export PROMETHEUS_ENABLE=true`.\n- **More Information**: For detailed instructions, see the [OpenIM Configuration Guide](https://docs.openim.io/configurations/prometheus-integration).\n\n**Node Exporter**:\n\n- **Container Deployment**: Use the container `quay.io/prometheus/node-exporter` for effective node monitoring.\n- **Access Dashboard**: Visit the [Node Exporter Full Feature Dashboard](https://grafana.com/grafana/dashboards/1860-node-exporter-full/) for dashboard integration either through YAML file download or ID.\n- **Deployment Guide**: For deployment steps, consult the [Node Exporter Deployment Documentation](https://prometheus.io/docs/guides/node-exporter/).\n\n**Middleware Metrics**: Different middlewares require unique steps and configurations for monitoring:\n\n- MySQL:\n    - **Configuration**: Make sure MySQL is set up for performance monitoring.\n    - **Guide**: See the [MySQL Monitoring Configuration Guide](https://grafana.com/docs/grafana/latest/datasources/mysql/).\n- Redis:\n    - **Configuration**: Adjust Redis settings to enable monitoring data export.\n    - **Guide**: Consult the [Redis Monitoring Guide](https://grafana.com/docs/grafana/latest/datasources/redis/).\n- MongoDB:\n    - **Configuration**: Configure MongoDB for monitoring metrics.\n    - **Guide**: Visit the [MongoDB Monitoring Guide](https://grafana.com/grafana/plugins/grafana-mongodb-datasource/).\n- Kafka:\n    - **Configuration**: Set up Kafka for Prometheus monitoring integration.\n    - **Guide**: Refer to the [Kafka Monitoring Guide](https://grafana.com/grafana/plugins/grafana-kafka-datasource/).\n- Zookeeper:\n    - **Configuration**: Ensure Prometheus can monitor Zookeeper.\n    - **Guide**: Check out the [Zookeeper Monitoring Configuration](https://grafana.com/docs/grafana/latest/datasources/zookeeper/).\n\n**Importing Steps**:\n\n1. Access the Dashboard Import Interface:\n\n   + Click the `+` icon on the left menu or in the top right corner of Grafana, then select \"Create.\"\n   + Choose \"Import\" to access the dashboard import interface.\n\n2. **Perform Dashboard Import**:\n   + **Upload via File**: Directly upload your YAML file.\n   + **Paste Content**: Open the YAML file, copy its content, and paste it into the import interface.\n   + **Import via Grafana.com Dashboard**: Visit [Grafana Dashboards](https://grafana.com/grafana/dashboards/), search for the desired dashboard, and import it using its ID.\n3. **Configure the Dashboard**:\n   + Select the appropriate data source, such as the previously configured Prometheus.\n   + Adjust other settings, such as the dashboard name or folder.\n4. **Save and View the Dashboard**:\n   + After configuring, click \"Import\" to complete the process.\n   + Immediately view the new dashboard after successful import.\n\n**Graph Examples:**\n\n![image-20231114194451673](http://sm.nsddd.top/sm202311141944953.png)\n\n\n\n###  1.3. <a name='MonitoringRunninginDockerGuide'></a>Monitoring Running in Docker Guide\n\n####  1.3.1. <a name='Introduction'></a>Introduction\n\nThis guide provides the steps to run OpenIM using Docker. OpenIM is an open-source instant messaging solution that can be quickly deployed using Docker. For more information, please refer to the [OpenIM Docker GitHub](https://github.com/openimsdk/openim-docker).\n\n####  1.3.2. <a name='Prerequisites'></a>Prerequisites\n\n+ Ensure that Docker and Docker Compose are installed.\n+ Basic understanding of Docker and containerization technology.\n\n####  1.3.3. <a name='Step1:ClonetheRepository'></a>Step 1: Clone the Repository\n\nFirst, clone the OpenIM Docker repository:\n\n```bash\ngit clone https://github.com/openimsdk/openim-docker.git\n```\n\nNavigate to the repository directory and check the `README` file for more information and configuration options.\n\n####  1.3.4. <a name='Step2:StartDockerCompose'></a>Step 2: Start Docker Compose\n\nIn the repository directory, run the following command to start the service:\n\n```bash\ndocker-compose up -d\n```\n\nThis will download the required Docker images and start the OpenIM service.\n\n####  1.3.5. <a name='Step3:UsetheOpenIMWebInterface'></a>Step 3: Use the OpenIM Web Interface\n\n+ Open a browser in private mode and access [OpenIM Web](http://localhost:11001/).\n+ Register two users and try adding friends.\n+ Test sending messages and pictures.\n\n####  1.3.6. <a name='RunningEffect'></a>Running Effect\n\n![image-20231115100811208](http://sm.nsddd.top/sm202311151008639.png)\n\n####  1.3.7. <a name='Step4:AccesstheAdminPanel'></a>Step 4: Access the Admin Panel\n\n+ Access the [OpenIM Admin Panel](http://localhost:11002/).\n+ Log in using the default username and password (`admin1:admin1`).\n\nRunning Effect Image:\n\n![image-20231115101039837](http://sm.nsddd.top/sm202311151010116.png)\n\n####  1.3.8. <a name='Step5:AccesstheMonitoringInterface'></a>Step 5: Access the Monitoring Interface\n\n+ Log in to the [Monitoring Interface](http://localhost:3000/login) using the credentials (`admin:admin`).\n\n####  1.3.9. <a name='NextSteps'></a>Next Steps\n\n+ Configure and manage the services following the steps provided in the OpenIM source code.\n+ Refer to the `README` file for advanced configuration and management.\n\n####  1.3.10. <a name='Troubleshooting'></a>Troubleshooting\n\n+ If you encounter any issues, please check the documentation on [OpenIM Docker GitHub](https://github.com/openimsdk/openim-docker) or search for related issues in the Issues section.\n+ If the problem persists, you can create an issue on the [openim-docker](https://github.com/openimsdk/openim-docker/issues/new/choose) repository or the [openim-server](https://github.com/openimsdk/open-im-server/issues/new/choose) repository.\n\n\n\n##  2. <a name='Kubernetes'></a>Kubernetes\n\nRefer to [openimsdk/helm-charts](https://github.com/openimsdk/helm-charts).\n\nWhen deploying and monitoring OpenIM in a Kubernetes environment, you will focus on three main metrics: middleware, custom OpenIM metrics, and Node Exporter. Here are detailed steps and guidelines:\n\n###  2.1. <a name='MiddlewareMonitoring'></a>Middleware Monitoring\n\nMiddleware monitoring is crucial to ensure the overall system's stability. Typically, this includes monitoring the following components:\n\n+ **MySQL**: Monitor database performance, query latency, and more.\n+ **Redis**: Track operation latency, memory usage, and more.\n+ **MongoDB**: Observe database operations, resource usage, and more.\n+ **Kafka**: Monitor message throughput, latency, and more.\n+ **Zookeeper**: Keep an eye on cluster status, performance metrics, and more.\n\nFor Kubernetes environments, you can use the corresponding Prometheus Exporters to collect monitoring data for these middleware components.\n\n###  2.2. <a name='CustomOpenIMMetrics'></a>Custom OpenIM Metrics\n\nCustom OpenIM metrics provide essential information about the OpenIM application itself, such as user activity, message traffic, system performance, and more. To monitor these metrics in Kubernetes:\n\n+ Ensure OpenIM application configurations expose Prometheus metrics.\n+ When deploying using Helm charts (refer to [OpenIM Helm Charts](https://github.com/openimsdk/helm-charts)), pay attention to configuring relevant monitoring settings.\n\n###  2.3. <a name='NodeExporter'></a>Node Exporter\n\nNode Exporter is used to collect hardware and operating system-level metrics for Kubernetes nodes, such as CPU, memory, disk usage, and more. To integrate Node Exporter in Kubernetes:\n\n+ Deploy Node Exporter using the appropriate Helm chart. You can find information and guides on [Prometheus Community](https://prometheus.io/docs/guides/node-exporter/).\n+ Ensure Node Exporter's data is collected by Prometheus instances within your cluster.\n\n\n\n##  3. <a name='SettingUpandConfiguringAlertManagerUsingEnvironmentVariablesandmakeinit'></a>Setting Up and Configuring AlertManager Using Environment Variables and `make init`\n\n###  3.1. <a name='Introduction-1'></a>Introduction\n\nAlertManager, a component of the Prometheus monitoring system, handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver. This document outlines how to set up and configure AlertManager using environment variables and the `make init` command. We will focus on configuring key fields like the sender's email, SMTP settings, and SMTP authentication password.\n\n###  3.2. <a name='Prerequisites-1'></a>Prerequisites\n\n+ Basic knowledge of terminal and command-line operations.\n+ AlertManager installed on your system.\n+ Access to an SMTP server for sending emails.\n\n###  3.3. <a name='ConfigurationSteps'></a>Configuration Steps\n\n####  3.3.1. <a name='ExportingEnvironmentVariables'></a>Exporting Environment Variables\n\nBefore initializing AlertManager, you need to set environment variables. These variables are used to configure the AlertManager settings without altering the code. Use the `export` command in your terminal. Here are some key variables you might set:\n\n+ `export ALERTMANAGER_RESOLVE_TIMEOUT='5m'`\n+ `export ALERTMANAGER_SMTP_FROM='alert@example.com'`\n+ `export ALERTMANAGER_SMTP_SMARTHOST='smtp.example.com:465'`\n+ `export ALERTMANAGER_SMTP_AUTH_USERNAME='alert@example.com'`\n+ `export ALERTMANAGER_SMTP_AUTH_PASSWORD='your_password'`\n+ `export ALERTMANAGER_SMTP_REQUIRE_TLS='false'`\n\n####  3.3.2. <a name='InitializingAlertManager'></a>Initializing AlertManager\n\nAfter setting the necessary environment variables, you can initialize AlertManager by running the `make init` command. This command typically runs a script that prepares AlertManager with the provided configuration.\n\n####  3.3.3. <a name='KeyConfigurationFields'></a>Key Configuration Fields\n\n##### a. Sender's Email (`ALERTMANAGER_SMTP_FROM`)\n\nThis variable sets the email address that will appear as the sender in the notifications sent by AlertManager.\n\n##### b. SMTP Configuration\n\n+ **SMTP Server (`ALERTMANAGER_SMTP_SMARTHOST`):** Specifies the address and port of the SMTP server used for sending emails.\n+ **SMTP Authentication Username (`ALERTMANAGER_SMTP_AUTH_USERNAME`):** The username for authenticating with the SMTP server.\n+ **SMTP Authentication Password (`ALERTMANAGER_SMTP_AUTH_PASSWORD`):** The password for SMTP server authentication. It's crucial to keep this value secure.\n\n####  3.3.4. <a name='ConfiguringSMTPAuthenticationPassword'></a>Configuring SMTP Authentication Password\n\nThe SMTP authentication password can be set using the `ALERTMANAGER_SMTP_AUTH_PASSWORD` environment variable. It's recommended to use a secure method to set this variable to avoid exposing sensitive information. For instance, you might read the password from a secure file or a secret management tool.\n\n####  3.3.5. <a name='UsefulLinksforCommonEmailServers'></a>Useful Links for Common Email Servers\n\nFor specific configurations related to common email servers, you may refer to their respective documentation:\n\n+ Gmail SMTP Settings:\n  + [Gmail SMTP Configuration](https://support.google.com/mail/answer/7126229?hl=en)\n+ Microsoft Outlook SMTP Settings:\n  + [Outlook Email Settings](https://support.microsoft.com/en-us/office/pop-imap-and-smtp-settings-8361e398-8af4-4e97-b147-6c6c4ac95353)\n+ Yahoo Mail SMTP Settings:\n  + [Yahoo SMTP Configuration](https://help.yahoo.com/kb/SLN4724.html)\n\n###  3.4. <a name='Conclusion'></a>Conclusion\n\nSetting up and configuring AlertManager with environment variables provides a flexible and secure way to manage alert settings. By following the above steps, you can easily configure AlertManager for your monitoring needs. Always ensure to secure sensitive information, especially when dealing with SMTP authentication credentials."
  },
  {
    "path": "docs/contrib/protoc-tools.md",
    "content": "# OpenIM Protoc Tool\n\n## Introduction\n\nOpenIM is passionate about ensuring that its suite of tools is custom-tailored to cater to the unique needs of its users. That commitment led us to develop and release our custom Protoc tool, version v1.0.0.\n\n### Why a Custom Version?\n\nThere are several reasons to choose our custom Protoc tool over generic open-source versions:\n\n- **Specialized Features**: OpenIM's Protoc tool has been enriched with features and plugins that are optimized for the OpenIM ecosystem. This makes it more aligned with the needs of OpenIM users.\n- **Optimized Performance**: Built from the ground up with OpenIM's infrastructure in mind, our tool guarantees faster and more efficient operations.\n- **Enhanced Compatibility**: Our Protoc tool ensures full compatibility with OpenIM's offerings, minimizing potential conflicts and integration challenges.\n- **Rich Output Support**: Unlike generic tools, our custom tool provides a wide array of output options including C++, C#, Java, Kotlin, Objective-C, PHP, Python, Ruby, and more. This allows developers to generate code for their preferred platform with ease.\n\n## Download\n\n+ https://github.com/OpenIMSDK/Open-IM-Protoc\n\nAccess the official release of the Protoc tool on the OpenIM repository here: [OpenIM Protoc Tool v1.0.0 Release](https://github.com/OpenIMSDK/Open-IM-Protoc/releases/tag/v1.0.0)\n\n### Direct Download Links:\n\n- **Windows**: [Download for Windows](https://github.com/OpenIMSDK/Open-IM-Protoc/releases/download/v1.0.0/windows.zip)\n- **Linux**: [Download for Linux](https://github.com/OpenIMSDK/Open-IM-Protoc/releases/download/v1.0.0/linux.zip)\n\n## Installation\n\nFor Windows:\n\n1. Navigate to the Windows download link provided above and download the version suitable for your system.\n2. Extract the contents of the zip file.\n3. Add the path of the extracted tool to your `PATH` environment variable to run the Protoc tool directly from the command line.\n\nFor Linux:\n\n1. Navigate to the Linux download link provided above and download the version suitable for your system.\n2. Extract the contents of the zip file.\n3. Use `chmod +x ./*` to make the extracted files executable.\n4. Add the path of the extracted tool to your `PATH` environment variable to run the Protoc tool directly from the command line.\n\n## Usage\n\nThe OpenIM Protoc tool provides a multitude of options for parsing `.proto` files and generating output:\n\n```\n\n./protoc [OPTION] PROTO_FILES\n```\n\nSome of the key options include:\n\n- `--proto_path=PATH`: Specify the directory to search for imports.\n- `--version`: Show version info.\n- `--encode=MESSAGE_TYPE`: Convert a text-format message of a given type from standard input to binary on standard output.\n- `--decode=MESSAGE_TYPE`: Convert a binary message of a given type from standard input to text format on standard output.\n- `--cpp_out=OUT_DIR`: Generate C++ header and source.\n- `--java_out=OUT_DIR`: Generate Java source file.\n\n... and many more. For a full list of options, run `./protoc --help` or refer to the official documentation."
  },
  {
    "path": "docs/contrib/release.md",
    "content": "# OpenIM Release Automation Design Document\n\nThis document outlines the automation process for releasing OpenIM. You can use the `make release` command for automated publishing. We will discuss how to use the `make release` command and Github Actions CICD separately, while also providing insight into the design principles involved.\n\n## Github Actions Automation\n\nIn our CICD pipeline, we have implemented logic for automating the release process using the goreleaser tool. To achieve this, follow these steps on your local machine or server:\n\n```bash\ngit clone https://github.com/openimsdk/open-im-server\ncd open-im-server\ngit tag -a v3.6.0 -s -m \"release: xxx\"\n# For pre-release versions: git tag -a v3.6.0-rc.0 -s -m \"pre-release: xxx\"\ngit push origin v3.6.0\n```\n\nThe remaining tasks are handled by automated processes:\n\n+ Automatically complete the release publication on Github\n+ Automatically build the `v3.6.0` version image and push it to aliyun, dockerhub, and github\n\nThrough these automated steps, we achieve rapid and efficient OpenIM version releases, simplifying the release process and enhancing productivity.\n\n\nCertainly, here is the continuation of the document in English:\n\n## Local Make Release Design\n\nThere are two primary scenarios for local usage:\n\n+ Advanced compilation and release, manually executed locally\n+ Quick compilation verification and version release, manually executed locally\n\n**These two scenarios can also be combined, for example, by tagging locally and then releasing:**\n\n```bash\ngit add .\ngit commit -a -s -m \"release(v3.6.0): ......\"\ngit tag v3.6.0\ngit release\ngit push origin main\n```\n\nIn a local environment, you can use the `make release` command to complete the release process. The main implementation logic can be found in the `/data/workspaces/open-im-server/scripts/lib/release.sh` file. First, let's explore its usage through the help information.\n\n### Help Information\n\nTo view the help information, execute the following command:\n\n```bash\n$ ./scripts/release.sh --help\nUsage: release.sh [options]\nOptions:\n  -h, --help                Display this help message\n  -se, --setup-env          Execute environment setup\n  -vp, --verify-prereqs     Execute prerequisite verification\n  -bc, --build-command      Execute build command\n  -bi, --build-image        Execute build image (default is not executed)\n  -pt, --package-tarballs   Execute tarball packaging\n  -ut, --upload-tarballs    Execute tarball upload\n  -gr, --github-release     Execute GitHub release\n  -gc, --generate-changelog Execute changelog generation\n```\n\n### Default Behavior\n\nIf no options are provided, all operations are executed by default:\n\n```bash\n# If no options are provided, enable all operations by default\nif [ \"$#\" -eq 0 ]; then\n    perform_setup_env=true\n    perform_verify_prereqs=true\n    perform_build_command=true\n    perform_package_tarballs=true\n    perform_upload_tarballs=true\n    perform_github_release=true\n    perform_generate_changelog=true\n    # TODO: Defaultly not enable build_image\n    # perform_build_image=true\nfi\n```\n\n### Environment Variable Setup\n\nBefore starting, you need to set environment variables:\n\n```bash\nexport TENCENT_SECRET_KEY=OZZ****************************\nexport TENCENT_SECRET_ID=AKI****************************\n```\n\n### Modifying COS Account and Password\n\nIf you need to change the COS account, password, and bucket information, please modify the following section in the `/data/workspaces/open-im-server/scripts/lib/release.sh` file:\n\n```bash\nreadonly BUCKET=\"openim-1306374445\"\nreadonly REGION=\"ap-guangzhou\"\nreadonly COS_RELEASE_DIR=\"openim-release\"\n```\n\n### GitHub Release Configuration\n\nIf you intend to use the GitHub Release feature, you also need to set the environment variable:\n\n```bash\nexport GITHUB_TOKEN=\"your_github_token\"\n```\n\n### Modifying GitHub Release Basic Information\n\nIf you need to modify the basic information of GitHub Release, please edit the following section in the `/data/workspaces/open-im-server/scripts/lib/release.sh` file:\n\n```bash\n# OpenIM GitHub account information\nreadonly OPENIM_GITHUB_ORG=openimsdk\nreadonly OPENIM_GITHUB_REPO=open-im-server\n```\n\nThis setup allows you to configure and execute the local release process according to your specific needs.\n\n\n### GitHub Release Versioning Rules\n\nFirstly, it's important to note that GitHub Releases should primarily be for pre-release versions. However, goreleaser might provide a `prerelease: auto` option, which automatically marks versions with pre-release indicators like `-rc1`, `-beta`, etc., as pre-releases.\n\nSo, if your most recent tag does not have pre-release indicators such as `-rc1` or `-beta`, even if you use `make release` for pre-release versions, goreleaser might still consider them as formal releases.\n\nTo avoid this issue, I have added the `--draft` flag to github-release. This way, all releases are created as drafts.\n\n## CICD Release Documentation Design\n\nThe release records still require manual composition for GitHub Release. This is different from github-release.\n\nThis approach ensures that all releases are initially created as drafts, allowing you to manually review and edit the release documentation on GitHub. This manual step provides more control and allows you to curate release notes and other information before making them public.\n\n\n## Makefile Section\n\nThis document aims to elaborate and explain key sections of the OpenIM Release automation design, including the Makefile section and functions within the code. Below, we will provide a detailed explanation of the logic and functions of each section.\n\nIn the project's root directory, the Makefile imports a subdirectory:\n\n```makefile\ninclude scripts/make-rules/release.mk\n```\n\nAnd defines the `release` target as follows:\n\n```makefile\n## release: release the project ✨\n.PHONY: release release: release.verify release.ensure-tag\n    @scripts/release.sh\n```\n\n### Importing Subdirectory\n\nAt the beginning of the Makefile, the `include scripts/make-rules/release.mk` statement imports the `release.mk` file from the subdirectory. This file contains rules and configurations related to releases to be used in subsequent operations.\n\n### The `release` Target\n\nThe Makefile defines a target named `release`, which is used to execute the project's release operation. This target is marked as a phony target (`.PHONY`), meaning it doesn't represent an actual file or directory but serves as an identifier for executing a series of actions.\n\nIn the `release` target, two dependency targets are executed first: `release.verify` and `release.ensure-tag`. Afterward, the `scripts/release.sh` script is called to perform the actual release operation.\n\n## Logic of `release.verify` and `release.ensure-tag`\n\n```makefile\n## release.verify: Check if a tool is installed and install it\n.PHONY: release.verify\nrelease.verify: tools.verify.git-chglog tools.verify.github-release tools.verify.coscmd tools.verify.coscli\n\n## release.ensure-tag: ensure tag\n.PHONY: release.ensure-tag\nrelease.ensure-tag: tools.verify.gsemver\n    @scripts/ensure-tag.sh\n```\n\n### `release.verify` Target\n\nThe `release.verify` target is used to check and install tools. It depends on four sub-targets: `tools.verify.git-chglog`, `tools.verify.github-release`, `tools.verify.coscmd`, and `tools.verify.coscli`. These sub-targets aim to check if specific tools are installed and attempt to install them if they are not.\n\nThe purpose of this target is to ensure that the necessary tools required for the release process are available so that subsequent operations can be executed smoothly.\n\n### `release.ensure-tag` Target\n\nThe `release.ensure-tag` target is used to ensure that the project has a version tag. It depends on the sub-target `tools.verify.gsemver`, indicating that it should check if the `gsemver` tool is installed before executing.\n\nWhen the `release.ensure-tag` target is executed, it calls the `scripts/ensure-tag.sh` script to ensure that the project has a version tag. Version tags are typically used to identify specific versions of the project for management and release in version control systems.\n\n## Logic of `release.sh` Script\n\n```bash\nopenim::golang::setup_env\nopenim::build::verify_prereqs\nopenim::release::verify_prereqs\n#openim::build::build_image\nopenim::build::build_command\nopenim::release::package_tarballs\nopenim::release::upload_tarballs\ngit push origin ${VERSION}\n#openim::release::github_release\n#openim::release::generate_changelog\n```\n\nThe `release.sh` script is responsible for executing the actual release operations. Below is the logic of this script:\n\n1. `openim::golang::setup_env`: This function sets up some configurations for the Golang development environment.\n\n2. `openim::build::verify_prereqs`: This function is used to verify whether the prerequisites for building are met. This includes checking dependencies, environment variables, and more.\n\n3. `openim::release::verify_prereqs`: Similar to the previous function, this one is used to verify whether the prerequisites for the release are met. It focuses on conditions relevant to the release.\n\n4. `openim::build::build_command`: This function is responsible for building the project's command, which typically involves compiling the project or performing other build operations.\n\n5. `openim::release::package_tarballs`: This function is used to package tarball files required for the release. These tarballs are usually used for distribution packages during the release.\n\n6. `openim::release::upload_tarballs`: This function is used to upload the packaged tarball files, typically to a distribution platform or repository.\n\n7. `git push origin ${VERSION}`: This line of command pushes the version tag to the remote Git repository's `origin` branch, marking this release in the version control system.\n\nIn the comments, you can see that there are some operations that are commented out, such as `openim::build::build_image`, `openim::release::github_release`, and `openim::release::generate_changelog`. These operations are related to building images, releasing to GitHub, and generating changelogs, and they can be enabled in the release process as needed.\n\nLet's take a closer look at the function responsible for packaging the tarball files:\n\n```bash\nfunction openim::release::package_tarballs() {\n  # Clean out any old releases\n  rm -rf \"${RELEASE_STAGE}\" \"${RELEASE_TARS}\" \"${RELEASE_IMAGES}\"\n  mkdir -p \"${RELEASE_TARS}\"\n  openim::release::package_src_tarball &\n  openim::release::package_client_tarballs &\n  openim::release::package_openim_manifests_tarball &\n  openim::release::package_server_tarballs &\n  openim::util::wait-for-jobs || { openim::log::error \"previous tarball phase failed\"; return 1; }\n\n  openim::release::package_final_tarball & # _final depends on some of the previous phases\n  openim::util::wait-for-jobs || { openim::log::error \"previous tarball phase failed\"; return 1; }\n}\n```\n\nThe `openim::release::package_tarballs()` function is responsible for packaging the tarball files required for the release. Here is the specific logic of this function:\n\n1. `rm -rf \"${RELEASE_STAGE}\" \"${RELEASE_TARS}\" \"${RELEASE_IMAGES}\"`: First, the function removes any old release directories and files to ensure a clean starting state.\n\n2. `mkdir -p \"${RELEASE_TARS}\"`: Next, it creates a directory `${RELEASE_TARS}` to store the packaged tarball files. If the directory doesn't exist, it will be created.\n\n3. `openim::release::package_final_tarball &`: This is an asynchronous operation that depends on some of the previous phases. It is likely used to package the final tarball file, which includes the contents of all previous asynchronous operations.\n\n4. `openim::util::wait-for-jobs`: It waits for all asynchronous operations to complete. If any of the previous asynchronous operations fail, an error will be returned.\n"
  },
  {
    "path": "docs/contrib/test.md",
    "content": "# OpenIM RPC Service Test Control Script Documentation\n\nThis document serves as a comprehensive guide to understanding and utilizing the `test.sh` script for testing OpenIM RPC services. The `test.sh` script is a collection of bash functions designed to test various aspects of the OpenIM RPC services, ensuring that each part of the API is functioning as expected.\n\n+ Scripts：https://github.com/openimsdk/open-im-server/tree/main/scripts/install/test.sh\n\nFor some complex, bulky functional tests, performance tests, and various e2e tests, We are all in the current warehouse to https://github.com/openimsdk/open-im-server/tree/main/test or https://github.com/openim-sigs/test-infra directory In the.\n\n+ About OpenIM Feature [Test Docs](https://docs.google.com/spreadsheets/d/1zELWkwxgOOZ7u5pmYCqqaFnvZy2SVajv/edit?usp=sharing&ouid=103266350914914783293&rtpof=true&sd=true)\n\n## Util Test\n\nLet's restructure and enhance the document under a unified second-level heading, adding clarity and details for better comprehension and visual appeal.\n\n---\n\n## Development Guide\n\n### Comprehensive Testing Instructions\n\n#### Running Unit Tests\n\n- **Command**: To execute unit tests, input the following in your terminal:\n  ```\n  make test\n  ```\n\n#### Evaluating Test Coverage\n\n- **Overview**: It's crucial to assess how much of your code is covered by tests.\n- **Command**:\n  ```bash\n  make cover\n  ```\n  This command generates a report detailing the percentage of your code tested, ensuring adherence to quality standards.\n\n#### Conducting API Tests\n\n- **Purpose**: API tests validate the interaction and functionality of your application's interfaces.\n- **How to Run**:\n  ```\n  make test-api\n  ```\n  Use this to check the integrity and reliability of your API endpoints.\n\n#### End-to-End (E2E) Testing\n\n- **Scope**: E2E tests simulate real-user scenarios from start to finish.\n- **Execution**:\n  ```\n  make test-e2e\n  ```\n  This comprehensive testing ensures your application performs as expected in real-world situations.\n\n### Crafting Unit Test Cases\n\n#### Setup for Test Case Generation\n\n- **Installation**: Install the `gotests` tool to generate test cases automatically.\n  ```bash\n  make install.gotests\n  ```\n  This command installs the `gotests` tool for test case generation.\n\n- **Environment Preparation**: Define your test template environment variable and generate test cases as shown below:\n  ```bash\n  export GOTESTS_TEMPLATE=testify\n  gotests -i -w -only keyFunc .\n  ```\n  This prepares your environment for test case generation using the `testify` template.\n\n#### Isolating Function Tests\n\n- **Single Function Testing**: When you need to focus on testing a single function for detailed examination.\n- **Method**:\n  ```bash\n  go test -v -run TestKeyFunc\n  ```\n  This command specifically runs tests for `TestKeyFunc`, allowing targeted debugging and validation.\n\n### Important Note\n\n- **Quality Assurance**: Throughout your development process, it is imperative to ensure that the unit test coverage meets or surpasses the standards set by OpenIM.\n- **Maintaining Standards**: Regularly running your tests with\n  ```make test```\n  supports maintaining high code quality and adherence to OpenIM's rigorous testing benchmarks.\n\n## E2E Test\n\nTODO\n\n## Api Test\n\nThe `test.sh` script is located within the `./scripts/install/` directory of the OpenIM service's codebase. To use the script, navigate to this directory from your terminal:\n\n```bash\ncd ./scripts/install/\nchmod +x test.sh\n```\n\n### Running the Entire Test Suite\n\nTo execute all available tests, you can either call the script directly or use the `make` command:\n\n```\n./test.sh openim::test::test\n```\n\nOr, if you have a `Makefile` that defines the `test-api` target:\n\n```bash\nmake test-api\n```\n\nAlternatively, you can invoke specific test functions by passing them as arguments:\n\n```\n./test.sh openim::test::<function_name>\n```\n\nThis `make` command should be equivalent to running `./test.sh openim::test::test`, provided that the `Makefile` is configured accordingly.\n\n\n\n### Executing Individual Test Functions\n\nIf you wish to run a specific set of tests, you can call the relevant function by passing it as an argument to the script. Here are some examples:\n\n**Message Tests:**\n\n```bash\n./test.sh openim::test::msg\n```\n\n**Authentication Tests:**\n\n```bash\n./test.sh openim::test::auth\n```\n\n**User Tests:**\n\n```bash\n./test.sh openim::test::user\n```\n\n**Friend Tests:**\n\n```bash\n./test.sh openim::test::friend\n```\n\n**Group Tests:**\n\n```bash\n./test.sh openim::test::group\n```\n\nEach of these commands will run the test suite associated with the specific functionality of the OpenIM service.\n\n\n\n### Detailed Function Test Examples\n\nT**esting Message Sending and Receiving:**\n\nTo test message functionality, the `openim::test::msg` function is called. It will register a user, send a message, and clear messages to ensure that the messaging service is operational.\n\n```bash\n./test.sh openim::test::msg\n```\n\n**Testing User Registration and Account Checks:**\n\nThe `openim::test::user` function will create new user accounts and perform a series of checks on these accounts to verify that user registration and account queries are functioning properly.\n\n```bash\n./test.sh openim::test::user\n```\n\n**Testing Friend Management:**\n\nBy invoking `openim::test::friend`, the script will test adding friends, checking friendship status, managing friend requests, and handling blacklisting.\n\n```bash\n./test.sh openim::test::friend\n```\n\n**Testing Group Operations:**\n\nThe `openim::test::group` function tests group creation, member addition, information retrieval, and member management within groups.\n\n```bash\n./test.sh openim::test::group\n```\n\n### Log Output\n\nEach test function will output logs to the terminal to confirm the success or failure of the tests. These logs are crucial for identifying issues and verifying that each part of the service is tested thoroughly.\n\nEach function logs its success upon completion, which aids in debugging and understanding the test flow. The success message is standardized across functions:\n\n```\nopenim::log::success \"<Test suite name> completed successfully.\"\n```\n\nBy following the guidelines and instructions outlined in this document, you can effectively utilize the `test.sh` script to test and verify the OpenIM RPC services' functionality.\n\n\n\n## Function feature\n\n| Function Name                                        | Corresponding API/Action                      | Function Purpose                                             |\n| ---------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ |\n| `openim::test::msg`                                  | Messaging Operations                          | Tests all aspects of messaging, including sending, receiving, and managing messages. |\n| `openim::test::auth`                                 | Authentication Operations                     | Validates the authentication process and session management, including token handling and forced logout. |\n| `openim::test::user`                                 | User Account Operations                       | Covers testing for user account creation, retrieval, updating, and overall management. |\n| `openim::test::friend`                               | Friend Relationship Operations                | Ensures friend management functions correctly, including requests, listing, and blacklisting. |\n| `openim::test::group`                                | Group Management Operations                   | Checks group-related functionalities like creation, invitation, information retrieval, and member management. |\n| `openim::test::send_msg`                             | Send Message API                              | Simulates sending a message from one user to another or within a group. |\n| `openim::test::revoke_msg`                           | Revoke Message API (TODO)                     | (Planned) Will test the revocation of a previously sent message. |\n| `openim::test::user_register`                        | User Registration API                         | Registers a new user in the system to validate the registration process. |\n| `openim::test::check_account`                        | Account Check API                             | Checks if an account exists for a given user ID.             |\n| `openim::test::user_clear_all_msg`                   | Clear All Messages API                        | Clears all messages for a given user to validate message history management. |\n| `openim::test::get_token`                            | Token Retrieval API                           | Retrieves an authentication token to validate token management. |\n| `openim::test::force_logout`                         | Force Logout API                              | Forces a logout for a test user to validate session control. |\n| `openim::test::check_user_account`                   | User Account Existence Check API              | Confirms the existence of a test user's account.             |\n| `openim::test::get_users`                            | Get Users API                                 | Retrieves a list of users to validate user query functionality. |\n| `openim::test::get_users_info`                       | Get User Information API                      | Obtains detailed information for a given user.               |\n| `openim::test::get_users_online_status`              | Get User Online Status API                    | Checks the online status of a user to validate presence functionality. |\n| `openim::test::update_user_info`                     | Update User Information API                   | Updates a user's information to validate account update capabilities. |\n| `openim::test::get_subscribe_users_status`           | Get Subscribed Users' Status API              | Retrieves the status of users that a test user has subscribed to. |\n| `openim::test::subscribe_users_status`               | Subscribe to Users' Status API                | Subscribes a test user to a set of user statuses.            |\n| `openim::test::set_global_msg_recv_opt`              | Set Global Message Receiving Option API       | Sets the message receiving option for a test user.           |\n| `openim::test::is_friend`                            | Check Friendship Status API                   | Verifies if two users are friends within the system.         |\n| `openim::test::add_friend`                           | Send Friend Request API                       | Sends a friend request from one user to another.             |\n| `openim::test::get_friend_list`                      | Get Friend List API                           | Retrieves the friend list of a test user.                    |\n| `openim::test::get_friend_apply_list`                | Get Friend Application List API               | Retrieves friend applications for a test user.               |\n| `openim::test::get_self_friend_apply_list`           | Get Self-Friend Application List API          | Retrieves the friend applications that the user has applied for. |\n| `openim::test::add_black`                            | Add User to Blacklist API                     | Adds a user to the test user's blacklist to validate blacklist functionality. |\n| `openim::test::remove_black`                         | Remove User from Blacklist API                | Removes a user from the test user's blacklist.               |\n| `openim::test::get_black_list`                       | Get Blacklist API                             | Retrieves the blacklist for a test user.                     |\n| `openim::test::create_group`                         | Group Creation API                            | Creates a new group with test users to validate group creation. |\n| `openim::test::invite_user_to_group`                 | Invite User to Group API                      | Invites a user to join a group to test invitation functionality. |\n| `openim::test::transfer_group`                       | Group Ownership Transfer API                  | Tests the transfer of group ownership from one member to another. |\n| `openim::test::get_groups_info`                      | Get Group Information API                     | Retrieves information for specified groups to validate group query functionality. |\n| `openim::test::kick_group`                           | Kick User from Group API                      | Simulates kicking a user from a group to test group membership management. |\n| `openim::test::get_group_members_info`               | Get Group Members Information API             | Obtains detailed information for members of a specified group. |\n| `openim::test::get_group_member_list`                | Get Group Member List API                     | Retrieves a list of members for a given group to ensure member listing is functional. |\n| `openim::test::get_joined_group_list`                | Get Joined Group List API                     | Retrieves a list of groups that a user has joined to validate user's group memberships. |\n| `openim::test::set_group_member_info`                | Set Group Member Information API              | Updates the information for a group member to test the update functionality. |\n| `openim::test::mute_group`                           | Mute Group API                                | Tests the ability to mute a group, disabling message notifications for its members. |\n| `openim::test::cancel_mute_group`                    | Cancel Mute Group API                         | Tests the ability to cancel the mute status of a group, re-enabling message notifications. |\n| `openim::test::dismiss_group`                        | Dismiss Group API                             | Tests the ability to dismiss and delete a group from the system. |\n| `openim::test::cancel_mute_group_member`             | Cancel Mute Group Member API                  | Tests the ability to cancel mute status for a specific group member. |\n| `openim::test::join_group`                           | Join Group API (TODO)                         | (Planned) Will test the functionality for a user to join a specified group. |\n| `openim::test::set_group_info`                       | Set Group Information API                     | Tests the ability to update the group information, such as the name or description. |\n| `openim::test::quit_group`                           | Quit Group API                                | Tests the functionality for a user to leave a specified group. |\n| `openim::test::get_recv_group_applicationList`       | Get Received Group Application List API       | Retrieves the list of group applications received by a user to validate application management. |\n| `openim::test::group_application_response`           | Group Application Response API (TODO)         | (Planned) Will test the functionality to respond to a group join request. |\n| `openim::test::get_user_req_group_applicationList`   | Get User Requested Group Application List API | Retrieves the list of group applications requested by a user to validate tracking of user's applications. |\n| `openim::test::mute_group_member`                    | Mute Group Member API                         | Tests the ability to mute a specific member within a group, disabling their ability to send messages. |\n| `openim::test::get_group_users_req_application_list` | Get Group Users Request Application List API  | Retrieves a list of user requests for group applications to validate group request management. |\n"
  },
  {
    "path": "docs/contrib/util-go.md",
    "content": "# utils go \n\n+ [toold readme](https://github.com/openimsdk/open-im-server/tree/main/tools)\n\nabout scripts fix:\n```\n\"${OPENIM-ROOT}/_output/bin/tools/${platform}/${lookfor}\"\n```\n"
  },
  {
    "path": "docs/contrib/util-makefile.md",
    "content": "# Open-IM-Server Development Tools Guide\n\n- [Open-IM-Server Development Tools Guide](#open-im-server-development-tools-guide)\n  - [Introduction](#introduction)\n  - [Getting Started](#getting-started)\n  - [Toolset Categories](#toolset-categories)\n  - [Installation Commands](#installation-commands)\n    - [Basic Installation](#basic-installation)\n    - [Individual Tool Installation](#individual-tool-installation)\n    - [Tool Verification](#tool-verification)\n  - [Detailed Tool Descriptions](#detailed-tool-descriptions)\n  - [Best Practices](#best-practices)\n  - [Conclusion](#conclusion)\n\n\n## Introduction\n\nOpen-IM-Server boasts a robust set of tools encapsulated within its Makefile system, designed to ease development, code formatting, and tool management. This guide aims to familiarize developers with the features and usage of the Makefile toolset provided within the Open-IM-Server project.\n\n## Getting Started\n\nExecuting `make tools` ensures verification and installation of the default tools:\n\n- golangci-lint\n- goimports\n- addlicense\n- deepcopy-gen\n- conversion-gen\n- ginkgo\n- go-junit-report\n- go-gitlint\n\nThe installation path is situated at `./_output/tools/`.\n\n## Toolset Categories\n\nThe Makefile logically groups tools into different categories for better management:\n\n- **Development Tools**: `BUILD_TOOLS`\n- **Code Analysis Tools**: `ANALYSIS_TOOLS`\n- **Code Generation Tools**: `GENERATION_TOOLS`\n- **Testing Tools**: `TEST_TOOLS`\n- **Version Control Tools**: `VERSION_CONTROL_TOOLS`\n- **Utility Tools**: `UTILITY_TOOLS`\n- **Tencent Cloud Object Storage Tools**: `COS_TOOLS`\n\n## Installation Commands\n\n1. **golangci-lint**: high performance Go code linter with integration of multiple inspection tools.\n2. **goimports**: Used to format Go source files and automatically add or remove imports.\n3. **addlicense**: Adds a license header to the source file.\n4. **deepcopy-gen and conversions-gen **: Generate deepcopy and conversion functionality.\n5. **ginkgo**: Testing framework for Go.\n6. **go-junit-report**: Converts Go test output to junit xml format.\n7. **go-gitlint**: For checking git commit information. ... (And so on, list all the tools according to the `make tools.help` output above)...\n\n \n\n### Basic Installation\n\n- `tools.install`: Installs tools mentioned in the `BUILD_TOOLS` list.\n- `tools.install-all`: Installs all tools from the `ALL_TOOLS` list.\n\n### Individual Tool Installation\n\n- `tools.install.%`: Installs a single tool in the `$GOBIN/` directory.\n- `tools.install-all.%`: Parallelly installs an individual tool located in `./tools/*`.\n\n### Tool Verification\n\n- `tools.verify.%`: Checks if a specific tool is installed, and if not, installs it.\n\n## Detailed Tool Descriptions\n\nThe following commands serve the purpose of installing particular development tools:\n\n- `install.golangci-lint`: Installs `golangci-lint`.\n- `install.addlicense`: Installs `addlicense`. ... (and so on for every tool as mentioned in the provided Makefile source)...\n\nThe commands primarily leverage Go's `install` operation, fetching and installing tools from their respective repositories. This method is especially convenient as it auto-handles dependencies and installation paths. For tools not written directly with Go (like `install.coscli`), other installation methods like wget or pip are employed.\n\n## Best Practices\n\n1. **Regular Updates**: To ensure tools are up-to-date, periodically run the `make tools` command.\n2. **Individual Tools**: If only specific tools are required, employ the `make install.<tool-name>` command for individual installations.\n3. **Verification**: Before code submissions, use the `make tools.verify.%` command to guarantee that all necessary tools are present and up-to-date.\n\n## Conclusion\n\nThe Makefile provided by Open-IM-Server presents a centralized approach to manage and install all necessary tools during the development process. It ensures that all developers employ consistent tool versions, reducing potential issues due to version disparities. Whether you're a maintainer or a contributor to the Open-IM-Server project, understanding the workings of this Makefile will significantly enhance your developmental efficiency.\n"
  },
  {
    "path": "docs/contrib/util-scripts.md",
    "content": "# OpenIM Bash Utility Script\n\nThis script offers a variety of utilities and helpers to enhance and simplify operations related to the OpenIM project.\n\n## Table of Contents\n\n- [OpenIM Bash Utility Script](#openim-bash-utility-script)\n  - [Table of Contents](#table-of-contents)\n  - [brief descriptions of each function](#brief-descriptions-of-each-function)\n  - [Introduction](#introduction)\n  - [Usage](#usage)\n    - [SSH Key Setup](#ssh-key-setup)\n  - [openim::util::ensure-gnu-sed](#openimutilensure-gnu-sed)\n  - [openim::util::ensure-gnu-date](#openimutilensure-gnu-date)\n  - [openim::util::check-file-in-alphabetical-order](#openimutilcheck-file-in-alphabetical-order)\n  - [openim::util::require-jq](#openimutilrequire-jq)\n  - [openim::util::md5](#openimutilmd5)\n  - [openim::util::read-array](#openimutilread-array)\n  - [Color Definitions](#color-definitions)\n  - [openim::util::desc and related functions](#openimutildesc-and-related-functions)\n  - [openim::util::onCtrlC](#openimutilonctrlc)\n  - [openim::util::list-to-string](#openimutillist-to-string)\n  - [openim::util::remove-space](#openimutilremove-space)\n  - [openim::util::gencpu](#openimutilgencpu)\n  - [openim::util::gen-os-arch](#openimutilgen-os-arch)\n  - [openim::util::download-file](#openimutildownload-file)\n  - [openim::util::get-public-ip](#openimutilget-public-ip)\n  - [openim::util::extract-tarball](#openimutilextract-tarball)\n  - [openim::util::check-port-open](#openimutilcheck-port-open)\n  - [openim::util::file-lines-count](#openimutilfile-lines-count)\n\n\n##  brief descriptions of each function \n\n**English:**\n1. `openim::util::ensure-gnu-sed` - Determines if GNU version of `sed` exists on the system and sets its name.\n2. `openim::util::ensure-gnu-date` - Determines if GNU version of `date` exists on the system and sets its name.\n3. `openim::util::check-file-in-alphabetical-order` - Checks if a file is sorted in alphabetical order.\n4. `openim::util::require-jq` - Checks if `jq` is installed.\n5. `openim::util::md5` - Outputs the MD5 hash of a file.\n6. `openim::util::read-array` - Reads content from standard input into an array.\n7. `openim::util::desc` - Displays descriptive information.\n8. `openim::util::run::prompt` - Displays a prompt.\n9. `openim::util::run::maybe-first-prompt` - Possibly displays the first prompt based on whether it's started or not.\n10. `openim::util::run` - Executes a command and captures its output.\n11. `openim::util::run::relative` - Returns paths relative to the current script.\n12. `openim::util::onCtrlC` - Performs an action when Ctrl+C is pressed.\n13. `openim::util::list-to-string` - Converts a list into a string.\n14. `openim::util::remove-space` - Removes spaces from a string.\n15. `openim::util::gencpu` - Retrieves CPU information.\n16. `openim::util::gen-os-arch` - Generates a repository directory based on the operating system and architecture.\n17. `openim::util::download-file` - Downloads a file from a URL.\n18. `openim::util::get-public-ip` - Retrieves the public IP address of the machine.\n19. `openim::util::extract-tarball` - Extracts a tarball to a specified directory.\n20. `openim::util::check-port-open` - Checks if a given port is open on the machine.\n21. `openim::util::file-lines-count` - Counts the number of lines in a file.\n\n\n\n## Introduction\n\nThis script is mainly used to validate whether the code is correctly formatted by `gofmt`. Apart from that, it offers utilities like setting up SSH keys, various wait conditions, host and platform detection, documentation generation, etc. \n\n## Usage\n\n### SSH Key Setup\n\nTo set up an SSH key:\n\n```bash\n#1. Write IPs in a file, one IP per line. Let's name it hosts-file.\n#2. Modify the default username and password in the script.\nhosts-file-path=\"path/to/your/hosts/file\"\nopenim:util::setup_ssh_key_copy \"$hosts-file-path\" \"root\" \"123\"\n```\n\n## openim::util::ensure-gnu-sed\n\nEnsures the presence of the GNU version of the `sed` command. Different operating systems may have variations of the `sed` command, and this utility function is used to make sure the script uses the GNU version. If it finds the GNU `sed`, it sets the `SED` variable accordingly. If not found, it checks for `gsed`, which is usually the name of GNU `sed` on macOS. If neither is found, an error message is displayed.\n\n\n\n## openim::util::ensure-gnu-date\n\nSimilar to the function for `sed`, this function ensures the script uses the GNU version of the `date` command. If it identifies the GNU `date`, it sets the `DATE` variable. On macOS, it looks for `gdate` as an alternative. In the absence of both, an error message is recommended.\n\n\n\n## openim::util::check-file-in-alphabetical-order\n\nThis function checks if the contents of a given file are sorted in alphabetical order. If not, it provides a command suggestion for the user to sort the file correctly.\n\n\n\n## openim::util::require-jq\n\nVerifies the installation of `jq`, a popular command-line JSON parser. If it's not present, a prompt to install it is displayed.\n\n\n\n## openim::util::md5\n\nA cross-platform function that computes the MD5 hash of its input. This function takes into account the differences in the `md5` command between macOS and Linux.\n\n\n\n## openim::util::read-array\n\nA function designed to read from stdin and populate an array, line by line. It's provided as an alternative to `mapfile -t` and is compatible with bash 3.\n\n\n\n## Color Definitions\n\nThe script also defines a set of colors to enhance its console output. These include colors like red, yellow, green, blue, cyan, etc., which can be used for better user experience and clear logs.\n\n\n\n## openim::util::desc and related functions\n\nThese functions seem to aid in building interactive demonstrations or tutorials in the terminal. They use the `pv` utility to control the display rate of the output, emulating typing. There's also functionality to handle user prompts and execute commands while capturing their output.\n\n\n\n## openim::util::onCtrlC\n\nHandles the `CTRL+C` command. It terminates background processes of the script when the user interrupts it using `CTRL+C`.\n\n\n\n## openim::util::list-to-string\n\nTransforms a list format (like `[10023, 2323, 3434]`) to a space-separated string (`10023 2323 3434`). Also removes unnecessary spaces and characters.\n\n\n\n## openim::util::remove-space\n\nRemoves spaces from a given string.\n\n\n\n## openim::util::gencpu\n\nFetches the number of CPUs using the `lscpu` command.\n\n\n\n## openim::util::gen-os-arch\n\nIdentifies the operating system and architecture of the system running the script. This is useful to determine directories or binaries specific to that OS and architecture.\n\n\n\n## openim::util::download-file\n\nThis function can be used to download a file from a URL. If `curl` is available, it uses `curl`. If not, it falls back to `wget`.\n\n```bash\nfunction openim::util::download-file() {\n  local url=\"$1\"\n  local dest=\"$2\"\n\n  if command -v curl &>/dev/null; then\n    curl -L \"${url}\" -o \"${dest}\"\n  elif command -v wget &>/dev/null; then\n    wget \"${url}\" -O \"${dest}\"\n  else\n    openim::log::error \"Neither curl nor wget available. Cannot download file.\"\n    return 1\n  fi\n}\n```\n\n\n\n## openim::util::get-public-ip\n\nFetches the public IP address of the machine.\n\n```bash\nfunction openim::util::get-public-ip() {\n  if command -v curl &>/dev/null; then\n    curl -s https://ipinfo.io/ip\n  elif command -v wget &>/dev/null; then\n    wget -qO- https://ipinfo.io/ip\n  else\n    openim::log::error \"Neither curl nor wget available. Cannot fetch public IP.\"\n    return 1\n  fi\n}\n```\n\n\n\n## openim::util::extract-tarball\n\nThis function extracts a tarball to a specified directory.\n\n```bash\nfunction openim::util::extract-tarball() {\n  local tarball=\"$1\"\n  local dest=\"$2\"\n\n  mkdir -p \"${dest}\"\n  tar -xzf \"${tarball}\" -C \"${dest}\"\n}\n```\n\n\n\n## openim::util::check-port-open\n\nChecks if a given port is open on the local machine.\n\n```bash\nfunction openim::util::check-port-open() {\n  local port=\"$1\"\n  if command -v nc &>/dev/null; then\n    echo -n > /dev/tcp/127.0.0.1/\"${port}\" 2>&1\n    return $?\n  elif command -v telnet &>/dev/null; then\n    telnet 127.0.0.1 \"${port}\" 2>&1 | grep -q \"Connected\"\n    return $?\n  else\n    openim::log::error \"Neither nc nor telnet available. Cannot check port.\"\n    return 1\n  fi\n}\n```\n\n\n\n## openim::util::file-lines-count\n\nCounts the number of lines in a file.\n\n```bash\nfunction openim::util::file-lines-count() {\n  local file=\"$1\"\n  if [[ -f \"${file}\" ]]; then\n    wc -l < \"${file}\"\n  else\n    openim::log::error \"File does not exist: ${file}\"\n    return 1\n  fi\n}\n```"
  },
  {
    "path": "docs/contrib/version.md",
    "content": "# OpenIM Branch Management and Versioning: A Blueprint for High-Grade Software Development\n\n[📚 **OpenIM TOC**](#openim-branch-management-and-versioning-a-blueprint-for-high-grade-software-development)\n- [OpenIM Branch Management and Versioning: A Blueprint for High-Grade Software Development](#openim-branch-management-and-versioning-a-blueprint-for-high-grade-software-development)\n  - [Unfolding the Mechanism of OpenIM Version Maintenance](#unfolding-the-mechanism-of-openim-version-maintenance)\n  - [Main Branch: The Heart of OpenIM Development](#main-branch-the-heart-of-openim-development)\n  - [Release Branch: The Beacon of Stability](#release-branch-the-beacon-of-stability)\n  - [Tag Management: The Cornerstone of Version Control](#tag-management-the-cornerstone-of-version-control)\n  - [Release Management: A Guided Tour](#release-management-a-guided-tour)\n  - [Milestones, Branching, and Addressing Major Bugs](#milestones-branching-and-addressing-major-bugs)\n  - [Version Skew Policy](#version-skew-policy)\n    - [Supported version skew](#supported-version-skew)\n    - [OpenIM Versioning, Branching, and Tag Strategy](#openim-versioning-branching-and-tag-strategy)\n      - [Supported Version Skew](#supported-version-skew-1)\n        - [openim-api](#openim-api)\n        - [openim-rpc-\\* Components](#openim-rpc--components)\n        - [Other OpenIM Services](#other-openim-services)\n      - [Supported Component Upgrade Order](#supported-component-upgrade-order)\n        - [openim-api](#openim-api-1)\n        - [openim-rpc-\\* Components](#openim-rpc--components-1)\n        - [Other OpenIM Services](#other-openim-services-1)\n      - [Conclusion](#conclusion)\n  - [Applying Principles: A Git Workflow Example](#applying-principles-a-git-workflow-example)\n  - [Release Process](#release-process)\n  - [Docker Images Version Management](#docker-images-version-management)\n  - [More](#more)\n\n\nAt OpenIM, we acknowledge the profound impact of implementing a robust and efficient version management system, hence we abide by the established standards of [Semantic Versioning 2.0.0](https://semver.org/lang/zh-CN/).\n\nOur software blueprint orchestrates a tripartite version management system that integrates the `main` branch, the `release` branch, and `tag` management. These constituents operate in synchrony to preserve the reliability and traceability of our software across various stages of development.\n\n## Unfolding the Mechanism of OpenIM Version Maintenance\n\nOur version maintenance protocol revolves around two primary branches, namely: `main` and `release`. We resort to Semantic Versioning 2.0.0 for marking distinctive versions of our software, representing substantial milestones in its evolution.\n\nIn the OpenIM repository, version identification strictly complies with the `MAJOR.MINOR.PATCH` protocol. Herein:\n\n- The `MAJOR` version indicates a shift arising from incompatible changes to the API.\n- The `MINOR` version suggests the addition of features in a backward-compatible manner.\n- The `PATCH` version flags backward-compatible bug fixes.\n\n## Main Branch: The Heart of OpenIM Development\n\nThe `main` branch is the operational heart of our development process. Housing the most recent and advanced features, this branch serves as the nerve center for all enhancements and updates. It encapsulates the freshest, though possibly unstable, facets of the software. Visit our `main` branch [here](https://github.com/openimsdk/open-im-server/tree/main).\n\n## Release Branch: The Beacon of Stability\n\nFor every major release, we curate a corresponding `release` branch, e.g., `release-v3.1`. This branch symbolizes an embodiment of stability and ensures an updated version of the software, providing a dependable option for users favoring stability over nascent, yet possibly unstable, features. Visit the `release-v3.1` branch [here](https://github.com/openimsdk/open-im-server/tree/release-v3.1).\n\n## Tag Management: The Cornerstone of Version Control\n\nIn OpenIM's version control system, the role of `tags` stands paramount. Owing to their immutable nature, tags can be effectively utilized to retrieve a specific version of the software. Explore our library of tags [here](https://github.com/openimsdk/open-im-server/tags).\n\nOur Docker image versions are intimately entwined with these tripartite components. For instance, a Docker image tag may correspond to `ghcr.io/openimsdk/openim-server:v3.1.0`, a release to `ghcr.io/openimsdk/openim-server:release-v3.0`, and the main branch to `ghcr.io/openimsdk/openim-server:main` or `ghcr.io/openimsdk/openim-server:latest`.\n\nTo further clarify, the semantics of our version numbers are as follows:\n\n- **Revision version number**: This represents bug fixes or code optimizations. Typically, it entails no new feature additions and ensures backward compatibility.\n- **Build version number**: Auto-generated by the system, each code submission prompts an automatic increment by 1.\n- **Version modifiers**: These hint at the software's development stage and stability. Some commonly used modifiers are `alpha`, `beta`, `rc`, `ga`, `r/release/or nothing`, and `lts`.\n  - `alpha`: An internal testing version with numerous bugs, typically used for communication among developers.\n  - `beta`: A test version with numerous bugs, generally used for testing by eager community members, who provide feedback to the developers.\n  - `rc`: Release candidate, which is to be released as the official version. It's the last test version before the official version.\n  - `ga`: General Availability, the first stable release.\n  - `r/release/or nothing`: The final release version, intended for general users.\n  - `lts`: Long Term Support, the official will specify the maintenance year for this version and will fix all bugs discovered in this version.\n\nWhenever a project undergoes a partial functional addition, the minor version number increments by 1, resetting the revision version number to 0. In contrast, any major project overhaul results in an increment by 1 in the major version number. The build number, typically auto-generated during the compilation process, only requires format definition, thereby eliminating manual control.\n\n## Release Management: A Guided Tour\n\nOur GitHub repository at https://github.com/openimsdk/open-im-server/releases associates a release with each tag, with a distinction between Pre-release and Latest, determined by the branch source. Every significant feature launch prompts the issue of a `release` branch, such as `release-v3.2`, as a beacon of stability and Latest release.\n\nPre-releases correspond to releases from the `main` branch, denoting tags with Version modifiers such as `v3.2.1-beta.0`, `v3.2.1-rc.1`, etc. If you are seeking the most recent, albeit possibly unstable, release with new features, these tags, originating from the latest `main` branch code, are your go-to.\n\nConversely, if stability is your primary concern, you should opt for the release tagged Latest, denoted by tags without Version modifiers, such as `v3.2.1`, `v3.2.2` etc. These tags are linked to the latest stable maintenance branch, like `release-v3.2`.\n\n## Milestones, Branching, and Addressing Major Bugs\n\n**About:**\n\n+ [OpenIM Milestones](https://github.com/openimsdk/open-im-server/milestones)\n+ [OpenIM Tags](https://github.com/openimsdk/open-im-server/tags)\n+ [OpenIM Branches](https://github.com/openimsdk/open-im-server/branches)\n\nWe create a new branch, such as `release-v3.1`, for each significant milestone (e.g., v3.1.0), housing all relevant code for that release. All enhancements and bug fixes targeting the subsequent version (e.g., v3.2.0) are integrated into this branch.\n\n`PATCH` versions (represented by Z in `X.Y.Z`) are primarily propelled by bug fixes, and their release may be either priority-driven or scheduled. In contrast, `MINOR` versions (represented by Y in `X.Y.Z`) are contingent upon the project's roadmap, milestone completion, or a pre-established timeline, always maintaining backward-compatible APIs.\n\nWhen dealing with major bugs, we selectively merge the fix into the affected version (e.g., v3.1 or the `release-v3.1` branch), as well as the `main` branch. This dual pronged strategy ensures that users on older versions receive crucial bug fixes, while also keeping the `main` branch updated.\n\nWe reinforce our approach to branch management and versioning with stringent testing protocols. Automated tests and code review sessions form vital components of maintaining a robust and reliable codebase.\n\n## Version Skew Policy\n\nThis document describes the maximum version skew supported between various openim components. Specific cluster deployment tools may place additional restrictions on version skew.\n\n### Supported version skew\n\nIn highly-available (HA) clusters, the newest and oldest `openim-api` instances must be within one minor version.\n\n### OpenIM Versioning, Branching, and Tag Strategy\n\nSimilar to Kubernetes, OpenIM has a strict versioning, branching, and tag strategy to ensure compatibility among its various services and components. This document outlines the policies, especially focusing on the version skew supported between OpenIM's components. Given that the current version is v3.3, the policy references will be centered around this version.\n\n#### Supported Version Skew\n\n##### openim-api\n\nIn highly-available (HA) clusters, the newest and oldest `openim-api` instances must be within one minor version.\n\nExample:\n\n+ Newest `openim-api` is at v3.3\n+ Other `openim-api` instances are supported at v3.3 and v3.2\n\n##### openim-rpc-* Components\n\nAll `openim-rpc-*` components (e.g., `openim-rpc-auth`, `openim-rpc-conversation`, etc.) should adhere to the following rules:\n\n1. They must not be newer than `openim-api`.\n2. They may be up to one minor version older than `openim-api`.\n\nExample:\n\n+ `openim-api` is at v3.3\n+ All `openim-rpc-*` components are supported at v3.3 and v3.2\n\nNote: If version skew exists between `openim-api` instances in an HA cluster, this narrows the allowed `openim-rpc-*` components versions.\n\n##### Other OpenIM Services\n\nOther OpenIM services such as `openim-cmdutils`, `openim-crontask`, `openim-msggateway`, etc. should adhere to the following rules:\n\n1. These services must not be newer than `openim-api`.\n2. They are expected to match the `openim-api` minor version but may be up to one minor version older (to allow live upgrades).\n\nExample:\n\n+ `openim-api` is at v3.3\n+ `openim-msggateway`, `openim-cmdutils`, and other services are supported at v3.3 and v3.2\n\n#### Supported Component Upgrade Order\n\nThe version skew policy has implications on the order in which components should be upgraded. Below is the recommended order to transition an existing cluster from version v3.2 to v3.3:\n\n##### openim-api\n\nPre-requisites:\n\n1. In a single-instance cluster, the existing `openim-api` instance is v3.2.\n2. In an HA cluster, all `openim-api` instances are at v3.2 or v3.3.\n3. All `openim-rpc-*` and other OpenIM services communicating with this server are at version v3.2.\n\nUpgrade Procedure:\n\n1. Upgrade `openim-api` to v3.3.\n\n##### openim-rpc-* Components\n\nPre-requisites:\n\n1. The `openim-api` instances these components communicate with are at v3.3.\n\nUpgrade Procedure:\n\n2. Upgrade all `openim-rpc-*` components to v3.3.\n\n##### Other OpenIM Services\n\nPre-requisites:\n\n1. The `openim-api` instances these services communicate with are at v3.3.\n\nUpgrade Procedure:\n\n2. Upgrade other OpenIM services such as `openim-msggateway`, `openim-cmdutils`, etc., to v3.3.\n\n#### Conclusion\n\nJust like Kubernetes, it's essential for OpenIM to have a strict versioning and upgrade policy to ensure seamless operation and compatibility among its various services. Adhering to the policies outlined above will help in achieving this goal.\n\n\n## Applying Principles: A Git Workflow Example\n\nThe workflow to address a bug fix might follow these steps:\n\n```bash\n# Checkout the branch for the version that needs the bug fix\ngit checkout release-v3.1\n\n# Create a new branch for the bug fix\ngit checkout -b bug/bug-name\n\n# ... Make changes, commit your work ...\n\n# Push the branch to your remote repository\ngit push origin bug/bug-name\n\n# After the pull request is merged into the release-v3.1 branch, \n# checkout and update your main branch\ngit checkout main\ngit pull origin main\n\n# Merge or rebase the changes from release-v3.1 into main\ngit merge release-v3.1\n\n# Push the updates to the main branch\ngit push origin main\n```\n\n##  Release Process\n\n```\nPublishing v3.2.0: A Step-by-Step Guide\n(1) Create the tag v3.2.0-alpha.0 from the main branch.\n(2) Bugs are fixed on the main branch. Once the bugs are resolved, tag the main branch as v3.2.0-rc.0.\n(3) After further testing, if v3.2.0-rc.0 is deemed stable, create a branch named release-v3.2 from the tag v3.2.0-rc.0.\n(4) From the release-v3.2 branch, create the tag v3.2.0. At this point, the official release of v3.2.0 is complete.\n\nAfter the release of v3.2.0, if urgent bugs are discovered, fix them on the release-v3.2 branch. Then, submit two pull requests (PRs) to both the main and release-v3.2 branches. Tag the release-v3.2 branch as v3.2.1.\n```\n\nThroughout this process, active communication within the team is pivotal to maintaining transparency and consensus on changes.\n\n## Docker Images Version Management\n\nFor more details on managing Docker image versions, visit [OpenIM Docker Images Administration](https://github.com/openimsdk/open-im-server/blob/main/docs/contrib/images.md).\n\n## More \n\nMore on multi-branch version management design and version management design at helm charts：\n\nAbout Helm's version management strategy for Multiple Apps and multiple Services:\n\n+ [中文版本管理文档](https://github.com/openimsdk/helm-charts/blob/main/docs/contrib/version-zh.md)\n+ [English version management documents](https://github.com/openimsdk/helm-charts/blob/main/docs/contrib/version.md)\n"
  },
  {
    "path": "docs/contributing/CONTRIBUTING-JP.md",
    "content": "# How do I contribute code to OpenIM\n\n<p align=\"center\">\n  <a href=\"./CONTRIBUTING.md\">English</a> · \n  <a href=\"./CONTRIBUTING-zh_CN.md\">中文</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-UA.md\">Українська</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-CS.md\">Česky</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-HU.md\">Magyar</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ES.md\">Español</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FA.md\">فارسی</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FR.md\">Français</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-DE.md\">Deutsch</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-PL.md\">Polski</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ID.md\">Indonesian</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FI.md\">Suomi</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ML.md\">മലയാളം</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-JP.md\">日本語</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-NL.md\">Nederlands</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-IT.md\">Italiano</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-RU.md\">Русский</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-PTBR.md\">Português (Brasil)</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-EO.md\">Esperanto</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-KR.md\">한국어</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-AR.md\">العربي</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-VN.md\">Tiếng Việt</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-DA.md\">Dansk</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-GR.md\">Ελληνικά</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-TR.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>"
  },
  {
    "path": "docs/contributing/CONTRIBUTING-PL.md",
    "content": "# How do I contribute code to OpenIM\n\n<p align=\"center\">\n  <a href=\"./CONTRIBUTING.md\">English</a> · \n  <a href=\"./CONTRIBUTING-zh_CN.md\">中文</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-UA.md\">Українська</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-CS.md\">Česky</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-HU.md\">Magyar</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ES.md\">Español</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FA.md\">فارسی</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FR.md\">Français</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-DE.md\">Deutsch</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-PL.md\">Polski</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ID.md\">Indonesian</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-FI.md\">Suomi</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-ML.md\">മലയാളം</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-JP.md\">日本語</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-NL.md\">Nederlands</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-IT.md\">Italiano</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-RU.md\">Русский</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-PTBR.md\">Português (Brasil)</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-EO.md\">Esperanto</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-KR.md\">한국어</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-AR.md\">العربي</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-VN.md\">Tiếng Việt</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-DA.md\">Dansk</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-GR.md\">Ελληνικά</a> · \n  <a href=\"docs/contributing/CONTRIBUTING-TR.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>"
  },
  {
    "path": "docs/readme/README_cs.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ O OpenIM\n\nOpenIM je platforma služeb speciálně navržená pro integraci chatu, audio-video hovorů, upozornění a chatbotů AI do aplikací. Poskytuje řadu výkonných rozhraní API a webhooků, které vývojářům umožňují snadno začlenit tyto interaktivní funkce do svých aplikací. OpenIM není samostatná chatovací aplikace, ale spíše slouží jako platforma pro podporu jiných aplikací při dosahování bohatých komunikačních funkcí. Následující diagram ilustruje interakci mezi AppServer, AppClient, OpenIMServer a OpenIMSDK pro podrobné vysvětlení.\n\n![App-OpenIM Relationship](../images/oepnim-design.png)\n\n## 🚀 O OpenIMSDK\n\n**OpenIMSDK** je IM SDK navržený pro**OpenIMServer**, vytvořený speciálně pro vkládání do klientských aplikací. Jeho hlavní vlastnosti a moduly jsou následující:\n\n- 🌟 Hlavní vlastnosti:\n\n  - 📦 Místní úložiště\n  - 🔔 Zpětná volání posluchačů\n  - 🛡️ API obalování\n  - 🌐 Správa připojení\n\n- 📚 hlavní moduly:\n\n  1. 🚀 Inicializace a přihlášení\n  2. 👤 Správa uživatelů\n  3. 👫 Správa přátel\n  4. 🤖 Skupinové funkce\n  5. 💬 Zpracování konverzace\n\nJe postaven pomocí Golang a podporuje nasazení napříč platformami, což zajišťuje konzistentní přístup na všech platformách.\n\n👉 **[Prozkoumat GO SDK](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 O OpenIMServeru\n\n- **OpenIMServer** má následující vlastnosti:\n  - 🌐 Architektura mikroslužeb: Podporuje režim clusteru, včetně brány a více služeb RPC.\n  - 🚀 Různé metody nasazení: Podporuje nasazení prostřednictvím zdrojového kódu, Kubernetes nebo Docker.\n  - Podpora masivní uživatelské základny: Super velké skupiny se stovkami tisíc uživatelů, desítkami milionů uživatelů a miliardami zpráv.\n\n### Vylepšené obchodní funkce:\n\n- **REST API**: OpenIMServer nabízí REST API pro podnikové systémy, jejichž cílem je poskytnout podnikům více funkcí, jako je vytváření skupin a odesílání push zpráv přes backendová rozhraní.\n- **Webhooks**: OpenIMServer poskytuje možnosti zpětného volání pro rozšíření více obchodních formulářů. Zpětné volání znamená, že OpenIMServer odešle požadavek na obchodní server před nebo po určité události, jako jsou zpětná volání před nebo po odeslání zprávy.\n\n👉 **[Další informace](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Celková architektura\n\nPonořte se do srdce funkčnosti Open-IM-Server s naším diagramem architektury.\n\n![Overall Architecture](../images/architecture-layers.png)\n\n## :rocket: Rychlý start\n\nPodporujeme mnoho platforem. Zde jsou adresy pro rychlou práci na webové stránce:\n\n👉 **[Online webová ukázka OpenIM](https://web-enterprise.rentsoft.cn/)**\n\n🤲 Pro usnadnění uživatelské zkušenosti nabízíme různá řešení nasazení. Způsob nasazení si můžete vybrat ze seznamu níže:\n\n- **[Průvodce nasazením zdrojového kódu](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Docker Deployment Guide](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Průvodce nasazením Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Průvodce nasazením pro vývojáře Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: Chcete-li začít vyvíjet OpenIM\n\n[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM Naším cílem je vybudovat špičkovou open source komunitu. Máme soubor standardů v [komunitním repozitáři](https://github.com/OpenIMSDK/community).\n\nPokud byste chtěli přispět do tohoto úložiště Open-IM-Server, přečtěte si naši [dokumentaci pro přispěvatele](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md).\n\nNež začnete, ujistěte se, že jsou vaše změny vyžadovány. Nejlepší pro to je vytvořit [nová diskuze](https://github.com/openimsdk/open-im-server/discussions/new/choose) NEBO [Slack Communication](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), nebo pokud narazíte na problém, [nahlásit jej](https://github.com/openimsdk/open-im-server/issues/new/choose) jako první.\n\n- [OpenIM API Reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [Protokolování OpenIM Bash](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [Akce OpenIM CI/CD](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [Konvence kódu OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [Pokyny k zavázání OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [Průvodce vývojem OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [Struktura adresáře OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [Nastavení prostředí OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [Referenční kód chybového kódu OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [Pracovní postup OpenIM Git](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [OpenIM Git Cherry Pick Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [Pracovní postup OpenIM GitHub](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [standardy kódu OpenIM Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [Pokyny pro obrázky OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [Počáteční konfigurace OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [Průvodce instalací OpenIM Docker](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [nstalace systému OpenIM OpenIM Linux](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [OpenIM Linux Development Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [Průvodce místními akcemi OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [Konvence protokolování OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [Offline nasazení OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [Nástroje protokolu OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [Příručka testování OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [OpenIM Utility Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [OpenIM Makefile Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [OpenIM Script Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [OpenIM Versioning](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [Spravovat backend a monitorovat nasazení](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Průvodce nasazením pro vývojáře Mac pro OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: Společenství\n\n- 📚 [Komunita OpenIM](https://github.com/OpenIMSDK/community)\n- 💕 [Zájmová skupina OpenIM](https://github.com/Openim-sigs)\n- 🚀 [Připojte se k naší komunitě Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [Připojte se k našemu wechatu](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: Komunitní setkání\n\nChceme, aby se do naší komunity a přispívání kódu zapojil kdokoli, nabízíme dárky a odměny a vítáme vás, abyste se k nám připojili každý čtvrtek večer.\n\nNaše konference je v [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, pak můžete vyhledat kanál Open-IM-Server a připojit se\n\nZaznamenáváme si každou [dvoutýdenní schůzku](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)do [diskuzí na GitHubu](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), naše historické poznámky ze schůzek a také záznamy schůzek jsou k dispozici na [Dokumenty Google :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing).\n\n## :eyes: Kdo používá OpenIM\n\nPodívejte se na naši stránku [případové studie uživatelů](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md), kde najdete seznam uživatelů projektu. Neváhejte zanechat[📝komentář](https://github.com/openimsdk/open-im-server/issues/379) a podělte se o svůj případ použití.\n\n## :page_facing_up: License\n\nOpenIM je licencován pod licencí Apache 2.0. Úplný text licence naleznete v [LICENCE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE).\n\nLogo OpenIM, včetně jeho variací a animovaných verzí, zobrazené v tomto úložišti [OpenIM](https://github.com/openimsdk/open-im-server)v adresářích [assets/logo](./assets/logo) a [assets/logo-gif](assets/logo-gif) je chráněno autorským právem.\n\n## 🔮 Děkujeme našim přispěvatelům!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_da.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## :busts_in_silhouette: Fællesskab\n\n- 📚 [OpenIM-fællesskab](https://github.com/OpenIMSDK/community)\n- 💕 [OpenIM-interessegruppe](https://github.com/Openim-sigs)\n- 🚀 [Deltag i vores Slack-fællesskab](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [Deltag i vores WeChat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n- 👫 [Deltag i vores Reddit](https://www.reddit.com/r/OpenIMessaging)\n- 💬 [Følg vores Twitter-konto](https://twitter.com/openimsdk)\n\n## Ⓜ️ Om OpenIM\n\nOpenIM er en serviceplatform designet specifikt til integration af chat, lyd-videoopkald, notifikationer og AI-chatbots i applikationer. Den tilbyder en række kraftfulde API'er og Webhooks, som gør det let for udviklere at integrere disse interaktive funktioner i deres applikationer. OpenIM er ikke en selvstændig chatapplikation, men fungerer snarere som en platform, der understøtter andre applikationer i at opnå omfattende kommunikationsfunktionaliteter. Følgende diagram illustrerer interaktionen mellem AppServer, AppClient, OpenIMServer og OpenIMSDK for at forklare detaljeret.\n\n![App-OpenIM Relationship](../images/oepnim-design.png)\n\n## 🚀 Om OpenIMSDK\n\n**OpenIMSDK** er en IM SDK designet til **OpenIMServer**, skabt specifikt til indlejring i klientapplikationer. Dens vigtigste funktioner og moduler er som følger:\n\n- 🌟 Hovedfunktioner:\n\n  - 📦 Lokal lagring\n  - 🔔 Lytter-callbacks\n  - 🛡️ API-indkapsling\n  - 🌐 Forbindelsesstyring\n\n  ## 📚 Hovedmoduler:\n\n  1. 🚀 Initialisering og login\n  2. 👤 Brugerstyring\n  3. 👫 Venstyring\n  4. 🤖 Gruppefunktioner\n  5. 💬 Håndtering af samtaler\n\nDet er bygget ved hjælp af Golang og understøtter tværplatformsudrulning, hvilket sikrer en konsekvent adgangsoplevelse på tværs af alle platforme.\n\n👉 **[Udforsk GO SDK](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 Om OpenIMServer\n\n- **OpenIMServer** har følgende karakteristika:\n  - 🌐 Mikroservicarkitektur: Understøtter klyngetilstand, inklusive en gateway og flere rpc-tjenester.\n  - 🚀 Forskellige udrulningsmetoder: Understøtter udrulning via kildekode, Kubernetes eller Docker.\n  - Støtte til massiv brugerbase: Super store grupper med hundredtusinder af brugere, titusinder af brugere og milliarder af beskeder.\n\n### Forbedret forretningsfunktionalitet:\n\n- **REST API**：OpenIMServer tilbyder REST API'er til forretningssystemer, med det formål at give virksomheder flere funktioner, såsom at oprette grupper og sende push-beskeder gennem backend-grænseflader.\n- **Webhooks**：OpenIMServer giver mulighed for callback-funktionalitet for at udvide flere forretningsformer. Et callback betyder, at OpenIMServer sender en anmodning til forretningsserveren før eller efter en bestemt begivenhed, som callbacks før eller efter at have sendt en besked.\n\n👉 **[Lær mere](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Samlet Arkitektur\n\nDyk ned i hjertet af Open-IM-Servers funktionalitet med vores arkitekturdiagram.\n\n![Overall Architecture](../images/architecture-layers.png)\n\n## :rocket: Hurtig start\n\nVi understøtter mange platforme. Her er adresserne for hurtig oplevelse på websiden:\n\n👉 **[OpenIM online demo](https://www.openim.io/zh/commercial)**\n\n🤲 For at lette brugeroplevelsen tilbyder vi forskellige udrulningsløsninger. Du kan vælge din udrulningsmetode fra listen nedenfor:\n\n- **[Vejledning til udrulning af kildekode](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Vejledning til Docker-udrulning](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Vejledning til Kubernetes-udrulning](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Vejledning til Mac-udviklerudrulning](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: For at starte udviklingen af OpenIM\n\n[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM Vores mål er at bygge et topniveau åben kildekode-fællesskab. Vi har et sæt standarder i [Community-repositoriet](https://github.com/OpenIMSDK/community).\n\nHvis du gerne vil bidrage til dette Open-IM-Server-repositorium, bedes du læse vores [dokumentation for bidragydere](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md).\n\nFør du starter, skal du sikre dig, at dine ændringer er efterspurgte. Det bedste for det er at oprette en [ny diskussion](https://github.com/openimsdk/open-im-server/discussions/new/choose) ELLER [Slack-kommunikation](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), eller hvis du finder et problem, [rapportere det](https://github.com/openimsdk/open-im-server/issues/new/choose) først.\n\n- [OpenIM API-referencer](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [OpenIM Bash-logging](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [OpenIM CI/CD-handlinger](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [OpenIM kodekonventioner](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [OpenIM commit-retningslinjer](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [OpenIM udviklingsguide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [OpenIM mappestruktur](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [OpenIM miljøopsætning](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [OpenIM fejlkode-reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [OpenIM Git-arbejdsgang](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [OpenIM Git Cherry Pick-guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [OpenIM GitHub-arbejdsgang](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [OpenIM Go kode-standarder](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [OpenIM billedretningslinjer](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [OpenIM initialkonfiguration](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [OpenIM Docker installationsguide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [OpenIM OpenIM Linux-systeminstallation](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [OpenIM Linux-udviklingsguide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [OpenIM lokale handlingsguide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [OpenIM logningskonventioner](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [OpenIM offline-udrulning](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [OpenIM Protoc-værktøjer](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [OpenIM testguide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [OpenIM Utility Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [OpenIM Makefile-værktøjer](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [OpenIM skriptværktøjer](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [OpenIM versionsstyring](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [Administrer backend og overvåg udrulning](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Mac-udviklerudrulningsguide for OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :calendar: Fællesskabsmøder\n\nVi ønsker, at alle involverer sig i vores fællesskab og bidrager med kode, vi tilbyder gaver og belønninger, og vi byder dig velkommen til at deltage hver torsdag aften.\n\nVores konference er på [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, derefter kan du søge Open-IM-Server pipeline for at deltage.\n\nVi tager [notater](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) af hvert fjortendages møde i [GitHub-diskussioner](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Vores historiske mødenotater samt genudsendelser af møderne er tilgængelige på [Google Docs](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) 📑.\n\n## :eyes: Hvem Bruger OpenIM\n\nTjek vores side med [brugercasestudier](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) for en liste over projektbrugerne. Tøv ikke med at efterlade en 📝[kommentar](https://github.com/openimsdk/open-im-server/issues/379) og dele dit brugstilfælde.\n\n## :page_facing_up: Licens\n\nOpenIM er licenseret under Apache 2.0-licensen. Se [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) for den fulde licens tekst.\n\nOpenIM-logoet, inklusive dets variationer og animerede versioner, vist i dette repositorium [OpenIM](https://github.com/openimsdk/open-im-server) under mapperne [assets/logo](../../assets/logo) og [assets/logo-gif](../../assets/logo-gif), er beskyttet af ophavsretslove.\n\n## 🔮 Tak til vores bidragydere!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_el.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ Σχετικά με το OpenIM\n\nΤο OpenIM είναι μια πλατφόρμα υπηρεσιών σχεδιασμένη ειδικά για την ενσωμάτωση συνομιλίας, κλήσεων ήχου-βίντεο, ειδοποιήσεων και chatbots AI σε εφαρμογές. Παρέχει μια σειρά από ισχυρά API και Webhooks, επιτρέποντας στους προγραμματιστές να ενσωματώσουν εύκολα αυτές τις αλληλεπιδραστικές λειτουργίες στις εφαρμογές τους. Το OpenIM δεν είναι μια αυτόνομη εφαρμογή συνομιλίας, αλλά λειτουργεί ως πλατφόρμα υποστήριξης άλλων εφαρμογών για την επίτευξη πλούσιων λειτουργιών επικοινωνίας. Το παρακάτω διάγραμμα απεικονίζει την αλληλεπίδραση μεταξύ AppServer, AppClient, OpenIMServer και OpenIMSDK για να εξηγήσει αναλυτικά.\n\n![App-OpenIM Relationship](../../docs/images/oepnim-design.png)\n\n## 🚀 Σχετικά με το OpenIMSDK\n\nΤο **OpenIMSDK** είναι ένα SDK για αμεση ανταλλαγή μηνυμάτων σχεδιασμένο για το **OpenIMServer**, δημιουργήθηκε ειδικά για ενσωμάτωση σε εφαρμογές πελατών. Οι κύριες δυνατότητες και μονάδες του είναι οι εξής:\n\n- 🌟 Κύριες Δυνατότητες:\n\n  - 📦 Τοπική αποθήκευση\n  - 🔔 Callbacks ακροατών\n  - 🛡️ Περιτύλιγμα API\n  - 🌐 Διαχείριση σύνδεσης\n\n- 📚 Κύριες Μονάδες:\n\n  1. 🚀 Αρχικοποίηση και Σύνδεση\n  2. 👤 Διαχείριση Χρηστών\n  3. 👫 Διαχείριση Φίλων\n  4. 🤖 Λειτουργίες Ομάδας\n  5. 💬 Διαχείριση Συνομιλιών\n\nΕίναι κατασκευασμένο χρησιμοποιώντας Golang και υποστηρίζει διασταυρούμενη πλατφόρμα ανάπτυξης, διασφαλίζοντας μια συνεπή εμπειρία πρόσβασης σε όλες τις πλατφόρμες.\n\n👉 **[Εξερευνήστε το GO SDK](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 Σχετικά με το OpenIMServer\n\n- Το **OpenIMServer** έχει τις ακόλουθες χαρακτηριστικές:\n  - 🌐 Αρχιτεκτονική μικροϋπηρεσιών: Υποστηρίζει λειτουργία σε σύμπλεγμα, περιλαμβάνοντας έναν πύλη και πολλαπλές υπηρεσίες rpc.\n  - 🚀 Διάφοροι τρόποι ανάπτυξης: Υποστηρίζει ανάπτυξη μέσω πηγαίου κώδικα, Kubernetes, ή Docker.\n  - Υποστήριξη για τεράστια βάση χρηστών: Πολύ μεγάλες ομάδες με εκατοντάδες χιλιάδες χρήστες, δεκάδες εκατομμύρια χρήστες και δισεκατομμύρια μηνύματα.\n\n### Ενισχυμένη Επιχειρηματική Λειτουργικότητα:\n\n- **REST API**: Το OpenIMServer προσφέρει REST APIs για επιχειρηματικά συστήματα, με στόχο την ενδυνάμωση των επιχειρήσεων με περισσότερες λειτουργικότητες, όπως η δημιουργία ομάδων και η αποστολή μηνυμάτων push μέσω backend διεπαφών.\n- **Webhooks**: Το OpenIMServer παρέχει δυνατότητες επανάκλησης για την επέκταση περισσότερων επιχειρηματικών μορφών. Μια επανάκληση σημαίνει ότι το OpenIMServer στέλνει ένα αίτημα στον επιχειρηματικό διακομιστή πριν ή μετά από ένα συγκεκριμένο γεγονός, όπως επανακλήσεις πριν ή μετά την αποστολή ενός μηνύματος.\n\n👉 **[Μάθετε περισσότερα](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Συνολική Αρχιτεκτονική\n\nΕξερευνήστε σε βάθος τη λειτουργικότητα του Open-IM-Server με το διάγραμμα αρχιτεκτονικής μας.\n\n![Overall Architecture](../../docs/images/architecture-layers.png)\n\n## :rocket: Γρήγορη Εκκίνηση\n\nΥποστηρίζουμε πολλές πλατφόρμες. Εδώ είναι οι διευθύνσεις για γρήγορη εμπειρία στην πλευρά του διαδικτύου:\n\n👉 **[Διαδικτυακή επίδειξη του OpenIM](https://web-enterprise.rentsoft.cn/)**\n\n🤲 Για να διευκολύνουμε την εμπειρία του χρήστη, προσφέρουμε διάφορες λύσεις ανάπτυξης. Μπορείτε να επιλέξετε τη μέθοδο ανάπτυξης σας από την παρακάτω λίστα:\n\n- **[Οδηγός Ανάπτυξης Κώδικα Πηγής](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[δηγός Ανάπτυξης μέσω Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Οδηγός Ανάπτυξης Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Οδηγός Ανάπτυξης για Αναπτυξιακούς στο Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: Για να Αρχίσετε την Ανάπτυξη του OpenIM\n\n[![Άνοιγμα σε Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM Στόχος μας είναι να δημιουργήσουμε μια κορυφαίου επιπέδου ανοιχτή πηγή κοινότητας. Διαθέτουμε ένα σύνολο προτύπων, στο [Αποθετήριο Κοινότητας](https://github.com/OpenIMSDK/community).\n\nΕάν θέλετε να συνεισφέρετε σε αυτό το αποθετήριο Open-IM-Server, παρακαλούμε διαβάστε την [τεκμηρίωση συνεισφέροντος](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md).\n\nΠριν ξεκινήσετε, παρακαλούμε βεβαιωθείτε ότι οι αλλαγές σας είναι ζητούμενες. Το καλύτερο για αυτό είναι να δημιουργήσετε ένα [νέα συζήτηση](https://github.com/openimsdk/open-im-server/discussions/new/choose) ή [Επικοινωνία Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), ή αν βρείτε ένα ζήτημα, [αναφέρετέ το](https://github.com/openimsdk/open-im-server/issues/new/choose) πρώτα.\n\n- [Αναφορά API του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [Καταγραφή Bash του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [Ενέργειες CI/CD του OpenIMs](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [Συμβάσεις Κώδικα του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [Οδηγίες Commit του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [Οδηγός Ανάπτυξης του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [Δομή Καταλόγου του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [Ρύθμιση Περιβάλλοντος του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [Αναφορά Κωδικών Σφάλματος του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [Ροή Εργασίας Git του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [Οδηγός Cherry Pick του Git του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [Ροή Εργασίας GitHub του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [Πρότυπα Κώδικα Go του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [Οδηγίες Εικόνας του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [Αρχική Διαμόρφωση του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [Οδηγός Εγκατάστασης Docker του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [Οδηγός Εγκατάστασης Συστήματος Linux του Open](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [Οδηγός Ανάπτυξης Linux του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [Οδηγός Τοπικών Δράσεων του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [Συμβάσεις Καταγραφής του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [Αποστολή Εκτός Σύνδεσης του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [Εργαλεία Protoc του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [Οδηγός Δοκιμών του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [Χρησιμότητα Go του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [Χρησιμότητες Makefile του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [Χρησιμότητες Σεναρίου του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [Έκδοση του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [Διαχείριση backend και παρακολούθηση ανάπτυξης](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Οδηγός Ανάπτυξης για Προγραμματιστές Mac του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: Κοινότητα\n\n- 📚 [Κοινότητα OpenIM](https://github.com/OpenIMSDK/community)\n- 💕 [Ομάδα Ενδιαφέροντος OpenIM](https://github.com/Openim-sigs)\n- 🚀 [Εγγραφείτε στην κοινότητα Slack μας](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [γγραφείτε στην ομάδα μας wechat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: Συναντήσεις της κοινότητας\n\nΘέλουμε οποιονδήποτε να εμπλακεί στην κοινότητά μας και να συνεισφέρει κώδικα. Προσφέρουμε δώρα και ανταμοιβές και σας καλωσορίζουμε να μας ενταχθείτε κάθε Πέμπτη βράδυ.\n\nΗ διάσκεψή μας είναι στο [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, στη συνέχεια μπορείτε να αναζητήσετε τη διαδικασία Open-IM-Server για να συμμετάσχετε\n\nΚάνουμε σημειώσεις για κάθε μια [Σημειώνουμε κάθε διμηνιαία συνάντηση](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) στις [συζητήσεις του GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Οι ιστορικές μας σημειώσεις συναντήσεων, καθώς και οι επαναλήψεις των συναντήσεων είναι διαθέσιμες στο[Έγγραφα της Google :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing).\n\n## :eyes: Ποιοί Χρησιμοποιούν το OpenIM\n\nΕλέγξτε τη σελίδα με τις [μελέτες περίπτωσης χρήσης ](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) μας για μια λίστα των χρηστών του έργου. Μην διστάσετε να αφήσετε ένα[📝σχόλιο](https://github.com/openimsdk/open-im-server/issues/379) και να μοιραστείτε την περίπτωση χρήσης σας.\n\n## :page_facing_up: Άδεια Χρήσης\n\nΤο OpenIM διατίθεται υπό την άδεια Apache 2.0. Δείτε τη [ΑΔΕΙΑ ΧΡΗΣΗΣ](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) για το πλήρες κείμενο της άδειας.\n\nΤο λογότυπο του OpenIM, συμπεριλαμβανομένων των παραλλαγών και των κινούμενων εικόνων, που εμφανίζονται σε αυτό το αποθετήριο[OpenIM](https://github.com/openimsdk/open-im-server) υπό τις διευθύνσεις [assets/logo](../../assets/logo) και [assets/logo-gif](../../assets/logo-gif) προστατεύονται από τους νόμους περί πνευματικής ιδιοκτησίας.\n\n## 🔮 Ευχαριστούμε τους συνεισφέροντες μας!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_es.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ Acerca de OpenIM\n\nOpenIM es una plataforma de servicio diseñada específicamente para integrar chat, llamadas de audio y video, notificaciones y chatbots de IA en aplicaciones. Proporciona una gama de potentes API y Webhooks, lo que permite a los desarrolladores incorporar fácilmente estas características interactivas en sus aplicaciones. OpenIM no es una aplicación de chat independiente, sino que sirve como una plataforma para apoyar a otras aplicaciones en lograr funcionalidades de comunicación enriquecidas. El siguiente diagrama ilustra la interacción entre AppServer, AppClient, OpenIMServer y OpenIMSDK para explicar en detalle.\n\n![Relación App-OpenIM](../../docs/images/oepnim-design.png)\n\n## 🚀 Acerca de OpenIMSDK\n\n**OpenIMSDK** es un SDK de mensajería instantánea diseñado para **OpenIMServer**, creado específicamente para su incorporación en aplicaciones cliente. Sus principales características y módulos son los siguientes:\n\n- 🌟 Características Principales:\n\n  - 📦 Almacenamiento local\n  - 🔔 Callbacks de escuchas\n  - 🛡️ Envoltura de API\n  - 🌐 Gestión de conexiones\n\n- 📚 Módulos Principales:\n\n  1. 🚀 Inicialización y acceso\n  2. 👤 Gestión de usuarios\n  3. 👫 Gestión de amigos\n  4. 🤖 Funciones de grupo\n  5. 💬 Manejo de conversaciones\n\nEstá construido con Golang y soporta despliegue multiplataforma, asegurando una experiencia de acceso consistente en todas las plataformas.\n\n👉 **[Explora el SDK de GO](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 Acerca de OpenIMServer\n\n- **OpenIMServer** tiene las siguientes características:\n  - 🌐 Arquitectura de microservicios: Soporta modo cluster, incluyendo un gateway y múltiples servicios rpc.\n  - 🚀 Métodos de despliegue diversos: Soporta el despliegue a través de código fuente, Kubernetes o Docker.\n  - Soporte para una base de usuarios masiva: Grupos super grandes con cientos de miles de usuarios, decenas de millones de usuarios y miles de millones de mensajes.\n\n### Funcionalidad Empresarial Mejorada:\n\n- **API REST**: OpenIMServer ofrece APIs REST para sistemas empresariales, destinadas a empoderar a las empresas con más funcionalidades, como la creación de grupos y el envío de mensajes push a través de interfaces de backend.\n- **Webhooks**: OpenIMServer proporciona capacidades de callback para extender más formas de negocio. Un callback significa que OpenIMServer envía una solicitud al servidor empresarial antes o después de un cierto evento, como callbacks antes o después de enviar un mensaje.\n\n👉 **[Aprende más](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Arquitectura General\n\nAdéntrate en el corazón de la funcionalidad de Open-IM-Server con nuestro diagrama de arquitectura.\n\n![Arquitectura General](../../docs/images/architecture-layers.png)\n\n## :rocket: Inicio Rápido\n\n:rocket: Inicio Rápido\nApoyamos muchas plataformas. Aquí están las direcciones para una experiencia rápida en el lado web:\n\n👉 **[ Demostración web en línea de OpenIM](https://web-enterprise.rentsoft.cn/)**\n\n🤲 Para facilitar la experiencia del usuario, ofrecemos varias soluciones de despliegue. Puedes elegir tu método de despliegue de la lista a continuación:\n\n- **[Guía de Despliegue de Código Fuente](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Guía de Despliegue con Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Guía de Despliegue con Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Guía de Despliegue para Desarrolladores en Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: Para Comenzar a Desarrollar en OpenIM\n\n[![Abrir en Contenedor de Desarrollo](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nNuestro objetivo en OpenIM es construir una comunidad de código abierto de nivel superior. Tenemos un conjunto de estándares,\nen el [repositorio de la Comunidad.](https://github.com/OpenIMSDK/community).\n\nSi te gustaría contribuir a este repositorio de Open-IM-Server, por favor lee nuestra [documentación para colaboradores](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md).\n\nAntes de comenzar, asegúrate de que tus cambios sean demandados. Lo mejor para eso es crear una [nueva discusión](https://github.com/openimsdk/open-im-server/discussions/new/choose) O [Comunicación en Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), o si encuentras un problema, [repórtalo](https://github.com/openimsdk/open-im-server/issues/new/choose) primero.\n\n- [Referencia de API de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [Registro de Bash de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [Acciones de CI/CD de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [Convenciones de Código de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [Guías de Commit de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [Guía de Desarrollo de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [Estructura de Directorios de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [Configuración de Entorno de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [Referencia de Códigos de Error de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [Flujo de Trabajo de Git de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [Guía de Cherry Pick de Git de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [Flujo de Trabajo de GitHub de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [Estándares de Código Go de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [Guías de Imágenes de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [Configuración Inicial de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [Guía de Instalación de Docker de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [Instalación del Sistema Linux de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [Guía de Desarrollo Linux de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [Guía de Acciones Locales de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [Convenciones de Registro de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [Despliegue sin Conexión de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [Herramientas Protoc de OpenIMM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [Guía de Pruebas de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [Utilidades Go de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [Utilidades de Makefile de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [Utilidades de Script de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [Versionado de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [Gestión de backend y despliegue de monitoreo](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Guía de Despliegue para Desarrolladores Mac de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: Comunidad\n\n- 📚 [Comunidad de OpenIM](https://github.com/OpenIMSDK/community)\n- 💕 [Grupo de Interés de OpenIM](https://github.com/Openim-sigs)\n- 🚀 [Únete a nuestra comunidad de Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [Únete a nuestro wechat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: Reuniones de la Comunidad\n\nQueremos que cualquiera se involucre en nuestra comunidad y contribuya con código, ofrecemos regalos y recompensas, y te damos la bienvenida para que te unas a nosotros cada jueves por la noche.\n\nNuestra conferencia está en [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, luego puedes buscar el pipeline de Open-IM-Server para unirte\n\nTomamos notas de cada [reunión quincenal](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) en [discusiones de GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Nuestras notas de reuniones históricas, así como las repeticiones de las reuniones están disponibles en [Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing).\n\n## :eyes: Quiénes Están Usando OpenIM\n\nConsulta nuestros [estudios de caso de usuarios](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) página para obtener una lista de los usuarios del proyecto. No dudes en dejar un [📝comentario](https://github.com/openimsdk/open-im-server/issues/379) y compartir tu caso de uso.\n\n## :page_facing_up: Licencia\n\nOpenIM está bajo la licencia Apache 2.0. Consulta [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) para ver el texto completo de la licencia.\n\nEl logotipo de OpenIM, incluyendo sus variaciones y versiones animadas, que se muestran en este repositorio [OpenIM](https://github.com/openimsdk/open-im-server) en los directorios [assets/logo](../../assets/logo) y [assets/logo-gif](assets/logo-gif) están protegidos por las leyes de derechos de autor.\n\n## 🔮 iGracias a nuestros colaboradores!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_fa.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## درباره OpenIM Ⓜ️\n\nOpenIM یک پلتفرم خدماتی است که به طور خاص برای ادغام چت، تماس های صوتی و تصویری، اعلان ها و چت ربات های هوش مصنوعی در برنامه ها طراحی شده است. این مجموعه ای از API ها و Webhook های قدرتمند را ارائه می دهد که به توسعه دهندگان این امکان را می دهد تا به راحتی این ویژگی های تعاملی را در برنامه های خود بگنجانند. OpenIM یک برنامه چت مستقل نیست، بلکه به عنوان یک پلتفرم برای پشتیبانی از برنامه های کاربردی دیگر در دستیابی به قابلیت های ارتباطی غنی عمل می کند. نمودار زیر تعامل بین AppServer، AppClient، OpenIMServer و OpenIMSDK را برای توضیح جزئیات نشان می دهد.\n\n![App-OpenIM Relationship](../images/oepnim-design.png)\n\n## 🚀 درباره OpenIMSDK\n\n**OpenIMSDK** یک IM SDK است که برای **OpenIMServer** طراحی شده است که به طور خاص برای جاسازی در برنامه های مشتری ایجاد شده است. ویژگی ها و ماژول های اصلی آن به شرح زیر است:\n\n- 🌟 ویژگی های اصلی:\n\n  - 📦 ذخیره سازی محلی\n  - 🔔 پاسخ تماس شنونده\n  - 🛡️ بسته بندی API\n  - 🌐 مدیریت اتصال\n\n- 📚 ماژول های اصلی:\n\n  1. 🚀 مقداردهی اولیه و ورود\n  2. 👤 مدیریت کاربر\n  3. 👫 مدیریت دوست\n  4. 🤖 توابع گروه\n  5. 💬 مدیریت مکالمه\n\nاین برنامه با استفاده از Golang ساخته شده است و از استقرار چند پلت فرم پشتیبانی می کند و تجربه دسترسی ثابت را در تمام پلتفرم ها تضمین می کند.\n\n👉 **[کاوش GO SDK](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 درباره OpenIMServer\n\n- **OpenIMServer** دارای ویژگی های زیر است:\n  - 🌐 معماری Microservice: از حالت کلاستر، از جمله یک دروازه و چندین سرویس rpc پشتیبانی می کند.\n  - 🚀 روش‌های استقرار متنوع: از استقرار از طریق کد منبع، Kubernetes یا Docker پشتیبانی می‌کند.\n  - پشتیبانی از پایگاه عظیم کاربران: گروه های فوق العاده بزرگ با صدها هزار کاربر، ده ها میلیون کاربر و میلیاردها پیام.\n\n### عملکردهای تجاری پیشرفته:\n\n- **REST API**: OpenIMServer APIهای REST را برای سیستم‌های تجاری ارائه می‌کند، با هدف توانمندسازی کسب‌وکارها با قابلیت‌های بیشتر، مانند ایجاد گروه‌ها و ارسال پیام‌های فشار از طریق رابط‌های باطنی.\n- **Webhooks**: OpenIMServer قابلیت های پاسخ به تماس را برای گسترش بیشتر فرم های تجاری ارائه می دهد. پاسخ به تماس به این معنی است که OpenIMServer درخواستی را قبل یا بعد از یک رویداد خاص به سرور تجاری ارسال می کند، مانند تماس های قبل یا بعد از ارسال یک پیام.\n\n👉 **[بیشتر بدانید](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: معماری کلی\n\nبا نمودار معماری ما به قلب عملکرد Open-IM-Server بپردازید.\n\n![Overall Architecture](../images/architecture-layers.png)\n\n## :rocket: شروع سریع\n\nما از بسیاری از پلتفرم ها پشتیبانی می کنیم. در اینجا آدرس هایی برای تجربه سریع در سمت وب آمده است:\n\n👉 **[نسخه نمایشی وب آنلاین OpenIM](https://web-enterprise.rentsoft.cn/)**\n\n🤲 برای تسهیل تجربه کاربر، ما راه حل های مختلف استقرار را ارائه می دهیم. می توانید روش استقرار خود را از لیست زیر انتخاب کنید:\n\n- **[راهنمای استقرار کد منبع](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[راهنمای استقرار داکر](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[راهنمای استقرار Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[راهنمای استقرار توسعه دهنده مک](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: برای شروع توسعه OpenIM\n\n[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM هدف ما ایجاد یک جامعه منبع باز سطح بالا است. ما مجموعه ای از استانداردها را در [مخزن انجمن](https://github.com/OpenIMSDK/community) داریم..\n\nاگر می‌خواهید در این مخزن Open-IM-Server مشارکت کنید، لطفاً [مستندات مشارکت‌کننده](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md) ما را بخوانید.\n\nقبل از شروع، لطفاً مطمئن شوید که تغییرات شما مورد تقاضا هستند. بهترین کار برای آن این است که یک [بحث جدید](https://github.com/openimsdk/open-im-server/discussions/new/choose) یا [ارتباط اسلک](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) ایجاد کنید، یا اگر مشکلی پیدا کردید، ابتدا [آن را گزارش کنید](https://github.com/openimsdk/open-im-server/issues/new/choose).\n\n- [مرجع OpenIM API](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [OpenIM Bash Logging](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [OpenIM CI/CD Actions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [کنوانسیون کد OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [دستورالعمل های تعهد OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [راهنمای توسعه OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [ساختار دایرکتوری OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [تنظیم محیط OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [مرجع کد خطا OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [OpenIM Git Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [راهنمای انتخاب گیلاس OpenIM Git](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [OpenIM GitHub Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [استانداردهای کد OpenIM Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [دستورالعمل های تصویر OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [پیکربندی اولیه OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [راهنمای نصب OpenIM Docker](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [نصب سیستم OpenIM Linux OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [راهنمای توسعه OpenIM Linux](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [راهنمای اقدامات محلی OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [OpenIM Logging Conventions](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [استقرار آفلاین OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [OpenIM Protoc Tools](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [راهنمای تست OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [OpenIM Utility Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [OpenIM Makefile Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [ابزارهای OpenIM Script](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [نسخه OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [مدیریت استقرار باطن و نظارت](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [راهنمای استقرار توسعه دهنده مک برای OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: انجمن\n\n- 📚 [انجمن OpenIM](https://github.com/OpenIMSDK/community)\n- 💕 [گروه علاقه OpenIM](https://github.com/Openim-sigs)\n- 🚀 [به انجمن Slack ما بپیوندید](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [به وی چت ما بپیوندید](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: جلسات جامعه\n\nما می‌خواهیم هر کسی در انجمن ما مشارکت کند و در کد مشارکت کند، ما هدایا و جوایزی ارائه می‌کنیم، و از شما استقبال می‌کنیم که هر پنجشنبه شب به ما بپیوندید.\n\nکنفرانس ما در [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯 است، سپس می توانید خط لوله Open-IM-Server را برای پیوستن جستجو کنید.\n\nما از هر [جلسه دو هفته‌ای](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) در [بحث‌های GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting) یادداشت‌برداری می‌کنیم، یادداشت‌های جلسه تاریخی ما، و همچنین بازپخش جلسات در [Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) موجود است.\n\n## :eyes: چه کسانی از OpenIM استفاده می کنند\n\nصفحه [مطالعات موردی کاربر](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) ما را برای لیستی از کاربران پروژه بررسی کنید. از گذاشتن [نظر📝](https://github.com/openimsdk/open-im-server/issues/379) و به اشتراک گذاری مورد استفاده خود دریغ نکنید.\n\n## :page_facing_up: مجوز\n\nOpenIM تحت مجوز Apache 2.0 مجوز دارد. برای متن کامل مجوز به [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) مراجعه کنید.\n\nنشان‌واره OpenIM، شامل انواع و نسخه‌های متحرک آن، که در این مخزن [OpenIM](https://github.com/openimsdk/open-im-server) تحت فهرست‌های [assets/logo](./assets/logo) و [assets/logo-gif](assets/logo-gif) نمایش داده می‌شود، توسط قوانین حق چاپ محافظت می‌شود.\n\n## 🔮 با تشکر از همکاران ما!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_fr.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ À propos de OpenIM\n\nOpenIM est une plateforme de services conçue spécifiquement pour intégrer des fonctionnalités de communication telles que le chat, les appels audio et vidéo, les notifications, ainsi que les robots de chat IA dans les applications. Elle offre une série d'API puissantes et de Webhooks, permettant aux développeurs d'incorporer facilement ces caractéristiques interactives dans leurs applications. OpenIM n'est pas en soi une application de chat autonome, mais sert de plateforme supportant d'autres applications pour réaliser des fonctionnalités de communication enrichies. L'image ci-dessous montre les relations d'interaction entre AppServer, AppClient, OpenIMServer et OpenIMSDK pour illustrer spécifiquement.\n\n![Relation App-OpenIM](../../images/oepnim-design.png)\n\n## 🚀 À propos de OpenIMSDK\n\n**OpenIMSDK** est un SDK IM conçu pour **OpenIMServer** spécialement créé pour être intégré dans les applications clientes. Ses principales fonctionnalités et modules comprennent :\n\n- 🌟 Fonctionnalités clés :\n\n  - 📦 Stockage local\n  - 🔔 Rappels de l'écouteur\n  - 🛡️ Encapsulation d'API\n  - 🌐 Gestion de la connexion\n\n  ## 📚 Modules principaux ：\n\n  1. 🚀 Initialisation et connexion\n  2. 👤 Gestion des utilisateurs\n  3. 👫 Gestion des amis\n  4. 🤖 Fonctionnalités de groupe\n  5. 💬 Traitement des conversations\n\nIl est construit avec Golang et supporte le déploiement multiplateforme, assurant une expérience d'accès cohérente sur toutes les plateformes。\n\n👉 **[Explorer le SDK GO](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 À propos de OpenIMServer\n\n- **OpenIMServer** présente les caractéristiques suivantes ：\n  - 🌐 Architecture microservices : prend en charge le mode cluster, incluant le gateway (passerelle) et plusieurs services rpc。\n  - 🚀 Divers modes de déploiement : supporte le déploiement via le code source, Kubernetes ou Docker。\n  - Support d'une masse d'utilisateurs : plus de cent mille pour les super grands groupes, des millions d'utilisateurs, et des milliards de messages。\n\n### Fonctionnalités commerciales améliorées :\n\n- **REST API**：OpenIMServer fournit une REST API pour les systèmes commerciaux, visant à accorder plus de fonctionnalités, telles que la création de groupes via l'interface backend, l'envoi de messages push, etc。\n- **Webhooks**：OpenIMServer offre des capacités de rappel pour étendre davantage les formes d'entreprise. Un rappel signifie que OpenIMServer enverra une requête au serveur d'entreprise avant ou après qu'un événement se soit produit, comme un rappel avant ou après l'envoi d'un message。\n\n👉 **[En savoir plus](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Architecture globale\n\nPlongez dans le cœur de la fonctionnalité d'Open-IM-Server avec notre diagramme d'architecture.\n\n![Architecture globale](../../images/architecture-layers.png)\n\n## :rocket: Démarrage rapide\n\nNous prenons en charge de nombreuses plateformes. Voici les adresses pour une expérience rapide du côté web :\n\n👉 **[Démo web en ligne OpenIM](https://www.openim.io/zh/commercial)**\n\n🤲 Pour faciliter l'expérience utilisateur, nous proposons plusieurs solutions de déploiement. Vous pouvez choisir votre méthode de déploiement selon la liste ci-dessous ：\n\n- **[Guide de déploiement du code source](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Guide de déploiement Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Guide de déploiement Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Guide de déploiement pour développeur Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: Commencer à développer avec OpenIM\n\nChez OpenIM, notre objectif est de construire une communauté open source de premier plan. Nous avons un ensemble de standards, disponibles dans le[ dépôt communautaire](https://github.com/OpenIMSDK/community)。\nSi vous souhaitez contribuer à ce dépôt Open-IM-Server, veuillez lire notre[ document pour les contributeurs](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)。\n\nAvant de commencer, assurez-vous que vos modifications sont nécessaires. La meilleure manière est de créer une[ nouvelle discussion ](https://github.com/openimsdk/open-im-server/discussions/new/choose) ou une [ communication Slack,](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)，ou si vous identifiez un problème, de[ signaler d'abord ](https://github.com/openimsdk/open-im-server/issues/new/choose)。\n\n- [Référence de l'API OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [Journalisation Bash OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [Actions CI/CD OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [Conventions de code OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [Directives de commit OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [Guide de développement OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [Structure de répertoire OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [Configuration de l'environnement OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [Référence des codes d'erreur OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [Workflow Git OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [Guide Cherry Pick Git OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [Workflow GitHub OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [Normes de code Go OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [Directives d'image OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [Configuration initiale OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [Guide d'installation Docker OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [Installation du système Linux OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [Guide de développement Linux OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [Guide des actions locales OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [Conventions de journalisation OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [Déploiement hors ligne OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [Outils Protoc OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [Guide de test OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [Utilitaire Go OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [Utilitaires Makefile OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [Utilitaires de script OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [Versionnement OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [Gérer le déploiement du backend et la surveillance](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Guide de déploiement pour développeur Mac pour OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n> ## :calendar: Réunions de la Communauté\n\nNous voulons que tout le monde s'implique dans notre communauté et contribue au code, nous offrons des cadeaux et des récompenses, et nous vous invitons à nous rejoindre chaque jeudi soir.\nNotre conférence se trouve dans le [ Slack OpenIM ](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, ensuite vous pouvez rechercher le pipeline Open-IM-Server pour rejoindre\n\nNous prenons des notes de chaque [réunion bihebdomadaire ](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) dans les [discussions GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Nos notes de réunion historiques, ainsi que les rediffusions des réunions sont disponibles sur [ Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing).\n\n## :eyes: Qui Utilise OpenIM\n\nConsultez notre page [ études de cas d'utilisateurs ](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) pour une liste des utilisateurs du projet. N'hésitez pas à laisser un [📝commentaire](https://github.com/openimsdk/open-im-server/issues/379) et partager votre cas d'utilisation.\n\n## :page_facing_up: License\n\nOpenIM est sous licence Apache 2.0. Voir [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) pour le texte complet de la licence.\n\nLe logo OpenIM, y compris ses variations et versions animées, affiché dans ce dépôt[OpenIM](https://github.com/openimsdk/open-im-server) sous les répertoires [assets/logo](../../assets/logo) et [assets/logo-gif](assets/logo-gif) sont protégés par les lois sur le droit d'auteur.\n\n## 🔮 Merci à nos contributeurs !\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_hu.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ Az OpenIM-ről\n\nAz OpenIM egy szolgáltatási platform, amelyet kifejezetten a csevegés, az audio-video hívások, az értesítések és az AI chatbotok alkalmazásokba történő integrálására terveztek. Számos hatékony API-t és Webhookot kínál, lehetővé téve a fejlesztők számára, hogy ezeket az interaktív szolgáltatásokat könnyen beépítsék alkalmazásaikba. Az OpenIM nem egy önálló csevegőalkalmazás, hanem platformként szolgál más alkalmazások támogatására a gazdag kommunikációs funkciók elérésében. A következő diagram az AppServer, az AppClient, az OpenIMServer és az OpenIMSDK közötti interakciót szemlélteti részletesen.\n\n![App-OpenIM Relationship](../images/oepnim-design.png)\n\n## 🚀 Az OpenIMSDK-ról\n\nAz **OpenIMSDK** egy **OpenIMServer** számára készült azonnali üzenetküldő SDK, amelyet kifejezetten ügyfélalkalmazásokba való beágyazáshoz hoztak létre. Fő jellemzői és moduljai a következők:\n\n- 🌟 Főbb jellemzők:\n\n  - 📦 Helyi raktár\n  - 🔔 Hallgatói visszahívások\n  - 🛡️ API-csomagolás\n  - 🌐 Kapcsolatkezelés\n\n- 📚 Fő modulok:\n\n  1. 🚀 Inicializálás és bejelentkezés\n  2. 👤 Felhasználókezelés\n  3. 👫 Barátkezelés\n  4. 🤖 Csoportfunkciók\n  5. 💬 Beszélgetéskezelés\n\nGolang használatával készült, és támogatja a többplatformos telepítést, biztosítva a konzisztens hozzáférési élményt minden platformon.\n\n👉 **[Fedezze fel a GO SDK-t](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 Az OpenIMServerről\n\n- **OpenIMServer** a következő jellemzőkkel rendelkezik:\n  - 🌐 Mikroszolgáltatási architektúra: Támogatja a fürt módot, beleértve az átjárót és több rpc szolgáltatást.\n  - 🚀 Változatos telepítési módszerek: Támogatja a forráskódon, Kubernetesen vagy Dockeren keresztül történő telepítést.\n  - Hatalmas felhasználói bázis támogatása: Szuper nagy csoportok több százezer felhasználóval, több tízmillió felhasználóval és több milliárd üzenettel.\n\n### Továbbfejlesztett üzleti funkcionalitás:\n\n- **REST API**: Az OpenIMServer REST API-kat kínál az üzleti rendszerek számára, amelyek célja, hogy a vállalkozásokat több funkcióval ruházza fel, mint például csoportok létrehozása és push üzenetek küldése háttérfelületeken keresztül.\n- **Webhooks**: Az OpenIMServer visszahívási lehetőségeket biztosít több üzleti forma kiterjesztéséhez. A visszahívás azt jelenti, hogy az OpenIMServer kérelmet küld az üzleti szervernek egy bizonyos esemény előtt vagy után, például visszahívásokat üzenet küldése előtt vagy után.\n\n👉 **[Tudj meg többet](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Általános építészet\n\nMerüljön el az Open-IM-Server funkcióinak szívében az architektúra diagramunk segítségével.\n\n![Overall Architecture](../images/architecture-layers.png)\n\n## :rocket: Gyors indítás\n\nSzámos platformot támogatunk. Íme a címek a gyors weboldali használathoz:\n\n👉 **[OpenIM online webdemó](https://web-enterprise.rentsoft.cn/)**\n\n🤲 A felhasználói élmény megkönnyítése érdekében különféle telepítési megoldásokat kínálunk. Az alábbi listából választhatja ki a telepítési módot:\n\n- **[Forráskód-telepítési útmutató](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Docker telepítési útmutató](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Kubernetes telepítési útmutató](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Mac fejlesztői telepítési útmutató](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: Az OpenIM fejlesztésének megkezdéséhez\n\n[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM Célunk egy felső szintű nyílt forráskódú közösség felépítése. Van egy szabványkészletünk a [Közösségi adattárban](https://github.com/OpenIMSDK/community).\n\nHa hozzá szeretne járulni ehhez az Open-IM-Server adattárhoz, kérjük, olvassa el [közreműködői dokumentációnkat](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md).\n\nMielőtt elkezdené, győződjön meg arról, hogy a változtatásokra van-e igény. Erre a legjobb egy [új beszélgetés](https://github.com/openimsdk/open-im-server/discussions/new/choose) VAGY [Slack Communication](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)létrehozása, vagy ha problémát talál, először [jelentse](https://github.com/openimsdk/open-im-server/issues/new/choose) first.\n\n- [OpenIM API referencia](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [OpenIM Bash naplózás](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [OpenIM CI/CD műveletek](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [OpenIM Code-egyezmények](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [OpenIM Commit Guidelines](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [OpenIM fejlesztési útmutató](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [OpenIM címtárszerkezet](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [OpenIM környezet beállítása](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [OpenIM hibakód hivatkozás](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [OpenIM Git Workflow](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [OpenIM Git Cherry Pick Guide](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [OpenIM GitHub munkafolyamat](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [OpenIM Go Code szabványok](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [OpenIM képre vonatkozó irányelvek](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [OpenIM kezdeti konfiguráció](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [OpenIM Docker telepítési útmutató](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [OpenIM OpenIM Linux rendszertelepítés](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [OpenIM Linux fejlesztési útmutató](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [OpenIM helyi műveletek útmutatója](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [OpenIM naplózási egyezmények](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [OpenIM offline telepítés](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [OpenIM Protoc Tools](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [OpenIM tesztelési útmutató](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [OpenIM Utility Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [OpenIM Makefile Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [OpenIM Script Utilities](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [OpenIM verzió](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [A háttérrendszer kezelése és a telepítés figyelése](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Mac Developer Deployment Guide for OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: Közösség\n\n- 📚 [OpenIM közösség](https://github.com/OpenIMSDK/community)\n- 💕 [OpenIM érdeklődési csoport](https://github.com/Openim-sigs)\n- 🚀 [Csatlakozz a Slack közösségünkhöz](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [Csatlakozz a wechathez](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: Közösségi Találkozók\n\nSzeretnénk, ha bárki bekapcsolódna közösségünkbe és hozzájárulna kódunkhoz, ajándékokat és jutalmakat kínálunk, és szeretettel várjuk, hogy csatlakozzon hozzánk minden csütörtök este.\n\nKonferenciánk az [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯alatt van, akkor kereshet az Open-IM-Server folyamatban a csatlakozáshoz\n\nA [GitHub-beszélgetések](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)minden [kéthetente történő megbeszélésről](https://github.com/openimsdk/open-im-server/discussions/categories/meeting) jegyzeteket készítünk. A találkozók történeti feljegyzései, valamint az értekezletek visszajátszásai a [Google Dokumentumok :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) webhelyen érhetők el.\n\n## :eyes: Kik használják az OpenIM-et\n\nTekintse meg [felhasználói esettanulmányok](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) oldalunkat a projekt felhasználóinak listájáért. Ne habozzon, hagyjon [📝megjegyzést](https://github.com/openimsdk/open-im-server/issues/379), és ossza meg használati esetét.\n\n## :page_facing_up: Engedély\n\nAz OpenIM licence az Apache 2.0 licence alá tartozik. A teljes licencszövegért lásd: [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE).\n\nAz ebben az [OpenIM](https://github.com/openimsdk/open-im-server) tárolóban az [assets/logo](./assets/logo) és [assets/logo-gif](assets/logo-gif) könyvtárak alatt megjelenő OpenIM logót, beleértve annak változatait és animált változatait, szerzői jogi törvények védik.\n\n## 🔮 Köszönjük közreműködőinknek!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_ja.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ OpenIM について\n\nOpenIM は、アプリケーション内でチャット、音声通話、通知、AI チャットボットなどの通信機能を統合するために特別に設計されたサービスプラットフォームです。一連の強力な API と Webhooks を提供することで、開発者はアプリケーションに簡単にこれらの通信機能を統合できます。OpenIM 自体は独立したチャットアプリではなく、アプリケーションにサポートを提供し、豊富な通信機能を実現するプラットフォームです。以下の図は、AppServer、AppClient、OpenIMServer、OpenIMSDK 間の相互作用を示しています。\n\n![App-OpenIM Relationship](../images/oepnim-design.png)\n\n## 🚀 OpenIMSDK について\n\n**OpenIMSDK**は、**OpenIMServer**用に設計された IM SDK で、クライアントアプリケーションに組み込むためのものです。主な機能とモジュールは以下の通りです：\n\n- 🌟 主な機能：\n\n  - 📦 ローカルストレージ\n  - 🔔 リスナーコールバック\n  - 🛡️ API のラッピング\n  - 🌐 接続管理\n\n  ## 📚 主なモジュール：\n\n  1. 🚀 初初期化とログイン\n  2. 👤 ユーザー管理\n  3. 👫 友達管理\n  4. 🤖 グループ機能\n  5. 💬 会話処理\n\nGolang を使用して構築され、クロスプラットフォームの導入をサポートし、すべてのプラットフォームで一貫したアクセス体験を提供します。\n\n👉 **[GO SDK を探索する](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 OpenIMServer について\n\n- **OpenIMServer** には以下の特徴があります：\n  - 🌐 マイクロサービスアーキテクチャ：クラスターモードをサポートし、ゲートウェイ（gateway）と複数の rpc サービスを含みます。\n  - 🚀 多様なデプロイメント方法：ソースコード、kubernetes、または docker でのデプロイメントをサポートします。\n  - 海量ユーザーサポート：十万人規模の超大型グループ、千万人のユーザー、および百億のメッセージ\n\n### 強化されたビジネス機能：\n\n- **REST API**：OpenIMServer は、ビジネスシステム用の REST API を提供しており、ビジネスにさらに多くの機能を提供することを目指しています。たとえば、バックエンドインターフェースを通じてグループを作成したり、プッシュメッセージを送信したりするなどです。\n- **Webhooks**：OpenIMServer は、より多くのビジネス形態を拡張するためのコールバック機能を提供しています。コールバックとは、特定のイベントが発生する前後に、OpenIMServer がビジネスサーバーにリクエストを送信することを意味します。例えば、メッセージ送信の前後のコールバックなどです。\n\n👉 **[もっと詳しく知る](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: 全体のアーキテクチャ\n\nOpen-IM-Server の機能の核心に迫るために、アーキテクチャダイアグラムをご覧ください。\n\n![Overall Architecture](../images/architecture-layers.png)\n\n## :rocket: クイックスタート\n\niOS/Android/H5/PC/Web でのオンライン体験：\n\n👉 **[OpenIM online demo](https://www.openim.io/zh/commercial)**\n\n🤲 ユーザー体験を容易にするために、私たちは様々なデプロイメントソリューションを提供しています。以下のリストから、ご自身のデプロイメント方法を選択できます：\n\n- **[ソースコードデプロイメントガイド](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Docker デプロイメントガイド](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Kubernetes デプロイメントガイド](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Mac 開発者向けデプロイメントガイド](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: OpenIM の開発を始める\n\n[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM 私たちの目標は、トップレベルのオープンソースコミュニティを構築することです。[コミュニティリポジトリ](https://github.com/OpenIMSDK/community)には一連の基準があります。\n\nこの Open-IM-Server リポジトリに貢献したい場合は、[貢献者ドキュメントをお読みください](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)。\n\n始める前に、変更に必要があることを確認してください。最良の方法は、[新しいディスカッション](https://github.com/openimsdk/open-im-server/discussions/new/choose)や[Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)での通信を作成すること、または問題を発見した場合は、まずそれを[報告](https://github.com/openimsdk/open-im-server/issues/new/choose)することです。\n\n- [OpenIM API リファレンス](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [OpenIM Bash ロギング](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [OpenIM CI/CD アクション](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [OpenIM コード規約](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [OpenIM コミットガイドライン](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [OpenIM 開発ガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [OpenIM ディレクトリ構造](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [OpenIM 環境設定](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [OpenIM エラーコードリファレンス](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [OpenIM Git ワークフロー](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [OpenIM Git チェリーピックガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [OpenIM GitHub ワークフロー](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [OpenIM Go コード基準](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [OpenIM 画像ガイドライン](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [OpenIM 初期設定](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [OpenIM Docker インストールガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [OpenIM Linux システムインストール](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [OpenIM Linux 開発ガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [OpenIM ローカルアクションガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [OpenIM ロギング規約](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [OpenIM オフラインデプロイメント](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [OpenIM Protoc ツール](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [OpenIM テスティングガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [OpenIM ユーティリティ Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [OpenIM Makefile ユーティリティ](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [OpenIM スクリプトユーティリティ](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [OpenIM バージョニング](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [バックエンド管理とモニターデプロイメント](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [OpenIM 用 Mac 開発者デプロイメントガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: コミュニティ\n\n- 📚 [OpenIM コミュニティ](https://github.com/OpenIMSDK/community)\n- 💕 [OpenIM 興味グループ](https://github.com/Openim-sigs)\n- 🚀 [私たちの Slack コミュニティに参加する](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [私たちの WeChat（微信群）に参加する](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: コミュニティミーティング\n\n私たちは、誰もがコミュニティに参加し、コードに貢献してもらいたいと考えています。私たちは、ギフトや報酬を提供し、毎週木曜日の夜に参加していただくことを歓迎します。\n\n私たちの会議は[OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)🎯 で行われます。そこで Open-IM-Server パイプラインを検索して参加できます。\n\n私たちは[隔週の会議](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)のメモを[GitHub ディスカッション](https://github.com/openimsdk/open-im-server/discussions/categories/meeting)に記録しています。歴史的な会議のメモや会議のリプレイは[Google Docs📑](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing)で利用可能です。\n\n## :eyes: OpenIM を使用している人たち\n\nプロジェクトユーザーのリストについては、[ユーザーケーススタディ](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md)ページをご覧ください。[コメント 📝](https://github.com/openimsdk/open-im-server/issues/379)を残して、あなたの使用例を共有することを躊躇しないでください。\n\n## :page_facing_up: ライセンス\n\nOpenIM は Apache 2.0 ライセンスの下でライセンスされています。完全なライセンステキストについては、[LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE)を参照してください。\n\nこのリポジトリに表示される[OpenIM](https://github.com/openimsdk/open-im-server)ロゴ、そのバリエーション、およびアニメーションバージョン（[assets/logo](./assets/logo)および[assets/logo-gif](assets/logo-gif)ディレクトリ内）は、著作権法によって保護されています。\n\n## 🔮 貢献者の皆様に感謝します！\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_ko.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ OpenIM에 대하여\n\nOpenIM은 채팅, 오디오-비디오 통화, 알림 및 AI 챗봇을 애플리케이션에 통합하기 위해 특별히 설계된 서비스 플랫폼입니다. 이 플랫폼은 강력한 API와 웹훅을 제공하여 개발자가 이러한 상호작용 기능을 애플리케이션에 쉽게 통합할 수 있게 합니다. OpenIM은 독립 실행형 채팅 애플리케이션이 아니라, 다른 애플리케이션들이 풍부한 커뮤니케이션 기능을 달성할 수 있도록 지원하는 플랫폼으로서의 역할을 합니다. 다음 다이어그램은 AppServer, AppClient, OpenIMServer, 및 OpenIMSDK 간의 상호작용을 자세히 설명하기 위해 제시되었습니다.\n\n![App-OpenIM Relationship](../images/oepnim-design.png)\n\n## 🚀 OpenIMSDK에 대하여\n\n**OpenIMSDK**는**OpenIMServer**를 위해 특별히 제작된 IM SDK로, 클라이언트 애플리케이션 내에 내장하기 위해 설계되었습니다. 그 주요 기능 및 모듈은 다음과 같습니다:\n\n- 🌟 주요 기능:\n\n  - 📦 로컬 스토리지\n  - 🔔 리스너 콜백\n  - 🛡️ API 래핑\n  - 🌐 연결 관리\n\n  ## 📚 주요 모듈:\n\n  1. 🚀 초기화 및 로그인\n  2. 👤 사용자 관리\n  3. 👫 친구 관리\n  4. 🤖 그룹 기능\n  5. 💬 대화 처리\n\n이는 Golang을 사용하여 구축되었으며, 모든 플랫폼에서 일관된 접근 경험을 보장하는 크로스 플랫폼 배포를 지원합니다.\n\n👉 **[GO SDK 탐색하기](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 OpenIMServer에 대하여\n\n- **OpenIMServer** 는 다음과 같은 특성을 가지고 있습니다:\n  - 🌐 마이크로서비스 아키텍처: 게이트웨이 및 다수의 rpc 서비스를 포함하는 클러스터 모드를 지원합니다.\n  - 🚀 다양한 배포 방법: 소스 코드, 쿠버네티스 또는 도커를 통한 배포를 지원합니다.\n  - 대규모 사용자 기반 지원: 수십만 명의 사용자를 포함하는 초대형 그룹, 수천만 명의 사용자 및 수십억 건의 메시지를 지원합니다.\n\n### 강화된 비즈니스 기능:\n\n- **REST API**：OpenIMServer는 비즈니스 시스템을 위한 REST API를 제공하여, 백엔드 인터페이스를 통해 그룹 생성 및 푸시 메시지 전송과 같은 더 많은 기능을 비즈니스에 제공하기 위해 설계되었습니다.\n- **Webhooks**：OpenIMServer는 더 많은 비즈니스 형태를 확장할 수 있는 콜백 기능을 제공합니다. 콜백이란 메시지 전송 전후와 같은 특정 이벤트 전후에 OpenIMServer가 비즈니스 서버로 요청을 보내는 것을 의미합니다.\n\n👉 **[더 알아보기](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: 전체 아키텍처\n\nOpen-IM-Server의 기능의 핵심으로 들어가 우리의 아키텍처 다이어그램을 자세히 살펴보세요.\n\n![Overall Architecture](../images/architecture-layers.png)\n\n## :rocket: 빠른 시작\n\n우리는 많은 플랫폼을 지원합니다. 웹 측에서 빠른 체험을 위한 주소는 다음과 같습니다:\n\n👉 **[OpenIM online demo](https://www.openim.io/zh/commercial)**\n\n🤲 사용자 경험을 용이하게 하기 위해, 다양한 배포 솔루션을 제공합니다. 아래 목록에서 배포 방법을 선택할 수 있습니다:\n\n- **[소스 코드 배포 가이드](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[docker 배포 가이드](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Kubernetes 배포 가이드](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Mac 개발자 배포 가이드](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: OpenIM 개발 시작하기\n\n[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM의 목표는 최상위 수준의 오픈 소스 커뮤니티를 구축하는 것입니다. 우리는 [커뮤니티 리포지토리에서](https://github.com/OpenIMSDK/community) 일련의 표준을 가지고 있습니다.\n\n이 Open-IM-Server 리포지토리에 기여하고 싶다면, 우리의 [기여자 문서](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)를 읽어주세요.\n\n시작하기 전에, 변경 사항이 필요한지 확인해 주세요. 가장 좋은 방법은 [새로운 토론](https://github.com/openimsdk/open-im-server/discussions/new/choose)을 생성하거나 [Slack 통신을](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 하거나, 문제를 발견했다면 먼저 [보고](https://github.com/openimsdk/open-im-server/issues/new/choose)하는 것입니다.\n\n- [OpenIM API 참조](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [OpenIM Bash 로깅](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [OpenIM CI/CD 액션](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [OpenIM 코드 규칙](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [OpenIM 커밋 가이드라인](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [OpenIM 개발 가이드](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [OpenIM 디렉토리 구조](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [OpenIM 환경 설정](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [OpenIM 오류 코드 참조](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [OpenIM Git 작업 흐름](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [OpenIM Git 체리 픽 가이드](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [OpenIM GitHub 작업 흐름](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [OpenIM Go 코드 표준](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [OpenIM 이미지 가이드라인](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [OpenIM 초기 구성](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [OpenIM docker 설치 가이드](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [OpenIM OpenIM Linux 설치](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [OpenIM Linux 개발 가이드](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [OpenIM 로컬 액션 가이드](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [OpenIM 로깅 규칙](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [OpenIM 오프라인 배포](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [OpenIM Protoc 도구](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [OpenIM 테스트 가이드](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [OpenIM 유틸리티 Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [OpenIM 메이크파일 유틸리티](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [OpenIM 스크립트 유틸리티](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [OpenIM 버전 관리](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [백엔드 관리 및 모니터 배포](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [맥 개발자 배포 가이드 for OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: 커뮤니티\n\n- 📚 [OpenIM 커뮤니티](https://github.com/OpenIMSDK/community)\n- 💕 [OpenIM 관심 그룹](https://github.com/Openim-sigs)\n- 🚀 [우리의 Slack 커뮤니티에 가입하기](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [우리의 위챗(微信群)에 가입하기](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: 커뮤니티 미팅\n\n우리는 누구나 커뮤니티에 참여하고 코드를 기여할 수 있도록 하며, 선물과 보상을 제공하며, 매주 목요일 밤에 여러분을 환영합니다.\n\n우리의 회의는 [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯에서 이루어지며, Open-IM-Server 파이프라인을 검색하여 참여할 수 있습니다.\n\n우리는 격주 회의의 메모를 [GitHub 토론](https://github.com/openimsdk/open-im-server/discussions/categories/meeting)에서 기록하며, 우리의 역사적 [회의](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) 노트와 회의 재생은 [Google Docs 📑](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing)에서 이용할 수 있습니다.\n\n## :eyes: OpenIM을 사용하는 사람들\n\n프로젝트 사용자 목록을 위한 우리의 [사용자 사례 연구](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) 페이지를 확인하세요. 사용 사례를 공유하고 싶다면 주저하지 말고 [📝코멘트](https://github.com/openimsdk/open-im-server/issues/379)를 남겨주세요.\n\n## :page_facing_up: 라이선스\n\nOpenIM은 Apache 2.0 라이선스에 따라 라이선스가 부여됩니다. 전체 라이선스 텍스트는 [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE)에서 확인할 수 있습니다.\n\n이 리포지토리 [OpenIM](https://github.com/openimsdk/open-im-server)에 표시된 OpenIM 로고, 그 변형 및 애니메이션 버전은 [assets/logo](../../assets/logo) 및 [assets/logo-gif](../../assets/logo-gif) 디렉토리 아래에 있으며, 저작권 법에 의해 보호됩니다.\n\n## 🔮 우리의 기여자들에게 감사합니다!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_tr.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ OpenIM Hakkında\n\nOpenIM, uygulamalara sohbet, sesli-görüntülü aramalar, bildirimler ve AI sohbet robotları entegre etmek için özel olarak tasarlanmış bir hizmet platformudur. Güçlü API'ler ve Webhook'lar sunarak, geliştiricilerin bu etkileşimli özellikleri uygulamalarına kolayca dahil etmelerini sağlar. OpenIM bağımsız bir sohbet uygulaması değildir, ancak zengin iletişim işlevselliği sağlama amacıyla diğer uygulamaları destekleyen bir platform olarak hizmet verir. Aşağıdaki diyagram, AppServer, AppClient, OpenIMServer ve OpenIMSDK arasındaki etkileşimi detaylandırmak için açıklar.\n\n![App-OpenIM Relationship](../images/oepnim-design.png)\n\n## 🚀 OpenIMSDK Hakkında\n\n**OpenIMSDK**, müşteri uygulamalarına gömülmek üzere özel olarak oluşturulan **OpenIMServer** için tasarlanmış bir IM SDK'sıdır. Ana özellikleri ve modülleri aşağıdaki gibidir:\n\n- 🌟 Ana Özellikler:\n\n  - 📦 Yerel depolama\n  - 🔔 Dinleyici geri çağırmaları\n  - 🛡️ API sarımı\n  - 🌐 Bağlantı yönetimi\n\n  ## 📚 Ana Modüller:\n\n  1. 🚀 Başlatma ve Giriş\n  2. 👤 Kullanıcı Yönetimi\n  3. 👫 Arkadaş Yönetimi\n  4. 🤖 Grup Fonksiyonları\n  5. 💬 Konuşma Yönetimi\n\nGolang kullanılarak inşa edilmiş ve tüm platformlarda tutarlı bir erişim deneyimi sağlayacak şekilde çapraz platform dağıtımını destekler.\n\n👉 **[GO SDK Keşfet](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 OpenIMServer Hakkında\n\n- **OpenIMServer** aşağıdaki özelliklere sahiptir:\n  - 🌐 Mikroservis mimarisi: Bir kapı ve çoklu rpc servisleri içeren küme modunu destekler.\n  - 🚀 Çeşitli dağıtım yöntemleri: Kaynak kodu, Kubernetes veya Docker aracılığıyla dağıtımı destekler.\n  - Büyük kullanıcı tabanı desteği: Yüz binlerce kullanıcısı olan süper büyük gruplar, on milyonlarca kullanıcı ve milyarlarca mesaj.\n\n### Geliştirilmiş İşlevsellik:\n\n- **REST API**：OpenIMServer, işletmeleri gruplar oluşturma ve arka plan arayüzleri aracılığıyla itme mesajları gönderme gibi daha fazla işlevsellikle güçlendirmeyi amaçlayan iş sistemleri için REST API'leri sunar.\n- **Webhooks**：OpenIMServer, daha fazla iş formunu genişletme yetenekleri sağlayan geri çağırma özellikleri sunar. Geri çağırma, OpenIMServer'ın belirli bir olaydan önce veya sonra, örneğin bir mesaj göndermeden önce veya sonra iş sunucusuna bir istek göndermesi anlamına gelir.\n\n👉 **[Daha fazla bilgi edinin](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Genel Mimarisi\n\nMimari diyagramımızla Open-IM-Server'ın işlevselliğinin kalbine dalın.\n\n![Overall Architecture](../images/architecture-layers.png)\n\n## :rocket: Hızlı Başlangıç\n\nBirçok platformu destekliyoruz. Web tarafında hızlı deneyim için adresler şunlardır:\n\n👉 **[OpenIM online demo](https://www.openim.io/zh/commercial)**\n\n🤲 Kullanıcı deneyimini kolaylaştırmak için çeşitli dağıtım çözümleri sunuyoruz. Aşağıdaki listeden dağıtım yönteminizi seçebilirsiniz:\n\n- **[Kaynak Kodu Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Docker Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Kubernetes Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Mac Geliştirici Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: OpenIM Geliştirmeye Başlamak\n\n[![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM Amacımız, üst düzey bir açık kaynak topluluğu oluşturmaktır. [Topluluk deposunda](https://github.com/OpenIMSDK/community) bir dizi standartımız var.\n\nBu Open-IM-Server deposuna katkıda bulunmak istiyorsanız, lütfen katkıda bulunanlar için [dokümantasyonumuzu](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md) okuyun.\n\nBaşlamadan önce, lütfen değişikliklerinizin talep edildiğinden emin olun. Bunun için en iyisi, [yeni bir tartışma OLUŞTURMAK](https://github.com/openimsdk/open-im-server/discussions/new/choose) veya [Slack İletişimi](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) kurmak, ya da bir sorun bulursanız, önce bunu [rapor](https://github.com/openimsdk/open-im-server/issues/new/choose) etmektir.\n\n- [OpenIM API Referansı](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [OpenIM Bash Günlüğü](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [OpenIM CI/CD İşlemleri](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [OpenIM Kod Kuralları](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [OpenIM Taahhüt Kuralları](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [OpenIM Geliştirme Kılavuzu](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [OpenIM Dizin Yapısı](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [OpenIM Ortam Kurulumu](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [OpenIM Hata Kodu Referansı](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [OpenIM Git İş Akışı](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [OpenIM Git Cherry Pick Kılavuzu](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [OpenIM GitHub İş Akışı](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [OpenIM Go Kod Standartları](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [OpenIM Görüntü Kuralları](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [OpenIM İlk Yapılandırma](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [OpenIM Docker Kurulum Kılavuzu](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [OpenIM Linux Sistem Kurulumu](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [OpenIM Linux Geliştirme Kılavuzu](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [OpenIM Yerel İşlemler Kılavuzu](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [OpenIM Günlük Kuralları](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [OpenIM Çevrimdışı Dağıtım](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [OpenIM Protoc Araçları](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [OpenIM Test Kılavuzu](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [OpenIM Yardımcı Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [OpenIM Makefile Yardımcı Programları](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [OOpenIM Betik Yardımcı Programları](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [OpenIM Sürümleme](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [Arka uç yönetimi ve izleme dağıtımı](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Mac Geliştirici Dağıtım Kılavuzu for OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: Topluluk\n\n- 📚 [OpenIM Topluluğu](https://github.com/OpenIMSDK/community)\n- 💕 [OpenIM İlgi Grubu](https://github.com/Openim-sigs)\n- 🚀 [Slack topluluğumuza katılın](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [Wechat grubumuza katılın (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: Topluluk Toplantıları\n\nTopluluğumuza herkesin katılmasını ve kod katkısında bulunmasını istiyoruz, hediyeler ve ödüller sunuyoruz ve sizi her Perşembe gecesi bize katılmaya davet ediyoruz.\n\nKonferansımız [OpenIM Slack'te](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, ardından Open-IM-Server boru hattını arayıp katılabilirsiniz.\n\nİki haftada bir yapılan toplantının [notlarını](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) [GitHub tartışmalarında alıyoruz](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Tarihi toplantı notlarımız ve toplantıların tekrarları [Google Docs'ta](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) 📑 mevcut.\n\n## :eyes: Kimler OpenIM Kullanıyor\n\nProje kullanıcılarının bir listesi için [kullanıcı vaka çalışmaları](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) sayfamıza göz atın. Bir 📝[yorum](https://github.com/openimsdk/open-im-server/issues/379) bırakmaktan ve kullanım durumunuzu paylaşmaktan çekinmeyin.\n\n## :page_facing_up: Lisans\n\nOpenIM, Apache 2.0 lisansı altında lisanslanmıştır. Tam lisans metni için [LICENSE'ı](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) görün.\n\nBu depoda, [assets/logo](../../assets/logo) ve [assets/logo-gif](../../assets/logo-gif) dizinlerinde görüntülenen [OpenIM](https://github.com/openimsdk/open-im-server) logosu, çeşitleri ve animasyonlu versiyonları, telif hakkı yasaları tarafından korunmaktadır.\n\n## 🔮 Katkıda bulunanlarımıza teşekkürler!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_uk.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md\">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ Про OpenIM\n\nOpenIM — це сервісна платформа, спеціально розроблена для інтеграції чату, аудіо-відеодзвінків, сповіщень і чат-ботів штучного інтелекту в програми. Він надає ряд потужних API і веб-хуків, що дозволяє розробникам легко включати ці інтерактивні функції у свої програми. OpenIM не є окремою програмою для чату, а скоріше служить платформою для підтримки інших програм у досягненні широких можливостей спілкування. На наступній діаграмі детально показано взаємодію між AppServer, AppClient, OpenIMServer і OpenIMSDK.\n\n![App-OpenIM Relationship](../images/oepnim-design.png)\n\n## 🚀 Про OpenIMSDK\n\n**OpenIMSDK** – це пакет IM SDK, розроблений для **OpenIMServer**, створений спеціально для вбудовування в клієнтські програми. Його основні функції та модулі такі:\n\n- 🌟 Основні характеристики:\n\n  - 📦 Локальне сховище\n  - 🔔 Зворотні виклики слухача\n  - 🛡️ Обгортка API\n  - 🌐 Керування підключенням\n\n- 📚 Основні модулі:\n\n  1. 🚀 Ініціалізація та вхід\n  2. 👤 Керування користувачами\n  3. 👫 Керування друзями\n  4. 🤖 Групові функції\n  5. 💬 Ведення розмови\n\nВін створений за допомогою Golang і підтримує кросплатформне розгортання, забезпечуючи послідовний доступ на всіх платформах.\n\n👉 **[Дослідити GO SDK](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 Про OpenIMServer\n\n- **OpenIMServer** має такі характеристики:\n  - 🌐 Архітектура мікросервісу: підтримує режим кластера, включаючи шлюз і кілька служб rpc.\n  - 🚀 Різноманітні методи розгортання: підтримує розгортання через вихідний код, Kubernetes або Docker.\n  - Підтримка величезної бази користувачів: надвеликі групи із сотнями тисяч користувачів, десятками мільйонів користувачів і мільярдами повідомлень.\n\n### Розширена бізнес-функціональність:\n\n- **REST API**: OpenIMServer пропонує REST API для бізнес-систем, спрямованих на надання компаніям додаткових можливостей, таких як створення груп і надсилання push-повідомлень через серверні інтерфейси.\n- **Веб-перехоплення**: OpenIMServer надає можливості зворотного виклику, щоб розширити більше бізнес-форм. Зворотний виклик означає, що OpenIMServer надсилає запит на бізнес-сервер до або після певної події, як зворотні виклики до або після надсилання повідомлення.\n\n👉 **[Докладніше](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Загальна архітектура\n\nПориньте в серце функціональності Open-IM-Server за допомогою нашої діаграми архітектури.\n\n![Overall Architecture](../images/architecture-layers.png)\n\n## :rocket: Швидкий початок\n\nМи підтримуємо багато платформ. Ось адреси для швидкого використання веб-сайту:\n\n👉 **[Онлайн-демонстрація OpenIM](https://web-enterprise.rentsoft.cn/)**\n\n🤲 Щоб полегшити роботу користувача, ми пропонуємо різні рішення для розгортання. Ви можете вибрати спосіб розгортання зі списку нижче:\n\n- **[Посібник із розгортання вихідного коду](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Посібник із розгортання Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Посібник із розгортання Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Посібник із розгортання розробника Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: Щоб розпочати розробку OpenIM\n\n[![Відкрити в контейнері для розробників](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nOpenIM. Наша мета — побудувати спільноту з відкритим кодом найвищого рівня. У нас є набір стандартів у [репозиторії спільноти](https://github.com/OpenIMSDK/community).\n\nЯкщо ви хочете внести свій внесок у це сховище Open-IM-Server, прочитайте нашу [документацію для учасників](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md).\n\nПерш ніж почати, переконайтеся, що ваші зміни затребувані. Найкраще для цього створити [нове обговорення](https://github.com/openimsdk/open-im-server/discussions/new/choose) АБО [Нездійснене спілкування](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)або, якщо ви виявите проблему, спершу [повідомити про неї](https://github.com/openimsdk/open-im-server/issues/new/choose).\n\n- [Довідка щодо API OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [Ведення журналу OpenIM Bash](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [Дії OpenIM CI/CD](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [Положення про код OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [Інструкції щодо фіксації OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [Посібник з розробки OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [Структура каталогу OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [Налаштування середовища OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [Довідка про код помилки OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [Робочий процес OpenIM Git](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [Посібник із вибору OpenIM Git Cherry](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [Робочий процес OpenIM GitHub](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [Стандарти коду OpenIM Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [Інструкції щодо зображення OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [Початкова конфігурація OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [Посібник із встановлення OpenIM Docker](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [Встановлення системи OpenIM OpenIM Linux](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [Посібник із розробки OpenIM Linux](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [Локальний посібник із дій OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [Положення про протоколювання OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [Офлайн-розгортання OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [Інструменти OpenIM Protoc](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [Посібник з тестування OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [Утиліта OpenIM Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [Утиліти OpenIM Makefile](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [Утиліти сценарію OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [Версії OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [Керування серверною частиною та моніторинг розгортання](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Посібник із розгортання розробника Mac для OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: Спільнота\n\n- 📚 [Спільнота OpenIM](https://github.com/OpenIMSDK/community)\n- 💕 [Група інтересів OpenIM](https://github.com/Openim-sigs)\n- 🚀 [Приєднайтеся до нашої спільноти Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [Приєднайтеся до нашого wechat](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: Збори громади\n\nМи хочемо, щоб будь-хто долучився до нашої спільноти та додав код, ми пропонуємо подарунки та нагороди, і ми запрошуємо вас приєднатися до нас щочетверга ввечері.\n\nНаша конференція знаходиться в [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, тоді ви можете шукати конвеєр Open-IM-Server, щоб приєднатися.\n\nМи робимо нотатки про кожну [двотижневу зустріч](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)в [обговореннях GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting). Наші історичні нотатки зустрічей, а також повтори зустрічей доступні в[Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing).\n\n## :eyes: Хто використовує OpenIM\n\nПерегляньте нашу сторінку [тематичні дослідження користувачів](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md), щоб отримати список користувачів проекту. Не соромтеся залишити [📝коментар](https://github.com/openimsdk/open-im-server/issues/379)і поділитися своїм випадком використання.\n\n## :page_facing_up: Ліцензія\n\nOpenIM ліцензовано за ліцензією Apache 2.0. Див. [ЛІЦЕНЗІЯ](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) для повного тексту ліцензії.\n\nЛоготип OpenIM, включаючи його варіації та анімовані версії, що відображаються в цьому сховищі[OpenIM](https://github.com/openimsdk/open-im-server)у каталогах [assets/logo](./assets/logo)і [assets/logo-gif](assets/logo-gif) , захищені законами про авторське право.\n\n## 🔮 Дякуємо нашим дописувачам!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "docs/readme/README_vi.md",
    "content": "<p align=\"center\">\n    <a href=\"https://openim.io\">\n        <img src=\"../../assets/logo-gif/openim-logo.gif\" width=\"60%\" height=\"30%\"/>\n    </a>\n</p>\n\n<div align=\"center\">\n\n[![Stars](https://img.shields.io/github/stars/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=ff69b4)](https://github.com/openimsdk/open-im-server/stargazers)\n[![Forks](https://img.shields.io/github/forks/openimsdk/open-im-server?style=for-the-badge&logo=github&colorB=blue)](https://github.com/openimsdk/open-im-server/network/members)\n[![Codecov](https://img.shields.io/codecov/c/github/openimsdk/open-im-server?style=for-the-badge&logo=codecov&colorB=orange)](https://app.codecov.io/gh/openimsdk/open-im-server)\n[![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server)\n[![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3)\n[![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)\n[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n[![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045)\n[![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22)\n[![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/)\n\n<p align=\"center\">\n  <a href=\"../../README.md\">English</a> · \n  <a href=\"../../README_zh_CN.md\">中文</a> · \n  <a href=\"./README_uk.md \">Українська</a> · \n  <a href=\"./README_cs.md\">Česky</a> · \n  <a href=\"./README_hu.md\">Magyar</a> · \n  <a href=\"./README_es.md\">Español</a> · \n  <a href=\"./README_fa.md\">فارسی</a> · \n  <a href=\"./README_fr.md\">Français</a> · \n  <a href=\"./README_de.md\">Deutsch</a> · \n  <a href=\"./README_pl.md\">Polski</a> · \n  <a href=\"./README_id.md\">Indonesian</a> · \n  <a href=\"./README_fi.md\">Suomi</a> · \n  <a href=\"./README_ml.md\">മലയാളം</a> · \n  <a href=\"./README_ja.md\">日本語</a> · \n  <a href=\"./README_nl.md\">Nederlands</a> · \n  <a href=\"./README_it.md\">Italiano</a> · \n  <a href=\"./README_ru.md\">Русский</a> · \n  <a href=\"./README_pt_BR.md\">Português (Brasil)</a> · \n  <a href=\"./README_eo.md\">Esperanto</a> · \n  <a href=\"./README_ko.md\">한국어</a> · \n  <a href=\"./README_ar.md\">العربي</a> · \n  <a href=\"./README_vi.md\">Tiếng Việt</a> · \n  <a href=\"./README_da.md\">Dansk</a> · \n  <a href=\"./README_el.md\">Ελληνικά</a> · \n  <a href=\"./README_tr.md\">Türkçe</a>\n</p>\n\n</div>\n\n</p>\n\n## Ⓜ️ Về OpenIM\n\nOpenIM là một nền tảng dịch vụ được thiết kế đặc biệt cho việc tích hợp chat, cuộc gọi âm thanh-video, thông báo và chatbot AI vào các ứng dụng. Nó cung cấp một loạt các API mạnh mẽ và Webhooks, giúp các nhà phát triển dễ dàng tích hợp các tính năng tương tác này vào ứng dụng của mình. OpenIM không phải là một ứng dụng chat độc lập, mà là một nền tảng hỗ trợ các ứng dụng khác để đạt được các chức năng giao tiếp phong phú. Sơ đồ sau đây minh họa sự tương tác giữa AppServer, AppClient, OpenIMServer và OpenIMSDK để giải thích chi tiết.\n\n![App-OpenIM Relationship](../../docs/images/oepnim-design.png)\n\n## 🚀 Về OpenIMSDK\n\n**OpenIMSDK** là một SDK IM được thiết kế cho **OpenIMServer**, được tạo ra đặc biệt để nhúng vào các ứng dụng khách. Các tính năng chính và các mô-đun của nó như sau:\n\n- 🌟 Các Tính Năng Chính:\n\n  - 📦 Lưu trữ cục bộ\n  - 🔔 Gọi lại sự kiện (Listener callbacks)\n  - 🛡️ Bọc API\n  - 🌐 Quản lý kết nối\n\n- 📚 Các Mô-đun Chính:\n\n  1. 🚀 Khởi tạo và Đăng nhập\n  2. 👤 Quản lý Người dùng\n  3. 👫 Quản lý Bạn bè\n  4. 🤖 Chức năng Nhóm\n  5. 💬 Xử lý Cuộc trò chuyện\n\nNó được xây dựng bằng Golang và hỗ trợ triển khai đa nền tảng, đảm bảo trải nghiệm truy cập nhất quán trên tất cả các nền tảng\n\n👉 **[Khám phá GO SDK](https://github.com/openimsdk/openim-sdk-core)**\n\n## 🌐 Về OpenIMServer\n\n- **OpenIMServer** có những đặc điểm sau:\n  - 🌐 Kiến trúc vi dịch vụ: Hỗ trợ chế độ cluster, bao gồm một gateway và nhiều dịch vụ rpc.\n  - 🚀 Phương pháp triển khai đa dạng: Hỗ trợ triển khai qua mã nguồn, Kubernetes hoặc Docker.\n  - Hỗ trợ cho cơ sở người dùng lớn: Nhóm siêu lớn với hàng trăm nghìn người dùng, hàng chục triệu người dùng và hàng tỷ tin nhắn.\n\n### Tăng cường Chức năng Kinh doanh:\n\n- **REST API**: OpenIMServer cung cấp REST APIs cho các hệ thống kinh doanh, nhằm tăng cường khả năng cho doanh nghiệp với nhiều chức năng hơn, như tạo nhóm và gửi tin nhắn đẩy qua giao diện backend.\n- **Webhooks**: OpenIMServer cung cấp khả năng gọi lại để mở rộng thêm hình thức kinh doanh. Một gọi lại có nghĩa là OpenIMServer gửi một yêu cầu đến máy chủ kinh doanh trước hoặc sau một sự kiện nhất định, giống như gọi lại trước hoặc sau khi gửi một tin nhắn.\n\n👉 **[Learn more](https://docs.openim.io/guides/introduction/product)**\n\n## :building_construction: Kiến trúc tổng thể\n\nLàm sâu sắc vào trái tim của chức năng Open-IM-Server với sơ đồ kiến trúc của chúng tôi.\n\n![Overall Architecture](../../docs/images/architecture-layers.png)\n\n## :rocket: Bắt đầu nhanh\n\nChúng tôi hỗ trợ nhiều nền tảng. Dưới đây là các địa chỉ để trải nghiệm nhanh trên phía web：\n\n👉 **[Demo web trực tuyến OpenIM](https://web-enterprise.rentsoft.cn/)**\n\n🤲 Để tạo thuận lợi cho trải nghiệm người dùng, chúng tôi cung cấp các giải pháp triển khai đa dạng. Bạn có thể chọn phương thức triển khai từ danh sách dưới đây:\n\n- **[Hướng dẫn Triển khai Mã Nguồn](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)**\n- **[Hướng dẫn Triển khai Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)**\n- **[Hướng dẫn Triển khai Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)**\n- **[Hướng dẫn Triển khai cho Nhà Phát Triển Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)**\n\n## :hammer_and_wrench: Để Bắt Đầu Phát Triển OpenIM\n\n[![Mở trong Dev Contain](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server)\n\nMục tiêu của OpenIM là xây dựng một cộng đồng mã nguồn mở cấp cao. Chúng tôi có một bộ tiêu chuẩn, Trong [kho lưu trữ Cộng đồng](https://github.com/OpenIMSDK/community).\n\nNếu bạn muốn đóng góp cho kho lưu trữ Open-IM-Server này, vui lòng đọc [tài liệu hướng dẫn cho người đóng góp](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md).\n\nTrước khi bạn bắt đầu, hãy chắc chắn rằng các thay đổi của bạn được yêu cầu. Cách tốt nhất là tạo một [cuộc thảo luận mới](https://github.com/openimsdk/open-im-server/discussions/new/choose) hoặc [Giao tiếp Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), hoặc nếu bạn tìm thấy một vấn đề, [báo cáo nó ](https://github.com/openimsdk/open-im-server/issues/new/choose) trước.\n\n- [Tham khảo API OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md)\n- [Nhật ký Bash OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md)\n- [Hành động CI/CD OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md)\n- [Quy ước Mã OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md)\n- [Hướng dẫn Commit OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/commit.md)\n- [Hướng dẫn Phát triển OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/development.md)\n- [Cấu trúc Thư mục OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/directory.md)\n- [Cài đặt Môi trường OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/environment.md)\n- [Tham khảo Mã Lỗi OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/error-code.md)\n- [Quy trình Git OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/git-workflow.md)\n- [Hướng dẫn Cherry Pick Git OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/gitcherry-pick.md)\n- [Quy trình GitHub OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/github-workflow.md)\n- [Tiêu chuẩn Mã Go OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/go-code.md)\n- [Hướng dẫn Hình ảnh OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/images.md)\n- [Cấu hình Ban đầu OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/init-config.md)\n- [Hướng dẫn Cài đặt Docker OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-docker.md)\n- [Hướng dẫn Cài đặt Hệ thống Linux OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/install-openim-linux-system.md)\n- [Hướng dẫn Phát triển Linux OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/linux-development.md)\n- [Hướng dẫn Hành động Địa phương OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/local-actions.md)\n- [Quy ước Nhật ký OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/logging.md)\n- [Triển khai Ngoại tuyến OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md)\n- [Công cụ Protoc OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md)\n- [Hướng dẫn Kiểm thử OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md)\n- [Utility Go OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md)\n- [Tiện ích Makefile OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md)\n- [Tiện ích Kịch bản OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md)\n- [Quản lý Phiên bản OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md)\n- [Quản lý triển khai và giám sát backend](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md)\n- [Hướng dẫn Triển khai cho Nhà Phát triển Mac OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md)\n\n## :busts_in_silhouette: Cộng đồng\n\n- 📚 [Cộng đồng OpenIM](https://github.com/OpenIMSDK/community)\n- 💕 [Nhóm Quan tâm OpenIM](https://github.com/Openim-sigs)\n- 🚀 [Tham gia cộng đồng Slack của chúng tôi](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)\n- :eyes: [Tham gia nhóm WeChat của chúng tôi (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg)\n\n## :calendar: Cuộc họp Cộng đồng\n\nChúng tôi muốn bất kỳ ai cũng có thể tham gia cộng đồng và đóng góp mã nguồn, chúng tôi cung cấp quà tặng và phần thưởng, và chúng tôi chào đón bạn tham gia cùng chúng tôi mỗi tối thứ Năm.\n\nHội nghị của chúng tôi được tổ chức trên Slack của [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, sau đó bạn có thể tìm kiếm pipeline Open-IM-Server để tham gia\n\nChúng tôi ghi chú mỗi [cuộc họp hai tuần một lần](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) trong [các cuộc thảo luận GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), ghi chú cuộc họp lịch sử của chúng tôi cũng như các bản ghi lại của cuộc họp có sẵn tại [Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing).\n\n## :eyes: Ai Đang Sử Dụng OpenIM\n\nXem trangr [các nghiên cứu trường hợp người dùng](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) của chúng tôi để biết danh sách các người dùng dự án. Đừng ngần ngại để lại [📝bình luận](https://github.com/openimsdk/open-im-server/issues/379) và chia sẻ trường hợp sử dụng của bạn.\n\n## :page_facing_up: Giấy phép\n\nOpenIM được cấp phép theo giấy phép Apache 2.0. Xem [GIẤY PHÉP](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) để biết toàn bộ nội dung giấy phép.\n\nLogo OpenIM, bao gồm các biến thể và phiên bản hoạt hình, được hiển thị trong kho lưu trữ này [OpenIM](https://github.com/openimsdk/open-im-server) dưới các thư mục [assets/logo](../../assets/logo) và [assets/logo-gif](assets/logo-gif) được bảo vệ bởi luật bản quyền.\n\n## 🔮 Cảm ơn các đóng góp của bạn!\n\n<a href=\"https://github.com/openimsdk/open-im-server/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=openimsdk/open-im-server\" />\n</a>\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/openimsdk/open-im-server/v3\n\ngo 1.25.0\n\nrequire (\n\tfirebase.google.com/go/v4 v4.14.1\n\tgithub.com/dtm-labs/rockscache v0.1.1\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgithub.com/go-playground/validator/v10 v10.20.0\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.1\n\tgithub.com/gorilla/websocket v1.5.1\n\tgithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0\n\tgithub.com/mitchellh/mapstructure v1.5.0\n\tgithub.com/openimsdk/protocol v0.0.73-alpha.19\n\tgithub.com/openimsdk/tools v0.0.50-alpha.113\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/prometheus/client_golang v1.18.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgo.mongodb.org/mongo-driver v1.14.0\n\tgoogle.golang.org/api v0.170.0\n\tgoogle.golang.org/grpc v1.71.0\n\tgoogle.golang.org/protobuf v1.36.4\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire github.com/google/uuid v1.6.0\n\nrequire (\n\tgithub.com/IBM/sarama v1.43.0\n\tgithub.com/fatih/color v1.14.1\n\tgithub.com/gin-contrib/gzip v1.0.1\n\tgithub.com/go-redis/redis v6.15.9+incompatible\n\tgithub.com/go-redis/redismock/v9 v9.2.0\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7\n\tgithub.com/kelindar/bitmap v1.5.2\n\tgithub.com/likexian/gokit v0.25.13\n\tgithub.com/openimsdk/gomake v0.0.17\n\tgithub.com/redis/go-redis/v9 v9.4.0\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/shirou/gopsutil v3.21.11+incompatible\n\tgithub.com/spf13/viper v1.18.2\n\tgo.etcd.io/etcd/client/v3 v3.5.13\n\tgo.uber.org/automaxprocs v1.5.3\n\tgolang.org/x/sync v0.10.0\n\tk8s.io/api v0.31.2\n\tk8s.io/apimachinery v0.31.2\n\tk8s.io/client-go v0.31.2\n)\n\nrequire (\n\tcloud.google.com/go v0.112.1 // indirect\n\tcloud.google.com/go/compute/metadata v0.6.0 // indirect\n\tcloud.google.com/go/firestore v1.15.0 // indirect\n\tcloud.google.com/go/iam v1.1.7 // indirect\n\tcloud.google.com/go/longrunning v0.5.5 // indirect\n\tcloud.google.com/go/storage v1.40.0 // indirect\n\tgithub.com/MicahParks/keyfunc v1.9.0 // indirect\n\tgithub.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.32.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/config v1.28.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.43.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect\n\tgithub.com/aws/smithy-go v1.22.1 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bmatcuk/doublestar/v4 v4.10.0 // indirect\n\tgithub.com/bytedance/sonic v1.11.6 // indirect\n\tgithub.com/bytedance/sonic/loader v0.1.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/clbanning/mxj v1.8.4 // indirect\n\tgithub.com/cloudwego/base64x v0.1.4 // indirect\n\tgithub.com/cloudwego/iasm v0.2.0 // indirect\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/eapache/go-resiliency v1.6.0 // indirect\n\tgithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.11.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.7.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.3 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-logr/logr v1.4.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.19.6 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.2 // indirect\n\tgithub.com/go-openapi/swag v0.22.4 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-zookeeper/zk v1.0.3 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/golang/snappy v0.0.4 // indirect\n\tgithub.com/google/gnostic-models v0.6.8 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/gofuzz v1.2.0 // indirect\n\tgithub.com/google/s2a-go v0.1.7 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.12.3 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/jinzhu/copier v0.4.0 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/kelindar/simd v1.1.2 // indirect\n\tgithub.com/klauspost/compress v1.17.7 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.7 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/lestrrat-go/strftime v1.0.6 // indirect\n\tgithub.com/lithammer/shortuuid v3.0.0+incompatible // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect\n\tgithub.com/magefile/mage v1.15.0 // indirect\n\tgithub.com/magiconair/properties v1.8.7 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect\n\tgithub.com/minio/md5-simd v1.1.2 // indirect\n\tgithub.com/minio/minio-go/v7 v7.0.69 // indirect\n\tgithub.com/minio/sha256-simd v1.0.1 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect\n\tgithub.com/mozillazg/go-httpheader v0.4.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.2 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.21 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/prometheus/client_model v0.5.0 // indirect\n\tgithub.com/prometheus/common v0.45.0 // indirect\n\tgithub.com/prometheus/procfs v0.12.0 // indirect\n\tgithub.com/qiniu/go-sdk/v7 v7.18.2 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect\n\tgithub.com/rs/xid v1.5.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.4.0 // indirect\n\tgithub.com/sagikazarmark/slog-shim v0.1.0 // indirect\n\tgithub.com/sercand/kuberesolver/v6 v6.0.1 // indirect\n\tgithub.com/shirou/gopsutil/v3 v3.24.5 // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.2 // indirect\n\tgithub.com/shoenig/go-m1cpu v0.1.6 // indirect\n\tgithub.com/sourcegraph/conc v0.3.0 // indirect\n\tgithub.com/spf13/afero v1.11.0 // indirect\n\tgithub.com/spf13/cast v1.6.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tencentyun/cos-go-sdk-v5 v0.7.47 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/xdg-go/pbkdf2 v1.0.0 // indirect\n\tgithub.com/xdg-go/scram v1.1.2 // indirect\n\tgithub.com/xdg-go/stringprep v1.0.4 // indirect\n\tgithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.13 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect\n\tgo.opentelemetry.io/otel v1.34.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.34.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.34.0 // indirect\n\tgo.uber.org/atomic v1.9.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgolang.org/x/arch v0.7.0 // indirect\n\tgolang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect\n\tgolang.org/x/image v0.15.0 // indirect\n\tgolang.org/x/net v0.34.0 // indirect\n\tgolang.org/x/oauth2 v0.25.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/term v0.28.0 // indirect\n\tgolang.org/x/text v0.21.0 // indirect\n\tgolang.org/x/time v0.5.0 // indirect\n\tgoogle.golang.org/appengine/v2 v2.0.2 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgorm.io/gorm v1.25.8 // indirect\n\tk8s.io/klog/v2 v2.130.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect\n\tk8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect\n\tsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect\n\tsigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect\n\tsigs.k8s.io/yaml v1.4.0 // indirect\n)\n\nrequire (\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/spf13/cobra v1.8.0\n\tgithub.com/ugorji/go/codec v1.2.12 // indirect\n\tgo.uber.org/zap v1.24.0 // indirect\n\tgolang.org/x/crypto v0.32.0 // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=\ncloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=\ncloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=\ncloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=\ncloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=\ncloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=\ncloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=\ncloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=\ncloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=\ncloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=\ncloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw=\ncloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=\nfirebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=\nfirebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/IBM/sarama v1.43.0 h1:YFFDn8mMI2QL0wOrG0J2sFoVIAFl7hS9JQi2YZsXtJc=\ngithub.com/IBM/sarama v1.43.0/go.mod h1:zlE6HEbC/SMQ9mhEYaF7nNLYOUyrs0obySKCckWP9BM=\ngithub.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=\ngithub.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=\ngithub.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=\ngithub.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=\ngithub.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=\ngithub.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo=\ngithub.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 h1:ZY3108YtBNq96jNZTICHxN1gSBSbnvIdYwwqnvCV4Mc=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ=\ngithub.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=\ngithub.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4 h1:40Q4X5ebZruRtknEZH/bg91sT5pR853F7/1X9QRbI54=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4/go.mod h1:u77N7eEECzUv7F0xl2gcfK/vzc8wcjWobpy+DcrLJ5E=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4 h1:6DRKQc+9cChgzL5gplRGusI5dBGeiEod4m/pmGbcX48=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4/go.mod h1:s8ORvrW4g4v7IvYKIAoBg17w3GQ+XuwXDXYrQ5SkzU0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4 h1:o3DcfCxGDIT20pTbVKVhp3vWXOj/VvgazNJvumWeYW0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4/go.mod h1:Uy0KVOxuTK2ne+/PKQ+VvEeWmjMMksE17k/2RK/r5oM=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.43.1 h1:1w11lfXOa8HoHoSlNtt4mqv/N3HmDOa+OnUH3Y9DHm8=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.43.1/go.mod h1:dqJ5JBL0clzgHriH35Amx3LRFY6wNIPUX7QO/BerSBo=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg=\ngithub.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=\ngithub.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=\ngithub.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=\ngithub.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=\ngithub.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=\ngithub.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=\ngithub.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=\ngithub.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=\ngithub.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=\ngithub.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\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/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=\ngithub.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=\ngithub.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=\ngithub.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=\ngithub.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/dtm-labs/rockscache v0.1.1 h1:6S1vgaHvGqrLd8Ka4hRTKeKPV7v+tT0MSkTIX81LRyA=\ngithub.com/dtm-labs/rockscache v0.1.1/go.mod h1:c76WX0kyIibmQ2ACxUXvDvaLykoPakivMqIxt+UzE7A=\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/eapache/go-resiliency v1.6.0 h1:CqGDTLtpwuWKn6Nj3uNUdflaq+/kIPsg0gfNzHton30=\ngithub.com/eapache/go-resiliency v1.6.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=\ngithub.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=\ngithub.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=\ngithub.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=\ngithub.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=\ngithub.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=\ngithub.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=\ngithub.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=\ngithub.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=\ngithub.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=\ngithub.com/go-logr/logr v1.4.2/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/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=\ngithub.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=\ngithub.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=\ngithub.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=\ngithub.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=\ngithub.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=\ngithub.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=\ngithub.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=\ngithub.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=\ngithub.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=\ngithub.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg=\ngithub.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=\ngithub.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=\ngithub.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\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/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=\ngithub.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=\ngithub.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=\ngithub.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=\ngithub.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=\ngithub.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=\ngithub.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=\ngithub.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=\ngithub.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/kelindar/bitmap v1.5.2 h1:XwX7CTvJtetQZ64zrOkApoZZHBJRkjE23NfqUALA/HE=\ngithub.com/kelindar/bitmap v1.5.2/go.mod h1:j3qZjxH9s4OtvsnFTP2bmPkjqil9Y2xQlxPYHexasEA=\ngithub.com/kelindar/simd v1.1.2 h1:KduKb+M9cMY2HIH8S/cdJyD+5n5EGgq+Aeeleos55To=\ngithub.com/kelindar/simd v1.1.2/go.mod h1:inq4DFudC7W8L5fhxoeZflLRNpWSs0GNx6MlWFvuvr0=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=\ngithub.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=\ngithub.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=\ngithub.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=\ngithub.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=\ngithub.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=\ngithub.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=\ngithub.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=\ngithub.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=\ngithub.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVkM=\ngithub.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4=\ngithub.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w=\ngithub.com/lithammer/shortuuid v3.0.0+incompatible/go.mod h1:FR74pbAuElzOUuenUHTK2Tciko1/vKuIKS9dSkDrA4w=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=\ngithub.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\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/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=\ngithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=\ngithub.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=\ngithub.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=\ngithub.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=\ngithub.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=\ngithub.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=\ngithub.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=\ngithub.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=\ngithub.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiYgOq6hK4w=\ngithub.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=\ngithub.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=\ngithub.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=\ngithub.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=\ngithub.com/openimsdk/gomake v0.0.17 h1:q8haP48VOH45WhJRiLj1YSBJyUFJqD8CTedH65i1YH8=\ngithub.com/openimsdk/gomake v0.0.17/go.mod h1:nnjS8yCtrPJAt1knMbyPiUwCH2gpyBzj/EZAONfUOXg=\ngithub.com/openimsdk/protocol v0.0.73-alpha.19 h1:CvXoDF2U73UcMhLnrtMFks2Aw+bXiDgH8AITEt783/s=\ngithub.com/openimsdk/protocol v0.0.73-alpha.19/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw=\ngithub.com/openimsdk/tools v0.0.50-alpha.113 h1:rhLWaSJuhjgJFNVzmpChLCG7dPXS0+bte+CPI0008Us=\ngithub.com/openimsdk/tools v0.0.50-alpha.113/go.mod h1:x9i/e+WJFW4tocy6RNJQ9NofQiP3KJ1Y576/06TqOG4=\ngithub.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=\ngithub.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=\ngithub.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=\ngithub.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=\ngithub.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=\ngithub.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=\ngithub.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=\ngithub.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=\ngithub.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=\ngithub.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=\ngithub.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=\ngithub.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=\ngithub.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=\ngithub.com/qiniu/go-sdk/v7 v7.18.2 h1:vk9eo5OO7aqgAOPF0Ytik/gt7CMKuNgzC/IPkhda6rk=\ngithub.com/qiniu/go-sdk/v7 v7.18.2/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=\ngithub.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=\ngithub.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=\ngithub.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=\ngithub.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=\ngithub.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=\ngithub.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=\ngithub.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=\ngithub.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=\ngithub.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=\ngithub.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=\ngithub.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=\ngithub.com/sercand/kuberesolver/v6 v6.0.1 h1:XZUTA0gy/lgDYp/UhEwv7Js24F1j8NJ833QrWv0Xux4=\ngithub.com/sercand/kuberesolver/v6 v6.0.1/go.mod h1:C0tsTuRMONSY+Xf7pv7RMW1/JlewY1+wS8SZE+1lf1s=\ngithub.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=\ngithub.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=\ngithub.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=\ngithub.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=\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/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=\ngithub.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=\ngithub.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=\ngithub.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=\ngithub.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=\ngithub.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=\ngithub.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=\ngithub.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\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/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=\ngithub.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=\ngithub.com/tencentyun/cos-go-sdk-v5 v0.7.47 h1:uoS4Sob16qEYoapkqJq1D1Vnsy9ira9BfNUMtoFYTI4=\ngithub.com/tencentyun/cos-go-sdk-v5 v0.7.47/go.mod h1:DH9US8nB+AJXqwu/AMOrCFN1COv3dpytXuJWHgdg7kE=\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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=\ngithub.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=\ngithub.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=\ngithub.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=\ngithub.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4=\ngo.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8=\ngo.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js=\ngo.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI=\ngo.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=\ngo.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=\ngo.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=\ngo.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=\ngo.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=\ngo.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=\ngo.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=\ngo.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=\ngo.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=\ngo.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=\ngo.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=\ngo.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=\ngo.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=\ngo.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=\ngo.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=\ngolang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=\ngolang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=\ngolang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=\ngolang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=\ngolang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=\ngolang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=\ngolang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=\ngolang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=\ngolang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=\ngolang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=\ngolang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=\ngolang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=\ngolang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=\ngoogle.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=\ngoogle.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=\ngoogle.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=\ngoogle.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=\ngoogle.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=\ngoogle.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=\ngorm.io/gorm v1.25.8 h1:WAGEZ/aEcznN4D03laj8DKnehe1e9gYQAjW8xyPRdeo=\ngorm.io/gorm v1.25.8/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nk8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=\nk8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=\nk8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=\nk8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=\nk8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=\nk8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=\nk8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=\nk8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=\nk8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=\nk8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=\nk8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=\nk8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nnullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=\nsigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=\nsigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=\nsigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=\nsigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=\nsigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=\n"
  },
  {
    "path": "install.sh",
    "content": "#!/usr/bin/env bash\n\n# Copyright © 2023 OpenIM. All rights reserved.\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# https://gist.github.com/cubxxw/28f997f2c9aff408630b072f010c1d64\n#\n\nset -e\n\n\n############################## OpenIM Github ##############################\n# ... rest of the script ...\n\n# TODO\n# You can configure this script in three ways. \n# 1. First, set the variables in this column with more comments. \n# 2. The second is to pass an environment variable via a flag such as --help. \n# 3. The third way is to set the variable externally, or pass it in as an environment variable\n\n# Default configuration for OpenIM Repo\n# The OpenIM Repo settings can be customized according to your needs.\n\n# OpenIM Repo owner, by default it's set to \"OpenIMSDK\". If you're using a different owner, replace accordingly.\nOWNER=\"OpenIMSDK\" \n\n# The repository name, by default it's \"Open-IM-Server\". If you're using a different repository, replace accordingly.\nREPO=\"Open-IM-Server\" \n\n# Version of Go you want to use, make sure it is compatible with your OpenIM-Server requirements.\n# Default is 1.18, if you want to use a different version, replace accordingly.\nGO_VERSION=\"1.20\"\n\n# Default HTTP_PORT is 80. If you want to use a different port, uncomment and replace the value.\n# HTTP_PORT=80\n\n# CPU core number for concurrent execution. By default it's determined automatically.\n# Uncomment the next line if you want to set it manually.\n# CPU=$(grep -c ^processor /proc/cpuinfo)\n\n# By default, the script uses the latest tag from OpenIM-Server releases.\n# If you want to use a specific tag, uncomment and replace \"v3.0.0\" with the desired tag.\n# LATEST_TAG=v3.0.0\n\n# Default OpenIM install directory is /tmp. If you want to use a different directory, uncomment and replace \"/test\".\n# DOWNLOAD_OPENIM_DIR=\"/test\"\n\n# GitHub proxy settings. If you are using a proxy, uncomment and replace the empty field with your proxy URL.\nPROXY=\n\n# If you have a GitHub token, replace the empty field with your token.\nGITHUB_TOKEN=\n\n# Default user is \"root\". If you need to modify it, uncomment and replace accordingly.\n# OPENIM_USER=root \n\n# Default password for redis, mysql, mongo, as well as accessSecret in config/config.yaml.\n# Remember, it should be a combination of 8 or more numbers and letters. If you want to set a different password, uncomment and replace \"openIM123\".\n# PASSWORD=openIM123\n\n# Default endpoint for minio's external service IP and port. If you want to use a different endpoint, uncomment and replace.\n# ENDPOINT=http://127.0.0.1:10005 \n\n# Default API_URL, replace if necessary. \n# API_URL=http://127.0.0.1:10002/object/\n\n# Default data directory. If you want to specify a different directory, uncomment and replace \"./\".\n# DATA_DIR=./\n\n############################## OpenIM Functions ##############################\n# Install horizon of the script\n#\n# Pre-requisites:\n#   - git\n#   - make\n#   - jq\n#   - docker\n#   - docker-compose\n#   - go\n#\n\n# Check if the script is run as root\nfunction check_isroot() {\n    if [ \"$EUID\" -ne 0 ]; then\n    fatal \"Please run the script as root or use sudo.\"\n    fi\n}\n\n# check if the current directory is a OpenIM git repository\nfunction check_git_repo() {\n    if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then\n        # Inside a git repository\n        for remote in $(git remote); do\n            repo_url=$(git remote get-url $remote)\n            if [[ $repo_url == \"https://github.com/openimsdk/open-im-server.git\" || \\\n                  $repo_url == \"https://github.com/openimsdk/open-im-server\" || \\\n                  $repo_url == \"git@github.com:openimsdk/open-im-server.git\" ]]; then\n                # If it's OpenIMSDK repository\n                info \"Current directory is OpenIMSDK git repository.\"\n                info \"Executing installation directly.\"\n                install_openim\n                exit 0\n            fi\n            debug \"Remote: $remote, URL: $repo_url\"\n        done\n        # If it's not OpenIMSDK repository\n        debug \"Current directory is not OpenIMSDK git repository.\"\n    fi\n    info \"Current directory is not a git repository.\"\n}\n\n# Function to update and install necessary tools\nfunction install_tools() {\n    info \"Checking and installing necessary tools, about git, make, jq, docker, docker-compose.\"\n    local tools=(\"git\" \"make\" \"jq\" \"docker\" \"docker-compose\")\n    local install_cmd update_cmd os\n\n    if grep -qEi \"debian|buntu|mint\" /etc/os-release; then\n        os=\"Ubuntu\"\n        install_cmd=\"sudo apt install -y\"\n        update_cmd=\"sudo apt update\"\n    elif grep -qEi \"fedora|rhel\" /etc/os-release; then\n        os=\"CentOS\"\n        install_cmd=\"sudo yum install -y\"\n        update_cmd=\"sudo yum update\"\n    else\n        fatal \"Unsupported OS, please use Ubuntu or CentOS.\"\n    fi\n\n    debug \"Detected OS: $os\"\n    info \"Updating system package repositories...\"\n    $update_cmd\n\n    for tool in \"${tools[@]}\"; do\n        if ! command -v $tool &> /dev/null; then\n            warn \"$tool is not installed. Installing now...\"\n            $install_cmd $tool\n            success \"$tool has been installed successfully.\"\n        else\n            info \"$tool is already installed.\"\n        fi\n    done\n}\n\n# Function to check if Docker and Docker Compose are installed\nfunction check_docker() {\n    if ! command -v docker &> /dev/null; then\n        fatal \"Docker is not installed. Please install Docker first.\"\n    fi\n    if ! command -v docker-compose &> /dev/null; then\n        fatal \"Docker Compose is not installed. Please install Docker Compose first.\"\n    fi\n}\n\n# Function to download and install Go if it's not already installed\nfunction install_go() {\n    command -v go >/dev/null 2>&1\n    # Determines if GO_VERSION is defined\n    if [ -z \"$GO_VERSION\" ]; then\n        GO_VERSION=\"1.20\"\n    fi\n\n    if [[ $? -ne 0 ]]; then\n        warn \"Go is not installed. Installing now...\"\n        curl -LO \"https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz\"\n        if [ $? -ne 0 ]; then\n            fatal \"Download failed! Please check your network connectivity.\"\n        fi\n        sudo tar -C /usr/local -xzf \"go${GO_VERSION}.linux-amd64.tar.gz\"\n        echo \"export PATH=$PATH:/usr/local/go/bin\" >> ~/.bashrc\n        source ~/.bashrc\n        success \"Go has been installed successfully.\"\n    else\n        info \"Go is already installed.\"\n    fi\n}\n\nfunction download_source_code() {\n\n    # If LATEST_TAG was not defined outside the function, get it here example: v3.0.1-beta.1\n    if [ -z \"$LATEST_TAG\" ]; then\n        LATEST_TAG=$(curl -s \"https://api.github.com/repos/$OWNER/$REPO/tags\" | jq -r '.[0].name')\n    fi\n\n    # If LATEST_TAG is still empty, set a default value\n    local DEFAULT_TAG=\"v3.0.0\"\n\n    LATEST_TAG=\"${LATEST_TAG:-$DEFAULT_TAG}\"\n\n    debug \"DEFAULT_TAG: $DEFAULT_TAG\"\n    info \"Use OpenIM Version LATEST_TAG: $LATEST_TAG\"\n\n    # If MODIFIED_TAG was not defined outside the function, modify it here,example: 3.0.1-beta.1\n    if [ -z \"$MODIFIED_TAG\" ]; then\n        MODIFIED_TAG=$(echo $LATEST_TAG | sed 's/v//')\n    fi\n\n    # If MODIFIED_TAG is still empty, set a default value\n    local DEFAULT_MODIFIED_TAG=\"${DEFAULT_TAG#v}\" \n    MODIFIED_TAG=\"${MODIFIED_TAG:-$DEFAULT_MODIFIED_TAG}\"\n    \n    debug \"MODIFIED_TAG: $MODIFIED_TAG\"\n\n    # Construct the tarball URL\n    TARBALL_URL=\"${PROXY}https://github.com/$OWNER/$REPO/archive/refs/tags/$LATEST_TAG.tar.gz\"\n\n    info \"Downloaded OpenIM TARBALL_URL: $TARBALL_URL\"\n\n    info \"Starting the OpenIM automated one-click deployment script.\"\n\n    # Set the download and extract directory to /tmp\n    if [ -z \"$DOWNLOAD_OPENIM_DIR\" ]; then\n        DOWNLOAD_OPENIM_DIR=\"/tmp\"\n    fi\n\n    # Check if /tmp directory exists\n    if [ ! -d \"$DOWNLOAD_OPENIM_DIR\" ]; then\n        warn \"$DOWNLOAD_OPENIM_DIR does not exist. Creating it...\"\n        mkdir -p \"$DOWNLOAD_OPENIM_DIR\"\n    fi\n\n    info \"Downloading OpenIM source code from $TARBALL_URL to $DOWNLOAD_OPENIM_DIR\"\n    \n    curl -L -o \"${DOWNLOAD_OPENIM_DIR}/${MODIFIED_TAG}.tar.gz\" $TARBALL_URL\n\n    tar -xzvf \"${DOWNLOAD_OPENIM_DIR}/${MODIFIED_TAG}.tar.gz\" -C \"$DOWNLOAD_OPENIM_DIR\"\n    cd \"$DOWNLOAD_OPENIM_DIR/$REPO-$MODIFIED_TAG\"\n    git init && git add . && git commit -m \"init\"  --no-verify\n\n    success \"Source code downloaded and extracted to $REPO-$MODIFIED_TAG\"\n}\n\nfunction set_openim_env() {\n    warn \"This command can only be executed once. It will modify the component passwords in docker-compose based on the PASSWORD variable in .env, and modify the component passwords in config/config.yaml. If the password in .env changes, you need to first execute docker-compose down; rm components -rf and then execute this command.\"\n    # Set default values for user input\n    # If the OPENIM_USER environment variable is not set, it defaults to 'root'\n    if [ -z \"$OPENIM_USER\" ]; then\n        OPENIM_USER=\"root\"\n        debug \"OPENIM_USER is not set. Defaulting to 'root'.\"\n    fi\n\n    # If the PASSWORD environment variable is not set, it defaults to 'openIM123'\n    # This password applies to redis, mysql, mongo, as well as accessSecret in config/config.yaml\n    if [ -z \"$PASSWORD\" ]; then\n        PASSWORD=\"openIM123\"\n        debug \"PASSWORD is not set. Defaulting to 'openIM123'.\"\n    fi\n\n    # If the ENDPOINT environment variable is not set, it defaults to 'http://127.0.0.1:10005'\n    # This is minio's external service IP and port, or it could be a domain like storage.xx.xx\n    # The app must be able to access this IP and port or domain\n    if [ -z \"$ENDPOINT\" ]; then\n        ENDPOINT=\"http://127.0.0.1:10005\"\n        debug \"ENDPOINT is not set. Defaulting to 'http://127.0.0.1:10005'.\"\n    fi\n\n    # If the API_URL environment variable is not set, it defaults to 'http://127.0.0.1:10002/object/'\n    # The app must be able to access this IP and port or domain\n    if [ -z \"$API_URL\" ]; then\n        API_URL=\"http://127.0.0.1:10002/object/\"\n        debug \"API_URL is not set. Defaulting to 'http://127.0.0.1:10002/object/'.\"\n    fi\n\n    # If the DATA_DIR environment variable is not set, it defaults to the current directory './'\n    # This can be set to a directory with large disk space\n    if [ -z \"$DATA_DIR\" ]; then\n        DATA_DIR=\"./\"\n        debug \"DATA_DIR is not set. Defaulting to './'.\"\n    fi\n}\n\nfunction install_openim() {\n    info \"Installing OpenIM\"\n    make -j${CPU} install V=1\n\n    info \"Checking installation\"\n    make check\n\n    success \"OpenIM installation completed successfully. Happy chatting!\"\n}\n\n############################## OpenIM Help ##############################\n\n# Function to display help message\nfunction cmd_help() {\n    openim_color\n    color_echo ${BRIGHT_GREEN_PREFIX} \"Usage: $0 [options]\"\n    color_echo ${BRIGHT_GREEN_PREFIX} \"Options:\"\n    echo\n    color_echo ${BLUE_PREFIX} \"-i,  --install       ${CYAN_PREFIX}Execute the installation logic of the script${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-u,  --user          ${CYAN_PREFIX}set user (default: root)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-p,  --password      ${CYAN_PREFIX}set password (default: openIM123)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-e,  --endpoint      ${CYAN_PREFIX}set endpoint (default: http://127.0.0.1:10005)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-a,  --api           ${CYAN_PREFIX}set API URL (default: http://127.0.0.1:10002/object/)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-d,  --directory     ${CYAN_PREFIX}set directory for large disk space (default: ./)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-h,  --help          ${CYAN_PREFIX}display this help message and exit${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-cn, --china         ${CYAN_PREFIX}set to use the Chinese domestic proxy${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-t,  --tag           ${CYAN_PREFIX}specify the tag (default option, set to latest if not specified)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-r,  --release       ${CYAN_PREFIX}specify the release branch (cannot be used with the tag option)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-gt, --github-token  ${CYAN_PREFIX}set the GITHUB_TOKEN (default: not set)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"-g,  --go-version    ${CYAN_PREFIX}set the Go language version (default: GO_VERSION=\\\"1.20\\\")${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"--install-dir        ${CYAN_PREFIX}set the OpenIM installation directory (default: /tmp)${COLOR_SUFFIX}\"\n    color_echo ${BLUE_PREFIX} \"--cpu                ${CYAN_PREFIX}set the number of concurrent processes${COLOR_SUFFIX}\"\n    echo\n    color_echo ${RED_PREFIX} \"Note: Only one of the -t/--tag or -r/--release options can be used at a time.${COLOR_SUFFIX}\"\n    color_echo ${RED_PREFIX} \"If both are used or none of them are used, the -t/--tag option will be prioritized.${COLOR_SUFFIX}\"\n    echo\n    exit 1\n}\n\nfunction parseinput() {\n    # set default values\n    # OPENIM_USER=root\n    # PASSWORD=openIM123\n    # ENDPOINT=http://127.0.0.1:10005\n    # API=http://127.0.0.1:10002/object/\n    # DIRECTORY=./\n    # CHINA=false\n    # TAG=latest\n    # RELEASE=\"\"\n    # GO_VERSION=1.20\n    # INSTALL_DIR=/tmp\n    # GITHUB_TOKEN=\"\"\n    # CPU=$(nproc)\n\n    if [ $# -eq 0 ]; then\n        cmd_help\n        exit 1\n    fi\n\n    while [ $# -gt 0 ]; do\n        case $1 in\n            -h|--help)\n                cmd_help\n                exit\n                ;;\n            -u|--user)\n                shift\n                OPENIM_USER=$1\n                ;;\n            -p|--password)\n                shift\n                PASSWORD=$1\n                ;;\n            -e|--endpoint)\n                shift\n                ENDPOINT=$1\n                ;;\n            -a|--api)\n                shift\n                API=$1\n                ;;\n            -d|--directory)\n                shift\n                DIRECTORY=$1\n                ;;\n            -cn|--china)\n                CHINA=true\n                ;;\n            -t|--tag)\n                shift\n                TAG=$1\n                ;;\n            -r|--release)\n                shift\n                RELEASE=$1\n                ;;\n            -g|--go-version)\n                shift\n                GO_VERSION=$1\n                ;;\n            --install-dir)\n                shift\n                INSTALL_DIR=$1\n                ;;\n            -gt|--github-token)\n                shift\n                GITHUB_TOKEN=$1\n                ;;\n            --cpu)\n                shift\n                CPU=$1\n                ;;\n            -i|--install)\n                openim_main\n                exit\n                ;;\n            *)\n                echo \"Unknown option: $1\"\n                cmd_help\n                exit 1\n                ;;\n        esac\n        shift\n    done\n}\n\n############################## OpenIM LOG ##############################\n# Set text color to cyan for header and URL\nprint_with_delay() {\n  text=\"$1\"\n  delay=\"$2\"\n\n  for i in $(seq 0 $((${#text}-1))); do\n    printf \"${text:$i:1}\"\n    sleep $delay\n  done\n  printf \"\\n\"\n}\n\nprint_progress() {\n  total=\"$1\"\n  delay=\"$2\"\n\n  printf \"[\"\n  for i in $(seq 1 $total); do\n    printf \"#\"\n    sleep $delay\n  done\n  printf \"]\\n\"\n}\n\n# Function for colored echo\ncolor_echo() {\n    COLOR=$1\n    shift\n    echo -e \"${COLOR} $* ${COLOR_SUFFIX}\"\n}\n\n# Color definitions\nfunction openim_color() {\n    COLOR_SUFFIX=\"\\033[0m\"      # End all colors and special effects\n\n    BLACK_PREFIX=\"\\033[30m\"     # Black prefix\n    RED_PREFIX=\"\\033[31m\"       # Red prefix\n    GREEN_PREFIX=\"\\033[32m\"     # Green prefix\n    YELLOW_PREFIX=\"\\033[33m\"    # Yellow prefix\n    BLUE_PREFIX=\"\\033[34m\"      # Blue prefix\n    SKY_BLUE_PREFIX=\"\\033[36m\"  # Sky blue prefix\n    WHITE_PREFIX=\"\\033[37m\"     # White prefix\n    BOLD_PREFIX=\"\\033[1m\"       # Bold prefix\n    UNDERLINE_PREFIX=\"\\033[4m\"  # Underline prefix\n    ITALIC_PREFIX=\"\\033[3m\"     # Italic prefix\n    BRIGHT_GREEN_PREFIX='\\033[1;32m' # Bright green prefix\n\n    CYAN_PREFIX=\"\\033[0;36m\"     # Cyan prefix\n}\n\n# --- helper functions for logs ---\ninfo() {\n    echo -e \"[${GREEN_PREFIX}INFO${COLOR_SUFFIX}] \" \"$@\"\n}\nwarn() {\n    echo -e \"[${YELLOW_PREFIX}WARN${COLOR_SUFFIX}] \" \"$@\" >&2\n}\nfatal() {\n    echo -e \"[${RED_PREFIX}ERROR${COLOR_SUFFIX}] \" \"$@\" >&2\n    exit 1\n}\ndebug() {\n    echo -e \"[${BLUE_PREFIX}DEBUG${COLOR_SUFFIX}]===> \" \"$@\"\n}\nsuccess() {\n    echo -e \"${BRIGHT_GREEN_PREFIX}=== [SUCCESS] ===${COLOR_SUFFIX}\\n=> \" \"$@\"\n}\n\nfunction openim_logo() {\n    # Set text color to cyan for header and URL\n    echo -e \"\\033[0;36m\"\n\n    # Display fancy ASCII Art logo\n    # look http://patorjk.com/software/taag/#p=display&h=1&v=1&f=Doh&t=OpenIM\n    print_with_delay '\n                                                                                                                      \n                                                                                                                      \n     OOOOOOOOO                                                               IIIIIIIIIIMMMMMMMM               MMMMMMMM\n   OO:::::::::OO                                                             I::::::::IM:::::::M             M:::::::M\n OO:::::::::::::OO                                                           I::::::::IM::::::::M           M::::::::M\nO:::::::OOO:::::::O                                                          II::::::IIM:::::::::M         M:::::::::M\nO::::::O   O::::::Oppppp   ppppppppp       eeeeeeeeeeee    nnnn  nnnnnnnn      I::::I  M::::::::::M       M::::::::::M\nO:::::O     O:::::Op::::ppp:::::::::p    ee::::::::::::ee  n:::nn::::::::nn    I::::I  M:::::::::::M     M:::::::::::M\nO:::::O     O:::::Op:::::::::::::::::p  e::::::eeeee:::::een::::::::::::::nn   I::::I  M:::::::M::::M   M::::M:::::::M\nO:::::O     O:::::Opp::::::ppppp::::::pe::::::e     e:::::enn:::::::::::::::n  I::::I  M::::::M M::::M M::::M M::::::M\nO:::::O     O:::::O p:::::p     p:::::pe:::::::eeeee::::::e  n:::::nnnn:::::n  I::::I  M::::::M  M::::M::::M  M::::::M\nO:::::O     O:::::O p:::::p     p:::::pe:::::::::::::::::e   n::::n    n::::n  I::::I  M::::::M   M:::::::M   M::::::M\nO:::::O     O:::::O p:::::p     p:::::pe::::::eeeeeeeeeee    n::::n    n::::n  I::::I  M::::::M    M:::::M    M::::::M\nO::::::O   O::::::O p:::::p    p::::::pe:::::::e             n::::n    n::::n  I::::I  M::::::M     MMMMM     M::::::M\nO:::::::OOO:::::::O p:::::ppppp:::::::pe::::::::e            n::::n    n::::nII::::::IIM::::::M               M::::::M\n OO:::::::::::::OO  p::::::::::::::::p  e::::::::eeeeeeee    n::::n    n::::nI::::::::IM::::::M               M::::::M\n   OO:::::::::OO    p::::::::::::::pp    ee:::::::::::::e    n::::n    n::::nI::::::::IM::::::M               M::::::M\n     OOOOOOOOO      p::::::pppppppp        eeeeeeeeeeeeee    nnnnnn    nnnnnnIIIIIIIIIIMMMMMMMM               MMMMMMMM\n                    p:::::p                                                                                           \n                    p:::::p                                                                                           \n                   p:::::::p                                                                                          \n                   p:::::::p                                                                                          \n                   p:::::::p                                                                                          \n                   ppppppppp                                                                                          \n                                                                                                                      \n    ' 0.0001\n\n    # Display product URL\n    print_with_delay \"Discover more and contribute at: https://github.com/openimsdk/open-im-server\" 0.01\n\n    # Reset text color back to normal\n    echo -e \"\\033[0m\"\n\n    # Set text color to green for product description\n    echo -e \"\\033[1;32m\"\n\n    print_with_delay \"Open-IM-Server: Reinventing Instant Messaging\" 0.01\n    print_progress 50 0.02\n\n    print_with_delay \"Open-IM-Server is not just a product; it's a revolution. It's about bringing the power of seamless, real-time messaging to your fingertips. And it's about joining a global community of developers, dedicated to pushing the boundaries of what's possible.\" 0.01\n\n    print_progress 50 0.02\n\n    # Reset text color back to normal\n    echo -e \"\\033[0m\"\n\n    # Set text color to yellow for the Slack link\n    echo -e \"\\033[1;33m\"\n\n    print_with_delay \"Join our developer community on Slack: https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A\" 0.01\n\n    # Reset text color back to normal\n    echo -e \"\\033[0m\"\n}\n\n# Main function to run the script\nfunction openim_main() {\n    check_git_repo\n    check_isroot\n    openim_color\n    install_tools\n    check_docker\n    install_go\n    download_source_code\n    set_openim_env\n    install_openim\n    openim_logo\n\n}\n\nparseinput \"$@\""
  },
  {
    "path": "internal/api/auth.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/tools/a2r\"\n)\n\ntype AuthApi struct {\n\tClient auth.AuthClient\n}\n\nfunc NewAuthApi(client auth.AuthClient) AuthApi {\n\treturn AuthApi{client}\n}\n\nfunc (o *AuthApi) GetAdminToken(c *gin.Context) {\n\ta2r.Call(c, auth.AuthClient.GetAdminToken, o.Client)\n}\n\nfunc (o *AuthApi) GetUserToken(c *gin.Context) {\n\ta2r.Call(c, auth.AuthClient.GetUserToken, o.Client)\n}\n\nfunc (o *AuthApi) ParseToken(c *gin.Context) {\n\ta2r.Call(c, auth.AuthClient.ParseToken, o.Client)\n}\n\nfunc (o *AuthApi) ForceLogout(c *gin.Context) {\n\ta2r.Call(c, auth.AuthClient.ForceLogout, o.Client)\n}\n"
  },
  {
    "path": "internal/api/config_manager.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/discovery/etcd\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n)\n\nconst (\n\t// wait for Restart http call return\n\twaitHttp = time.Millisecond * 200\n)\n\ntype ConfigManager struct {\n\timAdminUserID []string\n\tconfig        *config.AllConfig\n\tclient        *clientv3.Client\n\n\tconfigPath string\n}\n\nfunc NewConfigManager(IMAdminUserID []string, cfg *config.AllConfig, client *clientv3.Client, configPath string) *ConfigManager {\n\tcm := &ConfigManager{\n\t\timAdminUserID: IMAdminUserID,\n\t\tconfig:        cfg,\n\t\tclient:        client,\n\t\tconfigPath:    configPath,\n\t}\n\treturn cm\n}\n\nfunc (cm *ConfigManager) CheckAdmin(c *gin.Context) {\n\tif err := authverify.CheckAdmin(c); err != nil {\n\t\tapiresp.GinError(c, err)\n\t\tc.Abort()\n\t}\n}\n\nfunc (cm *ConfigManager) GetConfig(c *gin.Context) {\n\tvar req apistruct.GetConfigReq\n\tif err := c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tconf := cm.config.Name2Config(req.ConfigName)\n\tif conf == nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(\"config name not found\").Wrap())\n\t\treturn\n\t}\n\tb, err := json.Marshal(conf)\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\tapiresp.GinSuccess(c, string(b))\n}\n\nfunc (cm *ConfigManager) GetConfigList(c *gin.Context) {\n\tvar resp apistruct.GetConfigListResp\n\tresp.ConfigNames = cm.config.GetConfigNames()\n\tresp.Environment = runtimeenv.RuntimeEnvironment()\n\tresp.Version = version.Version\n\n\tapiresp.GinSuccess(c, resp)\n}\n\nfunc (cm *ConfigManager) SetConfig(c *gin.Context) {\n\tif cm.config.Discovery.Enable != config.ETCD {\n\t\tapiresp.GinError(c, errs.New(\"only etcd support set config\").Wrap())\n\t\treturn\n\t}\n\tvar req apistruct.SetConfigReq\n\tif err := c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tvar err error\n\tswitch req.ConfigName {\n\tcase cm.config.Discovery.GetConfigFileName():\n\t\terr = compareAndSave[config.Discovery](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Kafka.GetConfigFileName():\n\t\terr = compareAndSave[config.Kafka](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.LocalCache.GetConfigFileName():\n\t\terr = compareAndSave[config.LocalCache](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Log.GetConfigFileName():\n\t\terr = compareAndSave[config.Log](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Minio.GetConfigFileName():\n\t\terr = compareAndSave[config.Minio](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Mongo.GetConfigFileName():\n\t\terr = compareAndSave[config.Mongo](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Notification.GetConfigFileName():\n\t\terr = compareAndSave[config.Notification](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.API.GetConfigFileName():\n\t\terr = compareAndSave[config.API](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.CronTask.GetConfigFileName():\n\t\terr = compareAndSave[config.CronTask](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.MsgGateway.GetConfigFileName():\n\t\terr = compareAndSave[config.MsgGateway](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.MsgTransfer.GetConfigFileName():\n\t\terr = compareAndSave[config.MsgTransfer](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Push.GetConfigFileName():\n\t\terr = compareAndSave[config.Push](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Auth.GetConfigFileName():\n\t\terr = compareAndSave[config.Auth](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Conversation.GetConfigFileName():\n\t\terr = compareAndSave[config.Conversation](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Friend.GetConfigFileName():\n\t\terr = compareAndSave[config.Friend](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Group.GetConfigFileName():\n\t\terr = compareAndSave[config.Group](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Msg.GetConfigFileName():\n\t\terr = compareAndSave[config.Msg](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Third.GetConfigFileName():\n\t\terr = compareAndSave[config.Third](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.User.GetConfigFileName():\n\t\terr = compareAndSave[config.User](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Redis.GetConfigFileName():\n\t\terr = compareAndSave[config.Redis](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Share.GetConfigFileName():\n\t\terr = compareAndSave[config.Share](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tcase cm.config.Webhooks.GetConfigFileName():\n\t\terr = compareAndSave[config.Webhooks](c, cm.config.Name2Config(req.ConfigName), &req, cm)\n\tdefault:\n\t\tapiresp.GinError(c, errs.ErrArgs.Wrap())\n\t\treturn\n\t}\n\tif err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tapiresp.GinSuccess(c, nil)\n}\n\nfunc (cm *ConfigManager) SetConfigs(c *gin.Context) {\n\tif cm.config.Discovery.Enable != config.ETCD {\n\t\tapiresp.GinError(c, errs.New(\"only etcd support set config\").Wrap())\n\t\treturn\n\t}\n\tvar req apistruct.SetConfigsReq\n\tif err := c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tvar (\n\t\terr error\n\t\tops []*clientv3.Op\n\t)\n\n\tfor _, cf := range req.Configs {\n\t\tvar op *clientv3.Op\n\t\tswitch cf.ConfigName {\n\t\tcase cm.config.Discovery.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Discovery](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Kafka.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Kafka](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.LocalCache.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.LocalCache](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Log.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Log](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Minio.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Minio](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Mongo.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Mongo](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Notification.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Notification](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.API.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.API](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.CronTask.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.CronTask](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.MsgGateway.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.MsgGateway](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.MsgTransfer.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.MsgTransfer](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Push.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Push](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Auth.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Auth](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Conversation.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Conversation](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Friend.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Friend](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Group.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Group](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Msg.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Msg](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Third.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Third](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.User.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.User](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Redis.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Redis](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Share.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Share](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tcase cm.config.Webhooks.GetConfigFileName():\n\t\t\top, err = compareAndOp[config.Webhooks](c, cm.config.Name2Config(cf.ConfigName), &cf, cm)\n\t\tdefault:\n\t\t\tapiresp.GinError(c, errs.ErrArgs.Wrap())\n\t\t\treturn\n\t\t}\n\t\tif err != nil {\n\t\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\t\treturn\n\t\t}\n\t\tif op != nil {\n\t\t\tops = append(ops, op)\n\t\t}\n\t}\n\tif len(ops) > 0 {\n\t\ttx := cm.client.Txn(c)\n\t\tif _, err = tx.Then(datautil.Batch(func(op *clientv3.Op) clientv3.Op { return *op }, ops)...).Commit(); err != nil {\n\t\t\tapiresp.GinError(c, errs.WrapMsg(err, \"save to etcd failed\"))\n\t\t\treturn\n\t\t}\n\n\t}\n\n\tapiresp.GinSuccess(c, nil)\n}\n\nfunc compareAndOp[T any](c *gin.Context, old any, req *apistruct.SetConfigReq, cm *ConfigManager) (*clientv3.Op, error) {\n\tconf := new(T)\n\terr := json.Unmarshal([]byte(req.Data), &conf)\n\tif err != nil {\n\t\treturn nil, errs.ErrArgs.WithDetail(err.Error()).Wrap()\n\t}\n\teq := reflect.DeepEqual(old, conf)\n\tif eq {\n\t\treturn nil, nil\n\t}\n\tdata, err := json.Marshal(conf)\n\tif err != nil {\n\t\treturn nil, errs.ErrArgs.WithDetail(err.Error()).Wrap()\n\t}\n\top := clientv3.OpPut(etcd.BuildKey(req.ConfigName), string(data))\n\treturn &op, nil\n}\n\nfunc compareAndSave[T any](c *gin.Context, old any, req *apistruct.SetConfigReq, cm *ConfigManager) error {\n\tconf := new(T)\n\terr := json.Unmarshal([]byte(req.Data), &conf)\n\tif err != nil {\n\t\treturn errs.ErrArgs.WithDetail(err.Error()).Wrap()\n\t}\n\teq := reflect.DeepEqual(old, conf)\n\tif eq {\n\t\treturn nil\n\t}\n\tdata, err := json.Marshal(conf)\n\tif err != nil {\n\t\treturn errs.ErrArgs.WithDetail(err.Error()).Wrap()\n\t}\n\t_, err = cm.client.Put(c, etcd.BuildKey(req.ConfigName), string(data))\n\tif err != nil {\n\t\treturn errs.WrapMsg(err, \"save to etcd failed\")\n\t}\n\treturn nil\n}\n\nfunc (cm *ConfigManager) ResetConfig(c *gin.Context) {\n\tgo func() {\n\t\tif err := cm.resetConfig(c, true); err != nil {\n\t\t\tlog.ZError(c, \"reset config err\", err)\n\t\t}\n\t}()\n\tapiresp.GinSuccess(c, nil)\n}\n\nfunc (cm *ConfigManager) resetConfig(c *gin.Context, checkChange bool, ops ...clientv3.Op) error {\n\ttxn := cm.client.Txn(c)\n\ttype initConf struct {\n\t\told any\n\t\tnew any\n\t}\n\tconfigMap := map[string]*initConf{\n\t\tcm.config.Discovery.GetConfigFileName():    {old: &cm.config.Discovery, new: new(config.Discovery)},\n\t\tcm.config.Kafka.GetConfigFileName():        {old: &cm.config.Kafka, new: new(config.Kafka)},\n\t\tcm.config.LocalCache.GetConfigFileName():   {old: &cm.config.LocalCache, new: new(config.LocalCache)},\n\t\tcm.config.Log.GetConfigFileName():          {old: &cm.config.Log, new: new(config.Log)},\n\t\tcm.config.Minio.GetConfigFileName():        {old: &cm.config.Minio, new: new(config.Minio)},\n\t\tcm.config.Mongo.GetConfigFileName():        {old: &cm.config.Mongo, new: new(config.Mongo)},\n\t\tcm.config.Notification.GetConfigFileName(): {old: &cm.config.Notification, new: new(config.Notification)},\n\t\tcm.config.API.GetConfigFileName():          {old: &cm.config.API, new: new(config.API)},\n\t\tcm.config.CronTask.GetConfigFileName():     {old: &cm.config.CronTask, new: new(config.CronTask)},\n\t\tcm.config.MsgGateway.GetConfigFileName():   {old: &cm.config.MsgGateway, new: new(config.MsgGateway)},\n\t\tcm.config.MsgTransfer.GetConfigFileName():  {old: &cm.config.MsgTransfer, new: new(config.MsgTransfer)},\n\t\tcm.config.Push.GetConfigFileName():         {old: &cm.config.Push, new: new(config.Push)},\n\t\tcm.config.Auth.GetConfigFileName():         {old: &cm.config.Auth, new: new(config.Auth)},\n\t\tcm.config.Conversation.GetConfigFileName(): {old: &cm.config.Conversation, new: new(config.Conversation)},\n\t\tcm.config.Friend.GetConfigFileName():       {old: &cm.config.Friend, new: new(config.Friend)},\n\t\tcm.config.Group.GetConfigFileName():        {old: &cm.config.Group, new: new(config.Group)},\n\t\tcm.config.Msg.GetConfigFileName():          {old: &cm.config.Msg, new: new(config.Msg)},\n\t\tcm.config.Third.GetConfigFileName():        {old: &cm.config.Third, new: new(config.Third)},\n\t\tcm.config.User.GetConfigFileName():         {old: &cm.config.User, new: new(config.User)},\n\t\tcm.config.Redis.GetConfigFileName():        {old: &cm.config.Redis, new: new(config.Redis)},\n\t\tcm.config.Share.GetConfigFileName():        {old: &cm.config.Share, new: new(config.Share)},\n\t\tcm.config.Webhooks.GetConfigFileName():     {old: &cm.config.Webhooks, new: new(config.Webhooks)},\n\t}\n\n\tchangedKeys := make([]string, 0, len(configMap))\n\tfor k, v := range configMap {\n\t\terr := config.Load(cm.configPath, k, config.EnvPrefixMap[k], v.new)\n\t\tif err != nil {\n\t\t\tlog.ZError(c, \"load config failed\", err)\n\t\t\tcontinue\n\t\t}\n\t\tequal := reflect.DeepEqual(v.old, v.new)\n\t\tif !checkChange || !equal {\n\t\t\tchangedKeys = append(changedKeys, k)\n\t\t}\n\t}\n\n\tfor _, k := range changedKeys {\n\t\tdata, err := json.Marshal(configMap[k].new)\n\t\tif err != nil {\n\t\t\tlog.ZError(c, \"marshal config failed\", err)\n\t\t\tcontinue\n\t\t}\n\t\tops = append(ops, clientv3.OpPut(etcd.BuildKey(k), string(data)))\n\t}\n\tif len(ops) > 0 {\n\t\ttxn.Then(ops...)\n\t\t_, err := txn.Commit()\n\t\tif err != nil {\n\t\t\treturn errs.WrapMsg(err, \"commit etcd txn failed\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (cm *ConfigManager) Restart(c *gin.Context) {\n\tgo cm.restart(c)\n\tapiresp.GinSuccess(c, nil)\n}\n\nfunc (cm *ConfigManager) restart(c *gin.Context) {\n\ttime.Sleep(waitHttp) // wait for Restart http call return\n\tt := time.Now().Unix()\n\t_, err := cm.client.Put(c, etcd.BuildKey(etcd.RestartKey), strconv.Itoa(int(t)))\n\tif err != nil {\n\t\tlog.ZError(c, \"restart etcd put key failed\", err)\n\t}\n}\n\nfunc (cm *ConfigManager) SetEnableConfigManager(c *gin.Context) {\n\tif cm.config.Discovery.Enable != config.ETCD {\n\t\tapiresp.GinError(c, errs.New(\"only etcd support config manager\").Wrap())\n\t\treturn\n\t}\n\tvar req apistruct.SetEnableConfigManagerReq\n\tif err := c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tvar enableStr string\n\tif req.Enable {\n\t\tenableStr = etcd.Enable\n\t} else {\n\t\tenableStr = etcd.Disable\n\t}\n\tresp, err := cm.client.Get(c, etcd.BuildKey(etcd.EnableConfigCenterKey))\n\tif err != nil {\n\t\tapiresp.GinError(c, errs.WrapMsg(err, \"getEnableConfigManager failed\"))\n\t\treturn\n\t}\n\tif !(resp.Count > 0 && string(resp.Kvs[0].Value) == etcd.Enable) && req.Enable {\n\t\tgo func() {\n\t\t\ttime.Sleep(waitHttp) // wait for Restart http call return\n\t\t\terr := cm.resetConfig(c, false, clientv3.OpPut(etcd.BuildKey(etcd.EnableConfigCenterKey), enableStr))\n\t\t\tif err != nil {\n\t\t\t\tlog.ZError(c, \"resetConfig failed\", err)\n\t\t\t}\n\t\t}()\n\t} else {\n\t\t_, err = cm.client.Put(c, etcd.BuildKey(etcd.EnableConfigCenterKey), enableStr)\n\t\tif err != nil {\n\t\t\tapiresp.GinError(c, errs.WrapMsg(err, \"setEnableConfigManager failed\"))\n\t\t\treturn\n\t\t}\n\t}\n\n\tapiresp.GinSuccess(c, nil)\n}\n\nfunc (cm *ConfigManager) GetEnableConfigManager(c *gin.Context) {\n\tresp, err := cm.client.Get(c, etcd.BuildKey(etcd.EnableConfigCenterKey))\n\tif err != nil {\n\t\tapiresp.GinError(c, errs.WrapMsg(err, \"getEnableConfigManager failed\"))\n\t\treturn\n\t}\n\tvar enable bool\n\tif resp.Count > 0 && string(resp.Kvs[0].Value) == etcd.Enable {\n\t\tenable = true\n\t}\n\tapiresp.GinSuccess(c, &apistruct.GetEnableConfigManagerResp{Enable: enable})\n}\n"
  },
  {
    "path": "internal/api/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/tools/a2r\"\n)\n\ntype ConversationApi struct {\n\tClient conversation.ConversationClient\n}\n\nfunc NewConversationApi(client conversation.ConversationClient) ConversationApi {\n\treturn ConversationApi{client}\n}\n\nfunc (o *ConversationApi) GetAllConversations(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetAllConversations, o.Client)\n}\n\nfunc (o *ConversationApi) GetSortedConversationList(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetSortedConversationList, o.Client)\n}\n\nfunc (o *ConversationApi) GetConversation(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetConversation, o.Client)\n}\n\nfunc (o *ConversationApi) GetConversations(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetConversations, o.Client)\n}\n\nfunc (o *ConversationApi) SetConversations(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.SetConversations, o.Client)\n}\n\n//func (o *ConversationApi) GetConversationOfflinePushUserIDs(c *gin.Context) {\n//\ta2r.Call(c, conversation.ConversationClient.GetConversationOfflinePushUserIDs, o.Client)\n//}\n\nfunc (o *ConversationApi) GetFullOwnerConversationIDs(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetFullOwnerConversationIDs, o.Client)\n}\n\nfunc (o *ConversationApi) GetIncrementalConversation(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetIncrementalConversation, o.Client)\n}\n\nfunc (o *ConversationApi) GetOwnerConversation(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetOwnerConversation, o.Client)\n}\n\nfunc (o *ConversationApi) GetNotNotifyConversationIDs(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetNotNotifyConversationIDs, o.Client)\n}\n\nfunc (o *ConversationApi) GetPinnedConversationIDs(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.GetPinnedConversationIDs, o.Client)\n}\n\nfunc (o *ConversationApi) UpdateConversationsByUser(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.UpdateConversationsByUser, o.Client)\n}\n\nfunc (o *ConversationApi) DeleteConversations(c *gin.Context) {\n\ta2r.Call(c, conversation.ConversationClient.DeleteConversations, o.Client)\n}\n"
  },
  {
    "path": "internal/api/custom_validator.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/openimsdk/protocol/constant\"\n)\n\n// RequiredIf validates if the specified field is required based on the session type.\nfunc RequiredIf(fl validator.FieldLevel) bool {\n\tsessionType := fl.Parent().FieldByName(\"SessionType\").Int()\n\n\tswitch sessionType {\n\tcase constant.SingleChatType, constant.NotificationChatType:\n\t\treturn fl.FieldName() != \"RecvID\" || fl.Field().String() != \"\"\n\tcase constant.WriteGroupChatType, constant.ReadGroupChatType:\n\t\treturn fl.FieldName() != \"GroupID\" || fl.Field().String() != \"\"\n\tdefault:\n\t\treturn true\n\t}\n}\n"
  },
  {
    "path": "internal/api/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/openimsdk/protocol/relation\"\n\t\"github.com/openimsdk/tools/a2r\"\n)\n\ntype FriendApi struct {\n\tClient relation.FriendClient\n}\n\nfunc NewFriendApi(client relation.FriendClient) FriendApi {\n\treturn FriendApi{client}\n}\n\nfunc (o *FriendApi) ApplyToAddFriend(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.ApplyToAddFriend, o.Client)\n}\n\nfunc (o *FriendApi) RespondFriendApply(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.RespondFriendApply, o.Client)\n}\n\nfunc (o *FriendApi) DeleteFriend(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.DeleteFriend, o.Client)\n}\n\nfunc (o *FriendApi) GetFriendApplyList(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetPaginationFriendsApplyTo, o.Client)\n}\n\nfunc (o *FriendApi) GetDesignatedFriendsApply(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetDesignatedFriendsApply, o.Client)\n}\n\nfunc (o *FriendApi) GetSelfApplyList(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetPaginationFriendsApplyFrom, o.Client)\n}\n\nfunc (o *FriendApi) GetFriendList(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetPaginationFriends, o.Client)\n}\n\nfunc (o *FriendApi) GetDesignatedFriends(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetDesignatedFriends, o.Client)\n}\n\nfunc (o *FriendApi) SetFriendRemark(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.SetFriendRemark, o.Client)\n}\n\nfunc (o *FriendApi) AddBlack(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.AddBlack, o.Client)\n}\n\nfunc (o *FriendApi) GetPaginationBlacks(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetPaginationBlacks, o.Client)\n}\n\nfunc (o *FriendApi) GetSpecifiedBlacks(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetSpecifiedBlacks, o.Client)\n}\n\nfunc (o *FriendApi) RemoveBlack(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.RemoveBlack, o.Client)\n}\n\nfunc (o *FriendApi) ImportFriends(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.ImportFriends, o.Client)\n}\n\nfunc (o *FriendApi) IsFriend(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.IsFriend, o.Client)\n}\n\nfunc (o *FriendApi) GetFriendIDs(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetFriendIDs, o.Client)\n}\n\nfunc (o *FriendApi) GetSpecifiedFriendsInfo(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetSpecifiedFriendsInfo, o.Client)\n}\n\nfunc (o *FriendApi) UpdateFriends(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.UpdateFriends, o.Client)\n}\n\nfunc (o *FriendApi) GetIncrementalFriends(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetIncrementalFriends, o.Client)\n}\n\n// GetIncrementalBlacks is temporarily unused.\n// Deprecated: This function is currently unused and may be removed in future versions.\nfunc (o *FriendApi) GetIncrementalBlacks(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetIncrementalBlacks, o.Client)\n}\n\nfunc (o *FriendApi) GetFullFriendUserIDs(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetFullFriendUserIDs, o.Client)\n}\n\nfunc (o *FriendApi) GetSelfUnhandledApplyCount(c *gin.Context) {\n\ta2r.Call(c, relation.FriendClient.GetSelfUnhandledApplyCount, o.Client)\n}\n"
  },
  {
    "path": "internal/api/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/tools/a2r\"\n)\n\ntype GroupApi struct {\n\tClient group.GroupClient\n}\n\nfunc NewGroupApi(client group.GroupClient) GroupApi {\n\treturn GroupApi{client}\n}\n\nfunc (o *GroupApi) CreateGroup(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.CreateGroup, o.Client)\n}\n\nfunc (o *GroupApi) SetGroupInfo(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.SetGroupInfo, o.Client)\n}\n\nfunc (o *GroupApi) SetGroupInfoEx(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.SetGroupInfoEx, o.Client)\n}\n\nfunc (o *GroupApi) JoinGroup(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.JoinGroup, o.Client)\n}\n\nfunc (o *GroupApi) QuitGroup(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.QuitGroup, o.Client)\n}\n\nfunc (o *GroupApi) ApplicationGroupResponse(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GroupApplicationResponse, o.Client)\n}\n\nfunc (o *GroupApi) TransferGroupOwner(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.TransferGroupOwner, o.Client)\n}\n\nfunc (o *GroupApi) GetRecvGroupApplicationList(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroupApplicationList, o.Client)\n}\n\nfunc (o *GroupApi) GetUserReqGroupApplicationList(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetUserReqApplicationList, o.Client)\n}\n\nfunc (o *GroupApi) GetGroupUsersReqApplicationList(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroupUsersReqApplicationList, o.Client)\n}\n\nfunc (o *GroupApi) GetSpecifiedUserGroupRequestInfo(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetSpecifiedUserGroupRequestInfo, o.Client)\n}\n\nfunc (o *GroupApi) GetGroupsInfo(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroupsInfo, o.Client)\n\t//a2r.Call(c, group.GroupClient.GetGroupsInfo, o.Client, c, a2r.NewNilReplaceOption(group.GroupClient.GetGroupsInfo))\n}\n\nfunc (o *GroupApi) KickGroupMember(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.KickGroupMember, o.Client)\n}\n\nfunc (o *GroupApi) GetGroupMembersInfo(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroupMembersInfo, o.Client)\n\t//a2r.Call(c, group.GroupClient.GetGroupMembersInfo, o.Client, c, a2r.NewNilReplaceOption(group.GroupClient.GetGroupMembersInfo))\n}\n\nfunc (o *GroupApi) GetGroupMemberList(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroupMemberList, o.Client)\n}\n\nfunc (o *GroupApi) InviteUserToGroup(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.InviteUserToGroup, o.Client)\n}\n\nfunc (o *GroupApi) GetJoinedGroupList(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetJoinedGroupList, o.Client)\n}\n\nfunc (o *GroupApi) DismissGroup(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.DismissGroup, o.Client)\n}\n\nfunc (o *GroupApi) MuteGroupMember(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.MuteGroupMember, o.Client)\n}\n\nfunc (o *GroupApi) CancelMuteGroupMember(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.CancelMuteGroupMember, o.Client)\n}\n\nfunc (o *GroupApi) MuteGroup(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.MuteGroup, o.Client)\n}\n\nfunc (o *GroupApi) CancelMuteGroup(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.CancelMuteGroup, o.Client)\n}\n\nfunc (o *GroupApi) SetGroupMemberInfo(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.SetGroupMemberInfo, o.Client)\n}\n\nfunc (o *GroupApi) GetGroupAbstractInfo(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroupAbstractInfo, o.Client)\n}\n\n// func (g *Group) SetGroupMemberNickname(c *gin.Context) {\n//\ta2r.Call(c, group.GroupClient.SetGroupMemberNickname, g.userClient)\n//}\n//\n// func (g *Group) GetGroupAllMemberList(c *gin.Context) {\n//\ta2r.Call(c, group.GroupClient.GetGroupAllMember, g.userClient)\n//}\n\nfunc (o *GroupApi) GroupCreateCount(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GroupCreateCount, o.Client)\n}\n\nfunc (o *GroupApi) GetGroups(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroups, o.Client)\n}\n\nfunc (o *GroupApi) GetGroupMemberUserIDs(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroupMemberUserIDs, o.Client)\n}\n\nfunc (o *GroupApi) GetIncrementalJoinGroup(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetIncrementalJoinGroup, o.Client)\n}\n\nfunc (o *GroupApi) GetIncrementalGroupMember(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetIncrementalGroupMember, o.Client)\n}\n\nfunc (o *GroupApi) GetIncrementalGroupMemberBatch(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.BatchGetIncrementalGroupMember, o.Client)\n}\n\nfunc (o *GroupApi) GetFullGroupMemberUserIDs(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetFullGroupMemberUserIDs, o.Client)\n}\n\nfunc (o *GroupApi) GetFullJoinGroupIDs(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetFullJoinGroupIDs, o.Client)\n}\n\nfunc (o *GroupApi) GetGroupApplicationUnhandledCount(c *gin.Context) {\n\ta2r.Call(c, group.GroupClient.GetGroupApplicationUnhandledCount, o.Client)\n}\n"
  },
  {
    "path": "internal/api/init.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\tconf \"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/network\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\t\"google.golang.org/grpc\"\n)\n\ntype Config struct {\n\tconf.AllConfig\n\n\tConfigPath conf.Path\n\tIndex      conf.Index\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, service grpc.ServiceRegistrar) error {\n\tapiPort, err := datautil.GetElemByIndex(config.API.Api.Ports, int(config.Index))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trouter, err := newGinRouter(ctx, client, config)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tapiCtx, apiCancel := context.WithCancelCause(context.Background())\n\tdone := make(chan struct{})\n\tgo func() {\n\t\thttpServer := &http.Server{\n\t\t\tHandler: router,\n\t\t\tAddr:    net.JoinHostPort(network.GetListenIP(config.API.Api.ListenIP), strconv.Itoa(apiPort)),\n\t\t}\n\t\tgo func() {\n\t\t\tdefer close(done)\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tapiCancel(fmt.Errorf(\"recv ctx %w\", context.Cause(ctx)))\n\t\t\tcase <-apiCtx.Done():\n\t\t\t}\n\t\t\tlog.ZDebug(ctx, \"api server is shutting down\")\n\t\t\tif err := httpServer.Shutdown(context.Background()); err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"api server shutdown err\", err)\n\t\t\t}\n\t\t}()\n\t\tlog.CInfo(ctx, \"api server is init\", \"runtimeEnv\", runtimeenv.RuntimeEnvironment(), \"address\", httpServer.Addr, \"apiPort\", apiPort)\n\t\terr := httpServer.ListenAndServe()\n\t\tif err == nil {\n\t\t\terr = errors.New(\"api done\")\n\t\t}\n\t\tapiCancel(err)\n\t}()\n\n\t//if config.Discovery.Enable == conf.ETCD {\n\t//\tcm := disetcd.NewConfigManager(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient(), config.GetConfigNames())\n\t//\tcm.Watch(ctx)\n\t//}\n\t//sigs := make(chan os.Signal, 1)\n\t//signal.Notify(sigs, syscall.SIGTERM)\n\t//select {\n\t//case val := <-sigs:\n\t//\tlog.ZDebug(ctx, \"recv exit\", \"signal\", val.String())\n\t//\tcancel(fmt.Errorf(\"signal %s\", val.String()))\n\t//case <-ctx.Done():\n\t//}\n\t<-apiCtx.Done()\n\texitCause := context.Cause(apiCtx)\n\tlog.ZWarn(ctx, \"api server exit\", exitCause)\n\ttimer := time.NewTimer(time.Second * 15)\n\tdefer timer.Stop()\n\tselect {\n\tcase <-timer.C:\n\t\tlog.ZWarn(ctx, \"api server graceful stop timeout\", nil)\n\tcase <-done:\n\t\tlog.ZDebug(ctx, \"api server graceful stop done\")\n\t}\n\treturn exitCause\n}\n"
  },
  {
    "path": "internal/api/jssdk/jssdk.go",
    "content": "package jssdk\n\nimport (\n\t\"context\"\n\t\"sort\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/log\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/protocol/jssdk\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/relation\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nconst (\n\tmaxGetActiveConversation     = 500\n\tdefaultGetActiveConversation = 100\n)\n\nfunc NewJSSdkApi(userClient *rpcli.UserClient, relationClient *rpcli.RelationClient, groupClient *rpcli.GroupClient,\n\tconversationClient *rpcli.ConversationClient, msgClient *rpcli.MsgClient) *JSSdk {\n\treturn &JSSdk{\n\t\tuserClient:         userClient,\n\t\trelationClient:     relationClient,\n\t\tgroupClient:        groupClient,\n\t\tconversationClient: conversationClient,\n\t\tmsgClient:          msgClient,\n\t}\n}\n\ntype JSSdk struct {\n\tuserClient         *rpcli.UserClient\n\trelationClient     *rpcli.RelationClient\n\tgroupClient        *rpcli.GroupClient\n\tconversationClient *rpcli.ConversationClient\n\tmsgClient          *rpcli.MsgClient\n}\n\nfunc (x *JSSdk) GetActiveConversations(c *gin.Context) {\n\tcall(c, x.getActiveConversations)\n}\n\nfunc (x *JSSdk) GetConversations(c *gin.Context) {\n\tcall(c, x.getConversations)\n}\n\nfunc (x *JSSdk) fillConversations(ctx context.Context, conversations []*jssdk.ConversationMsg) error {\n\tif len(conversations) == 0 {\n\t\treturn nil\n\t}\n\tvar (\n\t\tuserIDs  []string\n\t\tgroupIDs []string\n\t)\n\tfor _, c := range conversations {\n\t\tif c.Conversation.GroupID == \"\" {\n\t\t\tuserIDs = append(userIDs, c.Conversation.UserID)\n\t\t} else {\n\t\t\tgroupIDs = append(groupIDs, c.Conversation.GroupID)\n\t\t}\n\t}\n\tvar (\n\t\tuserMap   map[string]*sdkws.UserInfo\n\t\tfriendMap map[string]*relation.FriendInfoOnly\n\t\tgroupMap  map[string]*sdkws.GroupInfo\n\t)\n\tif len(userIDs) > 0 {\n\t\tusers, err := x.userClient.GetUsersInfo(ctx, userIDs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfriends, err := x.relationClient.GetFriendsInfo(ctx, conversations[0].Conversation.OwnerUserID, userIDs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuserMap = datautil.SliceToMap(users, (*sdkws.UserInfo).GetUserID)\n\t\tfriendMap = datautil.SliceToMap(friends, (*relation.FriendInfoOnly).GetFriendUserID)\n\t}\n\tif len(groupIDs) > 0 {\n\t\tgroups, err := x.groupClient.GetGroupsInfo(ctx, groupIDs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgroupMap = datautil.SliceToMap(groups, (*sdkws.GroupInfo).GetGroupID)\n\t}\n\tfor _, c := range conversations {\n\t\tif c.Conversation.GroupID == \"\" {\n\t\t\tc.User = userMap[c.Conversation.UserID]\n\t\t\tc.Friend = friendMap[c.Conversation.UserID]\n\t\t} else {\n\t\t\tc.Group = groupMap[c.Conversation.GroupID]\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *JSSdk) getActiveConversations(ctx context.Context, req *jssdk.GetActiveConversationsReq) (*jssdk.GetActiveConversationsResp, error) {\n\tif req.Count <= 0 || req.Count > maxGetActiveConversation {\n\t\treq.Count = defaultGetActiveConversation\n\t}\n\treq.OwnerUserID = mcontext.GetOpUserID(ctx)\n\tconversationIDs, err := x.conversationClient.GetConversationIDs(ctx, req.OwnerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(conversationIDs) == 0 {\n\t\treturn &jssdk.GetActiveConversationsResp{}, nil\n\t}\n\n\tactiveConversation, err := x.msgClient.GetActiveConversation(ctx, conversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(activeConversation) == 0 {\n\t\treturn &jssdk.GetActiveConversationsResp{}, nil\n\t}\n\treadSeq, err := x.msgClient.GetHasReadSeqs(ctx, conversationIDs, req.OwnerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsortConversations := sortActiveConversations{\n\t\tConversation: activeConversation,\n\t}\n\tif len(activeConversation) > 1 {\n\t\tpinnedConversationIDs, err := x.conversationClient.GetPinnedConversationIDs(ctx, req.OwnerUserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsortConversations.PinnedConversationIDs = datautil.SliceSet(pinnedConversationIDs)\n\t}\n\tsort.Sort(&sortConversations)\n\tsortList := sortConversations.Top(int(req.Count))\n\tconversations, err := x.conversationClient.GetConversations(ctx, datautil.Slice(sortList, func(c *msg.ActiveConversation) string {\n\t\treturn c.ConversationID\n\t}), req.OwnerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmsgs, err := x.msgClient.GetSeqMessage(ctx, req.OwnerUserID, datautil.Slice(sortList, func(c *msg.ActiveConversation) *msg.ConversationSeqs {\n\t\treturn &msg.ConversationSeqs{\n\t\t\tConversationID: c.ConversationID,\n\t\t\tSeqs:           []int64{c.MaxSeq},\n\t\t}\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx.checkMessagesAndGetLastMessage(ctx, req.OwnerUserID, msgs)\n\tconversationMap := datautil.SliceToMap(conversations, func(c *conversation.Conversation) string {\n\t\treturn c.ConversationID\n\t})\n\tresp := make([]*jssdk.ConversationMsg, 0, len(sortList))\n\tfor _, c := range sortList {\n\t\tconv, ok := conversationMap[c.ConversationID]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tif msgList, ok := msgs[c.ConversationID]; ok && len(msgList.Msgs) > 0 {\n\t\t\tresp = append(resp, &jssdk.ConversationMsg{\n\t\t\t\tConversation: conv,\n\t\t\t\tLastMsg:      msgList.Msgs[0],\n\t\t\t\tMaxSeq:       c.MaxSeq,\n\t\t\t\tReadSeq:      readSeq[c.ConversationID],\n\t\t\t})\n\t\t}\n\n\t}\n\tif err := x.fillConversations(ctx, resp); err != nil {\n\t\treturn nil, err\n\t}\n\tvar unreadCount int64\n\tfor _, c := range activeConversation {\n\t\tcount := c.MaxSeq - readSeq[c.ConversationID]\n\t\tif count > 0 {\n\t\t\tunreadCount += count\n\t\t}\n\t}\n\treturn &jssdk.GetActiveConversationsResp{\n\t\tConversations: resp,\n\t\tUnreadCount:   unreadCount,\n\t}, nil\n}\n\nfunc (x *JSSdk) getConversations(ctx context.Context, req *jssdk.GetConversationsReq) (*jssdk.GetConversationsResp, error) {\n\treq.OwnerUserID = mcontext.GetOpUserID(ctx)\n\tconversations, err := x.conversationClient.GetConversations(ctx, req.ConversationIDs, req.OwnerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(conversations) == 0 {\n\t\treturn &jssdk.GetConversationsResp{}, nil\n\t}\n\treq.ConversationIDs = datautil.Slice(conversations, func(c *conversation.Conversation) string {\n\t\treturn c.ConversationID\n\t})\n\tmaxSeqs, err := x.msgClient.GetMaxSeqs(ctx, req.ConversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treadSeqs, err := x.msgClient.GetHasReadSeqs(ctx, req.ConversationIDs, req.OwnerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconversationSeqs := make([]*msg.ConversationSeqs, 0, len(conversations))\n\tfor _, c := range conversations {\n\t\tif seq := maxSeqs[c.ConversationID]; seq > 0 {\n\t\t\tconversationSeqs = append(conversationSeqs, &msg.ConversationSeqs{\n\t\t\t\tConversationID: c.ConversationID,\n\t\t\t\tSeqs:           []int64{seq},\n\t\t\t})\n\t\t}\n\t}\n\tvar msgs map[string]*sdkws.PullMsgs\n\tif len(conversationSeqs) > 0 {\n\t\tmsgs, err = x.msgClient.GetSeqMessage(ctx, req.OwnerUserID, conversationSeqs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tx.checkMessagesAndGetLastMessage(ctx, req.OwnerUserID, msgs)\n\tresp := make([]*jssdk.ConversationMsg, 0, len(conversations))\n\tfor _, c := range conversations {\n\t\tif msgList, ok := msgs[c.ConversationID]; ok && len(msgList.Msgs) > 0 {\n\t\t\tresp = append(resp, &jssdk.ConversationMsg{\n\t\t\t\tConversation: c,\n\t\t\t\tLastMsg:      msgList.Msgs[0],\n\t\t\t\tMaxSeq:       maxSeqs[c.ConversationID],\n\t\t\t\tReadSeq:      readSeqs[c.ConversationID],\n\t\t\t})\n\t\t}\n\n\t}\n\tif err := x.fillConversations(ctx, resp); err != nil {\n\t\treturn nil, err\n\t}\n\tvar unreadCount int64\n\tfor conversationID, maxSeq := range maxSeqs {\n\t\tcount := maxSeq - readSeqs[conversationID]\n\t\tif count > 0 {\n\t\t\tunreadCount += count\n\t\t}\n\t}\n\treturn &jssdk.GetConversationsResp{\n\t\tConversations: resp,\n\t\tUnreadCount:   unreadCount,\n\t}, nil\n}\n\n// This function checks whether the latest MaxSeq message is valid.\n// If not, it needs to fetch a valid message again.\nfunc (x *JSSdk) checkMessagesAndGetLastMessage(ctx context.Context, userID string, messages map[string]*sdkws.PullMsgs) {\n\tvar conversationIDs []string\n\n\tfor conversationID, message := range messages {\n\t\tallInValid := true\n\t\tfor _, data := range message.Msgs {\n\t\t\tif data.Status < constant.MsgStatusHasDeleted {\n\t\t\t\tallInValid = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif allInValid {\n\t\t\tconversationIDs = append(conversationIDs, conversationID)\n\t\t}\n\t}\n\tif len(conversationIDs) > 0 {\n\t\tresp, err := x.msgClient.GetLastMessage(ctx, &msg.GetLastMessageReq{\n\t\t\tUserID:          userID,\n\t\t\tConversationIDs: conversationIDs,\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"fetchLatestValidMessages\", err, \"conversationIDs\", conversationIDs)\n\t\t\treturn\n\t\t}\n\t\tfor conversationID, message := range resp.Msgs {\n\t\t\tmessages[conversationID] = &sdkws.PullMsgs{Msgs: []*sdkws.MsgData{message}}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "internal/api/jssdk/sort.go",
    "content": "package jssdk\n\nimport \"github.com/openimsdk/protocol/msg\"\n\ntype sortActiveConversations struct {\n\tConversation          []*msg.ActiveConversation\n\tPinnedConversationIDs map[string]struct{}\n}\n\nfunc (s sortActiveConversations) Top(limit int) []*msg.ActiveConversation {\n\tif limit > 0 && len(s.Conversation) > limit {\n\t\treturn s.Conversation[:limit]\n\t}\n\treturn s.Conversation\n}\n\nfunc (s sortActiveConversations) Len() int {\n\treturn len(s.Conversation)\n}\n\nfunc (s sortActiveConversations) Less(i, j int) bool {\n\tiv, jv := s.Conversation[i], s.Conversation[j]\n\t_, ip := s.PinnedConversationIDs[iv.ConversationID]\n\t_, jp := s.PinnedConversationIDs[jv.ConversationID]\n\tif ip != jp {\n\t\treturn ip\n\t}\n\treturn iv.LastTime > jv.LastTime\n}\n\nfunc (s sortActiveConversations) Swap(i, j int) {\n\ts.Conversation[i], s.Conversation[j] = s.Conversation[j], s.Conversation[i]\n}\n"
  },
  {
    "path": "internal/api/jssdk/tools.go",
    "content": "package jssdk\n\nimport (\n\t\"context\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openimsdk/tools/a2r\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/checker\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"io\"\n\t\"strings\"\n)\n\nfunc field[A, B, C any](ctx context.Context, fn func(ctx context.Context, req *A, opts ...grpc.CallOption) (*B, error), req *A, get func(*B) C) (C, error) {\n\tresp, err := fn(ctx, req)\n\tif err != nil {\n\t\tvar c C\n\t\treturn c, err\n\t}\n\treturn get(resp), nil\n}\n\nfunc call[A, B any](c *gin.Context, fn func(ctx context.Context, req *A) (*B, error)) {\n\tvar isJSON bool\n\tswitch contentType := c.GetHeader(\"Content-Type\"); {\n\tcase contentType == \"\":\n\t\tisJSON = true\n\tcase strings.Contains(contentType, \"application/json\"):\n\t\tisJSON = true\n\tcase strings.Contains(contentType, \"application/protobuf\"):\n\tcase strings.Contains(contentType, \"application/x-protobuf\"):\n\tdefault:\n\t\tapiresp.GinError(c, errs.ErrArgs.WrapMsg(\"unsupported content type\"))\n\t\treturn\n\t}\n\tvar req *A\n\tif isJSON {\n\t\tvar err error\n\t\treq, err = a2r.ParseRequest[A](c)\n\t\tif err != nil {\n\t\t\tapiresp.GinError(c, err)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tbody, err := io.ReadAll(c.Request.Body)\n\t\tif err != nil {\n\t\t\tapiresp.GinError(c, err)\n\t\t\treturn\n\t\t}\n\t\treq = new(A)\n\t\tif err := proto.Unmarshal(body, any(req).(proto.Message)); err != nil {\n\t\t\tapiresp.GinError(c, err)\n\t\t\treturn\n\t\t}\n\t\tif err := checker.Validate(&req); err != nil {\n\t\t\tapiresp.GinError(c, err)\n\t\t\treturn\n\t\t}\n\t}\n\tresp, err := fn(c, req)\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\tif isJSON {\n\t\tapiresp.GinSuccess(c, resp)\n\t\treturn\n\t}\n\tbody, err := proto.Marshal(any(resp).(proto.Message))\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\tapiresp.GinSuccess(c, body)\n}\n"
  },
  {
    "path": "internal/api/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"sync\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/mitchellh/mapstructure\"\n\t\"google.golang.org/protobuf/reflect/protoreflect\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/a2r\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/idutil\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n)\n\nvar (\n\tmsgDataDescriptor     []protoreflect.FieldDescriptor\n\tmsgDataDescriptorOnce sync.Once\n)\n\nfunc getMsgDataDescriptor() []protoreflect.FieldDescriptor {\n\tmsgDataDescriptorOnce.Do(func() {\n\t\tskip := make(map[string]struct{})\n\t\trespFields := new(msg.SendMsgResp).ProtoReflect().Descriptor().Fields()\n\t\tfor i := 0; i < respFields.Len(); i++ {\n\t\t\tfield := respFields.Get(i)\n\t\t\tif !field.HasJSONName() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tskip[field.JSONName()] = struct{}{}\n\t\t}\n\t\tfields := new(sdkws.MsgData).ProtoReflect().Descriptor().Fields()\n\t\tnum := fields.Len()\n\t\tmsgDataDescriptor = make([]protoreflect.FieldDescriptor, 0, num)\n\t\tfor i := 0; i < num; i++ {\n\t\t\tfield := fields.Get(i)\n\t\t\tif !field.HasJSONName() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := skip[field.JSONName()]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmsgDataDescriptor = append(msgDataDescriptor, fields.Get(i))\n\t\t}\n\t})\n\treturn msgDataDescriptor\n}\n\ntype MessageApi struct {\n\tClient        msg.MsgClient\n\tuserClient    *rpcli.UserClient\n\timAdminUserID []string\n\tvalidate      *validator.Validate\n}\n\nfunc NewMessageApi(client msg.MsgClient, userClient *rpcli.UserClient, imAdminUserID []string) MessageApi {\n\treturn MessageApi{Client: client, userClient: userClient, imAdminUserID: imAdminUserID, validate: validator.New()}\n}\n\nfunc (*MessageApi) SetOptions(options map[string]bool, value bool) {\n\tdatautil.SetSwitchFromOptions(options, constant.IsHistory, value)\n\tdatautil.SetSwitchFromOptions(options, constant.IsPersistent, value)\n\tdatautil.SetSwitchFromOptions(options, constant.IsSenderSync, value)\n\tdatautil.SetSwitchFromOptions(options, constant.IsConversationUpdate, value)\n}\n\nfunc (m *MessageApi) newUserSendMsgReq(_ *gin.Context, params *apistruct.SendMsg, data any) *msg.SendMsgReq {\n\tmsgData := &sdkws.MsgData{\n\t\tSendID:           params.SendID,\n\t\tGroupID:          params.GroupID,\n\t\tClientMsgID:      idutil.GetMsgIDByMD5(params.SendID),\n\t\tSenderPlatformID: params.SenderPlatformID,\n\t\tSenderNickname:   params.SenderNickname,\n\t\tSenderFaceURL:    params.SenderFaceURL,\n\t\tSessionType:      params.SessionType,\n\t\tMsgFrom:          constant.SysMsgType,\n\t\tContentType:      params.ContentType,\n\t\tCreateTime:       timeutil.GetCurrentTimestampByMill(),\n\t\tSendTime:         params.SendTime,\n\t\tOfflinePushInfo:  params.OfflinePushInfo,\n\t\tEx:               params.Ex,\n\t}\n\tvar newContent string\n\toptions := make(map[string]bool, 5)\n\tswitch params.ContentType {\n\tcase constant.OANotification:\n\t\tnotification := sdkws.NotificationElem{}\n\t\tnotification.Detail = jsonutil.StructToJsonString(params.Content)\n\t\tnewContent = jsonutil.StructToJsonString(&notification)\n\tcase constant.Text:\n\t\tfallthrough\n\tcase constant.AtText:\n\t\tif atElem, ok := data.(*apistruct.AtElem); ok {\n\t\t\tmsgData.AtUserIDList = atElem.AtUserList\n\t\t}\n\t\tfallthrough\n\tcase constant.Picture:\n\t\tfallthrough\n\tcase constant.Custom:\n\t\tfallthrough\n\tcase constant.Voice:\n\t\tfallthrough\n\tcase constant.Video:\n\t\tfallthrough\n\tcase constant.File:\n\t\tfallthrough\n\tdefault:\n\t\tnewContent = jsonutil.StructToJsonString(params.Content)\n\t}\n\tif params.IsOnlineOnly {\n\t\tm.SetOptions(options, false)\n\t}\n\tif params.NotOfflinePush {\n\t\tdatautil.SetSwitchFromOptions(options, constant.IsOfflinePush, false)\n\t}\n\tmsgData.Content = []byte(newContent)\n\tmsgData.Options = options\n\tpbData := msg.SendMsgReq{\n\t\tMsgData: msgData,\n\t}\n\treturn &pbData\n}\n\nfunc (m *MessageApi) GetSeq(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.GetMaxSeq, m.Client)\n}\n\nfunc (m *MessageApi) PullMsgBySeqs(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.PullMessageBySeqs, m.Client)\n}\n\nfunc (m *MessageApi) RevokeMsg(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.RevokeMsg, m.Client)\n}\n\nfunc (m *MessageApi) MarkMsgsAsRead(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.MarkMsgsAsRead, m.Client)\n}\n\nfunc (m *MessageApi) MarkConversationAsRead(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.MarkConversationAsRead, m.Client)\n}\n\nfunc (m *MessageApi) GetConversationsHasReadAndMaxSeq(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.GetConversationsHasReadAndMaxSeq, m.Client)\n}\n\nfunc (m *MessageApi) SetConversationHasReadSeq(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.SetConversationHasReadSeq, m.Client)\n}\n\nfunc (m *MessageApi) ClearConversationsMsg(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.ClearConversationsMsg, m.Client)\n}\n\nfunc (m *MessageApi) UserClearAllMsg(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.UserClearAllMsg, m.Client)\n}\n\nfunc (m *MessageApi) DeleteMsgs(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.DeleteMsgs, m.Client)\n}\n\nfunc (m *MessageApi) DeleteMsgPhysicalBySeq(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.DeleteMsgPhysicalBySeq, m.Client)\n}\n\nfunc (m *MessageApi) DeleteMsgPhysical(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.DeleteMsgPhysical, m.Client)\n}\n\nfunc (m *MessageApi) getSendMsgReq(c *gin.Context, req apistruct.SendMsg) (sendMsgReq *msg.SendMsgReq, err error) {\n\tvar data any\n\tlog.ZDebug(c, \"getSendMsgReq\", \"req\", req.Content)\n\tswitch req.ContentType {\n\tcase constant.Text:\n\t\tdata = &apistruct.TextElem{}\n\tcase constant.Picture:\n\t\tdata = &apistruct.PictureElem{}\n\tcase constant.Voice:\n\t\tdata = &apistruct.SoundElem{}\n\tcase constant.Video:\n\t\tdata = &apistruct.VideoElem{}\n\tcase constant.File:\n\t\tdata = &apistruct.FileElem{}\n\tcase constant.AtText:\n\t\tdata = &apistruct.AtElem{}\n\tcase constant.Custom:\n\t\tdata = &apistruct.CustomElem{}\n\tcase constant.MarkdownText:\n\t\tdata = &apistruct.MarkdownTextElem{}\n\tcase constant.Quote:\n\t\tdata = &apistruct.QuoteElem{}\n\tcase constant.OANotification:\n\t\tdata = &apistruct.OANotificationElem{}\n\t\treq.SessionType = constant.NotificationChatType\n\t\tif err = m.userClient.GetNotificationByID(c, req.SendID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, errs.WrapMsg(errs.ErrArgs, \"unsupported content type\", \"contentType\", req.ContentType)\n\t}\n\tif err := mapstructure.WeakDecode(req.Content, data); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"failed to decode message content\")\n\t}\n\tlog.ZDebug(c, \"getSendMsgReq\", \"decodedContent\", data)\n\tif err := m.validate.Struct(data); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"validation error\")\n\t}\n\treturn m.newUserSendMsgReq(c, &req, data), nil\n}\n\nfunc (m *MessageApi) getModifyFields(req, respModify *sdkws.MsgData) map[string]any {\n\tif req == nil || respModify == nil {\n\t\treturn nil\n\t}\n\tfields := make(map[string]any)\n\treqProtoReflect := req.ProtoReflect()\n\trespProtoReflect := respModify.ProtoReflect()\n\tfor _, descriptor := range getMsgDataDescriptor() {\n\t\treqValue := reqProtoReflect.Get(descriptor)\n\t\trespValue := respProtoReflect.Get(descriptor)\n\t\tif !reqValue.Equal(respValue) {\n\t\t\tval := respValue.Interface()\n\t\t\tname := descriptor.JSONName()\n\t\t\tif name == \"content\" {\n\t\t\t\tif bs, ok := val.([]byte); ok {\n\t\t\t\t\tval = string(bs)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfields[name] = val\n\t\t}\n\t}\n\tif len(fields) == 0 {\n\t\tfields = nil\n\t}\n\treturn fields\n}\n\nfunc (m *MessageApi) ginRespSendMsg(c *gin.Context, req *msg.SendMsgReq, resp *msg.SendMsgResp) {\n\tres := m.getModifyFields(req.MsgData, resp.Modify)\n\tresp.Modify = nil\n\tapiresp.GinSuccess(c, &apistruct.SendMsgResp{\n\t\tSendMsgResp: resp,\n\t\tModify:      res,\n\t})\n}\n\n// SendMessage handles the sending of a message. It's an HTTP handler function to be used with Gin framework.\nfunc (m *MessageApi) SendMessage(c *gin.Context) {\n\t// Initialize a request struct for sending a message.\n\treq := apistruct.SendMsgReq{}\n\n\t// Bind the JSON request body to the request struct.\n\tif err := c.BindJSON(&req); err != nil {\n\t\t// Respond with an error if request body binding fails.\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\n\t// Check if the user has the app manager role.\n\tif !authverify.IsAdmin(c) {\n\t\t// Respond with a permission error if the user is not an app manager.\n\t\tapiresp.GinError(c, errs.ErrNoPermission.WrapMsg(\"only app manager can send message\"))\n\t\treturn\n\t}\n\n\t// Prepare the message request with additional required data.\n\tsendMsgReq, err := m.getSendMsgReq(c, req.SendMsg)\n\tif err != nil {\n\t\t// Log and respond with an error if preparation fails.\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\n\t// Set the receiver ID in the message data.\n\tsendMsgReq.MsgData.RecvID = req.RecvID\n\n\t// Attempt to send the message using the client.\n\trespPb, err := m.Client.SendMsg(c, sendMsgReq)\n\tif err != nil {\n\t\t// Set the status to failed and respond with an error if sending fails.\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\n\t// Set the status to successful if the message is sent.\n\tvar status = constant.MsgSendSuccessed\n\n\t// Attempt to update the message sending status in the system.\n\t_, err = m.Client.SetSendMsgStatus(c, &msg.SetSendMsgStatusReq{\n\t\tStatus: int32(status),\n\t})\n\n\tif err != nil {\n\t\t// Log the error if updating the status fails.\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\n\t// Respond with a success message and the response payload.\n\tm.ginRespSendMsg(c, sendMsgReq, respPb)\n}\n\nfunc (m *MessageApi) SendBusinessNotification(c *gin.Context) {\n\treq := struct {\n\t\tKey              string `json:\"key\"`\n\t\tData             string `json:\"data\"`\n\t\tSendUserID       string `json:\"sendUserID\" binding:\"required\"`\n\t\tRecvUserID       string `json:\"recvUserID\"`\n\t\tRecvGroupID      string `json:\"recvGroupID\"`\n\t\tSendMsg          bool   `json:\"sendMsg\"`\n\t\tReliabilityLevel *int   `json:\"reliabilityLevel\"`\n\t}{}\n\tif err := c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tif req.RecvUserID == \"\" && req.RecvGroupID == \"\" {\n\t\tapiresp.GinError(c, errs.ErrArgs.WrapMsg(\"recvUserID and recvGroupID cannot be empty at the same time\"))\n\t\treturn\n\t}\n\tif req.RecvUserID != \"\" && req.RecvGroupID != \"\" {\n\t\tapiresp.GinError(c, errs.ErrArgs.WrapMsg(\"recvUserID and recvGroupID cannot be set at the same time\"))\n\t\treturn\n\t}\n\tvar sessionType int32\n\tif req.RecvUserID != \"\" {\n\t\tsessionType = constant.SingleChatType\n\t} else {\n\t\tsessionType = constant.ReadGroupChatType\n\t}\n\tif req.ReliabilityLevel == nil {\n\t\treq.ReliabilityLevel = datautil.ToPtr(1)\n\t}\n\tif !authverify.IsAdmin(c) {\n\t\tapiresp.GinError(c, errs.ErrNoPermission.WrapMsg(\"only app manager can send message\"))\n\t\treturn\n\t}\n\tsendMsgReq := msg.SendMsgReq{\n\t\tMsgData: &sdkws.MsgData{\n\t\t\tSendID:  req.SendUserID,\n\t\t\tRecvID:  req.RecvUserID,\n\t\t\tGroupID: req.RecvGroupID,\n\t\t\tContent: []byte(jsonutil.StructToJsonString(&sdkws.NotificationElem{\n\t\t\t\tDetail: jsonutil.StructToJsonString(&struct {\n\t\t\t\t\tKey  string `json:\"key\"`\n\t\t\t\t\tData string `json:\"data\"`\n\t\t\t\t}{Key: req.Key, Data: req.Data}),\n\t\t\t})),\n\t\t\tMsgFrom:     constant.SysMsgType,\n\t\t\tContentType: constant.BusinessNotification,\n\t\t\tSessionType: sessionType,\n\t\t\tCreateTime:  timeutil.GetCurrentTimestampByMill(),\n\t\t\tClientMsgID: idutil.GetMsgIDByMD5(mcontext.GetOpUserID(c)),\n\t\t\tOptions: config.GetOptionsByNotification(config.NotificationConfig{\n\t\t\t\tIsSendMsg:        req.SendMsg,\n\t\t\t\tReliabilityLevel: *req.ReliabilityLevel,\n\t\t\t\tUnreadCount:      false,\n\t\t\t}, nil),\n\t\t},\n\t}\n\trespPb, err := m.Client.SendMsg(c, &sendMsgReq)\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\tm.ginRespSendMsg(c, &sendMsgReq, respPb)\n}\n\nfunc (m *MessageApi) BatchSendMsg(c *gin.Context) {\n\tvar (\n\t\treq  apistruct.BatchSendMsgReq\n\t\tresp apistruct.BatchSendMsgResp\n\t)\n\tif err := c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tif err := authverify.CheckAdmin(c); err != nil {\n\t\tapiresp.GinError(c, errs.ErrNoPermission.WrapMsg(\"only app manager can send message\"))\n\t\treturn\n\t}\n\n\tvar recvIDs []string\n\tif req.IsSendAll {\n\t\tvar pageNumber int32 = 1\n\t\tconst showNumber = 500\n\t\tfor {\n\t\t\trecvIDsPart, err := m.userClient.GetAllUserIDs(c, pageNumber, showNumber)\n\t\t\tif err != nil {\n\t\t\t\tapiresp.GinError(c, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trecvIDs = append(recvIDs, recvIDsPart...)\n\t\t\tif len(recvIDsPart) < showNumber {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tpageNumber++\n\t\t}\n\t} else {\n\t\trecvIDs = req.RecvIDs\n\t}\n\tlog.ZDebug(c, \"BatchSendMsg nums\", \"nums \", len(recvIDs))\n\tsendMsgReq, err := m.getSendMsgReq(c, req.SendMsg)\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\tfor _, recvID := range recvIDs {\n\t\tsendMsgReq.MsgData.RecvID = recvID\n\t\trpcResp, err := m.Client.SendMsg(c, sendMsgReq)\n\t\tif err != nil {\n\t\t\tresp.FailedIDs = append(resp.FailedIDs, recvID)\n\t\t\tcontinue\n\t\t}\n\t\tresp.Results = append(resp.Results, &apistruct.SingleReturnResult{\n\t\t\tServerMsgID: rpcResp.ServerMsgID,\n\t\t\tClientMsgID: rpcResp.ClientMsgID,\n\t\t\tSendTime:    rpcResp.SendTime,\n\t\t\tRecvID:      recvID,\n\t\t\tModify:      m.getModifyFields(sendMsgReq.MsgData, rpcResp.Modify),\n\t\t})\n\t}\n\tapiresp.GinSuccess(c, resp)\n}\n\nfunc (m *MessageApi) SendSimpleMessage(c *gin.Context) {\n\tencodedKey, ok := c.GetQuery(webhook.Key)\n\tif !ok {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(\"missing key in query\").Wrap())\n\t\treturn\n\t}\n\n\tdecodedData, err := base64.StdEncoding.DecodeString(encodedKey)\n\tif err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tvar (\n\t\treq        apistruct.SendSingleMsgReq\n\t\tkeyMsgData apistruct.KeyMsgData\n\n\t\tsendID      string\n\t\tsessionType int32\n\t\trecvID      string\n\t)\n\tif err = c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\terr = json.Unmarshal(decodedData, &keyMsgData)\n\tif err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tif keyMsgData.GroupID != \"\" {\n\t\tsessionType = constant.ReadGroupChatType\n\t\tsendID = req.SendID\n\t} else {\n\t\tsessionType = constant.SingleChatType\n\t\tsendID = keyMsgData.RecvID\n\t\trecvID = keyMsgData.SendID\n\t}\n\t// check param\n\tif keyMsgData.SendID == \"\" {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(\"missing recvID or GroupID\").Wrap())\n\t\treturn\n\t}\n\tif sendID == \"\" {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(\"missing sendID\").Wrap())\n\t\treturn\n\t}\n\n\tcontent, err := jsonutil.JsonMarshal(apistruct.MarkdownTextElem{Content: req.Content})\n\tif err != nil {\n\t\tapiresp.GinError(c, errs.Wrap(err))\n\t\treturn\n\t}\n\tmsgData := &sdkws.MsgData{\n\t\tSendID:           sendID,\n\t\tRecvID:           recvID,\n\t\tGroupID:          keyMsgData.GroupID,\n\t\tClientMsgID:      idutil.GetMsgIDByMD5(sendID),\n\t\tSenderPlatformID: constant.AdminPlatformID,\n\t\tSessionType:      sessionType,\n\t\tMsgFrom:          constant.UserMsgType,\n\t\tContentType:      constant.MarkdownText,\n\t\tContent:          content,\n\t\tOfflinePushInfo:  req.OfflinePushInfo,\n\t\tEx:               req.Ex,\n\t}\n\n\tsendReq := &msg.SendSimpleMsgReq{\n\t\tMsgData: msgData,\n\t}\n\n\trespPb, err := m.Client.SendSimpleMsg(c, sendReq)\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\n\tvar status = constant.MsgSendSuccessed\n\n\t_, err = m.Client.SetSendMsgStatus(c, &msg.SetSendMsgStatusReq{\n\t\tStatus: int32(status),\n\t})\n\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\n\tm.ginRespSendMsg(c, &msg.SendMsgReq{MsgData: sendReq.MsgData}, &msg.SendMsgResp{\n\t\tServerMsgID: respPb.ServerMsgID,\n\t\tClientMsgID: respPb.ClientMsgID,\n\t\tSendTime:    respPb.SendTime,\n\t\tModify:      respPb.Modify,\n\t})\n}\n\nfunc (m *MessageApi) CheckMsgIsSendSuccess(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.GetSendMsgStatus, m.Client)\n}\n\nfunc (m *MessageApi) GetUsersOnlineStatus(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.GetSendMsgStatus, m.Client)\n}\n\nfunc (m *MessageApi) GetActiveUser(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.GetActiveUser, m.Client)\n}\n\nfunc (m *MessageApi) GetActiveGroup(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.GetActiveGroup, m.Client)\n}\n\nfunc (m *MessageApi) SearchMsg(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.SearchMessage, m.Client)\n}\n\nfunc (m *MessageApi) GetServerTime(c *gin.Context) {\n\ta2r.Call(c, msg.MsgClient.GetServerTime, m.Client)\n}\n"
  },
  {
    "path": "internal/api/prometheus_discovery.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\ntype PrometheusDiscoveryApi struct {\n\tconfig *Config\n\tkv     discovery.KeyValue\n}\n\nfunc NewPrometheusDiscoveryApi(config *Config, client discovery.SvcDiscoveryRegistry) *PrometheusDiscoveryApi {\n\tapi := &PrometheusDiscoveryApi{\n\t\tconfig: config,\n\t\tkv:     client,\n\t}\n\treturn api\n}\n\nfunc (p *PrometheusDiscoveryApi) discovery(c *gin.Context, key string) {\n\tvalue, err := p.kv.GetKeyWithPrefix(c, prommetrics.BuildDiscoveryKeyPrefix(key))\n\tif err != nil {\n\t\tif errors.Is(err, discovery.ErrNotSupported) {\n\t\t\tc.JSON(http.StatusOK, []struct{}{})\n\t\t\treturn\n\t\t}\n\t\tapiresp.GinError(c, errs.WrapMsg(err, \"get key value\"))\n\t\treturn\n\t}\n\tif len(value) == 0 {\n\t\tc.JSON(http.StatusOK, []*prommetrics.RespTarget{})\n\t\treturn\n\t}\n\tvar resp prommetrics.RespTarget\n\tfor i := range value {\n\t\tvar tmp prommetrics.Target\n\t\tif err = json.Unmarshal(value[i], &tmp); err != nil {\n\t\t\tapiresp.GinError(c, errs.WrapMsg(err, \"json unmarshal err\"))\n\t\t\treturn\n\t\t}\n\n\t\tresp.Targets = append(resp.Targets, tmp.Target)\n\t\tresp.Labels = tmp.Labels // default label is fixed. See prommetrics.BuildDefaultTarget\n\t}\n\n\tc.JSON(http.StatusOK, []*prommetrics.RespTarget{&resp})\n}\n\nfunc (p *PrometheusDiscoveryApi) Api(c *gin.Context) {\n\tp.discovery(c, prommetrics.APIKeyName)\n}\n\nfunc (p *PrometheusDiscoveryApi) User(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.User)\n}\n\nfunc (p *PrometheusDiscoveryApi) Group(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.Group)\n}\n\nfunc (p *PrometheusDiscoveryApi) Msg(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.Msg)\n}\n\nfunc (p *PrometheusDiscoveryApi) Friend(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.Friend)\n}\n\nfunc (p *PrometheusDiscoveryApi) Conversation(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.Conversation)\n}\n\nfunc (p *PrometheusDiscoveryApi) Third(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.Third)\n}\n\nfunc (p *PrometheusDiscoveryApi) Auth(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.Auth)\n}\n\nfunc (p *PrometheusDiscoveryApi) Push(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.Push)\n}\n\nfunc (p *PrometheusDiscoveryApi) MessageGateway(c *gin.Context) {\n\tp.discovery(c, p.config.Discovery.RpcService.MessageGateway)\n}\n\nfunc (p *PrometheusDiscoveryApi) MessageTransfer(c *gin.Context) {\n\tp.discovery(c, prommetrics.MessageTransferKeyName)\n}\n"
  },
  {
    "path": "internal/api/ratelimit.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/stability/ratelimit\"\n\t\"github.com/openimsdk/tools/stability/ratelimit/bbr\"\n)\n\ntype RateLimiter struct {\n\tEnable       bool          `yaml:\"enable\"`\n\tWindow       time.Duration `yaml:\"window\"`       // time duration per window\n\tBucket       int           `yaml:\"bucket\"`       // bucket number for each window\n\tCPUThreshold int64         `yaml:\"cpuThreshold\"` // CPU threshold; valid range 0–1000 (1000 = 100%)\n}\n\nfunc RateLimitMiddleware(config *RateLimiter) gin.HandlerFunc {\n\tif !config.Enable {\n\t\treturn func(c *gin.Context) {\n\t\t\tc.Next()\n\t\t}\n\t}\n\n\tlimiter := bbr.NewBBRLimiter(\n\t\tbbr.WithWindow(config.Window),\n\t\tbbr.WithBucket(config.Bucket),\n\t\tbbr.WithCPUThreshold(config.CPUThreshold),\n\t)\n\n\treturn func(c *gin.Context) {\n\t\tstatus := limiter.Stat()\n\n\t\tc.Header(\"X-BBR-CPU\", strconv.FormatInt(status.CPU, 10))\n\t\tc.Header(\"X-BBR-MinRT\", strconv.FormatInt(status.MinRt, 10))\n\t\tc.Header(\"X-BBR-MaxPass\", strconv.FormatInt(status.MaxPass, 10))\n\t\tc.Header(\"X-BBR-MaxInFlight\", strconv.FormatInt(status.MaxInFlight, 10))\n\t\tc.Header(\"X-BBR-InFlight\", strconv.FormatInt(status.InFlight, 10))\n\n\t\tdone, err := limiter.Allow()\n\t\tif err != nil {\n\n\t\t\tc.Header(\"X-RateLimit-Policy\", \"BBR\")\n\t\t\tc.Header(\"Retry-After\", calculateBBRRetryAfter(status))\n\t\t\tc.Header(\"X-RateLimit-Limit\", strconv.FormatInt(status.MaxInFlight, 10))\n\t\t\tc.Header(\"X-RateLimit-Remaining\", \"0\") // There is no concept of remaining quota in BBR.\n\n\t\t\tfmt.Println(\"rate limited:\", err, \"path:\", c.Request.URL.Path)\n\t\t\tlog.ZWarn(c, \"rate limited\", err, \"path\", c.Request.URL.Path)\n\t\t\tc.AbortWithStatus(http.StatusTooManyRequests)\n\t\t\tapiresp.GinError(c, errs.NewCodeError(http.StatusTooManyRequests, \"too many requests, please try again later\"))\n\t\t\treturn\n\t\t}\n\n\t\tc.Next()\n\t\tdone(ratelimit.DoneInfo{})\n\t}\n}\n\nfunc calculateBBRRetryAfter(status bbr.Stat) string {\n\tloadRatio := float64(status.CPU) / float64(status.CPU)\n\n\tif loadRatio < 0.8 {\n\t\treturn \"1\"\n\t}\n\tif loadRatio < 0.95 {\n\t\treturn \"2\"\n\t}\n\n\tbackoff := 1 + int64(math.Pow(loadRatio-0.95, 2)*50)\n\tif backoff > 5 {\n\t\tbackoff = 5\n\t}\n\treturn strconv.FormatInt(backoff, 10)\n}\n"
  },
  {
    "path": "internal/api/router.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-contrib/gzip\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/gin-gonic/gin/binding\"\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/api/jssdk\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\tpbAuth \"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/relation\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/discovery/etcd\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mw\"\n\t\"github.com/openimsdk/tools/mw/api\"\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n)\n\nconst (\n\tNoCompression      = -1\n\tDefaultCompression = 0\n\tBestCompression    = 1\n\tBestSpeed          = 2\n)\n\nfunc prommetricsGin() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc.Next()\n\t\tpath := c.FullPath()\n\t\tif c.Writer.Status() == http.StatusNotFound {\n\t\t\tprommetrics.HttpCall(\"<404>\", c.Request.Method, c.Writer.Status())\n\t\t} else {\n\t\t\tprommetrics.HttpCall(path, c.Request.Method, c.Writer.Status())\n\t\t}\n\t\tif resp := apiresp.GetGinApiResponse(c); resp != nil {\n\t\t\tprommetrics.APICall(path, c.Request.Method, resp.ErrCode)\n\t\t}\n\t}\n}\n\nfunc newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, cfg *Config) (*gin.Engine, error) {\n\tauthConn, err := client.GetConn(ctx, cfg.Discovery.RpcService.Auth)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserConn, err := client.GetConn(ctx, cfg.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupConn, err := client.GetConn(ctx, cfg.Discovery.RpcService.Group)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfriendConn, err := client.GetConn(ctx, cfg.Discovery.RpcService.Friend)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconversationConn, err := client.GetConn(ctx, cfg.Discovery.RpcService.Conversation)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tthirdConn, err := client.GetConn(ctx, cfg.Discovery.RpcService.Third)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmsgConn, err := client.GetConn(ctx, cfg.Discovery.RpcService.Msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgin.SetMode(gin.ReleaseMode)\n\tr := gin.New()\n\tif v, ok := binding.Validator.Engine().(*validator.Validate); ok {\n\t\t_ = v.RegisterValidation(\"required_if\", RequiredIf)\n\t}\n\tswitch cfg.API.Api.CompressionLevel {\n\tcase NoCompression:\n\tcase DefaultCompression:\n\t\tr.Use(gzip.Gzip(gzip.DefaultCompression))\n\tcase BestCompression:\n\t\tr.Use(gzip.Gzip(gzip.BestCompression))\n\tcase BestSpeed:\n\t\tr.Use(gzip.Gzip(gzip.BestSpeed))\n\t}\n\n\t// Use rate limiter middleware\n\tif cfg.API.RateLimiter.Enable {\n\t\trl := &RateLimiter{\n\t\t\tEnable:       cfg.API.RateLimiter.Enable,\n\t\t\tWindow:       cfg.API.RateLimiter.Window,\n\t\t\tBucket:       cfg.API.RateLimiter.Bucket,\n\t\t\tCPUThreshold: cfg.API.RateLimiter.CPUThreshold,\n\t\t}\n\t\tr.Use(RateLimitMiddleware(rl))\n\t}\n\n\tif config.Standalone() {\n\t\tr.Use(func(c *gin.Context) {\n\t\t\tc.Set(authverify.CtxAdminUserIDsKey, cfg.Share.IMAdminUser.UserIDs)\n\t\t})\n\t}\n\tr.Use(api.GinLogger(), prommetricsGin(), gin.RecoveryWithWriter(gin.DefaultErrorWriter, mw.GinPanicErr), mw.CorsHandler(),\n\t\tmw.GinParseOperationID(), GinParseToken(rpcli.NewAuthClient(authConn)), setGinIsAdmin(cfg.Share.IMAdminUser.UserIDs))\n\n\tu := NewUserApi(user.NewUserClient(userConn), client, cfg.Discovery.RpcService)\n\t{\n\t\tuserRouterGroup := r.Group(\"/user\")\n\t\tuserRouterGroup.POST(\"/user_register\", u.UserRegister)\n\t\tuserRouterGroup.POST(\"/update_user_info\", u.UpdateUserInfo)\n\t\tuserRouterGroup.POST(\"/update_user_info_ex\", u.UpdateUserInfoEx)\n\t\tuserRouterGroup.POST(\"/set_global_msg_recv_opt\", u.SetGlobalRecvMessageOpt)\n\t\tuserRouterGroup.POST(\"/get_users_info\", u.GetUsersPublicInfo)\n\t\tuserRouterGroup.POST(\"/get_all_users_uid\", u.GetAllUsersID)\n\t\tuserRouterGroup.POST(\"/account_check\", u.AccountCheck)\n\t\tuserRouterGroup.POST(\"/get_users\", u.GetUsers)\n\t\tuserRouterGroup.POST(\"/get_users_online_status\", u.GetUsersOnlineStatus)\n\t\tuserRouterGroup.POST(\"/get_users_online_token_detail\", u.GetUsersOnlineTokenDetail)\n\t\tuserRouterGroup.POST(\"/subscribe_users_status\", u.SubscriberStatus)\n\t\tuserRouterGroup.POST(\"/get_users_status\", u.GetUserStatus)\n\t\tuserRouterGroup.POST(\"/get_subscribe_users_status\", u.GetSubscribeUsersStatus)\n\n\t\tuserRouterGroup.POST(\"/process_user_command_add\", u.ProcessUserCommandAdd)\n\t\tuserRouterGroup.POST(\"/process_user_command_delete\", u.ProcessUserCommandDelete)\n\t\tuserRouterGroup.POST(\"/process_user_command_update\", u.ProcessUserCommandUpdate)\n\t\tuserRouterGroup.POST(\"/process_user_command_get\", u.ProcessUserCommandGet)\n\t\tuserRouterGroup.POST(\"/process_user_command_get_all\", u.ProcessUserCommandGetAll)\n\n\t\tuserRouterGroup.POST(\"/add_notification_account\", u.AddNotificationAccount)\n\t\tuserRouterGroup.POST(\"/update_notification_account\", u.UpdateNotificationAccountInfo)\n\t\tuserRouterGroup.POST(\"/search_notification_account\", u.SearchNotificationAccount)\n\n\t\tuserRouterGroup.POST(\"/get_user_client_config\", u.GetUserClientConfig)\n\t\tuserRouterGroup.POST(\"/set_user_client_config\", u.SetUserClientConfig)\n\t\tuserRouterGroup.POST(\"/del_user_client_config\", u.DelUserClientConfig)\n\t\tuserRouterGroup.POST(\"/page_user_client_config\", u.PageUserClientConfig)\n\t}\n\t// friend routing group\n\t{\n\t\tf := NewFriendApi(relation.NewFriendClient(friendConn))\n\t\tfriendRouterGroup := r.Group(\"/friend\")\n\t\tfriendRouterGroup.POST(\"/delete_friend\", f.DeleteFriend)\n\t\tfriendRouterGroup.POST(\"/get_friend_apply_list\", f.GetFriendApplyList)\n\t\tfriendRouterGroup.POST(\"/get_designated_friend_apply\", f.GetDesignatedFriendsApply)\n\t\tfriendRouterGroup.POST(\"/get_self_friend_apply_list\", f.GetSelfApplyList)\n\t\tfriendRouterGroup.POST(\"/get_friend_list\", f.GetFriendList)\n\t\tfriendRouterGroup.POST(\"/get_designated_friends\", f.GetDesignatedFriends)\n\t\tfriendRouterGroup.POST(\"/add_friend\", f.ApplyToAddFriend)\n\t\tfriendRouterGroup.POST(\"/add_friend_response\", f.RespondFriendApply)\n\t\tfriendRouterGroup.POST(\"/set_friend_remark\", f.SetFriendRemark)\n\t\tfriendRouterGroup.POST(\"/add_black\", f.AddBlack)\n\t\tfriendRouterGroup.POST(\"/get_black_list\", f.GetPaginationBlacks)\n\t\tfriendRouterGroup.POST(\"/get_specified_blacks\", f.GetSpecifiedBlacks)\n\t\tfriendRouterGroup.POST(\"/remove_black\", f.RemoveBlack)\n\t\tfriendRouterGroup.POST(\"/get_incremental_blacks\", f.GetIncrementalBlacks)\n\t\tfriendRouterGroup.POST(\"/import_friend\", f.ImportFriends)\n\t\tfriendRouterGroup.POST(\"/is_friend\", f.IsFriend)\n\t\tfriendRouterGroup.POST(\"/get_friend_id\", f.GetFriendIDs)\n\t\tfriendRouterGroup.POST(\"/get_specified_friends_info\", f.GetSpecifiedFriendsInfo)\n\t\tfriendRouterGroup.POST(\"/update_friends\", f.UpdateFriends)\n\t\tfriendRouterGroup.POST(\"/get_incremental_friends\", f.GetIncrementalFriends)\n\t\tfriendRouterGroup.POST(\"/get_full_friend_user_ids\", f.GetFullFriendUserIDs)\n\t\tfriendRouterGroup.POST(\"/get_self_unhandled_apply_count\", f.GetSelfUnhandledApplyCount)\n\t}\n\n\tg := NewGroupApi(group.NewGroupClient(groupConn))\n\t{\n\t\tgroupRouterGroup := r.Group(\"/group\")\n\t\tgroupRouterGroup.POST(\"/create_group\", g.CreateGroup)\n\t\tgroupRouterGroup.POST(\"/set_group_info\", g.SetGroupInfo)\n\t\tgroupRouterGroup.POST(\"/set_group_info_ex\", g.SetGroupInfoEx)\n\t\tgroupRouterGroup.POST(\"/join_group\", g.JoinGroup)\n\t\tgroupRouterGroup.POST(\"/quit_group\", g.QuitGroup)\n\t\tgroupRouterGroup.POST(\"/group_application_response\", g.ApplicationGroupResponse)\n\t\tgroupRouterGroup.POST(\"/transfer_group\", g.TransferGroupOwner)\n\t\tgroupRouterGroup.POST(\"/get_recv_group_applicationList\", g.GetRecvGroupApplicationList)\n\t\tgroupRouterGroup.POST(\"/get_user_req_group_applicationList\", g.GetUserReqGroupApplicationList)\n\t\tgroupRouterGroup.POST(\"/get_group_users_req_application_list\", g.GetGroupUsersReqApplicationList)\n\t\tgroupRouterGroup.POST(\"/get_specified_user_group_request_info\", g.GetSpecifiedUserGroupRequestInfo)\n\t\tgroupRouterGroup.POST(\"/get_groups_info\", g.GetGroupsInfo)\n\t\tgroupRouterGroup.POST(\"/kick_group\", g.KickGroupMember)\n\t\tgroupRouterGroup.POST(\"/get_group_members_info\", g.GetGroupMembersInfo)\n\t\tgroupRouterGroup.POST(\"/get_group_member_list\", g.GetGroupMemberList)\n\t\tgroupRouterGroup.POST(\"/invite_user_to_group\", g.InviteUserToGroup)\n\t\tgroupRouterGroup.POST(\"/get_joined_group_list\", g.GetJoinedGroupList)\n\t\tgroupRouterGroup.POST(\"/dismiss_group\", g.DismissGroup) //\n\t\tgroupRouterGroup.POST(\"/mute_group_member\", g.MuteGroupMember)\n\t\tgroupRouterGroup.POST(\"/cancel_mute_group_member\", g.CancelMuteGroupMember)\n\t\tgroupRouterGroup.POST(\"/mute_group\", g.MuteGroup)\n\t\tgroupRouterGroup.POST(\"/cancel_mute_group\", g.CancelMuteGroup)\n\t\tgroupRouterGroup.POST(\"/set_group_member_info\", g.SetGroupMemberInfo)\n\t\tgroupRouterGroup.POST(\"/get_group_abstract_info\", g.GetGroupAbstractInfo)\n\t\tgroupRouterGroup.POST(\"/get_groups\", g.GetGroups)\n\t\tgroupRouterGroup.POST(\"/get_group_member_user_id\", g.GetGroupMemberUserIDs)\n\t\tgroupRouterGroup.POST(\"/get_incremental_join_groups\", g.GetIncrementalJoinGroup)\n\t\tgroupRouterGroup.POST(\"/get_incremental_group_members\", g.GetIncrementalGroupMember)\n\t\tgroupRouterGroup.POST(\"/get_incremental_group_members_batch\", g.GetIncrementalGroupMemberBatch)\n\t\tgroupRouterGroup.POST(\"/get_full_group_member_user_ids\", g.GetFullGroupMemberUserIDs)\n\t\tgroupRouterGroup.POST(\"/get_full_join_group_ids\", g.GetFullJoinGroupIDs)\n\t\tgroupRouterGroup.POST(\"/get_group_application_unhandled_count\", g.GetGroupApplicationUnhandledCount)\n\t}\n\t// certificate\n\t{\n\t\ta := NewAuthApi(pbAuth.NewAuthClient(authConn))\n\t\tauthRouterGroup := r.Group(\"/auth\")\n\t\tauthRouterGroup.POST(\"/get_admin_token\", a.GetAdminToken)\n\t\tauthRouterGroup.POST(\"/get_user_token\", a.GetUserToken)\n\t\tauthRouterGroup.POST(\"/parse_token\", a.ParseToken)\n\t\tauthRouterGroup.POST(\"/force_logout\", a.ForceLogout)\n\n\t}\n\t// Third service\n\t{\n\t\tt := NewThirdApi(third.NewThirdClient(thirdConn), cfg.API.Prometheus.GrafanaURL)\n\t\tthirdGroup := r.Group(\"/third\")\n\t\tthirdGroup.GET(\"/prometheus\", t.GetPrometheus)\n\t\tthirdGroup.POST(\"/fcm_update_token\", t.FcmUpdateToken)\n\t\tthirdGroup.POST(\"/set_app_badge\", t.SetAppBadge)\n\n\t\tlogs := thirdGroup.Group(\"/logs\")\n\t\tlogs.POST(\"/upload\", t.UploadLogs)\n\t\tlogs.POST(\"/delete\", t.DeleteLogs)\n\t\tlogs.POST(\"/search\", t.SearchLogs)\n\n\t\tobjectGroup := r.Group(\"/object\")\n\n\t\tobjectGroup.POST(\"/part_limit\", t.PartLimit)\n\t\tobjectGroup.POST(\"/part_size\", t.PartSize)\n\t\tobjectGroup.POST(\"/initiate_multipart_upload\", t.InitiateMultipartUpload)\n\t\tobjectGroup.POST(\"/auth_sign\", t.AuthSign)\n\t\tobjectGroup.POST(\"/complete_multipart_upload\", t.CompleteMultipartUpload)\n\t\tobjectGroup.POST(\"/access_url\", t.AccessURL)\n\t\tobjectGroup.POST(\"/initiate_form_data\", t.InitiateFormData)\n\t\tobjectGroup.POST(\"/complete_form_data\", t.CompleteFormData)\n\t\tobjectGroup.GET(\"/*name\", t.ObjectRedirect)\n\t}\n\t// Message\n\tm := NewMessageApi(msg.NewMsgClient(msgConn), rpcli.NewUserClient(userConn), cfg.Share.IMAdminUser.UserIDs)\n\t{\n\t\tmsgGroup := r.Group(\"/msg\")\n\t\tmsgGroup.POST(\"/newest_seq\", m.GetSeq)\n\t\tmsgGroup.POST(\"/search_msg\", m.SearchMsg)\n\t\tmsgGroup.POST(\"/send_msg\", m.SendMessage)\n\t\tmsgGroup.POST(\"/send_business_notification\", m.SendBusinessNotification)\n\t\tmsgGroup.POST(\"/pull_msg_by_seq\", m.PullMsgBySeqs)\n\t\tmsgGroup.POST(\"/revoke_msg\", m.RevokeMsg)\n\t\tmsgGroup.POST(\"/mark_msgs_as_read\", m.MarkMsgsAsRead)\n\t\tmsgGroup.POST(\"/mark_conversation_as_read\", m.MarkConversationAsRead)\n\t\tmsgGroup.POST(\"/get_conversations_has_read_and_max_seq\", m.GetConversationsHasReadAndMaxSeq)\n\t\tmsgGroup.POST(\"/set_conversation_has_read_seq\", m.SetConversationHasReadSeq)\n\n\t\tmsgGroup.POST(\"/clear_conversation_msg\", m.ClearConversationsMsg)\n\t\tmsgGroup.POST(\"/user_clear_all_msg\", m.UserClearAllMsg)\n\t\tmsgGroup.POST(\"/delete_msgs\", m.DeleteMsgs)\n\t\tmsgGroup.POST(\"/delete_msg_phsical_by_seq\", m.DeleteMsgPhysicalBySeq)\n\t\tmsgGroup.POST(\"/delete_msg_physical\", m.DeleteMsgPhysical)\n\n\t\tmsgGroup.POST(\"/batch_send_msg\", m.BatchSendMsg)\n\t\tmsgGroup.POST(\"/send_simple_msg\", m.SendSimpleMessage)\n\t\tmsgGroup.POST(\"/check_msg_is_send_success\", m.CheckMsgIsSendSuccess)\n\t\tmsgGroup.POST(\"/get_server_time\", m.GetServerTime)\n\t}\n\t// Conversation\n\t{\n\t\tc := NewConversationApi(conversation.NewConversationClient(conversationConn))\n\t\tconversationGroup := r.Group(\"/conversation\")\n\t\tconversationGroup.POST(\"/get_sorted_conversation_list\", c.GetSortedConversationList)\n\t\tconversationGroup.POST(\"/get_all_conversations\", c.GetAllConversations)\n\t\tconversationGroup.POST(\"/get_conversation\", c.GetConversation)\n\t\tconversationGroup.POST(\"/get_conversations\", c.GetConversations)\n\t\tconversationGroup.POST(\"/set_conversations\", c.SetConversations)\n\t\t//conversationGroup.POST(\"/get_conversation_offline_push_user_ids\", c.GetConversationOfflinePushUserIDs)\n\t\tconversationGroup.POST(\"/get_full_conversation_ids\", c.GetFullOwnerConversationIDs)\n\t\tconversationGroup.POST(\"/get_incremental_conversations\", c.GetIncrementalConversation)\n\t\tconversationGroup.POST(\"/get_owner_conversation\", c.GetOwnerConversation)\n\t\tconversationGroup.POST(\"/get_not_notify_conversation_ids\", c.GetNotNotifyConversationIDs)\n\t\tconversationGroup.POST(\"/get_pinned_conversation_ids\", c.GetPinnedConversationIDs)\n\t\tconversationGroup.POST(\"/delete_conversations\", c.DeleteConversations)\n\t\tconversationGroup.POST(\"/update_conversations_by_user\", c.UpdateConversationsByUser)\n\t}\n\n\t{\n\t\tstatisticsGroup := r.Group(\"/statistics\")\n\t\tstatisticsGroup.POST(\"/user/register\", u.UserRegisterCount)\n\t\tstatisticsGroup.POST(\"/user/active\", m.GetActiveUser)\n\t\tstatisticsGroup.POST(\"/group/create\", g.GroupCreateCount)\n\t\tstatisticsGroup.POST(\"/group/active\", m.GetActiveGroup)\n\t}\n\n\t{\n\t\tj := jssdk.NewJSSdkApi(rpcli.NewUserClient(userConn), rpcli.NewRelationClient(friendConn),\n\t\t\trpcli.NewGroupClient(groupConn), rpcli.NewConversationClient(conversationConn), rpcli.NewMsgClient(msgConn))\n\t\tjssdk := r.Group(\"/jssdk\")\n\t\tjssdk.POST(\"/get_conversations\", j.GetConversations)\n\t\tjssdk.POST(\"/get_active_conversations\", j.GetActiveConversations)\n\t}\n\t{\n\t\tpd := NewPrometheusDiscoveryApi(cfg, client)\n\t\tproDiscoveryGroup := r.Group(\"/prometheus_discovery\")\n\t\tproDiscoveryGroup.GET(\"/api\", pd.Api)\n\t\tproDiscoveryGroup.GET(\"/user\", pd.User)\n\t\tproDiscoveryGroup.GET(\"/group\", pd.Group)\n\t\tproDiscoveryGroup.GET(\"/msg\", pd.Msg)\n\t\tproDiscoveryGroup.GET(\"/friend\", pd.Friend)\n\t\tproDiscoveryGroup.GET(\"/conversation\", pd.Conversation)\n\t\tproDiscoveryGroup.GET(\"/third\", pd.Third)\n\t\tproDiscoveryGroup.GET(\"/auth\", pd.Auth)\n\t\tproDiscoveryGroup.GET(\"/push\", pd.Push)\n\t\tproDiscoveryGroup.GET(\"/msg_gateway\", pd.MessageGateway)\n\t\tproDiscoveryGroup.GET(\"/msg_transfer\", pd.MessageTransfer)\n\t}\n\n\tvar etcdClient *clientv3.Client\n\tif cfg.Discovery.Enable == config.ETCD {\n\t\tetcdClient = client.(*etcd.SvcDiscoveryRegistryImpl).GetClient()\n\t}\n\tcm := NewConfigManager(cfg.Share.IMAdminUser.UserIDs, &cfg.AllConfig, etcdClient, string(cfg.ConfigPath))\n\t{\n\t\tconfigGroup := r.Group(\"/config\", cm.CheckAdmin)\n\t\tconfigGroup.POST(\"/get_config_list\", cm.GetConfigList)\n\t\tconfigGroup.POST(\"/get_config\", cm.GetConfig)\n\t\tconfigGroup.POST(\"/set_config\", cm.SetConfig)\n\t\tconfigGroup.POST(\"/reset_config\", cm.ResetConfig)\n\t\tconfigGroup.POST(\"/set_enable_config_manager\", cm.SetEnableConfigManager)\n\t\tconfigGroup.POST(\"/get_enable_config_manager\", cm.GetEnableConfigManager)\n\t}\n\t{\n\t\tr.POST(\"/restart\", cm.CheckAdmin, cm.Restart)\n\t}\n\treturn r, nil\n}\n\nfunc GinParseToken(authClient *rpcli.AuthClient) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tswitch c.Request.Method {\n\t\tcase http.MethodPost:\n\t\t\tfor _, wApi := range Whitelist {\n\t\t\t\tif strings.HasPrefix(c.Request.URL.Path, wApi) {\n\t\t\t\t\tc.Next()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttoken := c.Request.Header.Get(constant.Token)\n\t\t\tif token == \"\" {\n\t\t\t\tlog.ZWarn(c, \"header get token error\", servererrs.ErrArgs.WrapMsg(\"header must have token\"))\n\t\t\t\tapiresp.GinError(c, servererrs.ErrArgs.WrapMsg(\"header must have token\"))\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresp, err := authClient.ParseToken(c, token)\n\t\t\tif err != nil {\n\t\t\t\tapiresp.GinError(c, err)\n\t\t\t\tc.Abort()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tc.Set(constant.OpUserPlatform, constant.PlatformIDToName(int(resp.PlatformID)))\n\t\t\tc.Set(constant.OpUserID, resp.UserID)\n\t\t\tc.Next()\n\t\t}\n\t}\n}\n\nfunc setGinIsAdmin(imAdminUserID []string) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tc.Set(authverify.CtxAdminUserIDsKey, imAdminUserID)\n\t}\n}\n\n// Whitelist api not parse token\nvar Whitelist = []string{\n\t\"/auth/get_admin_token\",\n\t\"/auth/parse_token\",\n}\n"
  },
  {
    "path": "internal/api/third.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"context\"\n\t\"google.golang.org/grpc\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/a2r\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\ntype ThirdApi struct {\n\tGrafanaUrl string\n\tClient     third.ThirdClient\n}\n\nfunc NewThirdApi(client third.ThirdClient, grafanaUrl string) ThirdApi {\n\treturn ThirdApi{Client: client, GrafanaUrl: grafanaUrl}\n}\n\nfunc (o *ThirdApi) FcmUpdateToken(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.FcmUpdateToken, o.Client)\n}\n\nfunc (o *ThirdApi) SetAppBadge(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.SetAppBadge, o.Client)\n}\n\n// #################### s3 ####################\n\nfunc setURLPrefixOption[A, B, C any](_ func(client C, ctx context.Context, req *A, options ...grpc.CallOption) (*B, error), fn func(*A) error) *a2r.Option[A, B] {\n\treturn &a2r.Option[A, B]{\n\t\tBindAfter: fn,\n\t}\n}\n\nfunc setURLPrefix(c *gin.Context, urlPrefix *string) error {\n\thost := c.GetHeader(\"X-Request-Api\")\n\tif host != \"\" {\n\t\tif strings.HasSuffix(host, \"/\") {\n\t\t\t*urlPrefix = host + \"object/\"\n\t\t\treturn nil\n\t\t} else {\n\t\t\t*urlPrefix = host + \"/object/\"\n\t\t\treturn nil\n\t\t}\n\t}\n\tu := url.URL{\n\t\tScheme: \"http\",\n\t\tHost:   c.Request.Host,\n\t\tPath:   \"/object/\",\n\t}\n\tif c.Request.TLS != nil {\n\t\tu.Scheme = \"https\"\n\t}\n\t*urlPrefix = u.String()\n\treturn nil\n}\n\nfunc (o *ThirdApi) PartLimit(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.PartLimit, o.Client)\n}\n\nfunc (o *ThirdApi) PartSize(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.PartSize, o.Client)\n}\n\nfunc (o *ThirdApi) InitiateMultipartUpload(c *gin.Context) {\n\topt := setURLPrefixOption(third.ThirdClient.InitiateMultipartUpload, func(req *third.InitiateMultipartUploadReq) error {\n\t\treturn setURLPrefix(c, &req.UrlPrefix)\n\t})\n\ta2r.Call(c, third.ThirdClient.InitiateMultipartUpload, o.Client, opt)\n}\n\nfunc (o *ThirdApi) AuthSign(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.AuthSign, o.Client)\n}\n\nfunc (o *ThirdApi) CompleteMultipartUpload(c *gin.Context) {\n\topt := setURLPrefixOption(third.ThirdClient.CompleteMultipartUpload, func(req *third.CompleteMultipartUploadReq) error {\n\t\treturn setURLPrefix(c, &req.UrlPrefix)\n\t})\n\ta2r.Call(c, third.ThirdClient.CompleteMultipartUpload, o.Client, opt)\n}\n\nfunc (o *ThirdApi) AccessURL(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.AccessURL, o.Client)\n}\n\nfunc (o *ThirdApi) InitiateFormData(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.InitiateFormData, o.Client)\n}\n\nfunc (o *ThirdApi) CompleteFormData(c *gin.Context) {\n\topt := setURLPrefixOption(third.ThirdClient.CompleteFormData, func(req *third.CompleteFormDataReq) error {\n\t\treturn setURLPrefix(c, &req.UrlPrefix)\n\t})\n\ta2r.Call(c, third.ThirdClient.CompleteFormData, o.Client, opt)\n}\n\nfunc (o *ThirdApi) ObjectRedirect(c *gin.Context) {\n\tname := c.Param(\"name\")\n\tif name == \"\" {\n\t\tc.String(http.StatusBadRequest, \"name is empty\")\n\t\treturn\n\t}\n\tif name[0] == '/' {\n\t\tname = name[1:]\n\t}\n\toperationID := c.Query(\"operationID\")\n\tif operationID == \"\" {\n\t\toperationID = strconv.Itoa(rand.Int())\n\t}\n\tctx := mcontext.SetOperationID(c, operationID)\n\tquery := make(map[string]string)\n\tfor key, values := range c.Request.URL.Query() {\n\t\tif len(values) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tquery[key] = values[0]\n\t}\n\tresp, err := o.Client.AccessURL(ctx, &third.AccessURLReq{Name: name, Query: query})\n\tif err != nil {\n\t\tif errs.ErrArgs.Is(err) {\n\t\t\tc.String(http.StatusBadRequest, err.Error())\n\t\t\treturn\n\t\t}\n\t\tif errs.ErrRecordNotFound.Is(err) {\n\t\t\tc.String(http.StatusNotFound, err.Error())\n\t\t\treturn\n\t\t}\n\t\tc.String(http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tc.Redirect(http.StatusFound, resp.Url)\n}\n\n// #################### logs ####################.\nfunc (o *ThirdApi) UploadLogs(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.UploadLogs, o.Client)\n}\n\nfunc (o *ThirdApi) DeleteLogs(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.DeleteLogs, o.Client)\n}\n\nfunc (o *ThirdApi) SearchLogs(c *gin.Context) {\n\ta2r.Call(c, third.ThirdClient.SearchLogs, o.Client)\n}\n\nfunc (o *ThirdApi) GetPrometheus(c *gin.Context) {\n\tc.Redirect(http.StatusFound, o.GrafanaUrl)\n}\n"
  },
  {
    "path": "internal/api/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage api\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msggateway\"\n\t\"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/a2r\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n)\n\ntype UserApi struct {\n\tClient user.UserClient\n\tdiscov discovery.Conn\n\tconfig config.RpcService\n}\n\nfunc NewUserApi(client user.UserClient, discov discovery.Conn, config config.RpcService) UserApi {\n\treturn UserApi{Client: client, discov: discov, config: config}\n}\n\nfunc (u *UserApi) UserRegister(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.UserRegister, u.Client)\n}\n\n// UpdateUserInfo is deprecated. Use UpdateUserInfoEx\nfunc (u *UserApi) UpdateUserInfo(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.UpdateUserInfo, u.Client)\n}\n\nfunc (u *UserApi) UpdateUserInfoEx(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.UpdateUserInfoEx, u.Client)\n}\nfunc (u *UserApi) SetGlobalRecvMessageOpt(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.SetGlobalRecvMessageOpt, u.Client)\n}\n\nfunc (u *UserApi) GetUsersPublicInfo(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.GetDesignateUsers, u.Client)\n}\n\nfunc (u *UserApi) GetAllUsersID(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.GetAllUserID, u.Client)\n}\n\nfunc (u *UserApi) AccountCheck(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.AccountCheck, u.Client)\n}\n\nfunc (u *UserApi) GetUsers(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.GetPaginationUsers, u.Client)\n}\n\n// GetUsersOnlineStatus Get user online status.\nfunc (u *UserApi) GetUsersOnlineStatus(c *gin.Context) {\n\tvar req msggateway.GetUsersOnlineStatusReq\n\tif err := c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\tconns, err := u.discov.GetConns(c, u.config.MessageGateway)\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\n\tvar wsResult []*msggateway.GetUsersOnlineStatusResp_SuccessResult\n\tvar respResult []*msggateway.GetUsersOnlineStatusResp_SuccessResult\n\tflag := false\n\n\t// Online push message\n\tfor _, v := range conns {\n\t\tmsgClient := msggateway.NewMsgGatewayClient(v)\n\t\treply, err := msgClient.GetUsersOnlineStatus(c, &req)\n\t\tif err != nil {\n\t\t\tlog.ZDebug(c, \"GetUsersOnlineStatus rpc error\", err)\n\n\t\t\tparseError := apiresp.ParseError(err)\n\t\t\tif parseError.ErrCode == errs.NoPermissionError {\n\t\t\t\tapiresp.GinError(c, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\twsResult = append(wsResult, reply.SuccessResult...)\n\t\t}\n\t}\n\t// Traversing the userIDs in the api request body\n\tfor _, v1 := range req.UserIDs {\n\t\tflag = false\n\t\tres := new(msggateway.GetUsersOnlineStatusResp_SuccessResult)\n\t\t// Iterate through the online results fetched from various gateways\n\t\tfor _, v2 := range wsResult {\n\t\t\t// If matches the above description on the line, and vice versa\n\t\t\tif v2.UserID == v1 {\n\t\t\t\tflag = true\n\t\t\t\tres.UserID = v1\n\t\t\t\tres.Status = constant.Online\n\t\t\t\tres.DetailPlatformStatus = append(res.DetailPlatformStatus, v2.DetailPlatformStatus...)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !flag {\n\t\t\tres.UserID = v1\n\t\t\tres.Status = constant.Offline\n\t\t}\n\t\trespResult = append(respResult, res)\n\t}\n\tapiresp.GinSuccess(c, respResult)\n}\n\nfunc (u *UserApi) UserRegisterCount(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.UserRegisterCount, u.Client)\n}\n\n// GetUsersOnlineTokenDetail Get user online token details.\nfunc (u *UserApi) GetUsersOnlineTokenDetail(c *gin.Context) {\n\tvar wsResult []*msggateway.GetUsersOnlineStatusResp_SuccessResult\n\tvar respResult []*msggateway.SingleDetail\n\tflag := false\n\tvar req msggateway.GetUsersOnlineStatusReq\n\tif err := c.BindJSON(&req); err != nil {\n\t\tapiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap())\n\t\treturn\n\t}\n\tconns, err := u.discov.GetConns(c, u.config.MessageGateway)\n\tif err != nil {\n\t\tapiresp.GinError(c, err)\n\t\treturn\n\t}\n\t// Online push message\n\tfor _, v := range conns {\n\t\tmsgClient := msggateway.NewMsgGatewayClient(v)\n\t\treply, err := msgClient.GetUsersOnlineStatus(c, &req)\n\t\tif err != nil {\n\t\t\tlog.ZWarn(c, \"GetUsersOnlineStatus rpc err\", err)\n\t\t\tcontinue\n\t\t} else {\n\t\t\twsResult = append(wsResult, reply.SuccessResult...)\n\t\t}\n\t}\n\n\tfor _, v1 := range req.UserIDs {\n\t\tm := make(map[int32][]string, 10)\n\t\tflag = false\n\t\ttemp := new(msggateway.SingleDetail)\n\t\tfor _, v2 := range wsResult {\n\t\t\tif v2.UserID == v1 {\n\t\t\t\tflag = true\n\t\t\t\ttemp.UserID = v1\n\t\t\t\ttemp.Status = constant.Online\n\t\t\t\tfor _, status := range v2.DetailPlatformStatus {\n\t\t\t\t\tif v, ok := m[status.PlatformID]; ok {\n\t\t\t\t\t\tm[status.PlatformID] = append(v, status.Token)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tm[status.PlatformID] = []string{status.Token}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor p, tokens := range m {\n\t\t\tt := new(msggateway.SinglePlatformToken)\n\t\t\tt.PlatformID = p\n\t\t\tt.Token = tokens\n\t\t\tt.Total = int32(len(tokens))\n\t\t\ttemp.SinglePlatformToken = append(temp.SinglePlatformToken, t)\n\t\t}\n\n\t\tif flag {\n\t\t\trespResult = append(respResult, temp)\n\t\t}\n\t}\n\n\tapiresp.GinSuccess(c, respResult)\n}\n\n// SubscriberStatus Presence status of subscribed users.\nfunc (u *UserApi) SubscriberStatus(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.SubscribeOrCancelUsersStatus, u.Client)\n}\n\n// GetUserStatus Get the online status of the user.\nfunc (u *UserApi) GetUserStatus(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.GetUserStatus, u.Client)\n}\n\n// GetSubscribeUsersStatus Get the online status of subscribers.\nfunc (u *UserApi) GetSubscribeUsersStatus(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.GetSubscribeUsersStatus, u.Client)\n}\n\n// ProcessUserCommandAdd user general function add.\nfunc (u *UserApi) ProcessUserCommandAdd(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.ProcessUserCommandAdd, u.Client)\n}\n\n// ProcessUserCommandDelete user general function delete.\nfunc (u *UserApi) ProcessUserCommandDelete(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.ProcessUserCommandDelete, u.Client)\n}\n\n// ProcessUserCommandUpdate  user general function update.\nfunc (u *UserApi) ProcessUserCommandUpdate(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.ProcessUserCommandUpdate, u.Client)\n}\n\n// ProcessUserCommandGet user general function get.\nfunc (u *UserApi) ProcessUserCommandGet(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.ProcessUserCommandGet, u.Client)\n}\n\n// ProcessUserCommandGet user general function get all.\nfunc (u *UserApi) ProcessUserCommandGetAll(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.ProcessUserCommandGetAll, u.Client)\n}\n\nfunc (u *UserApi) AddNotificationAccount(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.AddNotificationAccount, u.Client)\n}\n\nfunc (u *UserApi) UpdateNotificationAccountInfo(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.UpdateNotificationAccountInfo, u.Client)\n}\n\nfunc (u *UserApi) SearchNotificationAccount(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.SearchNotificationAccount, u.Client)\n}\n\nfunc (u *UserApi) GetUserClientConfig(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.GetUserClientConfig, u.Client)\n}\n\nfunc (u *UserApi) SetUserClientConfig(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.SetUserClientConfig, u.Client)\n}\n\nfunc (u *UserApi) DelUserClientConfig(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.DelUserClientConfig, u.Client)\n}\n\nfunc (u *UserApi) PageUserClientConfig(c *gin.Context) {\n\ta2r.Call(c, user.UserClient.PageUserClientConfig, u.Client)\n}\n"
  },
  {
    "path": "internal/msggateway/callback.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\tcbapi \"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nfunc (ws *WsServer) webhookAfterUserOnline(ctx context.Context, after *config.AfterConfig, userID string, platformID int, isAppBackground bool, connID string) {\n\treq := cbapi.CallbackUserOnlineReq{\n\t\tUserStatusCallbackReq: cbapi.UserStatusCallbackReq{\n\t\t\tUserStatusBaseCallback: cbapi.UserStatusBaseCallback{\n\t\t\t\tCallbackCommand: cbapi.CallbackAfterUserOnlineCommand,\n\t\t\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\t\t\tPlatformID:      platformID,\n\t\t\t\tPlatform:        constant.PlatformIDToName(platformID),\n\t\t\t},\n\t\t\tUserID: userID,\n\t\t},\n\t\tSeq:             time.Now().UnixMilli(),\n\t\tIsAppBackground: isAppBackground,\n\t\tConnID:          connID,\n\t}\n\tws.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CommonCallbackResp{}, after)\n}\n\nfunc (ws *WsServer) webhookAfterUserOffline(ctx context.Context, after *config.AfterConfig, userID string, platformID int, connID string) {\n\treq := &cbapi.CallbackUserOfflineReq{\n\t\tUserStatusCallbackReq: cbapi.UserStatusCallbackReq{\n\t\t\tUserStatusBaseCallback: cbapi.UserStatusBaseCallback{\n\t\t\t\tCallbackCommand: cbapi.CallbackAfterUserOfflineCommand,\n\t\t\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\t\t\tPlatformID:      platformID,\n\t\t\t\tPlatform:        constant.PlatformIDToName(platformID),\n\t\t\t},\n\t\t\tUserID: userID,\n\t\t},\n\t\tSeq:    time.Now().UnixMilli(),\n\t\tConnID: connID,\n\t}\n\tws.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CallbackUserOfflineResp{}, after)\n}\n\nfunc (ws *WsServer) webhookAfterUserKickOff(ctx context.Context, after *config.AfterConfig, userID string, platformID int) {\n\treq := &cbapi.CallbackUserKickOffReq{\n\t\tUserStatusCallbackReq: cbapi.UserStatusCallbackReq{\n\t\t\tUserStatusBaseCallback: cbapi.UserStatusBaseCallback{\n\t\t\t\tCallbackCommand: cbapi.CallbackAfterUserKickOffCommand,\n\t\t\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\t\t\tPlatformID:      platformID,\n\t\t\t\tPlatform:        constant.PlatformIDToName(platformID),\n\t\t\t},\n\t\t\tUserID: userID,\n\t\t},\n\t\tSeq: time.Now().UnixMilli(),\n\t}\n\tws.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CommonCallbackResp{}, after)\n}\n"
  },
  {
    "path": "internal/msggateway/client.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nvar (\n\tErrConnClosed                = errs.New(\"conn has closed\")\n\tErrNotSupportMessageProtocol = errs.New(\"not support message protocol\")\n\tErrClientClosed              = errs.New(\"client actively close the connection\")\n\tErrPanic                     = errs.New(\"panic error\")\n)\n\nconst (\n\t// MessageText is for UTF-8 encoded text messages like JSON.\n\tMessageText = iota + 1\n\t// MessageBinary is for binary messages like protobufs.\n\tMessageBinary\n\t// CloseMessage denotes a close control message. The optional message\n\t// payload contains a numeric code and text. Use the FormatCloseMessage\n\t// function to format a close message payload.\n\tCloseMessage = 8\n\n\t// PingMessage denotes a ping control message. The optional message payload\n\t// is UTF-8 encoded text.\n\tPingMessage = 9\n\n\t// PongMessage denotes a pong control message. The optional message payload\n\t// is UTF-8 encoded text.\n\tPongMessage = 10\n)\n\ntype PingPongHandler func(string) error\n\ntype Client struct {\n\tw              *sync.Mutex\n\tconn           ClientConn\n\tPlatformID     int    `json:\"platformID\"`\n\tIsCompress     bool   `json:\"isCompress\"`\n\tUserID         string `json:\"userID\"`\n\tIsBackground   bool   `json:\"isBackground\"`\n\tSDKType        string `json:\"sdkType\"`\n\tSDKVersion     string `json:\"sdkVersion\"`\n\tEncoder        Encoder\n\tctx            *UserConnContext\n\tlongConnServer LongConnServer\n\tclosed         atomic.Bool\n\tclosedErr      error\n\ttoken          string\n\thbCtx          context.Context\n\thbCancel       context.CancelFunc\n\tsubLock        *sync.Mutex\n\tsubUserIDs     map[string]struct{} // client conn subscription list\n}\n\n// ResetClient updates the client's state with new connection and context information.\nfunc (c *Client) ResetClient(ctx *UserConnContext, conn ClientConn, longConnServer LongConnServer) {\n\tc.w = new(sync.Mutex)\n\tc.conn = conn\n\tc.PlatformID = ctx.GetPlatformID()\n\tc.IsCompress = ctx.GetCompression()\n\tc.IsBackground = ctx.GetBackground()\n\tc.UserID = ctx.GetUserID()\n\tc.ctx = ctx\n\tc.longConnServer = longConnServer\n\tc.IsBackground = false\n\tc.closed.Store(false)\n\tc.closedErr = nil\n\tc.token = ctx.GetToken()\n\tc.SDKType = ctx.GetSDKType()\n\tc.SDKVersion = ctx.GetSDKVersion()\n\tc.hbCtx, c.hbCancel = context.WithCancel(c.ctx)\n\tc.subLock = new(sync.Mutex)\n\tif c.subUserIDs != nil {\n\t\tclear(c.subUserIDs)\n\t}\n\tif c.SDKType == GoSDK {\n\t\tc.Encoder = NewGobEncoder()\n\t} else {\n\t\tc.Encoder = NewJsonEncoder()\n\t}\n\tc.subUserIDs = make(map[string]struct{})\n}\n\n// readMessage continuously reads messages from the connection.\nfunc (c *Client) readMessage() {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tc.closedErr = ErrPanic\n\t\t\tlog.ZPanic(c.ctx, \"socket have panic err:\", errs.ErrPanic(r))\n\t\t}\n\t\tc.close()\n\t}()\n\n\tfor {\n\t\tlog.ZDebug(c.ctx, \"readMessage\")\n\t\tmessage, returnErr := c.conn.ReadMessage()\n\t\tif returnErr != nil {\n\t\t\tlog.ZWarn(c.ctx, \"readMessage\", returnErr)\n\t\t\tc.closedErr = returnErr\n\t\t\treturn\n\t\t}\n\n\t\tif c.closed.Load() {\n\t\t\t// The scenario where the connection has just been closed, but the coroutine has not exited\n\t\t\tc.closedErr = ErrConnClosed\n\t\t\treturn\n\t\t}\n\n\t\tparseDataErr := c.handleMessage(message)\n\t\tif parseDataErr != nil {\n\t\t\tc.closedErr = parseDataErr\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// handleMessage processes a single message received by the client.\nfunc (c *Client) handleMessage(message []byte) error {\n\tif c.IsCompress {\n\t\tvar err error\n\t\tmessage, err = c.longConnServer.DecompressWithPool(message)\n\t\tif err != nil {\n\t\t\treturn errs.Wrap(err)\n\t\t}\n\t}\n\n\tvar binaryReq = getReq()\n\tdefer freeReq(binaryReq)\n\n\terr := c.Encoder.Decode(message, binaryReq)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := c.longConnServer.Validate(binaryReq); err != nil {\n\t\treturn err\n\t}\n\n\tif binaryReq.SendID != c.UserID {\n\t\treturn errs.New(\"exception conn userID not same to req userID\", \"binaryReq\", binaryReq.String())\n\t}\n\n\tctx := mcontext.WithMustInfoCtx(\n\t\t[]string{binaryReq.OperationID, binaryReq.SendID, constant.PlatformIDToName(c.PlatformID), c.ctx.GetConnID()},\n\t)\n\n\tlog.ZDebug(ctx, \"gateway req message\", \"req\", binaryReq.String())\n\n\tvar (\n\t\tresp       []byte\n\t\tmessageErr error\n\t)\n\n\tswitch binaryReq.ReqIdentifier {\n\tcase WSGetNewestSeq:\n\t\tresp, messageErr = c.longConnServer.GetSeq(ctx, binaryReq)\n\tcase WSSendMsg:\n\t\tresp, messageErr = c.longConnServer.SendMessage(ctx, binaryReq)\n\tcase WSSendSignalMsg:\n\t\tresp, messageErr = c.longConnServer.SendSignalMessage(ctx, binaryReq)\n\tcase WSPullMsgBySeqList:\n\t\tresp, messageErr = c.longConnServer.PullMessageBySeqList(ctx, binaryReq)\n\tcase WSPullMsg:\n\t\tresp, messageErr = c.longConnServer.GetSeqMessage(ctx, binaryReq)\n\tcase WSGetConvMaxReadSeq:\n\t\tresp, messageErr = c.longConnServer.GetConversationsHasReadAndMaxSeq(ctx, binaryReq)\n\tcase WsPullConvLastMessage:\n\t\tresp, messageErr = c.longConnServer.GetLastMessage(ctx, binaryReq)\n\tcase WsLogoutMsg:\n\t\tresp, messageErr = c.longConnServer.UserLogout(ctx, binaryReq)\n\tcase WsSetBackgroundStatus:\n\t\tresp, messageErr = c.setAppBackgroundStatus(ctx, binaryReq)\n\tcase WsSubUserOnlineStatus:\n\t\tresp, messageErr = c.longConnServer.SubUserOnlineStatus(ctx, c, binaryReq)\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t\"ReqIdentifier failed,sendID:%s,msgIncr:%s,reqIdentifier:%d\",\n\t\t\tbinaryReq.SendID,\n\t\t\tbinaryReq.MsgIncr,\n\t\t\tbinaryReq.ReqIdentifier,\n\t\t)\n\t}\n\n\treturn c.replyMessage(ctx, binaryReq, messageErr, resp)\n}\n\nfunc (c *Client) setAppBackgroundStatus(ctx context.Context, req *Req) ([]byte, error) {\n\tresp, isBackground, messageErr := c.longConnServer.SetUserDeviceBackground(ctx, req)\n\tif messageErr != nil {\n\t\treturn nil, messageErr\n\t}\n\n\tc.IsBackground = isBackground\n\t// TODO: callback\n\treturn resp, nil\n}\n\nfunc (c *Client) close() {\n\tc.w.Lock()\n\tdefer c.w.Unlock()\n\tif c.closed.Load() {\n\t\treturn\n\t}\n\tc.closed.Store(true)\n\tc.conn.Close()\n\tc.hbCancel() // Close server-initiated heartbeat.\n\tc.longConnServer.UnRegister(c)\n}\n\nfunc (c *Client) replyMessage(ctx context.Context, binaryReq *Req, err error, resp []byte) error {\n\terrResp := apiresp.ParseError(err)\n\tmReply := Resp{\n\t\tReqIdentifier: binaryReq.ReqIdentifier,\n\t\tMsgIncr:       binaryReq.MsgIncr,\n\t\tOperationID:   binaryReq.OperationID,\n\t\tErrCode:       errResp.ErrCode,\n\t\tErrMsg:        errResp.ErrMsg,\n\t\tData:          resp,\n\t}\n\tt := time.Now()\n\tlog.ZDebug(ctx, \"gateway reply message\", \"resp\", mReply.String())\n\terr = c.writeBinaryMsg(mReply)\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"wireBinaryMsg replyMessage\", err, \"resp\", mReply.String())\n\t}\n\tlog.ZDebug(ctx, \"wireBinaryMsg end\", \"time cost\", time.Since(t))\n\n\tif binaryReq.ReqIdentifier == WsLogoutMsg {\n\t\treturn errs.New(\"user logout\", \"operationID\", binaryReq.OperationID).Wrap()\n\t}\n\treturn nil\n}\n\nfunc (c *Client) PushMessage(ctx context.Context, msgData *sdkws.MsgData) error {\n\tvar msg sdkws.PushMessages\n\tconversationID := msgprocessor.GetConversationIDByMsg(msgData)\n\tm := map[string]*sdkws.PullMsgs{conversationID: {Msgs: []*sdkws.MsgData{msgData}}}\n\tif msgprocessor.IsNotification(conversationID) {\n\t\tmsg.NotificationMsgs = m\n\t} else {\n\t\tmsg.Msgs = m\n\t}\n\tlog.ZDebug(ctx, \"PushMessage\", \"msg\", &msg)\n\tdata, err := proto.Marshal(&msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp := Resp{\n\t\tReqIdentifier: WSPushMsg,\n\t\tOperationID:   mcontext.GetOperationID(ctx),\n\t\tData:          data,\n\t}\n\treturn c.writeBinaryMsg(resp)\n}\n\nfunc (c *Client) KickOnlineMessage() error {\n\tresp := Resp{\n\t\tReqIdentifier: WSKickOnlineMsg,\n\t}\n\tlog.ZDebug(c.ctx, \"KickOnlineMessage debug \")\n\terr := c.writeBinaryMsg(resp)\n\tc.close()\n\treturn err\n}\n\nfunc (c *Client) PushUserOnlineStatus(data []byte) error {\n\tresp := Resp{\n\t\tReqIdentifier: WsSubUserOnlineStatus,\n\t\tData:          data,\n\t}\n\treturn c.writeBinaryMsg(resp)\n}\n\nfunc (c *Client) writeBinaryMsg(resp Resp) error {\n\tif c.closed.Load() {\n\t\treturn nil\n\t}\n\n\tencodedBuf, err := c.Encoder.Encode(resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.w.Lock()\n\tdefer c.w.Unlock()\n\n\tif c.IsCompress {\n\t\tresultBuf, compressErr := c.longConnServer.CompressWithPool(encodedBuf)\n\t\tif compressErr != nil {\n\t\t\treturn compressErr\n\t\t}\n\t\treturn c.conn.WriteMessage(resultBuf)\n\t}\n\n\treturn c.conn.WriteMessage(encodedBuf)\n}\n"
  },
  {
    "path": "internal/msggateway/client_conn.go",
    "content": "package msggateway\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\n\t\"github.com/openimsdk/tools/log\"\n)\n\nvar ErrWriteFull = fmt.Errorf(\"websocket write buffer full,close connection\")\n\ntype ClientConn interface {\n\tReadMessage() ([]byte, error)\n\tWriteMessage(message []byte) error\n\tClose() error\n}\n\ntype websocketMessage struct {\n\tMessageType int\n\tData        []byte\n}\n\nfunc NewWebSocketClientConn(conn *websocket.Conn, readLimit int64, readTimeout time.Duration, pingInterval time.Duration) ClientConn {\n\tc := &websocketClientConn{\n\t\treadTimeout: readTimeout,\n\t\tconn:        conn,\n\t\twriter:      make(chan *websocketMessage, 256),\n\t\tdone:        make(chan struct{}),\n\t}\n\tif readLimit > 0 {\n\t\tc.conn.SetReadLimit(readLimit)\n\t}\n\tc.conn.SetPingHandler(c.pingHandler)\n\tc.conn.SetPongHandler(c.pongHandler)\n\n\tgo c.loopSend()\n\tif pingInterval > 0 {\n\t\tgo c.doPing(pingInterval)\n\t}\n\treturn c\n}\n\ntype websocketClientConn struct {\n\treadTimeout time.Duration\n\tconn        *websocket.Conn\n\twriter      chan *websocketMessage\n\tdone        chan struct{}\n\terr         atomic.Pointer[error]\n}\n\nfunc (c *websocketClientConn) ReadMessage() ([]byte, error) {\n\tbuf, err := c.readMessage()\n\tif err != nil {\n\t\treturn nil, c.closeBy(fmt.Errorf(\"read message %w\", err))\n\t}\n\treturn buf, nil\n}\n\nfunc (c *websocketClientConn) WriteMessage(message []byte) error {\n\treturn c.writeMessage(websocket.BinaryMessage, message)\n}\n\nfunc (c *websocketClientConn) Close() error {\n\treturn c.closeBy(fmt.Errorf(\"websocket connection closed\"))\n}\n\nfunc (c *websocketClientConn) closeBy(err error) error {\n\tif !c.err.CompareAndSwap(nil, &err) {\n\t\treturn *c.err.Load()\n\t}\n\tclose(c.done)\n\tlog.ZWarn(context.Background(), \"websocket connection closed\", err, \"remoteAddr\", c.conn.RemoteAddr(),\n\t\t\"chan length\", len(c.writer))\n\treturn err\n}\n\nfunc (c *websocketClientConn) writeMessage(messageType int, data []byte) error {\n\tif errPtr := c.err.Load(); errPtr != nil {\n\t\treturn *errPtr\n\t}\n\tselect {\n\tcase c.writer <- &websocketMessage{MessageType: messageType, Data: data}:\n\t\treturn nil\n\tdefault:\n\t\treturn c.closeBy(ErrWriteFull)\n\t}\n}\n\nfunc (c *websocketClientConn) loopSend() {\n\tdefer func() {\n\t\t_ = c.conn.Close()\n\t}()\n\tvar err error\n\tfor {\n\t\tselect {\n\t\tcase <-c.done:\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase msg := <-c.writer:\n\t\t\t\t\tswitch msg.MessageType {\n\t\t\t\t\tcase websocket.TextMessage, websocket.BinaryMessage:\n\t\t\t\t\t\terr = c.conn.WriteMessage(msg.MessageType, msg.Data)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\terr = c.conn.WriteControl(msg.MessageType, msg.Data, time.Time{})\n\t\t\t\t\t}\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t_ = c.closeBy(err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\tcase msg := <-c.writer:\n\t\t\tswitch msg.MessageType {\n\t\t\tcase websocket.TextMessage, websocket.BinaryMessage:\n\t\t\t\terr = c.conn.WriteMessage(msg.MessageType, msg.Data)\n\t\t\tdefault:\n\t\t\t\terr = c.conn.WriteControl(msg.MessageType, msg.Data, time.Time{})\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\t_ = c.closeBy(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *websocketClientConn) setReadDeadline() error {\n\tdeadline := time.Now().Add(c.readTimeout)\n\treturn c.conn.SetReadDeadline(deadline)\n}\n\nfunc (c *websocketClientConn) readMessage() ([]byte, error) {\n\tfor {\n\t\tif err := c.setReadDeadline(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmessageType, buf, err := c.conn.ReadMessage()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch messageType {\n\t\tcase websocket.BinaryMessage:\n\t\t\treturn buf, nil\n\t\tcase websocket.TextMessage:\n\t\t\tif err := c.onReadTextMessage(buf); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tcase websocket.PingMessage:\n\t\t\tif err := c.pingHandler(string(buf)); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tcase websocket.PongMessage:\n\t\t\tif err := c.pongHandler(string(buf)); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tcase websocket.CloseMessage:\n\t\t\tif len(buf) == 0 {\n\t\t\t\treturn nil, errors.New(\"websocket connection closed by peer\")\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"websocket connection closed by peer, data %s\", string(buf))\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unknown websocket message type %d\", messageType)\n\t\t}\n\t}\n}\n\nfunc (c *websocketClientConn) onReadTextMessage(buf []byte) error {\n\tvar msg struct {\n\t\tType string          `json:\"type\"`\n\t\tBody json.RawMessage `json:\"body\"`\n\t}\n\tif err := json.Unmarshal(buf, &msg); err != nil {\n\t\treturn err\n\t}\n\tswitch msg.Type {\n\tcase TextPong:\n\t\treturn nil\n\tcase TextPing:\n\t\tmsg.Type = TextPong\n\t\tmsgData, err := json.Marshal(msg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.writeMessage(websocket.TextMessage, msgData)\n\tdefault:\n\t\treturn fmt.Errorf(\"not support text message type %s\", msg.Type)\n\t}\n}\n\nfunc (c *websocketClientConn) pingHandler(appData string) error {\n\tlog.ZDebug(context.Background(), \"ping handler recv ping\", \"remoteAddr\", c.conn.RemoteAddr(), \"appData\", appData)\n\tif err := c.setReadDeadline(); err != nil {\n\t\treturn err\n\t}\n\terr := c.conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*1))\n\tif err != nil {\n\t\tlog.ZWarn(context.Background(), \"ping handler write pong error\", err, \"remoteAddr\", c.conn.RemoteAddr(), \"appData\", appData)\n\t}\n\tlog.ZDebug(context.Background(), \"ping handler write pong success\", \"remoteAddr\", c.conn.RemoteAddr(), \"appData\", appData)\n\treturn nil\n}\n\nfunc (c *websocketClientConn) pongHandler(string) error {\n\treturn nil\n}\n\nfunc (c *websocketClientConn) doPing(d time.Duration) {\n\tticker := time.NewTicker(d)\n\tdefer ticker.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-c.done:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\tif err := c.writeMessage(websocket.PingMessage, nil); err != nil {\n\t\t\t\t_ = c.closeBy(fmt.Errorf(\"send ping %w\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/msggateway/compressor.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"io\"\n\t\"sync\"\n\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nvar (\n\tgzipWriterPool = sync.Pool{New: func() any { return gzip.NewWriter(nil) }}\n\tgzipReaderPool = sync.Pool{New: func() any { return new(gzip.Reader) }}\n)\n\ntype Compressor interface {\n\tCompress(rawData []byte) ([]byte, error)\n\tCompressWithPool(rawData []byte) ([]byte, error)\n\tDeCompress(compressedData []byte) ([]byte, error)\n\tDecompressWithPool(compressedData []byte) ([]byte, error)\n}\n\ntype GzipCompressor struct {\n\tcompressProtocol string\n}\n\nfunc NewGzipCompressor() *GzipCompressor {\n\treturn &GzipCompressor{compressProtocol: \"gzip\"}\n}\n\nfunc (g *GzipCompressor) Compress(rawData []byte) ([]byte, error) {\n\tgzipBuffer := bytes.Buffer{}\n\tgz := gzip.NewWriter(&gzipBuffer)\n\n\tif _, err := gz.Write(rawData); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GzipCompressor.Compress: writing to gzip writer failed\")\n\t}\n\n\tif err := gz.Close(); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GzipCompressor.Compress: closing gzip writer failed\")\n\t}\n\n\treturn gzipBuffer.Bytes(), nil\n}\n\nfunc (g *GzipCompressor) CompressWithPool(rawData []byte) ([]byte, error) {\n\tgz := gzipWriterPool.Get().(*gzip.Writer)\n\tdefer gzipWriterPool.Put(gz)\n\n\tgzipBuffer := bytes.Buffer{}\n\tgz.Reset(&gzipBuffer)\n\n\tif _, err := gz.Write(rawData); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GzipCompressor.CompressWithPool: error writing data\")\n\t}\n\tif err := gz.Close(); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GzipCompressor.CompressWithPool: error closing gzip writer\")\n\t}\n\treturn gzipBuffer.Bytes(), nil\n}\n\nfunc (g *GzipCompressor) DeCompress(compressedData []byte) ([]byte, error) {\n\tbuff := bytes.NewBuffer(compressedData)\n\treader, err := gzip.NewReader(buff)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GzipCompressor.DeCompress: NewReader creation failed\")\n\t}\n\tdecompressedData, err := io.ReadAll(reader)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GzipCompressor.DeCompress: reading from gzip reader failed\")\n\t}\n\tif err = reader.Close(); err != nil {\n\t\t// Even if closing the reader fails, we've successfully read the data,\n\t\t// so we return the decompressed data and an error indicating the close failure.\n\t\treturn decompressedData, errs.WrapMsg(err, \"GzipCompressor.DeCompress: closing gzip reader failed\")\n\t}\n\treturn decompressedData, nil\n}\n\nfunc (g *GzipCompressor) DecompressWithPool(compressedData []byte) ([]byte, error) {\n\treader := gzipReaderPool.Get().(*gzip.Reader)\n\tdefer gzipReaderPool.Put(reader)\n\n\terr := reader.Reset(bytes.NewReader(compressedData))\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GzipCompressor.DecompressWithPool: resetting gzip reader failed\")\n\t}\n\n\tdecompressedData, err := io.ReadAll(reader)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GzipCompressor.DecompressWithPool: reading from pooled gzip reader failed\")\n\t}\n\tif err = reader.Close(); err != nil {\n\t\t// Similar to DeCompress, return the data and error for close failure.\n\t\treturn decompressedData, errs.WrapMsg(err, \"GzipCompressor.DecompressWithPool: closing pooled gzip reader failed\")\n\t}\n\treturn decompressedData, nil\n}\n"
  },
  {
    "path": "internal/msggateway/compressor_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"crypto/rand\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"sync\"\n\t\"testing\"\n\t\"unsafe\"\n)\n\nfunc mockRandom() []byte {\n\tbs := make([]byte, 50)\n\trand.Read(bs)\n\treturn bs\n}\n\nfunc TestCompressDecompress(t *testing.T) {\n\n\tcompressor := NewGzipCompressor()\n\n\tfor i := 0; i < 2000; i++ {\n\t\tsrc := mockRandom()\n\n\t\t// compress\n\t\tdest, err := compressor.CompressWithPool(src)\n\t\tif err != nil {\n\t\t\tt.Log(err)\n\t\t}\n\t\tassert.Equal(t, nil, err)\n\n\t\t// decompress\n\t\tres, err := compressor.DecompressWithPool(dest)\n\t\tif err != nil {\n\t\t\tt.Log(err)\n\t\t}\n\t\tassert.Equal(t, nil, err)\n\n\t\t// check\n\t\tassert.EqualValues(t, src, res)\n\t}\n}\n\nfunc TestCompressDecompressWithConcurrency(t *testing.T) {\n\twg := sync.WaitGroup{}\n\tcompressor := NewGzipCompressor()\n\n\tfor i := 0; i < 200; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tsrc := mockRandom()\n\n\t\t\t// compress\n\t\t\tdest, err := compressor.CompressWithPool(src)\n\t\t\tif err != nil {\n\t\t\t\tt.Log(err)\n\t\t\t}\n\t\t\tassert.Equal(t, nil, err)\n\n\t\t\t// decompress\n\t\t\tres, err := compressor.DecompressWithPool(dest)\n\t\t\tif err != nil {\n\t\t\t\tt.Log(err)\n\t\t\t}\n\t\t\tassert.Equal(t, nil, err)\n\n\t\t\t// check\n\t\t\tassert.EqualValues(t, src, res)\n\n\t\t}()\n\t}\n\twg.Wait()\n}\n\nfunc BenchmarkCompress(b *testing.B) {\n\tsrc := mockRandom()\n\tcompressor := NewGzipCompressor()\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := compressor.Compress(src)\n\t\tassert.Equal(b, nil, err)\n\t}\n}\n\nfunc BenchmarkCompressWithSyncPool(b *testing.B) {\n\tsrc := mockRandom()\n\n\tcompressor := NewGzipCompressor()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := compressor.CompressWithPool(src)\n\t\tassert.Equal(b, nil, err)\n\t}\n}\n\nfunc BenchmarkDecompress(b *testing.B) {\n\tsrc := mockRandom()\n\n\tcompressor := NewGzipCompressor()\n\tcomdata, err := compressor.Compress(src)\n\n\tassert.Equal(b, nil, err)\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := compressor.DeCompress(comdata)\n\t\tassert.Equal(b, nil, err)\n\t}\n}\n\nfunc BenchmarkDecompressWithSyncPool(b *testing.B) {\n\tsrc := mockRandom()\n\n\tcompressor := NewGzipCompressor()\n\tcomdata, err := compressor.Compress(src)\n\tassert.Equal(b, nil, err)\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := compressor.DecompressWithPool(comdata)\n\t\tassert.Equal(b, nil, err)\n\t}\n}\n\nfunc TestName(t *testing.T) {\n\tt.Log(unsafe.Sizeof(Client{}))\n\n}\n"
  },
  {
    "path": "internal/msggateway/constant.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport \"time\"\n\nconst (\n\tWsUserID                = \"sendID\"\n\tCommonUserID            = \"userID\"\n\tPlatformID              = \"platformID\"\n\tConnID                  = \"connID\"\n\tToken                   = \"token\"\n\tOperationID             = \"operationID\"\n\tCompression             = \"compression\"\n\tGzipCompressionProtocol = \"gzip\"\n\tBackgroundStatus        = \"isBackground\"\n\tSendResponse            = \"isMsgResp\"\n\tSDKType                 = \"sdkType\"\n\tSDKVersion              = \"sdkVersion\"\n)\n\nconst (\n\tGoSDK = \"go\"\n\tJsSDK = \"js\"\n)\n\nconst (\n\tWebSocket = iota + 1\n)\n\nconst (\n\t// Websocket Protocol.\n\tWSGetNewestSeq        = 1001\n\tWSPullMsgBySeqList    = 1002\n\tWSSendMsg             = 1003\n\tWSSendSignalMsg       = 1004\n\tWSPullMsg             = 1005\n\tWSGetConvMaxReadSeq   = 1006\n\tWsPullConvLastMessage = 1007\n\tWSPushMsg             = 2001\n\tWSKickOnlineMsg       = 2002\n\tWsLogoutMsg           = 2003\n\tWsSetBackgroundStatus = 2004\n\tWsSubUserOnlineStatus = 2005\n\tWSDataError           = 3001\n)\n\nconst (\n\t// Time allowed to write a message to the peer.\n\twriteWait = 10 * time.Second\n\n\t// Time allowed to read the next pong message from the peer.\n\tpongWait = 30 * time.Second\n\n\t// Send pings to peer with this period. Must be less than pongWait.\n\tpingPeriod = (pongWait * 9) / 10\n\n\t// Maximum message size allowed from peer.\n\tmaxMessageSize = 51200\n)\n"
  },
  {
    "path": "internal/msggateway/context.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/utils/encrypt\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n)\n\ntype UserConnContextInfo struct {\n\tToken        string `json:\"token\"`\n\tUserID       string `json:\"userID\"`\n\tPlatformID   int    `json:\"platformID\"`\n\tOperationID  string `json:\"operationID\"`\n\tCompression  string `json:\"compression\"`\n\tSDKType      string `json:\"sdkType\"`\n\tSendResponse bool   `json:\"sendResponse\"`\n\tBackground   bool   `json:\"background\"`\n\tSDKVersion   string `json:\"sdkVersion\"`\n}\n\ntype UserConnContext struct {\n\tRespWriter http.ResponseWriter\n\tReq        *http.Request\n\tPath       string\n\tMethod     string\n\tRemoteAddr string\n\tConnID     string\n\tinfo       *UserConnContextInfo\n}\n\nfunc (c *UserConnContext) Deadline() (deadline time.Time, ok bool) {\n\treturn\n}\n\nfunc (c *UserConnContext) Done() <-chan struct{} {\n\treturn nil\n}\n\nfunc (c *UserConnContext) Err() error {\n\treturn nil\n}\n\nfunc (c *UserConnContext) Value(key any) any {\n\tswitch key {\n\tcase constant.OpUserID:\n\t\treturn c.GetUserID()\n\tcase constant.OperationID:\n\t\treturn c.GetOperationID()\n\tcase constant.ConnID:\n\t\treturn c.GetConnID()\n\tcase constant.OpUserPlatform:\n\t\treturn c.GetPlatformID()\n\tcase constant.RemoteAddr:\n\t\treturn c.RemoteAddr\n\tcase SDKVersion:\n\t\treturn c.info.SDKVersion\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc newContext(respWriter http.ResponseWriter, req *http.Request) *UserConnContext {\n\tremoteAddr := req.RemoteAddr\n\tif forwarded := req.Header.Get(\"X-Forwarded-For\"); forwarded != \"\" {\n\t\tremoteAddr += \"_\" + forwarded\n\t}\n\treturn &UserConnContext{\n\t\tRespWriter: respWriter,\n\t\tReq:        req,\n\t\tPath:       req.URL.Path,\n\t\tMethod:     req.Method,\n\t\tRemoteAddr: remoteAddr,\n\t\tConnID:     encrypt.Md5(req.RemoteAddr + \"_\" + strconv.Itoa(int(timeutil.GetCurrentTimestampByMill()))),\n\t}\n}\n\nfunc newTempContext() *UserConnContext {\n\treturn &UserConnContext{\n\t\tReq:  &http.Request{URL: &url.URL{}},\n\t\tinfo: &UserConnContextInfo{},\n\t}\n}\n\nfunc (c *UserConnContext) ParseEssentialArgs() error {\n\tquery := c.Req.URL.Query()\n\tif data := query.Get(\"v\"); data != \"\" {\n\t\treturn c.parseByJson(data)\n\t} else {\n\t\treturn c.parseByQuery(query, c.Req.Header)\n\t}\n}\n\nfunc (c *UserConnContext) parseByQuery(query url.Values, header http.Header) error {\n\tinfo := UserConnContextInfo{\n\t\tToken:       query.Get(Token),\n\t\tUserID:      query.Get(WsUserID),\n\t\tOperationID: query.Get(OperationID),\n\t\tCompression: query.Get(Compression),\n\t\tSDKType:     query.Get(SDKType),\n\t\tSDKVersion:  query.Get(SDKVersion),\n\t}\n\tplatformID, err := strconv.Atoi(query.Get(PlatformID))\n\tif err != nil {\n\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"platformID is not int\")\n\t}\n\tinfo.PlatformID = platformID\n\tif val := query.Get(SendResponse); val != \"\" {\n\t\tok, err := strconv.ParseBool(val)\n\t\tif err != nil {\n\t\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"isMsgResp is not bool\")\n\t\t}\n\t\tinfo.SendResponse = ok\n\t}\n\tif info.Compression == \"\" {\n\t\tinfo.Compression = header.Get(Compression)\n\t}\n\tbackground, err := strconv.ParseBool(query.Get(BackgroundStatus))\n\tif err != nil {\n\t\treturn err\n\t}\n\tinfo.Background = background\n\treturn c.checkInfo(&info)\n}\n\nfunc (c *UserConnContext) parseByJson(data string) error {\n\treqInfo, err := base64.RawURLEncoding.DecodeString(data)\n\tif err != nil {\n\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"data is not base64\")\n\t}\n\tvar info UserConnContextInfo\n\tif err := json.Unmarshal(reqInfo, &info); err != nil {\n\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"data is not json\", \"info\", err.Error())\n\t}\n\treturn c.checkInfo(&info)\n}\n\nfunc (c *UserConnContext) checkInfo(info *UserConnContextInfo) error {\n\tif info.OperationID == \"\" {\n\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"operationID is empty\")\n\t}\n\tif info.Token == \"\" {\n\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"token is empty\")\n\t}\n\tif info.UserID == \"\" {\n\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"sendID is empty\")\n\t}\n\tif _, ok := constant.PlatformID2Name[info.PlatformID]; !ok {\n\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"platformID is invalid\")\n\t}\n\tswitch info.SDKType {\n\tcase \"\":\n\t\tinfo.SDKType = GoSDK\n\tcase GoSDK, JsSDK:\n\tdefault:\n\t\treturn servererrs.ErrConnArgsErr.WrapMsg(\"sdkType is invalid\")\n\t}\n\tc.info = info\n\treturn nil\n}\n\nfunc (c *UserConnContext) GetRemoteAddr() string {\n\treturn c.RemoteAddr\n}\n\nfunc (c *UserConnContext) SetHeader(key, value string) {\n\tc.RespWriter.Header().Set(key, value)\n}\n\nfunc (c *UserConnContext) ErrReturn(error string, code int) {\n\thttp.Error(c.RespWriter, error, code)\n}\n\nfunc (c *UserConnContext) GetConnID() string {\n\treturn c.ConnID\n}\n\nfunc (c *UserConnContext) GetUserID() string {\n\tif c == nil || c.info == nil {\n\t\treturn \"\"\n\t}\n\treturn c.info.UserID\n}\n\nfunc (c *UserConnContext) GetPlatformID() int {\n\tif c == nil || c.info == nil {\n\t\treturn 0\n\t}\n\treturn c.info.PlatformID\n}\n\nfunc (c *UserConnContext) GetOperationID() string {\n\tif c == nil || c.info == nil {\n\t\treturn \"\"\n\t}\n\treturn c.info.OperationID\n}\n\nfunc (c *UserConnContext) SetOperationID(operationID string) {\n\tif c.info == nil {\n\t\tc.info = &UserConnContextInfo{}\n\t}\n\tc.info.OperationID = operationID\n}\n\nfunc (c *UserConnContext) GetToken() string {\n\tif c == nil || c.info == nil {\n\t\treturn \"\"\n\t}\n\treturn c.info.Token\n}\n\nfunc (c *UserConnContext) GetCompression() bool {\n\treturn c != nil && c.info != nil && c.info.Compression == GzipCompressionProtocol\n}\n\nfunc (c *UserConnContext) GetSDKType() string {\n\tif c == nil || c.info == nil {\n\t\treturn GoSDK\n\t}\n\tswitch c.info.SDKType {\n\tcase \"\", GoSDK:\n\t\treturn GoSDK\n\tcase JsSDK:\n\t\treturn JsSDK\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (c *UserConnContext) GetSDKVersion() string {\n\tif c == nil || c.info == nil {\n\t\treturn \"\"\n\t}\n\treturn c.info.SDKVersion\n}\n\nfunc (c *UserConnContext) ShouldSendResp() bool {\n\treturn c != nil && c.info != nil && c.info.SendResponse\n}\n\nfunc (c *UserConnContext) SetToken(token string) {\n\tif c.info == nil {\n\t\tc.info = &UserConnContextInfo{}\n\t}\n\tc.info.Token = token\n}\n\nfunc (c *UserConnContext) GetBackground() bool {\n\treturn c != nil && c.info != nil && c.info.Background\n}\n"
  },
  {
    "path": "internal/msggateway/encoder.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"bytes\"\n\t\"encoding/gob\"\n\t\"encoding/json\"\n\n\t\"github.com/openimsdk/tools/errs\"\n)\n\ntype Encoder interface {\n\tEncode(data any) ([]byte, error)\n\tDecode(encodeData []byte, decodeData any) error\n}\n\ntype GobEncoder struct{}\n\nfunc NewGobEncoder() Encoder {\n\treturn GobEncoder{}\n}\n\nfunc (g GobEncoder) Encode(data any) ([]byte, error) {\n\tvar buff bytes.Buffer\n\tenc := gob.NewEncoder(&buff)\n\tif err := enc.Encode(data); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GobEncoder.Encode failed\", \"action\", \"encode\")\n\t}\n\treturn buff.Bytes(), nil\n}\n\nfunc (g GobEncoder) Decode(encodeData []byte, decodeData any) error {\n\tbuff := bytes.NewBuffer(encodeData)\n\tdec := gob.NewDecoder(buff)\n\tif err := dec.Decode(decodeData); err != nil {\n\t\treturn errs.WrapMsg(err, \"GobEncoder.Decode failed\", \"action\", \"decode\")\n\t}\n\treturn nil\n}\n\ntype JsonEncoder struct{}\n\nfunc NewJsonEncoder() Encoder {\n\treturn JsonEncoder{}\n}\n\nfunc (g JsonEncoder) Encode(data any) ([]byte, error) {\n\tb, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn nil, errs.New(\"JsonEncoder.Encode failed\", \"action\", \"encode\")\n\t}\n\treturn b, nil\n}\n\nfunc (g JsonEncoder) Decode(encodeData []byte, decodeData any) error {\n\terr := json.Unmarshal(encodeData, decodeData)\n\tif err != nil {\n\t\treturn errs.New(\"JsonEncoder.Decode failed\", \"action\", \"decode\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/msggateway/http_error.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/log\"\n)\n\nfunc httpError(ctx *UserConnContext, err error) {\n\tlog.ZWarn(ctx, \"ws connection error\", err)\n\tapiresp.HttpError(ctx.RespWriter, err)\n}\n"
  },
  {
    "path": "internal/msggateway/hub_server.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"context\"\n\t\"sync/atomic\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msggateway\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/mq/memamq\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc (s *Server) InitServer(ctx context.Context, config *Config, disCov discovery.Conn, server grpc.ServiceRegistrar) error {\n\tuserConn, err := disCov.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.userClient = rpcli.NewUserClient(userConn)\n\tif err := s.LongConnServer.SetDiscoveryRegistry(ctx, disCov, config); err != nil {\n\t\treturn err\n\t}\n\tmsggateway.RegisterMsgGatewayServer(server, s)\n\tif s.ready != nil {\n\t\treturn s.ready(s)\n\t}\n\treturn nil\n}\n\n//func (s *Server) Start(ctx context.Context, index int, conf *Config) error {\n//\treturn startrpc.Start(ctx, &conf.Discovery, &conf.MsgGateway.Prometheus, conf.MsgGateway.ListenIP,\n//\t\tconf.MsgGateway.RPC.RegisterIP,\n//\t\tconf.MsgGateway.RPC.AutoSetPorts, conf.MsgGateway.RPC.Ports, index,\n//\t\tconf.Discovery.RpcService.MessageGateway,\n//\t\tnil,\n//\t\tconf,\n//\t\t[]string{\n//\t\t\tconf.Share.GetConfigFileName(),\n//\t\t\tconf.Discovery.GetConfigFileName(),\n//\t\t\tconf.MsgGateway.GetConfigFileName(),\n//\t\t\tconf.WebhooksConfig.GetConfigFileName(),\n//\t\t\tconf.RedisConfig.GetConfigFileName(),\n//\t\t},\n//\t\t[]string{\n//\t\t\tconf.Discovery.RpcService.MessageGateway,\n//\t\t},\n//\t\ts.InitServer,\n//\t)\n//}\n\ntype Server struct {\n\tmsggateway.UnimplementedMsgGatewayServer\n\n\tLongConnServer LongConnServer\n\tconfig         *Config\n\tpushTerminal   map[int]struct{}\n\tready          func(srv *Server) error\n\tqueue          *memamq.MemoryQueue\n\tuserClient     *rpcli.UserClient\n}\n\nfunc (s *Server) SetLongConnServer(LongConnServer LongConnServer) {\n\ts.LongConnServer = LongConnServer\n}\n\nfunc NewServer(longConnServer LongConnServer, conf *Config, ready func(srv *Server) error) *Server {\n\ts := &Server{\n\t\tLongConnServer: longConnServer,\n\t\tpushTerminal:   make(map[int]struct{}),\n\t\tconfig:         conf,\n\t\tready:          ready,\n\t\tqueue:          memamq.NewMemoryQueue(512, 1024*16),\n\t}\n\ts.pushTerminal[constant.IOSPlatformID] = struct{}{}\n\ts.pushTerminal[constant.AndroidPlatformID] = struct{}{}\n\treturn s\n}\n\nfunc (s *Server) GetUsersOnlineStatus(ctx context.Context, req *msggateway.GetUsersOnlineStatusReq) (*msggateway.GetUsersOnlineStatusResp, error) {\n\tif !authverify.IsAdmin(ctx) {\n\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"only app manager\")\n\t}\n\tvar resp msggateway.GetUsersOnlineStatusResp\n\tfor _, userID := range req.UserIDs {\n\t\tclients, ok := s.LongConnServer.GetUserAllCons(userID)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\turesp := new(msggateway.GetUsersOnlineStatusResp_SuccessResult)\n\t\turesp.UserID = userID\n\t\tfor _, client := range clients {\n\t\t\tif client == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tps := new(msggateway.GetUsersOnlineStatusResp_SuccessDetail)\n\t\t\tps.PlatformID = int32(client.PlatformID)\n\t\t\tps.ConnID = client.ctx.GetConnID()\n\t\t\tps.Token = client.token\n\t\t\tps.IsBackground = client.IsBackground\n\t\t\turesp.Status = constant.Online\n\t\t\turesp.DetailPlatformStatus = append(uresp.DetailPlatformStatus, ps)\n\t\t}\n\t\tif uresp.Status == constant.Online {\n\t\t\tresp.SuccessResult = append(resp.SuccessResult, uresp)\n\t\t}\n\t}\n\treturn &resp, nil\n}\n\nfunc (s *Server) pushToUser(ctx context.Context, userID string, msgData *sdkws.MsgData) *msggateway.SingleMsgToUserResults {\n\tclients, ok := s.LongConnServer.GetUserAllCons(userID)\n\tif !ok {\n\t\tlog.ZDebug(ctx, \"push user not online\", \"userID\", userID)\n\t\treturn &msggateway.SingleMsgToUserResults{\n\t\t\tUserID: userID,\n\t\t}\n\t}\n\tlog.ZDebug(ctx, \"push user online\", \"clients\", clients, \"userID\", userID)\n\tresult := &msggateway.SingleMsgToUserResults{\n\t\tUserID: userID,\n\t\tResp:   make([]*msggateway.SingleMsgToUserPlatform, 0, len(clients)),\n\t}\n\tfor _, client := range clients {\n\t\tif client == nil {\n\t\t\tcontinue\n\t\t}\n\t\tuserPlatform := &msggateway.SingleMsgToUserPlatform{\n\t\t\tRecvPlatFormID: int32(client.PlatformID),\n\t\t}\n\t\tif client.IsBackground && client.PlatformID == constant.IOSPlatformID {\n\t\t\tuserPlatform.ResultCode = int64(servererrs.ErrIOSBackgroundPushErr.Code())\n\t\t\tresult.Resp = append(result.Resp, userPlatform)\n\t\t\tcontinue\n\t\t}\n\t\tif err := client.PushMessage(ctx, msgData); err != nil {\n\t\t\tlog.ZWarn(ctx, \"online push msg failed\", err, \"userID\", userID, \"platformID\", client.PlatformID)\n\t\t\tuserPlatform.ResultCode = int64(servererrs.ErrPushMsgErr.Code())\n\t\t} else if _, ok := s.pushTerminal[client.PlatformID]; ok {\n\t\t\tresult.OnlinePush = true\n\t\t}\n\t\tresult.Resp = append(result.Resp, userPlatform)\n\t}\n\treturn result\n}\n\nfunc (s *Server) SuperGroupOnlineBatchPushOneMsg(ctx context.Context, req *msggateway.OnlineBatchPushOneMsgReq) (*msggateway.OnlineBatchPushOneMsgResp, error) {\n\tif len(req.PushToUserIDs) == 0 {\n\t\treturn &msggateway.OnlineBatchPushOneMsgResp{}, nil\n\t}\n\tch := make(chan *msggateway.SingleMsgToUserResults, len(req.PushToUserIDs))\n\tvar count atomic.Int64\n\tcount.Add(int64(len(req.PushToUserIDs)))\n\tfor i := range req.PushToUserIDs {\n\t\tuserID := req.PushToUserIDs[i]\n\t\terr := s.queue.PushCtx(ctx, func() {\n\t\t\tch <- s.pushToUser(ctx, userID, req.MsgData)\n\t\t\tif count.Add(-1) == 0 {\n\t\t\t\tclose(ch)\n\t\t\t}\n\t\t})\n\t\tif err != nil {\n\t\t\tif count.Add(-1) == 0 {\n\t\t\t\tclose(ch)\n\t\t\t}\n\t\t\tlog.ZError(ctx, \"pushToUser MemoryQueue failed\", err, \"userID\", userID)\n\t\t\tch <- &msggateway.SingleMsgToUserResults{\n\t\t\t\tUserID: userID,\n\t\t\t}\n\t\t}\n\t}\n\tresp := &msggateway.OnlineBatchPushOneMsgResp{\n\t\tSinglePushResult: make([]*msggateway.SingleMsgToUserResults, 0, len(req.PushToUserIDs)),\n\t}\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlog.ZError(ctx, \"SuperGroupOnlineBatchPushOneMsg ctx done\", context.Cause(ctx))\n\t\t\tuserIDSet := datautil.SliceSet(req.PushToUserIDs)\n\t\t\tfor _, results := range resp.SinglePushResult {\n\t\t\t\tdelete(userIDSet, results.UserID)\n\t\t\t}\n\t\t\tfor userID := range userIDSet {\n\t\t\t\tresp.SinglePushResult = append(resp.SinglePushResult, &msggateway.SingleMsgToUserResults{\n\t\t\t\t\tUserID: userID,\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn resp, nil\n\t\tcase res, ok := <-ch:\n\t\t\tif !ok {\n\t\t\t\treturn resp, nil\n\t\t\t}\n\t\t\tresp.SinglePushResult = append(resp.SinglePushResult, res)\n\t\t}\n\t}\n}\n\nfunc (s *Server) KickUserOffline(ctx context.Context, req *msggateway.KickUserOfflineReq) (*msggateway.KickUserOfflineResp, error) {\n\tfor _, v := range req.KickUserIDList {\n\t\tclients, _, ok := s.LongConnServer.GetUserPlatformCons(v, int(req.PlatformID))\n\t\tif !ok {\n\t\t\tlog.ZDebug(ctx, \"conn not exist\", \"userID\", v, \"platformID\", req.PlatformID)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, client := range clients {\n\t\t\tlog.ZDebug(ctx, \"kick user offline\", \"userID\", v, \"platformID\", req.PlatformID, \"client\", client)\n\t\t\tif err := client.longConnServer.KickUserConn(client); err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"kick user offline failed\", err, \"userID\", v, \"platformID\", req.PlatformID)\n\t\t\t}\n\t\t}\n\t\tcontinue\n\t}\n\n\treturn &msggateway.KickUserOfflineResp{}, nil\n}\n\nfunc (s *Server) MultiTerminalLoginCheck(ctx context.Context, req *msggateway.MultiTerminalLoginCheckReq) (*msggateway.MultiTerminalLoginCheckResp, error) {\n\tif oldClients, userOK, clientOK := s.LongConnServer.GetUserPlatformCons(req.UserID, int(req.PlatformID)); userOK {\n\t\ttempUserCtx := newTempContext()\n\t\ttempUserCtx.SetToken(req.Token)\n\t\ttempUserCtx.SetOperationID(mcontext.GetOperationID(ctx))\n\t\tclient := &Client{}\n\t\tclient.ctx = tempUserCtx\n\t\tclient.token = req.Token\n\t\tclient.UserID = req.UserID\n\t\tclient.PlatformID = int(req.PlatformID)\n\t\ti := &kickHandler{\n\t\t\tclientOK:   clientOK,\n\t\t\toldClients: oldClients,\n\t\t\tnewClient:  client,\n\t\t}\n\t\ts.LongConnServer.SetKickHandlerInfo(i)\n\t}\n\treturn &msggateway.MultiTerminalLoginCheckResp{}, nil\n}\n"
  },
  {
    "path": "internal/msggateway/init.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpccache\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/openimsdk/tools/log\"\n)\n\ntype Config struct {\n\tMsgGateway     config.MsgGateway\n\tShare          config.Share\n\tRedisConfig    config.Redis\n\tWebhooksConfig config.Webhooks\n\tDiscovery      config.Discovery\n\tIndex          config.Index\n}\n\n// Start run ws server.\nfunc Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tlog.CInfo(ctx, \"MSG-GATEWAY server is initializing\", \"runtimeEnv\", runtimeenv.RuntimeEnvironment(),\n\t\t\"rpcPorts\", conf.MsgGateway.RPC.Ports,\n\t\t\"wsPort\", conf.MsgGateway.LongConnSvr.Ports, \"prometheusPorts\", conf.MsgGateway.Prometheus.Ports)\n\twsPort, err := datautil.GetElemByIndex(conf.MsgGateway.LongConnSvr.Ports, int(conf.Index))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdbb := dbbuild.NewBuilder(nil, &conf.RedisConfig)\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlongServer := NewWsServer(\n\t\tconf,\n\t\tWithPort(wsPort),\n\t\tWithMaxConnNum(int64(conf.MsgGateway.LongConnSvr.WebsocketMaxConnNum)),\n\t\tWithHandshakeTimeout(time.Duration(conf.MsgGateway.LongConnSvr.WebsocketTimeout)*time.Second),\n\t\tWithMessageMaxMsgLength(conf.MsgGateway.LongConnSvr.WebsocketMaxMsgLen),\n\t)\n\n\thubServer := NewServer(longServer, conf, func(srv *Server) error {\n\t\tvar err error\n\t\tlongServer.online, err = rpccache.NewOnlineCache(srv.userClient, nil, rdb, false, longServer.subscriberUserOnlineStatusChanges)\n\t\treturn err\n\t})\n\n\tif err := hubServer.InitServer(ctx, conf, client, server); err != nil {\n\t\treturn err\n\t}\n\n\tgo longServer.ChangeOnlineStatus(4)\n\n\treturn hubServer.LongConnServer.Run(ctx)\n}\n\n//\n//// Start run ws server.\n//func Start(ctx context.Context, index int, conf *Config) error {\n//\tlog.CInfo(ctx, \"MSG-GATEWAY server is initializing\", \"runtimeEnv\", runtimeenv.RuntimeEnvironment(),\n//\t\t\"rpcPorts\", conf.MsgGateway.RPC.Ports,\n//\t\t\"wsPort\", conf.MsgGateway.LongConnSvr.Ports, \"prometheusPorts\", conf.MsgGateway.Prometheus.Ports)\n//\twsPort, err := datautil.GetElemByIndex(conf.MsgGateway.LongConnSvr.Ports, index)\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\n//\trdb, err := redisutil.NewRedisClient(ctx, conf.RedisConfig.Build())\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\tlongServer := NewWsServer(\n//\t\tconf,\n//\t\tWithPort(wsPort),\n//\t\tWithMaxConnNum(int64(conf.MsgGateway.LongConnSvr.WebsocketMaxConnNum)),\n//\t\tWithHandshakeTimeout(time.Duration(conf.MsgGateway.LongConnSvr.WebsocketTimeout)*time.Second),\n//\t\tWithMessageMaxMsgLength(conf.MsgGateway.LongConnSvr.WebsocketMaxMsgLen),\n//\t)\n//\n//\thubServer := NewServer(longServer, conf, func(srv *Server) error {\n//\t\tvar err error\n//\t\tlongServer.online, err = rpccache.NewOnlineCache(srv.userClient, nil, rdb, false, longServer.subscriberUserOnlineStatusChanges)\n//\t\treturn err\n//\t})\n//\n//\tgo longServer.ChangeOnlineStatus(4)\n//\n//\tnetDone := make(chan error)\n//\tgo func() {\n//\t\terr = hubServer.Start(ctx, index, conf)\n//\t\tnetDone <- err\n//\t}()\n//\treturn hubServer.LongConnServer.Run(netDone)\n//}\n"
  },
  {
    "path": "internal/msggateway/message_handler.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"sync\"\n\n\t\"github.com/go-playground/validator/v10\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/push\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n)\n\nconst (\n\tTextPing = \"ping\"\n\tTextPong = \"pong\"\n)\n\ntype TextMessage struct {\n\tType string          `json:\"type\"`\n\tBody json.RawMessage `json:\"body\"`\n}\n\ntype Req struct {\n\tReqIdentifier int32  `json:\"reqIdentifier\" validate:\"required\"`\n\tToken         string `json:\"token\"`\n\tSendID        string `json:\"sendID\"        validate:\"required\"`\n\tOperationID   string `json:\"operationID\"   validate:\"required\"`\n\tMsgIncr       string `json:\"msgIncr\"       validate:\"required\"`\n\tData          []byte `json:\"data\"`\n}\n\nfunc (r *Req) String() string {\n\tvar tReq Req\n\ttReq.ReqIdentifier = r.ReqIdentifier\n\ttReq.Token = r.Token\n\ttReq.SendID = r.SendID\n\ttReq.OperationID = r.OperationID\n\ttReq.MsgIncr = r.MsgIncr\n\treturn jsonutil.StructToJsonString(tReq)\n}\n\nvar reqPool = sync.Pool{\n\tNew: func() any {\n\t\treturn new(Req)\n\t},\n}\n\nfunc getReq() *Req {\n\treq := reqPool.Get().(*Req)\n\treq.Data = nil\n\treq.MsgIncr = \"\"\n\treq.OperationID = \"\"\n\treq.ReqIdentifier = 0\n\treq.SendID = \"\"\n\treq.Token = \"\"\n\treturn req\n}\n\nfunc freeReq(req *Req) {\n\treqPool.Put(req)\n}\n\ntype Resp struct {\n\tReqIdentifier int32  `json:\"reqIdentifier\"`\n\tMsgIncr       string `json:\"msgIncr\"`\n\tOperationID   string `json:\"operationID\"`\n\tErrCode       int    `json:\"errCode\"`\n\tErrMsg        string `json:\"errMsg\"`\n\tData          []byte `json:\"data\"`\n}\n\nfunc (r *Resp) String() string {\n\tvar tResp Resp\n\ttResp.ReqIdentifier = r.ReqIdentifier\n\ttResp.MsgIncr = r.MsgIncr\n\ttResp.OperationID = r.OperationID\n\ttResp.ErrCode = r.ErrCode\n\ttResp.ErrMsg = r.ErrMsg\n\treturn jsonutil.StructToJsonString(tResp)\n}\n\ntype MessageHandler interface {\n\tGetSeq(ctx context.Context, data *Req) ([]byte, error)\n\tSendMessage(ctx context.Context, data *Req) ([]byte, error)\n\tSendSignalMessage(ctx context.Context, data *Req) ([]byte, error)\n\tPullMessageBySeqList(ctx context.Context, data *Req) ([]byte, error)\n\tGetConversationsHasReadAndMaxSeq(ctx context.Context, data *Req) ([]byte, error)\n\tGetSeqMessage(ctx context.Context, data *Req) ([]byte, error)\n\tUserLogout(ctx context.Context, data *Req) ([]byte, error)\n\tSetUserDeviceBackground(ctx context.Context, data *Req) ([]byte, bool, error)\n\tGetLastMessage(ctx context.Context, data *Req) ([]byte, error)\n}\n\nvar _ MessageHandler = (*GrpcHandler)(nil)\n\ntype GrpcHandler struct {\n\tvalidate   *validator.Validate\n\tmsgClient  *rpcli.MsgClient\n\tpushClient *rpcli.PushMsgServiceClient\n}\n\nfunc NewGrpcHandler(validate *validator.Validate, msgClient *rpcli.MsgClient, pushClient *rpcli.PushMsgServiceClient) *GrpcHandler {\n\treturn &GrpcHandler{\n\t\tvalidate:   validate,\n\t\tmsgClient:  msgClient,\n\t\tpushClient: pushClient,\n\t}\n}\n\nfunc (g *GrpcHandler) GetSeq(ctx context.Context, data *Req) ([]byte, error) {\n\treq := sdkws.GetMaxSeqReq{}\n\tif err := proto.Unmarshal(data.Data, &req); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GetSeq: error unmarshaling request\", \"action\", \"unmarshal\", \"dataType\", \"GetMaxSeqReq\")\n\t}\n\tif err := g.validate.Struct(&req); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GetSeq: validation failed\", \"action\", \"validate\", \"dataType\", \"GetMaxSeqReq\")\n\t}\n\tresp, err := g.msgClient.MsgClient.GetMaxSeq(ctx, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := proto.Marshal(resp)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"GetSeq: error marshaling response\", \"action\", \"marshal\", \"dataType\", \"GetMaxSeqResp\")\n\t}\n\treturn c, nil\n}\n\n// SendMessage handles the sending of messages through gRPC. It unmarshals the request data,\n// validates the message, and then sends it using the message RPC client.\nfunc (g *GrpcHandler) SendMessage(ctx context.Context, data *Req) ([]byte, error) {\n\tvar msgData sdkws.MsgData\n\tif err := proto.Unmarshal(data.Data, &msgData); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"SendMessage: error unmarshaling message data\", \"action\", \"unmarshal\", \"dataType\", \"MsgData\")\n\t}\n\n\tif err := g.validate.Struct(&msgData); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"SendMessage: message data validation failed\", \"action\", \"validate\", \"dataType\", \"MsgData\")\n\t}\n\n\treq := msg.SendMsgReq{MsgData: &msgData}\n\tresp, err := g.msgClient.MsgClient.SendMsg(ctx, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc, err := proto.Marshal(resp)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"SendMessage: error marshaling response\", \"action\", \"marshal\", \"dataType\", \"SendMsgResp\")\n\t}\n\n\treturn c, nil\n}\n\nfunc (g *GrpcHandler) SendSignalMessage(ctx context.Context, data *Req) ([]byte, error) {\n\tresp, err := g.msgClient.MsgClient.SendMsg(ctx, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := proto.Marshal(resp)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"error marshaling response\", \"action\", \"marshal\", \"dataType\", \"SendMsgResp\")\n\t}\n\treturn c, nil\n}\n\nfunc (g *GrpcHandler) PullMessageBySeqList(ctx context.Context, data *Req) ([]byte, error) {\n\treq := sdkws.PullMessageBySeqsReq{}\n\tif err := proto.Unmarshal(data.Data, &req); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"err proto unmarshal\", \"action\", \"unmarshal\", \"dataType\", \"PullMessageBySeqsReq\")\n\t}\n\tif err := g.validate.Struct(data); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"validation failed\", \"action\", \"validate\", \"dataType\", \"PullMessageBySeqsReq\")\n\t}\n\tresp, err := g.msgClient.MsgClient.PullMessageBySeqs(ctx, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := proto.Marshal(resp)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"error marshaling response\", \"action\", \"marshal\", \"dataType\", \"PullMessageBySeqsResp\")\n\t}\n\treturn c, nil\n}\n\nfunc (g *GrpcHandler) GetConversationsHasReadAndMaxSeq(ctx context.Context, data *Req) ([]byte, error) {\n\treq := msg.GetConversationsHasReadAndMaxSeqReq{}\n\tif err := proto.Unmarshal(data.Data, &req); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"err proto unmarshal\", \"action\", \"unmarshal\", \"dataType\", \"GetConversationsHasReadAndMaxSeq\")\n\t}\n\tif err := g.validate.Struct(data); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"validation failed\", \"action\", \"validate\", \"dataType\", \"GetConversationsHasReadAndMaxSeq\")\n\t}\n\tresp, err := g.msgClient.MsgClient.GetConversationsHasReadAndMaxSeq(ctx, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := proto.Marshal(resp)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"error marshaling response\", \"action\", \"marshal\", \"dataType\", \"GetConversationsHasReadAndMaxSeq\")\n\t}\n\treturn c, nil\n}\n\nfunc (g *GrpcHandler) GetSeqMessage(ctx context.Context, data *Req) ([]byte, error) {\n\treq := msg.GetSeqMessageReq{}\n\tif err := proto.Unmarshal(data.Data, &req); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"error unmarshaling request\", \"action\", \"unmarshal\", \"dataType\", \"GetSeqMessage\")\n\t}\n\tif err := g.validate.Struct(data); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"validation failed\", \"action\", \"validate\", \"dataType\", \"GetSeqMessage\")\n\t}\n\tresp, err := g.msgClient.MsgClient.GetSeqMessage(ctx, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := proto.Marshal(resp)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"error marshaling response\", \"action\", \"marshal\", \"dataType\", \"GetSeqMessage\")\n\t}\n\treturn c, nil\n}\n\nfunc (g *GrpcHandler) UserLogout(ctx context.Context, data *Req) ([]byte, error) {\n\treq := push.DelUserPushTokenReq{}\n\tif err := proto.Unmarshal(data.Data, &req); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"error unmarshaling request\", \"action\", \"unmarshal\", \"dataType\", \"DelUserPushTokenReq\")\n\t}\n\tresp, err := g.pushClient.PushMsgServiceClient.DelUserPushToken(ctx, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := proto.Marshal(resp)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"error marshaling response\", \"action\", \"marshal\", \"dataType\", \"DelUserPushTokenResp\")\n\t}\n\treturn c, nil\n}\n\nfunc (g *GrpcHandler) SetUserDeviceBackground(ctx context.Context, data *Req) ([]byte, bool, error) {\n\treq := sdkws.SetAppBackgroundStatusReq{}\n\tif err := proto.Unmarshal(data.Data, &req); err != nil {\n\t\treturn nil, false, errs.WrapMsg(err, \"error unmarshaling request\", \"action\", \"unmarshal\", \"dataType\", \"SetAppBackgroundStatusReq\")\n\t}\n\tif err := g.validate.Struct(data); err != nil {\n\t\treturn nil, false, errs.WrapMsg(err, \"validation failed\", \"action\", \"validate\", \"dataType\", \"SetAppBackgroundStatusReq\")\n\t}\n\treturn nil, req.IsBackground, nil\n}\n\nfunc (g *GrpcHandler) GetLastMessage(ctx context.Context, data *Req) ([]byte, error) {\n\tvar req msg.GetLastMessageReq\n\tif err := proto.Unmarshal(data.Data, &req); err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := g.msgClient.GetLastMessage(ctx, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn proto.Marshal(resp)\n}\n"
  },
  {
    "path": "internal/msggateway/online.go",
    "content": "package msggateway\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"strconv\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\tpbuser \"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc (ws *WsServer) ChangeOnlineStatus(concurrent int) {\n\tif concurrent < 1 {\n\t\tconcurrent = 1\n\t}\n\tconst renewalTime = cachekey.OnlineExpire / 3\n\t//const renewalTime = time.Second * 10\n\trenewalTicker := time.NewTicker(renewalTime)\n\n\trequestChs := make([]chan *pbuser.SetUserOnlineStatusReq, concurrent)\n\tchangeStatus := make([][]UserState, concurrent)\n\n\tfor i := 0; i < concurrent; i++ {\n\t\trequestChs[i] = make(chan *pbuser.SetUserOnlineStatusReq, 64)\n\t\tchangeStatus[i] = make([]UserState, 0, 100)\n\t}\n\n\tmergeTicker := time.NewTicker(time.Second)\n\n\tlocal2pb := func(u UserState) *pbuser.UserOnlineStatus {\n\t\treturn &pbuser.UserOnlineStatus{\n\t\t\tUserID:  u.UserID,\n\t\t\tOnline:  u.Online,\n\t\t\tOffline: u.Offline,\n\t\t}\n\t}\n\n\trNum := rand.Uint64()\n\tpushUserState := func(us ...UserState) {\n\t\tfor _, u := range us {\n\t\t\tsum := md5.Sum([]byte(u.UserID))\n\t\t\ti := (binary.BigEndian.Uint64(sum[:]) + rNum) % uint64(concurrent)\n\t\t\tchangeStatus[i] = append(changeStatus[i], u)\n\t\t\tstatus := changeStatus[i]\n\t\t\tif len(status) == cap(status) {\n\t\t\t\treq := &pbuser.SetUserOnlineStatusReq{\n\t\t\t\t\tStatus: datautil.Slice(status, local2pb),\n\t\t\t\t}\n\t\t\t\tchangeStatus[i] = status[:0]\n\t\t\t\tselect {\n\t\t\t\tcase requestChs[i] <- req:\n\t\t\t\tdefault:\n\t\t\t\t\tlog.ZError(context.Background(), \"user online processing is too slow\", nil)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tpushAllUserState := func() {\n\t\tfor i, status := range changeStatus {\n\t\t\tif len(status) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treq := &pbuser.SetUserOnlineStatusReq{\n\t\t\t\tStatus: datautil.Slice(status, local2pb),\n\t\t\t}\n\t\t\tchangeStatus[i] = status[:0]\n\t\t\tselect {\n\t\t\tcase requestChs[i] <- req:\n\t\t\tdefault:\n\t\t\t\tlog.ZError(context.Background(), \"user online processing is too slow\", nil)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar count atomic.Int64\n\toperationIDPrefix := fmt.Sprintf(\"p_%d_\", os.Getpid())\n\tdoRequest := func(req *pbuser.SetUserOnlineStatusReq) {\n\t\topIdCtx := mcontext.SetOperationID(context.Background(), operationIDPrefix+strconv.FormatInt(count.Add(1), 10))\n\t\tctx, cancel := context.WithTimeout(opIdCtx, time.Second*5)\n\t\tdefer cancel()\n\t\tif err := ws.userClient.SetUserOnlineStatus(ctx, req); err != nil {\n\t\t\tlog.ZError(ctx, \"update user online status\", err)\n\t\t}\n\t\tfor _, ss := range req.Status {\n\t\t\tfor _, online := range ss.Online {\n\t\t\t\tclient, _, _ := ws.clients.Get(ss.UserID, int(online))\n\t\t\t\tback := false\n\t\t\t\tif len(client) > 0 {\n\t\t\t\t\tback = client[0].IsBackground\n\t\t\t\t}\n\t\t\t\tws.webhookAfterUserOnline(ctx, &ws.msgGatewayConfig.WebhooksConfig.AfterUserOnline, ss.UserID, int(online), back, ss.ConnID)\n\t\t\t}\n\t\t\tfor _, offline := range ss.Offline {\n\t\t\t\tws.webhookAfterUserOffline(ctx, &ws.msgGatewayConfig.WebhooksConfig.AfterUserOffline, ss.UserID, int(offline), ss.ConnID)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := 0; i < concurrent; i++ {\n\t\tgo func(ch <-chan *pbuser.SetUserOnlineStatusReq) {\n\t\t\tfor req := range ch {\n\t\t\t\tdoRequest(req)\n\t\t\t}\n\t\t}(requestChs[i])\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase <-mergeTicker.C:\n\t\t\tpushAllUserState()\n\t\tcase now := <-renewalTicker.C:\n\t\t\tdeadline := now.Add(-cachekey.OnlineExpire / 3)\n\t\t\tusers := ws.clients.GetAllUserStatus(deadline, now)\n\t\t\tlog.ZDebug(context.Background(), \"renewal ticker\", \"deadline\", deadline, \"nowtime\", now, \"num\", len(users), \"users\", users)\n\t\t\tpushUserState(users...)\n\t\tcase state := <-ws.clients.UserState():\n\t\t\tlog.ZDebug(context.Background(), \"OnlineCache user online change\", \"userID\", state.UserID, \"online\", state.Online, \"offline\", state.Offline)\n\t\t\tpushUserState(state)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/msggateway/options.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msggateway\n\nimport \"time\"\n\ntype (\n\tOption  func(opt *configs)\n\tconfigs struct {\n\t\t// Long connection listening port\n\t\tport int\n\t\t// Maximum number of connections allowed for long connection\n\t\tmaxConnNum int64\n\t\t// Connection handshake timeout\n\t\thandshakeTimeout time.Duration\n\t\t// Maximum length allowed for messages\n\t\tmessageMaxMsgLength int\n\t\t// Websocket write buffer, default: 4096, 4kb.\n\t\twriteBufferSize int\n\t}\n)\n\nfunc WithPort(port int) Option {\n\treturn func(opt *configs) {\n\t\topt.port = port\n\t}\n}\n\nfunc WithMaxConnNum(num int64) Option {\n\treturn func(opt *configs) {\n\t\topt.maxConnNum = num\n\t}\n}\n\nfunc WithHandshakeTimeout(t time.Duration) Option {\n\treturn func(opt *configs) {\n\t\topt.handshakeTimeout = t\n\t}\n}\n\nfunc WithMessageMaxMsgLength(length int) Option {\n\treturn func(opt *configs) {\n\t\topt.messageMaxMsgLength = length\n\t}\n}\n\nfunc WithWriteBufferSize(size int) Option {\n\treturn func(opt *configs) {\n\t\topt.writeBufferSize = size\n\t}\n}\n"
  },
  {
    "path": "internal/msggateway/subscription.go",
    "content": "package msggateway\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"sync\"\n)\n\nfunc (ws *WsServer) subscriberUserOnlineStatusChanges(ctx context.Context, userID string, platformIDs []int32) {\n\tif ws.clients.RecvSubChange(userID, platformIDs) {\n\t\tlog.ZDebug(ctx, \"gateway receive subscription message and go back online\", \"userID\", userID, \"platformIDs\", platformIDs)\n\t} else {\n\t\tlog.ZDebug(ctx, \"gateway ignore user online status changes\", \"userID\", userID, \"platformIDs\", platformIDs)\n\t}\n\tws.pushUserIDOnlineStatus(ctx, userID, platformIDs)\n}\n\nfunc (ws *WsServer) SubUserOnlineStatus(ctx context.Context, client *Client, data *Req) ([]byte, error) {\n\tvar sub sdkws.SubUserOnlineStatus\n\tif err := proto.Unmarshal(data.Data, &sub); err != nil {\n\t\treturn nil, err\n\t}\n\tws.subscription.Sub(client, sub.SubscribeUserID, sub.UnsubscribeUserID)\n\tvar resp sdkws.SubUserOnlineStatusTips\n\tif len(sub.SubscribeUserID) > 0 {\n\t\tresp.Subscribers = make([]*sdkws.SubUserOnlineStatusElem, 0, len(sub.SubscribeUserID))\n\t\tfor _, userID := range sub.SubscribeUserID {\n\t\t\tplatformIDs, err := ws.online.GetUserOnlinePlatform(ctx, userID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tresp.Subscribers = append(resp.Subscribers, &sdkws.SubUserOnlineStatusElem{\n\t\t\t\tUserID:            userID,\n\t\t\t\tOnlinePlatformIDs: platformIDs,\n\t\t\t})\n\t\t}\n\t}\n\treturn proto.Marshal(&resp)\n}\n\nfunc newSubscription() *Subscription {\n\treturn &Subscription{\n\t\tuserIDs: make(map[string]*subClient),\n\t}\n}\n\ntype subClient struct {\n\tclients map[string]*Client\n}\n\ntype Subscription struct {\n\tlock    sync.RWMutex\n\tuserIDs map[string]*subClient // subscribe to the user's client connection\n}\n\nfunc (s *Subscription) DelClient(client *Client) {\n\tclient.subLock.Lock()\n\tuserIDs := datautil.Keys(client.subUserIDs)\n\tfor _, userID := range userIDs {\n\t\tdelete(client.subUserIDs, userID)\n\t}\n\tclient.subLock.Unlock()\n\tif len(userIDs) == 0 {\n\t\treturn\n\t}\n\taddr := client.ctx.GetRemoteAddr()\n\ts.lock.Lock()\n\tdefer s.lock.Unlock()\n\tfor _, userID := range userIDs {\n\t\tsub, ok := s.userIDs[userID]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tdelete(sub.clients, addr)\n\t\tif len(sub.clients) == 0 {\n\t\t\tdelete(s.userIDs, userID)\n\t\t}\n\t}\n}\n\nfunc (s *Subscription) GetClient(userID string) []*Client {\n\ts.lock.RLock()\n\tdefer s.lock.RUnlock()\n\tcs, ok := s.userIDs[userID]\n\tif !ok {\n\t\treturn nil\n\t}\n\tclients := make([]*Client, 0, len(cs.clients))\n\tfor _, client := range cs.clients {\n\t\tclients = append(clients, client)\n\t}\n\treturn clients\n}\n\nfunc (s *Subscription) Sub(client *Client, addUserIDs, delUserIDs []string) {\n\tif len(addUserIDs)+len(delUserIDs) == 0 {\n\t\treturn\n\t}\n\tvar (\n\t\tdel = make(map[string]struct{})\n\t\tadd = make(map[string]struct{})\n\t)\n\tclient.subLock.Lock()\n\tfor _, userID := range delUserIDs {\n\t\tif _, ok := client.subUserIDs[userID]; !ok {\n\t\t\tcontinue\n\t\t}\n\t\tdel[userID] = struct{}{}\n\t\tdelete(client.subUserIDs, userID)\n\t}\n\tfor _, userID := range addUserIDs {\n\t\tdelete(del, userID)\n\t\tif _, ok := client.subUserIDs[userID]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tclient.subUserIDs[userID] = struct{}{}\n\t\tadd[userID] = struct{}{}\n\t}\n\tclient.subLock.Unlock()\n\tif len(del)+len(add) == 0 {\n\t\treturn\n\t}\n\taddr := client.ctx.GetRemoteAddr()\n\ts.lock.Lock()\n\tdefer s.lock.Unlock()\n\tfor userID := range del {\n\t\tsub, ok := s.userIDs[userID]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tdelete(sub.clients, addr)\n\t\tif len(sub.clients) == 0 {\n\t\t\tdelete(s.userIDs, userID)\n\t\t}\n\t}\n\tfor userID := range add {\n\t\tsub, ok := s.userIDs[userID]\n\t\tif !ok {\n\t\t\tsub = &subClient{clients: make(map[string]*Client)}\n\t\t\ts.userIDs[userID] = sub\n\t\t}\n\t\tsub.clients[addr] = client\n\t}\n}\n\nfunc (ws *WsServer) pushUserIDOnlineStatus(ctx context.Context, userID string, platformIDs []int32) {\n\tclients := ws.subscription.GetClient(userID)\n\tif len(clients) == 0 {\n\t\treturn\n\t}\n\tonlineStatus, err := proto.Marshal(&sdkws.SubUserOnlineStatusTips{\n\t\tSubscribers: []*sdkws.SubUserOnlineStatusElem{{UserID: userID, OnlinePlatformIDs: platformIDs}},\n\t})\n\tif err != nil {\n\t\tlog.ZError(ctx, \"pushUserIDOnlineStatus json.Marshal\", err)\n\t\treturn\n\t}\n\tfor _, client := range clients {\n\t\tif err := client.PushUserOnlineStatus(onlineStatus); err != nil {\n\t\t\tlog.ZError(ctx, \"UserSubscribeOnlineStatusNotification push failed\", err, \"userID\", client.UserID, \"platformID\", client.PlatformID, \"changeUserID\", userID, \"changePlatformID\", platformIDs)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/msggateway/user_map.go",
    "content": "package msggateway\n\nimport (\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype UserMap interface {\n\tGetAll(userID string) ([]*Client, bool)\n\tGet(userID string, platformID int) ([]*Client, bool, bool)\n\tSet(userID string, v *Client)\n\tDeleteClients(userID string, clients []*Client) (isDeleteUser bool)\n\tUserState() <-chan UserState\n\tGetAllUserStatus(deadline time.Time, nowtime time.Time) []UserState\n\tRecvSubChange(userID string, platformIDs []int32) bool\n}\n\ntype UserState struct {\n\tUserID  string\n\tOnline  []int32\n\tOffline []int32\n}\n\ntype UserPlatform struct {\n\tTime    time.Time\n\tClients []*Client\n}\n\nfunc (u *UserPlatform) PlatformIDs() []int32 {\n\tif len(u.Clients) == 0 {\n\t\treturn nil\n\t}\n\tplatformIDs := make([]int32, 0, len(u.Clients))\n\tfor _, client := range u.Clients {\n\t\tplatformIDs = append(platformIDs, int32(client.PlatformID))\n\t}\n\treturn platformIDs\n}\n\nfunc (u *UserPlatform) PlatformIDSet() map[int32]struct{} {\n\tif len(u.Clients) == 0 {\n\t\treturn nil\n\t}\n\tplatformIDs := make(map[int32]struct{})\n\tfor _, client := range u.Clients {\n\t\tplatformIDs[int32(client.PlatformID)] = struct{}{}\n\t}\n\treturn platformIDs\n}\n\nfunc newUserMap() UserMap {\n\treturn &userMap{\n\t\tdata: make(map[string]*UserPlatform),\n\t\tch:   make(chan UserState, 10000),\n\t}\n}\n\ntype userMap struct {\n\tlock sync.RWMutex\n\tdata map[string]*UserPlatform\n\tch   chan UserState\n}\n\nfunc (u *userMap) RecvSubChange(userID string, platformIDs []int32) bool {\n\tu.lock.RLock()\n\tdefer u.lock.RUnlock()\n\tresult, ok := u.data[userID]\n\tif !ok {\n\t\treturn false\n\t}\n\tlocalPlatformIDs := result.PlatformIDSet()\n\tfor _, platformID := range platformIDs {\n\t\tdelete(localPlatformIDs, platformID)\n\t}\n\tif len(localPlatformIDs) == 0 {\n\t\treturn false\n\t}\n\tu.push(userID, result, nil)\n\treturn true\n}\n\nfunc (u *userMap) push(userID string, userPlatform *UserPlatform, offline []int32) bool {\n\tselect {\n\tcase u.ch <- UserState{UserID: userID, Online: userPlatform.PlatformIDs(), Offline: offline}:\n\t\tuserPlatform.Time = time.Now()\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (u *userMap) GetAll(userID string) ([]*Client, bool) {\n\tu.lock.RLock()\n\tdefer u.lock.RUnlock()\n\tresult, ok := u.data[userID]\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn result.Clients, true\n}\n\nfunc (u *userMap) Get(userID string, platformID int) ([]*Client, bool, bool) {\n\tu.lock.RLock()\n\tdefer u.lock.RUnlock()\n\tresult, ok := u.data[userID]\n\tif !ok {\n\t\treturn nil, false, false\n\t}\n\tvar clients []*Client\n\tfor _, client := range result.Clients {\n\t\tif client.PlatformID == platformID {\n\t\t\tclients = append(clients, client)\n\t\t}\n\t}\n\treturn clients, true, len(clients) > 0\n}\n\nfunc (u *userMap) Set(userID string, client *Client) {\n\tu.lock.Lock()\n\tdefer u.lock.Unlock()\n\tresult, ok := u.data[userID]\n\tif ok {\n\t\tresult.Clients = append(result.Clients, client)\n\t} else {\n\t\tresult = &UserPlatform{\n\t\t\tClients: []*Client{client},\n\t\t}\n\t\tu.data[userID] = result\n\t}\n\tu.push(client.UserID, result, nil)\n}\n\nfunc (u *userMap) DeleteClients(userID string, clients []*Client) (isDeleteUser bool) {\n\tif len(clients) == 0 {\n\t\treturn false\n\t}\n\tu.lock.Lock()\n\tdefer u.lock.Unlock()\n\tresult, ok := u.data[userID]\n\tif !ok {\n\t\treturn false\n\t}\n\toffline := make([]int32, 0, len(clients))\n\tdeleteAddr := datautil.SliceSetAny(clients, func(client *Client) string {\n\t\treturn client.ctx.GetRemoteAddr()\n\t})\n\ttmp := result.Clients\n\tresult.Clients = result.Clients[:0]\n\tfor _, client := range tmp {\n\t\tif _, delCli := deleteAddr[client.ctx.GetRemoteAddr()]; delCli {\n\t\t\toffline = append(offline, int32(client.PlatformID))\n\t\t} else {\n\t\t\tresult.Clients = append(result.Clients, client)\n\t\t}\n\t}\n\tdefer u.push(userID, result, offline)\n\tif len(result.Clients) > 0 {\n\t\treturn false\n\t}\n\tdelete(u.data, userID)\n\treturn true\n}\n\nfunc (u *userMap) GetAllUserStatus(deadline time.Time, nowtime time.Time) (result []UserState) {\n\tu.lock.RLock()\n\tdefer u.lock.RUnlock()\n\tresult = make([]UserState, 0, len(u.data))\n\tfor userID, userPlatform := range u.data {\n\t\tif deadline.Before(userPlatform.Time) {\n\t\t\tcontinue\n\t\t}\n\t\tuserPlatform.Time = nowtime\n\t\tonline := make([]int32, 0, len(userPlatform.Clients))\n\t\tfor _, client := range userPlatform.Clients {\n\t\t\tonline = append(online, int32(client.PlatformID))\n\t\t}\n\t\tresult = append(result, UserState{UserID: userID, Online: online})\n\t}\n\treturn result\n}\n\nfunc (u *userMap) UserState() <-chan UserState {\n\treturn u.ch\n}\n"
  },
  {
    "path": "internal/msggateway/ws_server.go",
    "content": "package msggateway\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpccache\"\n\tpbAuth \"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msggateway\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nvar wsSuccessResponse, _ = json.Marshal(&apiresp.ApiResponse{})\n\ntype LongConnServer interface {\n\tRun(ctx context.Context) error\n\twsHandler(w http.ResponseWriter, r *http.Request)\n\tGetUserAllCons(userID string) ([]*Client, bool)\n\tGetUserPlatformCons(userID string, platform int) ([]*Client, bool, bool)\n\tValidate(s any) error\n\tSetDiscoveryRegistry(ctx context.Context, client discovery.Conn, config *Config) error\n\tKickUserConn(client *Client) error\n\tUnRegister(c *Client)\n\tSetKickHandlerInfo(i *kickHandler)\n\tSubUserOnlineStatus(ctx context.Context, client *Client, data *Req) ([]byte, error)\n\tCompressor\n\tMessageHandler\n}\n\ntype WsServer struct {\n\twebsocket         *websocket.Upgrader\n\tmsgGatewayConfig  *Config\n\tport              int\n\twsMaxConnNum      int64\n\tregisterChan      chan *Client\n\tunregisterChan    chan *Client\n\tkickHandlerChan   chan *kickHandler\n\tclients           UserMap\n\tonline            rpccache.OnlineCache\n\tsubscription      *Subscription\n\tclientPool        sync.Pool\n\tonlineUserNum     atomic.Int64\n\tonlineUserConnNum atomic.Int64\n\thandshakeTimeout  time.Duration\n\twriteBufferSize   int\n\tvalidate          *validator.Validate\n\tdisCov            discovery.Conn\n\tCompressor\n\t//Encoder\n\tMessageHandler\n\twebhookClient *webhook.Client\n\tuserClient    *rpcli.UserClient\n\tauthClient    *rpcli.AuthClient\n\n\tready atomic.Bool\n}\n\ntype kickHandler struct {\n\tclientOK   bool\n\toldClients []*Client\n\tnewClient  *Client\n}\n\nfunc (ws *WsServer) SetDiscoveryRegistry(ctx context.Context, disCov discovery.Conn, config *Config) error {\n\tuserConn, err := disCov.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpushConn, err := disCov.GetConn(ctx, config.Discovery.RpcService.Push)\n\tif err != nil {\n\t\treturn err\n\t}\n\tauthConn, err := disCov.GetConn(ctx, config.Discovery.RpcService.Auth)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsgConn, err := disCov.GetConn(ctx, config.Discovery.RpcService.Msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tws.userClient = rpcli.NewUserClient(userConn)\n\tws.authClient = rpcli.NewAuthClient(authConn)\n\tws.MessageHandler = NewGrpcHandler(ws.validate, rpcli.NewMsgClient(msgConn), rpcli.NewPushMsgServiceClient(pushConn))\n\tws.disCov = disCov\n\n\tws.ready.Store(true)\n\treturn nil\n}\n\n//func (ws *WsServer) SetUserOnlineStatus(ctx context.Context, client *Client, status int32) {\n//\terr := ws.userClient.SetUserStatus(ctx, client.UserID, status, client.PlatformID)\n//\tif err != nil {\n//\t\tlog.ZWarn(ctx, \"SetUserStatus err\", err)\n//\t}\n//\tswitch status {\n//\tcase constant.Online:\n//\t\tws.webhookAfterUserOnline(ctx, &ws.msgGatewayConfig.WebhooksConfig.AfterUserOnline, client.UserID, client.PlatformID, client.IsBackground, client.ctx.GetConnID())\n//\tcase constant.Offline:\n//\t\tws.webhookAfterUserOffline(ctx, &ws.msgGatewayConfig.WebhooksConfig.AfterUserOffline, client.UserID, client.PlatformID, client.ctx.GetConnID())\n//\t}\n//}\n\nfunc (ws *WsServer) UnRegister(c *Client) {\n\tws.unregisterChan <- c\n}\n\nfunc (ws *WsServer) Validate(_ any) error {\n\treturn nil\n}\n\nfunc (ws *WsServer) GetUserAllCons(userID string) ([]*Client, bool) {\n\treturn ws.clients.GetAll(userID)\n}\n\nfunc (ws *WsServer) GetUserPlatformCons(userID string, platform int) ([]*Client, bool, bool) {\n\treturn ws.clients.Get(userID, platform)\n}\n\nfunc NewWsServer(msgGatewayConfig *Config, opts ...Option) *WsServer {\n\tvar config configs\n\tfor _, o := range opts {\n\t\to(&config)\n\t}\n\t//userRpcClient := rpcclient.NewUserRpcClient(client, config.Discovery.RpcService.User, config.Share.IMAdminUser)\n\tupgrader := &websocket.Upgrader{\n\t\tHandshakeTimeout: config.handshakeTimeout,\n\t\tCheckOrigin:      func(r *http.Request) bool { return true },\n\t}\n\tv := validator.New()\n\treturn &WsServer{\n\t\twebsocket:        upgrader,\n\t\tmsgGatewayConfig: msgGatewayConfig,\n\t\tport:             config.port,\n\t\twsMaxConnNum:     config.maxConnNum,\n\t\twriteBufferSize:  config.writeBufferSize,\n\t\thandshakeTimeout: config.handshakeTimeout,\n\t\tclientPool: sync.Pool{\n\t\t\tNew: func() any {\n\t\t\t\treturn new(Client)\n\t\t\t},\n\t\t},\n\t\tregisterChan:    make(chan *Client, 1000),\n\t\tunregisterChan:  make(chan *Client, 1000),\n\t\tkickHandlerChan: make(chan *kickHandler, 1000),\n\t\tvalidate:        v,\n\t\tclients:         newUserMap(),\n\t\tsubscription:    newSubscription(),\n\t\tCompressor:      NewGzipCompressor(),\n\t\twebhookClient:   webhook.NewWebhookClient(msgGatewayConfig.WebhooksConfig.URL),\n\t}\n}\n\nfunc (ws *WsServer) Run(ctx context.Context) error {\n\tvar client *Client\n\n\tctx, cancel := context.WithCancelCause(ctx)\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase client = <-ws.registerChan:\n\t\t\t\tws.registerClient(client)\n\t\t\tcase client = <-ws.unregisterChan:\n\t\t\t\tws.unregisterClient(client)\n\t\t\tcase onlineInfo := <-ws.kickHandlerChan:\n\t\t\t\tws.multiTerminalLoginChecker(onlineInfo.clientOK, onlineInfo.oldClients, onlineInfo.newClient)\n\t\t\t}\n\t\t}\n\t}()\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\twsServer := http.Server{Addr: fmt.Sprintf(\":%d\", ws.port), Handler: nil}\n\t\thttp.HandleFunc(\"/\", ws.wsHandler)\n\t\tgo func() {\n\t\t\tdefer close(done)\n\t\t\t<-ctx.Done()\n\t\t\t_ = wsServer.Shutdown(context.Background())\n\t\t}()\n\t\terr := wsServer.ListenAndServe()\n\t\tif err == nil {\n\t\t\terr = fmt.Errorf(\"http server closed\")\n\t\t}\n\t\tcancel(fmt.Errorf(\"msg gateway %w\", err))\n\t}()\n\n\t<-ctx.Done()\n\n\ttimeout := time.NewTimer(time.Second * 15)\n\tdefer timeout.Stop()\n\tselect {\n\tcase <-timeout.C:\n\t\tlog.ZWarn(ctx, \"msg gateway graceful stop timeout\", nil)\n\tcase <-done:\n\t\tlog.ZDebug(ctx, \"msg gateway graceful stop done\")\n\t}\n\treturn context.Cause(ctx)\n}\n\nconst concurrentRequest = 3\n\nfunc (ws *WsServer) sendUserOnlineInfoToOtherNode(ctx context.Context, client *Client) error {\n\tconns, err := ws.disCov.GetConns(ctx, ws.msgGatewayConfig.Discovery.RpcService.MessageGateway)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(conns) == 0 || (len(conns) == 1 && ws.disCov.IsSelfNode(conns[0])) {\n\t\treturn nil\n\t}\n\n\twg := errgroup.Group{}\n\twg.SetLimit(concurrentRequest)\n\n\t// Online push user online message to other node\n\tfor _, v := range conns {\n\t\tv := v\n\t\tlog.ZDebug(ctx, \"sendUserOnlineInfoToOtherNode conn\")\n\t\tif ws.disCov.IsSelfNode(v) {\n\t\t\tlog.ZDebug(ctx, \"Filter out this node\")\n\t\t\tcontinue\n\t\t}\n\n\t\twg.Go(func() error {\n\t\t\tmsgClient := msggateway.NewMsgGatewayClient(v)\n\t\t\t_, err := msgClient.MultiTerminalLoginCheck(ctx, &msggateway.MultiTerminalLoginCheckReq{\n\t\t\t\tUserID:     client.UserID,\n\t\t\t\tPlatformID: int32(client.PlatformID), Token: client.token,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"MultiTerminalLoginCheck err\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t_ = wg.Wait()\n\treturn nil\n}\n\nfunc (ws *WsServer) SetKickHandlerInfo(i *kickHandler) {\n\tws.kickHandlerChan <- i\n}\n\nfunc (ws *WsServer) registerClient(client *Client) {\n\tvar (\n\t\tuserOK     bool\n\t\tclientOK   bool\n\t\toldClients []*Client\n\t)\n\toldClients, userOK, clientOK = ws.clients.Get(client.UserID, client.PlatformID)\n\n\tlog.ZInfo(client.ctx, \"registerClient\", \"userID\", client.UserID, \"platformID\", client.PlatformID)\n\n\tif !userOK {\n\t\tws.clients.Set(client.UserID, client)\n\t\tlog.ZDebug(client.ctx, \"user not exist\", \"userID\", client.UserID, \"platformID\", client.PlatformID)\n\t\tprommetrics.OnlineUserGauge.Add(1)\n\t\tws.onlineUserNum.Add(1)\n\t\tws.onlineUserConnNum.Add(1)\n\t} else {\n\t\tws.multiTerminalLoginChecker(clientOK, oldClients, client)\n\t\tlog.ZDebug(client.ctx, \"user exist\", \"userID\", client.UserID, \"platformID\", client.PlatformID)\n\t\tif clientOK {\n\t\t\tws.clients.Set(client.UserID, client)\n\t\t\t// There is already a connection to the platform\n\t\t\tlog.ZDebug(client.ctx, \"repeat login\", \"userID\", client.UserID, \"platformID\",\n\t\t\t\tclient.PlatformID, \"old remote addr\", getRemoteAdders(oldClients))\n\t\t\tws.onlineUserConnNum.Add(1)\n\t\t} else {\n\t\t\tws.clients.Set(client.UserID, client)\n\t\t\tws.onlineUserConnNum.Add(1)\n\t\t}\n\t}\n\n\twg := sync.WaitGroup{}\n\tlog.ZDebug(client.ctx, \"ws.msgGatewayConfig.Discovery.Enable\", \"discoveryEnable\", ws.msgGatewayConfig.Discovery.Enable)\n\n\tif ws.msgGatewayConfig.Discovery.Enable != \"k8s\" {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\t_ = ws.sendUserOnlineInfoToOtherNode(client.ctx, client)\n\t\t}()\n\t}\n\n\t//wg.Add(1)\n\t//go func() {\n\t//\tdefer wg.Done()\n\t//\tws.SetUserOnlineStatus(client.ctx, client, constant.Online)\n\t//}()\n\n\twg.Wait()\n\n\tlog.ZDebug(client.ctx, \"user online\", \"online user Num\", ws.onlineUserNum.Load(), \"online user conn Num\", ws.onlineUserConnNum.Load())\n}\n\nfunc getRemoteAdders(client []*Client) string {\n\tvar ret string\n\tfor i, c := range client {\n\t\tif i == 0 {\n\t\t\tret = c.ctx.GetRemoteAddr()\n\t\t} else {\n\t\t\tret += \"@\" + c.ctx.GetRemoteAddr()\n\t\t}\n\t}\n\treturn ret\n}\n\nfunc (ws *WsServer) KickUserConn(client *Client) error {\n\tws.clients.DeleteClients(client.UserID, []*Client{client})\n\treturn client.KickOnlineMessage()\n}\n\nfunc (ws *WsServer) multiTerminalLoginChecker(clientOK bool, oldClients []*Client, newClient *Client) {\n\tkickTokenFunc := func(kickClients []*Client) {\n\t\tvar kickTokens []string\n\t\tws.clients.DeleteClients(newClient.UserID, kickClients)\n\t\tfor _, c := range kickClients {\n\t\t\tkickTokens = append(kickTokens, c.token)\n\t\t\terr := c.KickOnlineMessage()\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(c.ctx, \"KickOnlineMessage\", err)\n\t\t\t}\n\t\t}\n\t\tctx := mcontext.WithMustInfoCtx(\n\t\t\t[]string{newClient.ctx.GetOperationID(), newClient.ctx.GetUserID(),\n\t\t\t\tconstant.PlatformIDToName(newClient.PlatformID), newClient.ctx.GetConnID()},\n\t\t)\n\t\tif err := ws.authClient.KickTokens(ctx, kickTokens); err != nil {\n\t\t\tlog.ZWarn(newClient.ctx, \"kickTokens err\", err)\n\t\t}\n\t}\n\n\t// If reconnect: When multiple msgGateway instances are deployed, a client may disconnect from instance A and reconnect to instance B.\n\t// During this process, instance A might still be executing, resulting in two clients with the same token existing simultaneously.\n\t// This situation needs to be filtered to prevent duplicate clients.\n\tcheckSameTokenFunc := func(oldClients []*Client) []*Client {\n\t\tvar clientsNeedToKick []*Client\n\n\t\tfor _, c := range oldClients {\n\t\t\tif c.token == newClient.token {\n\t\t\t\tlog.ZDebug(newClient.ctx, \"token is same, not kick\",\n\t\t\t\t\t\"userID\", newClient.UserID,\n\t\t\t\t\t\"platformID\", newClient.PlatformID,\n\t\t\t\t\t\"token\", newClient.token)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tclientsNeedToKick = append(clientsNeedToKick, c)\n\t\t}\n\n\t\treturn clientsNeedToKick\n\t}\n\n\tswitch ws.msgGatewayConfig.Share.MultiLogin.Policy {\n\tcase constant.DefalutNotKick:\n\tcase constant.PCAndOther:\n\t\tif constant.PlatformIDToClass(newClient.PlatformID) == constant.TerminalPC {\n\t\t\treturn\n\t\t}\n\t\tclients, ok := ws.clients.GetAll(newClient.UserID)\n\t\tclientOK = ok\n\t\toldClients = make([]*Client, 0, len(clients))\n\t\tfor _, c := range clients {\n\t\t\tif constant.PlatformIDToClass(c.PlatformID) == constant.TerminalPC {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\toldClients = append(oldClients, c)\n\t\t}\n\n\t\tfallthrough\n\tcase constant.AllLoginButSameTermKick:\n\t\tif !clientOK {\n\t\t\treturn\n\t\t}\n\n\t\toldClients = checkSameTokenFunc(oldClients)\n\n\t\tws.clients.DeleteClients(newClient.UserID, oldClients)\n\t\tfor _, c := range oldClients {\n\t\t\terr := c.KickOnlineMessage()\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(c.ctx, \"KickOnlineMessage\", err)\n\t\t\t}\n\t\t}\n\n\t\tctx := mcontext.WithMustInfoCtx(\n\t\t\t[]string{newClient.ctx.GetOperationID(), newClient.ctx.GetUserID(),\n\t\t\t\tconstant.PlatformIDToName(newClient.PlatformID), newClient.ctx.GetConnID()},\n\t\t)\n\t\treq := &pbAuth.InvalidateTokenReq{\n\t\t\tPreservedToken: newClient.token,\n\t\t\tUserID:         newClient.UserID,\n\t\t\tPlatformID:     int32(newClient.PlatformID),\n\t\t}\n\t\tif err := ws.authClient.InvalidateToken(ctx, req); err != nil {\n\t\t\tlog.ZWarn(newClient.ctx, \"InvalidateToken err\", err, \"userID\", newClient.UserID,\n\t\t\t\t\"platformID\", newClient.PlatformID)\n\t\t}\n\tcase constant.AllLoginButSameClassKick:\n\t\tclients, ok := ws.clients.GetAll(newClient.UserID)\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\n\t\tvar kickClients []*Client\n\t\tfor _, client := range clients {\n\t\t\tif constant.PlatformIDToClass(client.PlatformID) == constant.PlatformIDToClass(newClient.PlatformID) {\n\t\t\t\t{\n\t\t\t\t\tkickClients = append(kickClients, client)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tkickClients = checkSameTokenFunc(kickClients)\n\n\t\tkickTokenFunc(kickClients)\n\t}\n}\n\nfunc (ws *WsServer) unregisterClient(client *Client) {\n\tdefer ws.clientPool.Put(client)\n\tisDeleteUser := ws.clients.DeleteClients(client.UserID, []*Client{client})\n\tif isDeleteUser {\n\t\tws.onlineUserNum.Add(-1)\n\t\tprommetrics.OnlineUserGauge.Dec()\n\t}\n\tws.onlineUserConnNum.Add(-1)\n\tws.subscription.DelClient(client)\n\t//ws.SetUserOnlineStatus(client.ctx, client, constant.Offline)\n\tlog.ZDebug(client.ctx, \"user offline\", \"close reason\", client.closedErr, \"online user Num\",\n\t\tws.onlineUserNum.Load(), \"online user conn Num\",\n\t\tws.onlineUserConnNum.Load(),\n\t)\n}\n\n// validateRespWithRequest checks if the response matches the expected userID and platformID.\nfunc (ws *WsServer) validateRespWithRequest(ctx *UserConnContext, resp *pbAuth.ParseTokenResp) error {\n\tuserID := ctx.GetUserID()\n\tplatformID := int32(ctx.GetPlatformID())\n\tif resp.UserID != userID {\n\t\treturn servererrs.ErrTokenInvalid.WrapMsg(fmt.Sprintf(\"token uid %s != userID %s\", resp.UserID, userID))\n\t}\n\tif resp.PlatformID != platformID {\n\t\treturn servererrs.ErrTokenInvalid.WrapMsg(fmt.Sprintf(\"token platform %d != platformID %d\", resp.PlatformID, platformID))\n\t}\n\treturn nil\n}\n\nfunc (ws *WsServer) handlerError(ctx *UserConnContext, w http.ResponseWriter, r *http.Request, err error) {\n\tif !ctx.ShouldSendResp() {\n\t\thttpError(ctx, err)\n\t\treturn\n\t}\n\t// the browser cannot get the response of upgrade failure\n\tdata, err := json.Marshal(apiresp.ParseError(err))\n\tif err != nil {\n\t\tlog.ZError(ctx, \"json marshal failed\", err)\n\t\treturn\n\t}\n\tconn, upgradeErr := ws.websocket.Upgrade(w, r, nil)\n\tif upgradeErr != nil {\n\t\tlog.ZWarn(ctx, \"websocket upgrade failed\", upgradeErr, \"respErr\", err, \"resp\", string(data))\n\t\treturn\n\t}\n\tdefer conn.Close()\n\tif err := conn.WriteMessage(websocket.TextMessage, data); err != nil {\n\t\tlog.ZWarn(ctx, \"WriteMessage failed\", err, \"respErr\", err, \"resp\", string(data))\n\t\treturn\n\t}\n}\n\nfunc (ws *WsServer) wsHandler(w http.ResponseWriter, r *http.Request) {\n\t// Create a new connection context\n\tconnContext := newContext(w, r)\n\n\t// Check if the current number of online user connections exceeds the maximum limit\n\tif ws.onlineUserConnNum.Load() >= ws.wsMaxConnNum {\n\t\t// If it exceeds the maximum connection number, return an error via HTTP and stop processing\n\t\tws.handlerError(connContext, w, r, servererrs.ErrConnOverMaxNumLimit.WrapMsg(\"over max conn num limit\"))\n\t\treturn\n\t}\n\n\t// Parse essential arguments (e.g., user ID, Token)\n\terr := connContext.ParseEssentialArgs()\n\tif err != nil {\n\t\t// If there's an error during parsing, return an error via HTTP and stop processing\n\t\tws.handlerError(connContext, w, r, err)\n\t\treturn\n\t}\n\n\t// Call the authentication client to parse the Token obtained from the context\n\tresp, err := ws.authClient.ParseToken(connContext, connContext.GetToken())\n\tif err != nil {\n\t\tws.handlerError(connContext, w, r, err)\n\t\treturn\n\t}\n\n\t// Validate the authentication response matches the request (e.g., user ID and platform ID)\n\terr = ws.validateRespWithRequest(connContext, resp)\n\tif err != nil {\n\t\t// If validation fails, return an error via HTTP and stop processing\n\t\tws.handlerError(connContext, w, r, err)\n\t\treturn\n\t}\n\tconn, err := ws.websocket.Upgrade(w, r, nil)\n\tif err != nil {\n\t\tlog.ZWarn(connContext, \"websocket upgrade failed\", err)\n\t\treturn\n\t}\n\tif connContext.ShouldSendResp() {\n\t\tif err := conn.WriteMessage(websocket.TextMessage, wsSuccessResponse); err != nil {\n\t\t\tlog.ZWarn(connContext, \"WriteMessage first response\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tlog.ZDebug(connContext, \"new conn\", \"token\", connContext.GetToken())\n\n\tvar pingInterval time.Duration\n\tif connContext.GetPlatformID() == constant.WebPlatformID {\n\t\tpingInterval = pingPeriod\n\t}\n\n\tclient := new(Client)\n\tclient.ResetClient(connContext, NewWebSocketClientConn(conn, maxMessageSize, pongWait, pingInterval), ws)\n\n\t// Register the client with the server and start message processing\n\tws.registerChan <- client\n\tgo client.readMessage()\n}\n"
  },
  {
    "path": "internal/msgtransfer/callback.go",
    "content": "package msgtransfer\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/stringutil\"\n\t\"google.golang.org/protobuf/proto\"\n\n\tcbapi \"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n)\n\nfunc toCommonCallback(ctx context.Context, msg *sdkws.MsgData, command string) cbapi.CommonCallbackReq {\n\treturn cbapi.CommonCallbackReq{\n\t\tSendID:           msg.SendID,\n\t\tServerMsgID:      msg.ServerMsgID,\n\t\tCallbackCommand:  command,\n\t\tClientMsgID:      msg.ClientMsgID,\n\t\tOperationID:      mcontext.GetOperationID(ctx),\n\t\tSenderPlatformID: msg.SenderPlatformID,\n\t\tSenderNickname:   msg.SenderNickname,\n\t\tSessionType:      msg.SessionType,\n\t\tMsgFrom:          msg.MsgFrom,\n\t\tContentType:      msg.ContentType,\n\t\tStatus:           msg.Status,\n\t\tSendTime:         msg.SendTime,\n\t\tCreateTime:       msg.CreateTime,\n\t\tAtUserIDList:     msg.AtUserIDList,\n\t\tSenderFaceURL:    msg.SenderFaceURL,\n\t\tContent:          GetContent(msg),\n\t\tSeq:              uint32(msg.Seq),\n\t\tEx:               msg.Ex,\n\t}\n}\n\nfunc GetContent(msg *sdkws.MsgData) string {\n\tif msg.ContentType >= constant.NotificationBegin && msg.ContentType <= constant.NotificationEnd {\n\t\tvar tips sdkws.TipsComm\n\t\t_ = proto.Unmarshal(msg.Content, &tips)\n\t\tcontent := tips.JsonDetail\n\t\treturn content\n\t} else {\n\t\treturn string(msg.Content)\n\t}\n}\n\nfunc (mc *OnlineHistoryMongoConsumerHandler) webhookAfterMsgSaveDB(ctx context.Context, after *config.AfterConfig, msg *sdkws.MsgData) {\n\tif !filterAfterMsg(msg, after) {\n\t\treturn\n\t}\n\n\tcbReq := &cbapi.CallbackAfterMsgSaveDBReq{\n\t\tCommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterMsgSaveDBCommand),\n\t}\n\n\tswitch msg.SessionType {\n\tcase constant.SingleChatType, constant.NotificationChatType:\n\t\tcbReq.RecvID = msg.RecvID\n\tcase constant.ReadGroupChatType:\n\t\tcbReq.GroupID = msg.GroupID\n\tdefault:\n\t}\n\n\tmc.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterMsgSaveDBResp{}, after, buildKeyMsgDataQuery(msg))\n}\n\nfunc buildKeyMsgDataQuery(msg *sdkws.MsgData) map[string]string {\n\tkeyMsgData := apistruct.KeyMsgData{\n\t\tSendID:  msg.SendID,\n\t\tRecvID:  msg.RecvID,\n\t\tGroupID: msg.GroupID,\n\t}\n\n\treturn map[string]string{\n\t\twebhook.Key: base64.StdEncoding.EncodeToString(stringutil.StructToJsonBytes(keyMsgData)),\n\t}\n}\n\nfunc filterAfterMsg(msg *sdkws.MsgData, after *config.AfterConfig) bool {\n\treturn filterMsg(msg, after.AttentionIds, after.DeniedTypes)\n}\n\nfunc filterMsg(msg *sdkws.MsgData, attentionIds []string, deniedTypes []int32) bool {\n\t// According to the attentionIds configuration, only some users are sent\n\tif len(attentionIds) != 0 && msg.ContentType == constant.SingleChatType && !datautil.Contain(msg.RecvID, attentionIds...) {\n\t\treturn false\n\t}\n\n\tif len(attentionIds) != 0 && msg.ContentType == constant.ReadGroupChatType && !datautil.Contain(msg.GroupID, attentionIds...) {\n\t\treturn false\n\t}\n\n\tif defaultDeniedTypes(msg.ContentType) {\n\t\treturn false\n\t}\n\n\tif len(deniedTypes) != 0 && datautil.Contain(msg.ContentType, deniedTypes...) {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc defaultDeniedTypes(contentType int32) bool {\n\tif contentType >= constant.NotificationBegin && contentType <= constant.NotificationEnd {\n\t\treturn true\n\t}\n\tif contentType == constant.Typing {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/msgtransfer/init.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msgtransfer\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/mqbuild\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/mq\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\n\tconf \"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"google.golang.org/grpc\"\n)\n\ntype MsgTransfer struct {\n\thistoryConsumer      mq.Consumer\n\thistoryMongoConsumer mq.Consumer\n\t// This consumer aggregated messages, subscribed to the topic:toRedis,\n\t//  the message is stored in redis, Incr Redis, and then the message is sent to toPush topic for push,\n\t// and the message is sent to toMongo topic for persistence\n\thistoryHandler *OnlineHistoryRedisConsumerHandler\n\t//This consumer handle message to mongo\n\thistoryMongoHandler *OnlineHistoryMongoConsumerHandler\n\tctx                 context.Context\n\t//cancel              context.CancelFunc\n}\n\ntype Config struct {\n\tMsgTransfer    conf.MsgTransfer\n\tRedisConfig    conf.Redis\n\tMongodbConfig  conf.Mongo\n\tKafkaConfig    conf.Kafka\n\tShare          conf.Share\n\tWebhooksConfig conf.Webhooks\n\tDiscovery      conf.Discovery\n\tIndex          conf.Index\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tbuilder := mqbuild.NewBuilder(&config.KafkaConfig)\n\n\tlog.CInfo(ctx, \"MSG-TRANSFER server is initializing\", \"runTimeEnv\", runtimeenv.RuntimeEnvironment(), \"prometheusPorts\",\n\t\tconfig.MsgTransfer.Prometheus.Ports, \"index\", config.Index)\n\tdbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)\n\tmgocli, err := dbb.Mongo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t//if config.Discovery.Enable == conf.ETCD {\n\t//\tcm := disetcd.NewConfigManager(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient(), []string{\n\t//\t\tconfig.MsgTransfer.GetConfigFileName(),\n\t//\t\tconfig.RedisConfig.GetConfigFileName(),\n\t//\t\tconfig.MongodbConfig.GetConfigFileName(),\n\t//\t\tconfig.KafkaConfig.GetConfigFileName(),\n\t//\t\tconfig.Share.GetConfigFileName(),\n\t//\t\tconfig.WebhooksConfig.GetConfigFileName(),\n\t//\t\tconfig.Discovery.GetConfigFileName(),\n\t//\t\tconf.LogConfigFileName,\n\t//\t})\n\t//\tcm.Watch(ctx)\n\t//}\n\tmongoProducer, err := builder.GetTopicProducer(ctx, config.KafkaConfig.ToMongoTopic)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpushProducer, err := builder.GetTopicProducer(ctx, config.KafkaConfig.ToPushTopic)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsgDocModel, err := mgo.NewMsgMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar msgModel cache.MsgCache\n\tif rdb == nil {\n\t\tcm, err := mgo.NewCacheMgo(mgocli.GetDB())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmsgModel = mcache.NewMsgCache(cm, msgDocModel)\n\t} else {\n\t\tmsgModel = redis.NewMsgCache(rdb, msgDocModel)\n\t}\n\tseqConversation, err := mgo.NewSeqConversationMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tseqConversationCache := redis.NewSeqConversationCacheRedis(rdb, seqConversation)\n\tseqUser, err := mgo.NewSeqUserMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tseqUserCache := redis.NewSeqUserCacheRedis(rdb, seqUser)\n\tmsgTransferDatabase, err := controller.NewMsgTransferDatabase(msgDocModel, msgModel, seqUserCache, seqConversationCache, mongoProducer, pushProducer)\n\tif err != nil {\n\t\treturn err\n\t}\n\thistoryConsumer, err := builder.GetTopicConsumer(ctx, config.KafkaConfig.ToRedisTopic)\n\tif err != nil {\n\t\treturn err\n\t}\n\thistoryMongoConsumer, err := builder.GetTopicConsumer(ctx, config.KafkaConfig.ToMongoTopic)\n\tif err != nil {\n\t\treturn err\n\t}\n\thistoryHandler, err := NewOnlineHistoryRedisConsumerHandler(ctx, client, config, msgTransferDatabase)\n\tif err != nil {\n\t\treturn err\n\t}\n\thistoryMongoHandler := NewOnlineHistoryMongoConsumerHandler(msgTransferDatabase, config)\n\n\tmsgTransfer := &MsgTransfer{\n\t\thistoryConsumer:      historyConsumer,\n\t\thistoryMongoConsumer: historyMongoConsumer,\n\t\thistoryHandler:       historyHandler,\n\t\thistoryMongoHandler:  historyMongoHandler,\n\t}\n\n\treturn msgTransfer.Start(ctx)\n}\n\nfunc (m *MsgTransfer) Start(ctx context.Context) error {\n\tvar cancel context.CancelCauseFunc\n\tm.ctx, cancel = context.WithCancelCause(ctx)\n\n\tgo func() {\n\t\tfor {\n\t\t\tif err := m.historyConsumer.Subscribe(m.ctx, m.historyHandler.HandlerRedisMessage); err != nil {\n\t\t\t\tcancel(fmt.Errorf(\"history consumer %w\", err))\n\t\t\t\tlog.ZError(m.ctx, \"historyConsumer err\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfn := func(msg mq.Message) error {\n\t\t\tm.historyMongoHandler.HandleChatWs2Mongo(msg)\n\t\t\treturn nil\n\t\t}\n\t\tfor {\n\t\t\tif err := m.historyMongoConsumer.Subscribe(m.ctx, fn); err != nil {\n\t\t\t\tcancel(fmt.Errorf(\"history mongo consumer %w\", err))\n\t\t\t\tlog.ZError(m.ctx, \"historyMongoConsumer err\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tgo m.historyHandler.HandleUserHasReadSeqMessages(m.ctx)\n\n\terr := m.historyHandler.redisMessageBatches.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\t<-m.ctx.Done()\n\treturn context.Cause(m.ctx)\n}\n"
  },
  {
    "path": "internal/msgtransfer/online_history_msg_handler.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msgtransfer\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/openimsdk/tools/mq\"\n\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/tools/discovery\"\n\n\t\"github.com/go-redis/redis\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/tools/batcher\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbconv \"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/stringutil\"\n)\n\nconst (\n\tsize              = 500\n\tmainDataBuffer    = 500\n\tsubChanBuffer     = 50\n\tworker            = 50\n\tinterval          = 100 * time.Millisecond\n\thasReadChanBuffer = 1000\n)\n\ntype ContextMsg struct {\n\tmessage *sdkws.MsgData\n\tctx     context.Context\n}\n\n// This structure is used for asynchronously writing the sender’s read sequence (seq) regarding a message into MongoDB.\n// For example, if the sender sends a message with a seq of 10, then their own read seq for this conversation should be set to 10.\ntype userHasReadSeq struct {\n\tconversationID string\n\tuserHasReadMap map[string]int64\n}\n\ntype OnlineHistoryRedisConsumerHandler struct {\n\tredisMessageBatches *batcher.Batcher[ConsumerMessage]\n\n\tmsgTransferDatabase         controller.MsgTransferDatabase\n\tconversationUserHasReadChan chan *userHasReadSeq\n\twg                          sync.WaitGroup\n\n\tgroupClient        *rpcli.GroupClient\n\tconversationClient *rpcli.ConversationClient\n}\n\ntype ConsumerMessage struct {\n\tCtx   context.Context\n\tKey   string\n\tValue []byte\n\tRaw   mq.Message\n}\n\nfunc NewOnlineHistoryRedisConsumerHandler(ctx context.Context, client discovery.Conn, config *Config, database controller.MsgTransferDatabase) (*OnlineHistoryRedisConsumerHandler, error) {\n\tgroupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconversationConn, err := client.GetConn(ctx, config.Discovery.RpcService.Conversation)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar och OnlineHistoryRedisConsumerHandler\n\toch.msgTransferDatabase = database\n\toch.conversationUserHasReadChan = make(chan *userHasReadSeq, hasReadChanBuffer)\n\toch.groupClient = rpcli.NewGroupClient(groupConn)\n\toch.conversationClient = rpcli.NewConversationClient(conversationConn)\n\toch.wg.Add(1)\n\n\tb := batcher.New[ConsumerMessage](\n\t\tbatcher.WithSize(size),\n\t\tbatcher.WithWorker(worker),\n\t\tbatcher.WithInterval(interval),\n\t\tbatcher.WithDataBuffer(mainDataBuffer),\n\t\tbatcher.WithSyncWait(true),\n\t\tbatcher.WithBuffer(subChanBuffer),\n\t)\n\tb.Sharding = func(key string) int {\n\t\thashCode := stringutil.GetHashCode(key)\n\t\treturn int(hashCode) % och.redisMessageBatches.Worker()\n\t}\n\tb.Key = func(consumerMessage *ConsumerMessage) string {\n\t\treturn consumerMessage.Key\n\t}\n\tb.Do = och.do\n\toch.redisMessageBatches = b\n\n\toch.redisMessageBatches.OnComplete = func(lastMessage *ConsumerMessage, totalCount int) {\n\t\tlastMessage.Raw.Mark()\n\t\tlastMessage.Raw.Commit()\n\t}\n\n\treturn &och, nil\n}\nfunc (och *OnlineHistoryRedisConsumerHandler) do(ctx context.Context, channelID int, val *batcher.Msg[ConsumerMessage]) {\n\tctx = mcontext.WithTriggerIDContext(ctx, val.TriggerID())\n\tctxMessages := och.parseConsumerMessages(ctx, val.Val())\n\tctx = withAggregationCtx(ctx, ctxMessages)\n\tlog.ZInfo(ctx, \"msg arrived channel\", \"channel id\", channelID, \"msgList length\", len(ctxMessages), \"key\", val.Key())\n\toch.doSetReadSeq(ctx, ctxMessages)\n\n\tstorageMsgList, notStorageMsgList, storageNotificationList, notStorageNotificationList :=\n\t\toch.categorizeMessageLists(ctxMessages)\n\tlog.ZDebug(ctx, \"number of categorized messages\", \"storageMsgList\", len(storageMsgList), \"notStorageMsgList\",\n\t\tlen(notStorageMsgList), \"storageNotificationList\", len(storageNotificationList), \"notStorageNotificationList\", len(notStorageNotificationList))\n\n\tconversationIDMsg := msgprocessor.GetChatConversationIDByMsg(ctxMessages[0].message)\n\tconversationIDNotification := msgprocessor.GetNotificationConversationIDByMsg(ctxMessages[0].message)\n\toch.handleMsg(ctx, val.Key(), conversationIDMsg, storageMsgList, notStorageMsgList)\n\toch.handleNotification(ctx, val.Key(), conversationIDNotification, storageNotificationList, notStorageNotificationList)\n}\n\nfunc (och *OnlineHistoryRedisConsumerHandler) doSetReadSeq(ctx context.Context, msgs []*ContextMsg) {\n\n\t// Outer map: conversationID -> (userID -> maxHasReadSeq)\n\tconversationUserSeq := make(map[string]map[string]int64)\n\n\tfor _, msg := range msgs {\n\t\tif msg.message.ContentType != constant.HasReadReceipt {\n\t\t\tcontinue\n\t\t}\n\t\tvar elem sdkws.NotificationElem\n\t\tif err := json.Unmarshal(msg.message.Content, &elem); err != nil {\n\t\t\tlog.ZWarn(ctx, \"Unmarshal NotificationElem error\", err, \"msg\", msg)\n\t\t\tcontinue\n\t\t}\n\t\tvar tips sdkws.MarkAsReadTips\n\t\tif err := json.Unmarshal([]byte(elem.Detail), &tips); err != nil {\n\t\t\tlog.ZWarn(ctx, \"Unmarshal MarkAsReadTips error\", err, \"msg\", msg)\n\t\t\tcontinue\n\t\t}\n\t\tif len(tips.ConversationID) == 0 || tips.HasReadSeq < 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Calculate the max seq from tips.Seqs\n\t\tfor _, seq := range tips.Seqs {\n\t\t\tif tips.HasReadSeq < seq {\n\t\t\t\ttips.HasReadSeq = seq\n\t\t\t}\n\t\t}\n\n\t\tif _, ok := conversationUserSeq[tips.ConversationID]; !ok {\n\t\t\tconversationUserSeq[tips.ConversationID] = make(map[string]int64)\n\t\t}\n\t\tif conversationUserSeq[tips.ConversationID][tips.MarkAsReadUserID] < tips.HasReadSeq {\n\t\t\tconversationUserSeq[tips.ConversationID][tips.MarkAsReadUserID] = tips.HasReadSeq\n\t\t}\n\t}\n\tlog.ZInfo(ctx, \"doSetReadSeq\", \"conversationUserSeq\", conversationUserSeq)\n\n\t// persist to db\n\tfor convID, userSeqMap := range conversationUserSeq {\n\t\tif err := och.msgTransferDatabase.SetHasReadSeqToDB(ctx, convID, userSeqMap); err != nil {\n\t\t\tlog.ZWarn(ctx, \"SetHasReadSeqToDB error\", err, \"conversationID\", convID, \"userSeqMap\", userSeqMap)\n\t\t}\n\t}\n\n}\n\nfunc (och *OnlineHistoryRedisConsumerHandler) parseConsumerMessages(ctx context.Context, consumerMessages []*ConsumerMessage) []*ContextMsg {\n\tvar ctxMessages []*ContextMsg\n\tfor i := 0; i < len(consumerMessages); i++ {\n\t\tctxMsg := &ContextMsg{}\n\t\tmsgFromMQ := &sdkws.MsgData{}\n\t\terr := proto.Unmarshal(consumerMessages[i].Value, msgFromMQ)\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"msg_transfer Unmarshal msg err\", err, string(consumerMessages[i].Value))\n\t\t\tcontinue\n\t\t}\n\t\tctxMsg.ctx = consumerMessages[i].Ctx\n\t\tctxMsg.message = msgFromMQ\n\t\tlog.ZDebug(ctx, \"message parse finish\", \"message\", msgFromMQ, \"key\", consumerMessages[i].Key)\n\t\tctxMessages = append(ctxMessages, ctxMsg)\n\t}\n\treturn ctxMessages\n}\n\n// Get messages/notifications stored message list, not stored and pushed message list.\nfunc (och *OnlineHistoryRedisConsumerHandler) categorizeMessageLists(totalMsgs []*ContextMsg) (storageMsgList,\n\tnotStorageMsgList, storageNotificationList, notStorageNotificationList []*ContextMsg) {\n\tfor _, v := range totalMsgs {\n\t\toptions := msgprocessor.Options(v.message.Options)\n\t\tif !options.IsNotNotification() {\n\t\t\t// clone msg from notificationMsg\n\t\t\tif options.IsSendMsg() {\n\t\t\t\tmsg := proto.Clone(v.message).(*sdkws.MsgData)\n\t\t\t\t// message\n\t\t\t\tif v.message.Options != nil {\n\t\t\t\t\tmsg.Options = msgprocessor.NewMsgOptions()\n\t\t\t\t}\n\t\t\t\tmsg.Options = msgprocessor.WithOptions(msg.Options,\n\t\t\t\t\tmsgprocessor.WithOfflinePush(options.IsOfflinePush()),\n\t\t\t\t\tmsgprocessor.WithUnreadCount(options.IsUnreadCount()),\n\t\t\t\t)\n\t\t\t\tv.message.Options = msgprocessor.WithOptions(\n\t\t\t\t\tv.message.Options,\n\t\t\t\t\tmsgprocessor.WithOfflinePush(false),\n\t\t\t\t\tmsgprocessor.WithUnreadCount(false),\n\t\t\t\t)\n\t\t\t\tctxMsg := &ContextMsg{\n\t\t\t\t\tmessage: msg,\n\t\t\t\t\tctx:     v.ctx,\n\t\t\t\t}\n\t\t\t\tstorageMsgList = append(storageMsgList, ctxMsg)\n\t\t\t}\n\t\t\tif options.IsHistory() {\n\t\t\t\tstorageNotificationList = append(storageNotificationList, v)\n\t\t\t} else {\n\t\t\t\tnotStorageNotificationList = append(notStorageNotificationList, v)\n\t\t\t}\n\t\t} else {\n\t\t\tif options.IsHistory() {\n\t\t\t\tstorageMsgList = append(storageMsgList, v)\n\t\t\t} else {\n\t\t\t\tnotStorageMsgList = append(notStorageMsgList, v)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (och *OnlineHistoryRedisConsumerHandler) handleMsg(ctx context.Context, key, conversationID string, storageList, notStorageList []*ContextMsg) {\n\tlog.ZInfo(ctx, \"handle storage msg\")\n\tfor _, storageMsg := range storageList {\n\t\tlog.ZDebug(ctx, \"handle storage msg\", \"msg\", storageMsg.message.String())\n\t}\n\n\toch.toPushTopic(ctx, key, conversationID, notStorageList)\n\tvar storageMessageList []*sdkws.MsgData\n\tfor _, msg := range storageList {\n\t\tstorageMessageList = append(storageMessageList, msg.message)\n\t}\n\tif len(storageMessageList) > 0 {\n\t\tmsg := storageMessageList[0]\n\t\tlastSeq, isNewConversation, userSeqMap, err := och.msgTransferDatabase.BatchInsertChat2Cache(ctx, conversationID, storageMessageList)\n\t\tif err != nil && !errors.Is(errs.Unwrap(err), redis.Nil) {\n\t\t\tlog.ZWarn(ctx, \"batch data insert to redis err\", err, \"storageMsgList\", storageMessageList)\n\t\t\treturn\n\t\t}\n\t\tlog.ZInfo(ctx, \"BatchInsertChat2Cache end\")\n\t\terr = och.msgTransferDatabase.SetHasReadSeqs(ctx, conversationID, userSeqMap)\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"SetHasReadSeqs error\", err, \"userSeqMap\", userSeqMap, \"conversationID\", conversationID)\n\t\t\tprommetrics.SeqSetFailedCounter.Inc()\n\t\t}\n\t\toch.conversationUserHasReadChan <- &userHasReadSeq{\n\t\t\tconversationID: conversationID,\n\t\t\tuserHasReadMap: userSeqMap,\n\t\t}\n\n\t\tif isNewConversation {\n\t\t\tswitch msg.SessionType {\n\t\t\tcase constant.ReadGroupChatType:\n\t\t\t\tlog.ZDebug(ctx, \"group chat first create conversation\", \"conversationID\",\n\t\t\t\t\tconversationID)\n\n\t\t\t\tuserIDs, err := och.groupClient.GetGroupMemberUserIDs(ctx, msg.GroupID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.ZWarn(ctx, \"get group member ids error\", err, \"conversationID\",\n\t\t\t\t\t\tconversationID)\n\t\t\t\t} else {\n\t\t\t\t\tlog.ZInfo(ctx, \"GetGroupMemberIDs end\")\n\n\t\t\t\t\tif err := och.conversationClient.CreateGroupChatConversations(ctx, msg.GroupID, userIDs); err != nil {\n\t\t\t\t\t\tlog.ZWarn(ctx, \"single chat first create conversation error\", err,\n\t\t\t\t\t\t\t\"conversationID\", conversationID)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase constant.SingleChatType, constant.NotificationChatType:\n\t\t\t\treq := &pbconv.CreateSingleChatConversationsReq{\n\t\t\t\t\tRecvID:           msg.RecvID,\n\t\t\t\t\tSendID:           msg.SendID,\n\t\t\t\t\tConversationID:   conversationID,\n\t\t\t\t\tConversationType: msg.SessionType,\n\t\t\t\t}\n\t\t\t\tif err := och.conversationClient.CreateSingleChatConversations(ctx, req); err != nil {\n\t\t\t\t\tlog.ZWarn(ctx, \"single chat or notification first create conversation error\", err,\n\t\t\t\t\t\t\"conversationID\", conversationID, \"sessionType\", msg.SessionType)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tlog.ZWarn(ctx, \"unknown session type\", nil, \"sessionType\",\n\t\t\t\t\tmsg.SessionType)\n\t\t\t}\n\t\t}\n\n\t\tlog.ZInfo(ctx, \"success incr to next topic\")\n\t\terr = och.msgTransferDatabase.MsgToMongoMQ(ctx, key, conversationID, storageMessageList, lastSeq)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"Msg To MongoDB MQ error\", err, \"conversationID\",\n\t\t\t\tconversationID, \"storageList\", storageMessageList, \"lastSeq\", lastSeq)\n\t\t}\n\t\tlog.ZInfo(ctx, \"MsgToMongoMQ end\")\n\n\t\toch.toPushTopic(ctx, key, conversationID, storageList)\n\t\tlog.ZInfo(ctx, \"toPushTopic end\")\n\t}\n}\n\nfunc (och *OnlineHistoryRedisConsumerHandler) handleNotification(ctx context.Context, key, conversationID string,\n\tstorageList, notStorageList []*ContextMsg) {\n\toch.toPushTopic(ctx, key, conversationID, notStorageList)\n\tvar storageMessageList []*sdkws.MsgData\n\tfor _, msg := range storageList {\n\t\tstorageMessageList = append(storageMessageList, msg.message)\n\t}\n\tif len(storageMessageList) > 0 {\n\t\tlastSeq, _, _, err := och.msgTransferDatabase.BatchInsertChat2Cache(ctx, conversationID, storageMessageList)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"notification batch insert to redis error\", err, \"conversationID\", conversationID,\n\t\t\t\t\"storageList\", storageMessageList)\n\t\t\treturn\n\t\t}\n\t\tlog.ZDebug(ctx, \"success to next topic\", \"conversationID\", conversationID)\n\t\terr = och.msgTransferDatabase.MsgToMongoMQ(ctx, key, conversationID, storageMessageList, lastSeq)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"Msg To MongoDB MQ error\", err, \"conversationID\",\n\t\t\t\tconversationID, \"storageList\", storageMessageList, \"lastSeq\", lastSeq)\n\t\t}\n\t\toch.toPushTopic(ctx, key, conversationID, storageList)\n\t}\n}\nfunc (och *OnlineHistoryRedisConsumerHandler) HandleUserHasReadSeqMessages(ctx context.Context) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.ZPanic(ctx, \"HandleUserHasReadSeqMessages Panic\", errs.ErrPanic(r))\n\t\t}\n\t}()\n\n\tdefer och.wg.Done()\n\n\tfor msg := range och.conversationUserHasReadChan {\n\t\tif err := och.msgTransferDatabase.SetHasReadSeqToDB(ctx, msg.conversationID, msg.userHasReadMap); err != nil {\n\t\t\tlog.ZWarn(ctx, \"set read seq to db error\", err, \"conversationID\", msg.conversationID, \"userSeqMap\", msg.userHasReadMap)\n\t\t}\n\t}\n\n\tlog.ZInfo(ctx, \"Channel closed, exiting handleUserHasReadSeqMessages\")\n}\nfunc (och *OnlineHistoryRedisConsumerHandler) Close() {\n\tclose(och.conversationUserHasReadChan)\n\toch.wg.Wait()\n}\n\nfunc (och *OnlineHistoryRedisConsumerHandler) toPushTopic(ctx context.Context, key, conversationID string, msgs []*ContextMsg) {\n\tfor _, v := range msgs {\n\t\tlog.ZDebug(ctx, \"push msg to topic\", \"msg\", v.message.String())\n\t\tif err := och.msgTransferDatabase.MsgToPushMQ(v.ctx, key, conversationID, v.message); err != nil {\n\t\t\tlog.ZError(ctx, \"msg to push topic error\", err, \"msg\", v.message.String())\n\t\t}\n\t}\n}\n\nfunc withAggregationCtx(ctx context.Context, values []*ContextMsg) context.Context {\n\tvar allMessageOperationID string\n\tfor i, v := range values {\n\t\tif opid := mcontext.GetOperationID(v.ctx); opid != \"\" {\n\t\t\tif i == 0 {\n\t\t\t\tallMessageOperationID += opid\n\t\t\t} else {\n\t\t\t\tallMessageOperationID += \"$\" + opid\n\t\t\t}\n\t\t}\n\t}\n\treturn mcontext.SetOperationID(ctx, allMessageOperationID)\n}\n\nfunc (och *OnlineHistoryRedisConsumerHandler) HandlerRedisMessage(msg mq.Message) error { // a instance in the consumer group\n\terr := och.redisMessageBatches.Put(msg.Context(), &ConsumerMessage{Ctx: msg.Context(), Key: msg.Key(), Value: msg.Value(), Raw: msg})\n\tif err != nil {\n\t\tlog.ZWarn(msg.Context(), \"put msg to  error\", err, \"key\", msg.Key(), \"value\", msg.Value())\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/msgtransfer/online_msg_to_mongo_handler.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msgtransfer\n\nimport (\n\t\"github.com/openimsdk/tools/mq\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\tpbmsg \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\ntype OnlineHistoryMongoConsumerHandler struct {\n\tmsgTransferDatabase controller.MsgTransferDatabase\n\tconfig              *Config\n\twebhookClient       *webhook.Client\n}\n\nfunc NewOnlineHistoryMongoConsumerHandler(database controller.MsgTransferDatabase, config *Config) *OnlineHistoryMongoConsumerHandler {\n\treturn &OnlineHistoryMongoConsumerHandler{\n\t\tmsgTransferDatabase: database,\n\t\tconfig:              config,\n\t\twebhookClient:       webhook.NewWebhookClient(config.WebhooksConfig.URL),\n\t}\n}\n\nfunc (mc *OnlineHistoryMongoConsumerHandler) HandleChatWs2Mongo(val mq.Message) {\n\tctx := val.Context()\n\tkey := val.Key()\n\tmsg := val.Value()\n\tmsgFromMQ := pbmsg.MsgDataToMongoByMQ{}\n\terr := proto.Unmarshal(msg, &msgFromMQ)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"unmarshall failed\", err, \"key\", key, \"len\", len(msg))\n\t\treturn\n\t}\n\tif len(msgFromMQ.MsgData) == 0 {\n\t\tlog.ZError(ctx, \"msgFromMQ.MsgData is empty\", nil, \"key\", key, \"msg\", msg)\n\t\treturn\n\t}\n\tlog.ZDebug(ctx, \"mongo consumer recv msg\", \"msgs\", msgFromMQ.String())\n\terr = mc.msgTransferDatabase.BatchInsertChat2DB(ctx, msgFromMQ.ConversationID, msgFromMQ.MsgData, msgFromMQ.LastSeq)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"batch data insert to mongo err\", err, \"msg\", msgFromMQ.MsgData, \"conversationID\", msgFromMQ.ConversationID)\n\t\tprommetrics.MsgInsertMongoFailedCounter.Inc()\n\t} else {\n\t\tprommetrics.MsgInsertMongoSuccessCounter.Inc()\n\t\tval.Mark()\n\t}\n\n\tfor _, msgData := range msgFromMQ.MsgData {\n\t\tmc.webhookAfterMsgSaveDB(ctx, &mc.config.WebhooksConfig.AfterMsgSaveDB, msgData)\n\t}\n\n\t//var seqs []int64\n\t//for _, msg := range msgFromMQ.MsgData {\n\t//\tseqs = append(seqs, msg.Seq)\n\t//}\n\t//if err := mc.msgTransferDatabase.DeleteMessagesFromCache(ctx, msgFromMQ.ConversationID, seqs); err != nil {\n\t//\tlog.ZError(ctx, \"remove cache msg from redis err\", err, \"msg\",\n\t//\t\tmsgFromMQ.MsgData, \"conversationID\", msgFromMQ.ConversationID)\n\t//}\n}\n"
  },
  {
    "path": "internal/push/callback.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage push\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nfunc (c *ConsumerHandler) webhookBeforeOfflinePush(ctx context.Context, before *config.BeforeConfig, userIDs []string, msg *sdkws.MsgData, offlinePushUserIDs *[]string) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tif msg.ContentType == constant.Typing {\n\t\t\treturn nil\n\t\t}\n\t\treq := &callbackstruct.CallbackBeforePushReq{\n\t\t\tUserStatusBatchCallbackReq: callbackstruct.UserStatusBatchCallbackReq{\n\t\t\t\tUserStatusBaseCallback: callbackstruct.UserStatusBaseCallback{\n\t\t\t\t\tCallbackCommand: callbackstruct.CallbackBeforeOfflinePushCommand,\n\t\t\t\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\t\t\t\tPlatformID:      int(msg.SenderPlatformID),\n\t\t\t\t\tPlatform:        constant.PlatformIDToName(int(msg.SenderPlatformID)),\n\t\t\t\t},\n\t\t\t\tUserIDList: userIDs,\n\t\t\t},\n\t\t\tOfflinePushInfo: msg.OfflinePushInfo,\n\t\t\tClientMsgID:     msg.ClientMsgID,\n\t\t\tSendID:          msg.SendID,\n\t\t\tGroupID:         msg.GroupID,\n\t\t\tContentType:     msg.ContentType,\n\t\t\tSessionType:     msg.SessionType,\n\t\t\tAtUserIDs:       msg.AtUserIDList,\n\t\t\tContent:         GetContent(msg),\n\t\t}\n\n\t\tresp := &callbackstruct.CallbackBeforePushResp{}\n\n\t\tif err := c.webhookClient.SyncPost(ctx, req.GetCallbackCommand(), req, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(resp.UserIDs) != 0 {\n\t\t\t*offlinePushUserIDs = resp.UserIDs\n\t\t}\n\t\tif resp.OfflinePushInfo != nil {\n\t\t\tmsg.OfflinePushInfo = resp.OfflinePushInfo\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (c *ConsumerHandler) webhookBeforeOnlinePush(ctx context.Context, before *config.BeforeConfig, userIDs []string, msg *sdkws.MsgData) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tif msg.ContentType == constant.Typing {\n\t\t\treturn nil\n\t\t}\n\t\treq := callbackstruct.CallbackBeforePushReq{\n\t\t\tUserStatusBatchCallbackReq: callbackstruct.UserStatusBatchCallbackReq{\n\t\t\t\tUserStatusBaseCallback: callbackstruct.UserStatusBaseCallback{\n\t\t\t\t\tCallbackCommand: callbackstruct.CallbackBeforeOnlinePushCommand,\n\t\t\t\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\t\t\t\tPlatformID:      int(msg.SenderPlatformID),\n\t\t\t\t\tPlatform:        constant.PlatformIDToName(int(msg.SenderPlatformID)),\n\t\t\t\t},\n\t\t\t\tUserIDList: userIDs,\n\t\t\t},\n\t\t\tClientMsgID: msg.ClientMsgID,\n\t\t\tSendID:      msg.SendID,\n\t\t\tGroupID:     msg.GroupID,\n\t\t\tContentType: msg.ContentType,\n\t\t\tSessionType: msg.SessionType,\n\t\t\tAtUserIDs:   msg.AtUserIDList,\n\t\t\tContent:     GetContent(msg),\n\t\t}\n\t\tresp := &callbackstruct.CallbackBeforePushResp{}\n\t\tif err := c.webhookClient.SyncPost(ctx, req.GetCallbackCommand(), req, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (c *ConsumerHandler) webhookBeforeGroupOnlinePush(\n\tctx context.Context,\n\tbefore *config.BeforeConfig,\n\tgroupID string,\n\tmsg *sdkws.MsgData,\n\tpushToUserIDs *[]string,\n) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tif msg.ContentType == constant.Typing {\n\t\t\treturn nil\n\t\t}\n\t\treq := callbackstruct.CallbackBeforeSuperGroupOnlinePushReq{\n\t\t\tUserStatusBaseCallback: callbackstruct.UserStatusBaseCallback{\n\t\t\t\tCallbackCommand: callbackstruct.CallbackBeforeGroupOnlinePushCommand,\n\t\t\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\t\t\tPlatformID:      int(msg.SenderPlatformID),\n\t\t\t\tPlatform:        constant.PlatformIDToName(int(msg.SenderPlatformID)),\n\t\t\t},\n\t\t\tClientMsgID: msg.ClientMsgID,\n\t\t\tSendID:      msg.SendID,\n\t\t\tGroupID:     groupID,\n\t\t\tContentType: msg.ContentType,\n\t\t\tSessionType: msg.SessionType,\n\t\t\tAtUserIDs:   msg.AtUserIDList,\n\t\t\tContent:     GetContent(msg),\n\t\t\tSeq:         msg.Seq,\n\t\t}\n\t\tresp := &callbackstruct.CallbackBeforeSuperGroupOnlinePushResp{}\n\t\tif err := c.webhookClient.SyncPost(ctx, req.GetCallbackCommand(), req, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(resp.UserIDs) != 0 {\n\t\t\t*pushToUserIDs = resp.UserIDs\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc GetContent(msg *sdkws.MsgData) string {\n\tif msg.ContentType >= constant.NotificationBegin && msg.ContentType <= constant.NotificationEnd {\n\t\tvar notification sdkws.NotificationElem\n\t\tif err := json.Unmarshal(msg.Content, &notification); err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn notification.Detail\n\t} else {\n\t\treturn string(msg.Content)\n\t}\n}\n"
  },
  {
    "path": "internal/push/offlinepush/dummy/push.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage dummy\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/options\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"sync/atomic\"\n)\n\nfunc NewClient() *Dummy {\n\treturn &Dummy{}\n}\n\ntype Dummy struct {\n\tv atomic.Bool\n}\n\nfunc (d *Dummy) Push(ctx context.Context, userIDs []string, title, content string, opts *options.Opts) error {\n\tif d.v.CompareAndSwap(false, true) {\n\t\tlog.ZWarn(ctx, \"dummy push\", nil, \"ps\", \"the offline push is not configured. to configure it, please go to config/openim-push.yml\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/push/offlinepush/fcm/push.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage fcm\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/options\"\n\t\"github.com/openimsdk/tools/utils/httputil\"\n\n\tfirebase \"firebase.google.com/go/v4\"\n\t\"firebase.google.com/go/v4/messaging\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"google.golang.org/api/option\"\n)\n\nconst SinglePushCountLimit = 400\n\nvar Terminal = []int{constant.IOSPlatformID, constant.AndroidPlatformID, constant.WebPlatformID}\n\ntype Fcm struct {\n\tfcmMsgCli *messaging.Client\n\tcache     cache.ThirdCache\n}\n\n// NewClient initializes a new FCM client using the Firebase Admin SDK.\n// It requires the FCM service account credentials file located within the project's configuration directory.\nfunc NewClient(pushConf *config.Push, cache cache.ThirdCache, fcmConfigPath string) (*Fcm, error) {\n\tvar opt option.ClientOption\n\tswitch {\n\tcase len(pushConf.FCM.FilePath) != 0:\n\t\t// with file path\n\t\tcredentialsFilePath := filepath.Join(fcmConfigPath, pushConf.FCM.FilePath)\n\t\topt = option.WithCredentialsFile(credentialsFilePath)\n\tcase len(pushConf.FCM.AuthURL) != 0:\n\t\t// with authentication URL\n\t\tclient := httputil.NewHTTPClient(httputil.NewClientConfig())\n\t\tresp, err := client.Get(pushConf.FCM.AuthURL)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\topt = option.WithCredentialsJSON(resp)\n\tdefault:\n\t\treturn nil, errs.New(\"no FCM config\").Wrap()\n\t}\n\n\tfcmApp, err := firebase.NewApp(context.Background(), nil, opt)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\tctx := context.Background()\n\tfcmMsgClient, err := fcmApp.Messaging(ctx)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn &Fcm{fcmMsgCli: fcmMsgClient, cache: cache}, nil\n}\n\nfunc (f *Fcm) Push(ctx context.Context, userIDs []string, title, content string, opts *options.Opts) error {\n\t// accounts->registrationToken\n\tallTokens := make(map[string][]string, 0)\n\tfor _, account := range userIDs {\n\t\tvar personTokens []string\n\t\tfor _, v := range Terminal {\n\t\t\tToken, err := f.cache.GetFcmToken(ctx, account, v)\n\t\t\tif err == nil {\n\t\t\t\tpersonTokens = append(personTokens, Token)\n\t\t\t}\n\t\t}\n\t\tallTokens[account] = personTokens\n\t}\n\tSuccess := 0\n\tFail := 0\n\tnotification := &messaging.Notification{}\n\tnotification.Body = content\n\tnotification.Title = title\n\tvar messages []*messaging.Message\n\tvar sendErrBuilder strings.Builder\n\tvar msgErrBuilder strings.Builder\n\tfor userID, personTokens := range allTokens {\n\t\tapns := &messaging.APNSConfig{Payload: &messaging.APNSPayload{Aps: &messaging.Aps{Sound: opts.IOSPushSound}}}\n\t\tmessageCount := len(messages)\n\t\tif messageCount >= SinglePushCountLimit {\n\t\t\tresponse, err := f.fcmMsgCli.SendEach(ctx, messages)\n\t\t\tif err != nil {\n\t\t\t\tFail = Fail + messageCount\n\t\t\t\t// Record push error\n\t\t\t\tsendErrBuilder.WriteString(err.Error())\n\t\t\t\tsendErrBuilder.WriteByte('.')\n\t\t\t} else {\n\t\t\t\tSuccess = Success + response.SuccessCount\n\t\t\t\tFail = Fail + response.FailureCount\n\t\t\t\tif response.FailureCount != 0 {\n\t\t\t\t\t// Record message error\n\t\t\t\t\tfor i := range response.Responses {\n\t\t\t\t\t\tif !response.Responses[i].Success {\n\t\t\t\t\t\t\tmsgErrBuilder.WriteString(response.Responses[i].Error.Error())\n\t\t\t\t\t\t\tmsgErrBuilder.WriteByte('.')\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\tmessages = messages[0:0]\n\t\t}\n\t\tif opts.IOSBadgeCount {\n\t\t\tunreadCountSum, err := f.cache.IncrUserBadgeUnreadCountSum(ctx, userID)\n\t\t\tif err == nil {\n\t\t\t\tapns.Payload.Aps.Badge = &unreadCountSum\n\t\t\t} else {\n\t\t\t\t// log.Error(operationID, \"IncrUserBadgeUnreadCountSum redis err\", err.Error(), uid)\n\t\t\t\tFail++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\tunreadCountSum, err := f.cache.GetUserBadgeUnreadCountSum(ctx, userID)\n\t\t\tif err == nil && unreadCountSum != 0 {\n\t\t\t\tapns.Payload.Aps.Badge = &unreadCountSum\n\t\t\t} else if errors.Is(err, redis.Nil) || unreadCountSum == 0 {\n\t\t\t\tzero := 1\n\t\t\t\tapns.Payload.Aps.Badge = &zero\n\t\t\t} else {\n\t\t\t\t// log.Error(operationID, \"GetUserBadgeUnreadCountSum redis err\", err.Error(), uid)\n\t\t\t\tFail++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tfor _, token := range personTokens {\n\t\t\ttemp := &messaging.Message{\n\t\t\t\tData:         map[string]string{\"ex\": opts.Ex},\n\t\t\t\tToken:        token,\n\t\t\t\tNotification: notification,\n\t\t\t\tAPNS:         apns,\n\t\t\t}\n\t\t\tmessages = append(messages, temp)\n\t\t}\n\t}\n\tmessageCount := len(messages)\n\tif messageCount > 0 {\n\t\tresponse, err := f.fcmMsgCli.SendEach(ctx, messages)\n\t\tif err != nil {\n\t\t\tFail = Fail + messageCount\n\t\t} else {\n\t\t\tSuccess = Success + response.SuccessCount\n\t\t\tFail = Fail + response.FailureCount\n\t\t}\n\t}\n\tif Fail != 0 {\n\t\treturn errs.New(fmt.Sprintf(\"%d message send failed;send err:%s;message err:%s\",\n\t\t\tFail, sendErrBuilder.String(), msgErrBuilder.String())).Wrap()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/push/offlinepush/getui/body.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage getui\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nvar (\n\tincOne          = datautil.ToPtr(\"+1\")\n\taddNum          = \"1\"\n\tdefaultStrategy = strategy{\n\t\tDefault: 1,\n\t}\n\tmsgCategory = \"CATEGORY_MESSAGE\"\n)\n\ntype Resp struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n\tData any    `json:\"data\"`\n}\n\nfunc (r *Resp) parseError() (err error) {\n\tswitch r.Code {\n\tcase tokenExpireCode:\n\t\terr = ErrTokenExpire\n\tcase 0:\n\t\terr = nil\n\tdefault:\n\t\terr = fmt.Errorf(\"code %d, msg %s\", r.Code, r.Msg)\n\t}\n\treturn err\n}\n\ntype RespI interface {\n\tparseError() error\n}\n\ntype AuthReq struct {\n\tSign      string `json:\"sign\"`\n\tTimestamp string `json:\"timestamp\"`\n\tAppKey    string `json:\"appkey\"`\n}\n\ntype AuthResp struct {\n\tExpireTime string `json:\"expire_time\"`\n\tToken      string `json:\"token\"`\n}\n\ntype TaskResp struct {\n\tTaskID string `json:\"taskID\"`\n}\n\ntype Settings struct {\n\tTTL      *int64   `json:\"ttl\"`\n\tStrategy strategy `json:\"strategy\"`\n}\n\ntype strategy struct {\n\tDefault int64 `json:\"default\"`\n\t//IOS     int64 `json:\"ios\"`\n\t//St      int64 `json:\"st\"`\n\t//Hw      int64 `json:\"hw\"`\n\t//Ho      int64 `json:\"ho\"`\n\t//XM      int64 `json:\"xm\"`\n\t//XMG     int64 `json:\"xmg\"`\n\t//VV      int64 `json:\"vv\"`\n\t//Op      int64 `json:\"op\"`\n\t//OpG     int64 `json:\"opg\"`\n\t//MZ      int64 `json:\"mz\"`\n\t//HosHw   int64 `json:\"hoshw\"`\n\t//WX      int64 `json:\"wx\"`\n}\n\ntype Audience struct {\n\tAlias []string `json:\"alias\"`\n}\n\ntype PushMessage struct {\n\tNotification *Notification `json:\"notification,omitempty\"`\n\tTransmission *string       `json:\"transmission,omitempty\"`\n}\n\ntype PushChannel struct {\n\tIos     *Ios     `json:\"ios\"`\n\tAndroid *Android `json:\"android\"`\n}\n\ntype PushReq struct {\n\tRequestID   *string      `json:\"request_id\"`\n\tSettings    *Settings    `json:\"settings\"`\n\tAudience    *Audience    `json:\"audience\"`\n\tPushMessage *PushMessage `json:\"push_message\"`\n\tPushChannel *PushChannel `json:\"push_channel\"`\n\tIsAsync     *bool        `json:\"is_async\"`\n\tTaskID      *string      `json:\"taskid\"`\n}\n\ntype Ios struct {\n\tNotificationType *string `json:\"type\"`\n\tAutoBadge        *string `json:\"auto_badge\"`\n\tAps              struct {\n\t\tSound string `json:\"sound\"`\n\t\tAlert Alert  `json:\"alert\"`\n\t} `json:\"aps\"`\n}\n\ntype Alert struct {\n\tTitle string `json:\"title\"`\n\tBody  string `json:\"body\"`\n}\n\ntype Android struct {\n\tUps struct {\n\t\tNotification Notification `json:\"notification\"`\n\t\tOptions      Options      `json:\"options\"`\n\t} `json:\"ups\"`\n}\n\ntype Notification struct {\n\tTitle       string `json:\"title\"`\n\tBody        string `json:\"body\"`\n\tChannelID   string `json:\"channelID\"`\n\tChannelName string `json:\"ChannelName\"`\n\tClickType   string `json:\"click_type\"`\n\tBadgeAddNum string `json:\"badge_add_num\"`\n\tCategory    string `json:\"category\"`\n}\n\ntype Options struct {\n\tHW struct {\n\t\tDefaultSound bool   `json:\"/message/android/notification/default_sound\"`\n\t\tChannelID    string `json:\"/message/android/notification/channel_id\"`\n\t\tSound        string `json:\"/message/android/notification/sound\"`\n\t\tImportance   string `json:\"/message/android/notification/importance\"`\n\t\tCategory     string `json:\"/message/android/category\"`\n\t} `json:\"HW\"`\n\tXM struct {\n\t\tChannelID string `json:\"/extra.channel_id\"`\n\t} `json:\"XM\"`\n\tVV struct {\n\t\tClassification int `json:\"/classification\"`\n\t} `json:\"VV\"`\n}\n\ntype Payload struct {\n\tIsSignal bool `json:\"isSignal\"`\n}\n\nfunc newPushReq(pushConf *config.Push, title, content string) PushReq {\n\tpushReq := PushReq{PushMessage: &PushMessage{Notification: &Notification{\n\t\tTitle:       title,\n\t\tBody:        content,\n\t\tClickType:   \"startapp\",\n\t\tChannelID:   pushConf.GeTui.ChannelID,\n\t\tChannelName: pushConf.GeTui.ChannelName,\n\t\tBadgeAddNum: addNum,\n\t\tCategory:    msgCategory,\n\t}}}\n\treturn pushReq\n}\n\nfunc newBatchPushReq(userIDs []string, taskID string) PushReq {\n\tIsAsync := true\n\treturn PushReq{Audience: &Audience{Alias: userIDs}, IsAsync: &IsAsync, TaskID: &taskID}\n}\n\nfunc (pushReq *PushReq) setPushChannel(title string, body string) {\n\tpushReq.PushChannel = &PushChannel{}\n\t// autoBadge := \"+1\"\n\tpushReq.PushChannel.Ios = &Ios{}\n\tnotify := \"notify\"\n\tpushReq.PushChannel.Ios.NotificationType = &notify\n\tpushReq.PushChannel.Ios.Aps.Sound = \"default\"\n\tpushReq.PushChannel.Ios.AutoBadge = incOne\n\tpushReq.PushChannel.Ios.Aps.Alert = Alert{\n\t\tTitle: title,\n\t\tBody:  body,\n\t}\n\tpushReq.PushChannel.Android = &Android{}\n\tpushReq.PushChannel.Android.Ups.Notification = Notification{\n\t\tTitle:     title,\n\t\tBody:      body,\n\t\tClickType: \"startapp\",\n\t}\n\tpushReq.PushChannel.Android.Ups.Options = Options{\n\t\tHW: struct {\n\t\t\tDefaultSound bool   `json:\"/message/android/notification/default_sound\"`\n\t\t\tChannelID    string `json:\"/message/android/notification/channel_id\"`\n\t\t\tSound        string `json:\"/message/android/notification/sound\"`\n\t\t\tImportance   string `json:\"/message/android/notification/importance\"`\n\t\t\tCategory     string `json:\"/message/android/category\"`\n\t\t}{ChannelID: \"RingRing4\", Sound: \"/raw/ring001\", Importance: \"NORMAL\", Category: \"IM\"},\n\t\tXM: struct {\n\t\t\tChannelID string `json:\"/extra.channel_id\"`\n\t\t}{ChannelID: \"high_system\"},\n\t\tVV: struct {\n\t\t\tClassification int \"json:\\\"/classification\\\"\"\n\t\t}{\n\t\t\tClassification: 1,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/push/offlinepush/getui/push.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage getui\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/options\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/httputil\"\n\t\"github.com/openimsdk/tools/utils/splitter\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar (\n\tErrTokenExpire = errs.New(\"token expire\")\n\tErrUserIDEmpty = errs.New(\"userIDs is empty\")\n)\n\nconst (\n\tpushURL      = \"/push/single/alias\"\n\tauthURL      = \"/auth\"\n\ttaskURL      = \"/push/list/message\"\n\tbatchPushURL = \"/push/list/alias\"\n\n\t// Codes.\n\ttokenExpireCode = 10001\n\ttokenExpireTime = 60 * 60 * 23\n\ttaskIDTTL       = 1000 * 60 * 60 * 24\n)\n\ntype Client struct {\n\tcache           cache.ThirdCache\n\ttokenExpireTime int64\n\ttaskIDTTL       int64\n\tpushConf        *config.Push\n\thttpClient      *httputil.HTTPClient\n}\n\nfunc NewClient(pushConf *config.Push, cache cache.ThirdCache) *Client {\n\treturn &Client{cache: cache,\n\t\ttokenExpireTime: tokenExpireTime,\n\t\ttaskIDTTL:       taskIDTTL,\n\t\tpushConf:        pushConf,\n\t\thttpClient:      httputil.NewHTTPClient(httputil.NewClientConfig()),\n\t}\n}\n\nfunc (g *Client) Push(ctx context.Context, userIDs []string, title, content string, opts *options.Opts) error {\n\ttoken, err := g.cache.GetGetuiToken(ctx)\n\tif err != nil {\n\t\tif errors.Is(err, redis.Nil) {\n\t\t\tlog.ZDebug(ctx, \"getui token not exist in redis\")\n\t\t\ttoken, err = g.getTokenAndSave2Redis(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\tpushReq := newPushReq(g.pushConf, title, content)\n\tpushReq.setPushChannel(title, content)\n\tif len(userIDs) > 1 {\n\t\tmaxNum := 999\n\t\tif len(userIDs) > maxNum {\n\t\t\ts := splitter.NewSplitter(maxNum, userIDs)\n\t\t\twg := sync.WaitGroup{}\n\t\t\twg.Add(len(s.GetSplitResult()))\n\t\t\tfor i, v := range s.GetSplitResult() {\n\t\t\t\tgo func(index int, userIDs []string) {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tfor i := 0; i < len(userIDs); i += maxNum {\n\t\t\t\t\t\tend := i + maxNum\n\t\t\t\t\t\tif end > len(userIDs) {\n\t\t\t\t\t\t\tend = len(userIDs)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err = g.batchPush(ctx, token, userIDs[i:end], pushReq); err != nil {\n\t\t\t\t\t\t\tlog.ZError(ctx, \"batchPush failed\", err, \"index\", index, \"token\", token, \"req\", pushReq)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif err = g.batchPush(ctx, token, userIDs, pushReq); err != nil {\n\t\t\t\t\t\tlog.ZError(ctx, \"batchPush failed\", err, \"index\", index, \"token\", token, \"req\", pushReq)\n\t\t\t\t\t}\n\t\t\t\t}(i, v.Item)\n\t\t\t}\n\t\t\twg.Wait()\n\t\t} else {\n\t\t\terr = g.batchPush(ctx, token, userIDs, pushReq)\n\t\t}\n\t} else if len(userIDs) == 1 {\n\t\terr = g.singlePush(ctx, token, userIDs[0], pushReq)\n\t} else {\n\t\treturn ErrUserIDEmpty\n\t}\n\tswitch err {\n\tcase ErrTokenExpire:\n\t\ttoken, err = g.getTokenAndSave2Redis(ctx)\n\t}\n\treturn err\n}\n\nfunc (g *Client) Auth(ctx context.Context, timeStamp int64) (token string, expireTime int64, err error) {\n\th := sha256.New()\n\th.Write(\n\t\t[]byte(g.pushConf.GeTui.AppKey + strconv.Itoa(int(timeStamp)) + g.pushConf.GeTui.MasterSecret),\n\t)\n\tsign := hex.EncodeToString(h.Sum(nil))\n\treqAuth := AuthReq{\n\t\tSign:      sign,\n\t\tTimestamp: strconv.Itoa(int(timeStamp)),\n\t\tAppKey:    g.pushConf.GeTui.AppKey,\n\t}\n\trespAuth := AuthResp{}\n\terr = g.request(ctx, authURL, reqAuth, \"\", &respAuth)\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\texpire, err := strconv.Atoi(respAuth.ExpireTime)\n\treturn respAuth.Token, int64(expire), err\n}\n\nfunc (g *Client) GetTaskID(ctx context.Context, token string, pushReq PushReq) (string, error) {\n\trespTask := TaskResp{}\n\tttl := int64(1000 * 60 * 5)\n\tpushReq.Settings = &Settings{TTL: &ttl, Strategy: defaultStrategy}\n\terr := g.request(ctx, taskURL, pushReq, token, &respTask)\n\tif err != nil {\n\t\treturn \"\", errs.Wrap(err)\n\t}\n\treturn respTask.TaskID, nil\n}\n\n// max num is 999.\nfunc (g *Client) batchPush(ctx context.Context, token string, userIDs []string, pushReq PushReq) error {\n\ttaskID, err := g.GetTaskID(ctx, token, pushReq)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpushReq = newBatchPushReq(userIDs, taskID)\n\treturn g.request(ctx, batchPushURL, pushReq, token, nil)\n}\n\nfunc (g *Client) singlePush(ctx context.Context, token, userID string, pushReq PushReq) error {\n\toperationID := mcontext.GetOperationID(ctx)\n\tpushReq.RequestID = &operationID\n\tpushReq.Audience = &Audience{Alias: []string{userID}}\n\treturn g.request(ctx, pushURL, pushReq, token, nil)\n}\n\nfunc (g *Client) request(ctx context.Context, url string, input any, token string, output any) error {\n\theader := map[string]string{\"token\": token}\n\tresp := &Resp{}\n\tresp.Data = output\n\treturn g.postReturn(ctx, g.pushConf.GeTui.PushUrl+url, header, input, resp, 3)\n}\n\nfunc (g *Client) postReturn(\n\tctx context.Context,\n\turl string,\n\theader map[string]string,\n\tinput any,\n\toutput RespI,\n\ttimeout int,\n) error {\n\terr := g.httpClient.PostReturn(ctx, url, header, input, output, timeout)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.ZDebug(ctx, \"postReturn\", \"url\", url, \"header\", header, \"input\", input, \"timeout\", timeout, \"output\", output)\n\treturn output.parseError()\n}\n\nfunc (g *Client) getTokenAndSave2Redis(ctx context.Context) (token string, err error) {\n\ttoken, _, err = g.Auth(ctx, time.Now().UnixNano()/1e6)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = g.cache.SetGetuiToken(ctx, token, 60*60*23)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn token, nil\n}\n\nfunc (g *Client) GetTaskIDAndSave2Redis(ctx context.Context, token string, pushReq PushReq) (taskID string, err error) {\n\tpushReq.Settings = &Settings{TTL: &g.taskIDTTL, Strategy: defaultStrategy}\n\ttaskID, err = g.GetTaskID(ctx, token, pushReq)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = g.cache.SetGetuiTaskID(ctx, taskID, g.tokenExpireTime)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn token, nil\n}\n"
  },
  {
    "path": "internal/push/offlinepush/jpush/body/audience.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage body\n\nconst (\n\tTAG            = \"tag\"\n\tTAGAND         = \"tag_and\"\n\tTAGNOT         = \"tag_not\"\n\tALIAS          = \"alias\"\n\tREGISTRATIONID = \"registration_id\"\n)\n\ntype Audience struct {\n\tObject   any\n\taudience map[string][]string\n}\n\nfunc (a *Audience) set(key string, v []string) {\n\tif a.audience == nil {\n\t\ta.audience = make(map[string][]string)\n\t\ta.Object = a.audience\n\t}\n\t// v, ok = this.audience[key]\n\t// if ok {\n\t//\treturn\n\t//}\n\ta.audience[key] = v\n}\n\nfunc (a *Audience) SetTag(tags []string) {\n\ta.set(TAG, tags)\n}\n\nfunc (a *Audience) SetTagAnd(tags []string) {\n\ta.set(TAGAND, tags)\n}\n\nfunc (a *Audience) SetTagNot(tags []string) {\n\ta.set(TAGNOT, tags)\n}\n\nfunc (a *Audience) SetAlias(alias []string) {\n\ta.set(ALIAS, alias)\n}\n\nfunc (a *Audience) SetRegistrationId(ids []string) {\n\ta.set(REGISTRATIONID, ids)\n}\n\nfunc (a *Audience) SetAll() {\n\ta.Object = \"all\"\n}\n"
  },
  {
    "path": "internal/push/offlinepush/jpush/body/message.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage body\n\ntype Message struct {\n\tMsgContent  string         `json:\"msg_content\"`\n\tTitle       string         `json:\"title,omitempty\"`\n\tContentType string         `json:\"content_type,omitempty\"`\n\tExtras      map[string]any `json:\"extras,omitempty\"`\n}\n\nfunc (m *Message) SetMsgContent(c string) {\n\tm.MsgContent = c\n}\n\nfunc (m *Message) SetTitle(t string) {\n\tm.Title = t\n}\n\nfunc (m *Message) SetContentType(c string) {\n\tm.ContentType = c\n}\n\nfunc (m *Message) SetExtras(key string, value any) {\n\tif m.Extras == nil {\n\t\tm.Extras = make(map[string]any)\n\t}\n\tm.Extras[key] = value\n}\n"
  },
  {
    "path": "internal/push/offlinepush/jpush/body/notification.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage body\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/options\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n)\n\ntype Notification struct {\n\tAlert   string  `json:\"alert,omitempty\"`\n\tAndroid Android `json:\"android,omitempty\"`\n\tIOS     Ios     `json:\"ios,omitempty\"`\n}\n\ntype Android struct {\n\tAlert  string `json:\"alert,omitempty\"`\n\tTitle  string `json:\"title,omitempty\"`\n\tIntent struct {\n\t\tURL string `json:\"url,omitempty\"`\n\t} `json:\"intent,omitempty\"`\n\tExtras map[string]string `json:\"extras,omitempty\"`\n}\ntype Ios struct {\n\tAlert          IosAlert          `json:\"alert,omitempty\"`\n\tSound          string            `json:\"sound,omitempty\"`\n\tBadge          string            `json:\"badge,omitempty\"`\n\tExtras         map[string]string `json:\"extras,omitempty\"`\n\tMutableContent bool              `json:\"mutable-content\"`\n}\n\ntype IosAlert struct {\n\tTitle string `json:\"title,omitempty\"`\n\tBody  string `json:\"body,omitempty\"`\n}\n\nfunc (n *Notification) SetAlert(alert string, title string, opts *options.Opts) {\n\tn.Alert = alert\n\tn.Android.Alert = alert\n\tn.Android.Title = title\n\tn.IOS.Alert.Body = alert\n\tn.IOS.Alert.Title = title\n\tn.IOS.Sound = opts.IOSPushSound\n\tif opts.IOSBadgeCount {\n\t\tn.IOS.Badge = \"+1\"\n\t}\n}\n\nfunc (n *Notification) SetExtras(extras map[string]string) {\n\tn.IOS.Extras = extras\n\tn.Android.Extras = extras\n}\n\nfunc (n *Notification) SetAndroidIntent(pushConf *config.Push) {\n\tn.Android.Intent.URL = pushConf.JPush.PushIntent\n}\n\nfunc (n *Notification) IOSEnableMutableContent() {\n\tn.IOS.MutableContent = true\n}\n"
  },
  {
    "path": "internal/push/offlinepush/jpush/body/options.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage body\n\ntype Options struct {\n\tApnsProduction bool `json:\"apns_production\"`\n}\n\nfunc (o *Options) SetApnsProduction(c bool) {\n\to.ApnsProduction = c\n}\n"
  },
  {
    "path": "internal/push/offlinepush/jpush/body/platform.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage body\n\nimport (\n\t\"github.com/openimsdk/tools/errs\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n)\n\nconst (\n\tANDROID      = \"android\"\n\tIOS          = \"ios\"\n\tQUICKAPP     = \"quickapp\"\n\tWINDOWSPHONE = \"winphone\"\n\tALL          = \"all\"\n)\n\ntype Platform struct {\n\tOs     any\n\tosArry []string\n}\n\nfunc (p *Platform) Set(os string) error {\n\tif p.Os == nil {\n\t\tp.osArry = make([]string, 0, 4)\n\t} else {\n\t\tswitch p.Os.(type) {\n\t\tcase string:\n\t\t\treturn errs.New(\"platform is all\")\n\t\tdefault:\n\t\t}\n\t}\n\n\tfor _, value := range p.osArry {\n\t\tif os == value {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tswitch os {\n\tcase IOS:\n\t\tfallthrough\n\tcase ANDROID:\n\t\tfallthrough\n\tcase QUICKAPP:\n\t\tfallthrough\n\tcase WINDOWSPHONE:\n\t\tp.osArry = append(p.osArry, os)\n\t\tp.Os = p.osArry\n\tdefault:\n\t\treturn errs.New(\"unknow platform\")\n\t}\n\n\treturn nil\n}\n\nfunc (p *Platform) SetPlatform(platform string) error {\n\tswitch platform {\n\tcase constant.AndroidPlatformStr:\n\t\treturn p.SetAndroid()\n\tcase constant.IOSPlatformStr:\n\t\treturn p.SetIOS()\n\tdefault:\n\t\treturn errs.New(\"platform err\")\n\t}\n}\n\nfunc (p *Platform) SetIOS() error {\n\treturn p.Set(IOS)\n}\n\nfunc (p *Platform) SetAndroid() error {\n\treturn p.Set(ANDROID)\n}\n\nfunc (p *Platform) SetQuickApp() error {\n\treturn p.Set(QUICKAPP)\n}\n\nfunc (p *Platform) SetWindowsPhone() error {\n\treturn p.Set(WINDOWSPHONE)\n}\n\nfunc (p *Platform) SetAll() {\n\tp.Os = ALL\n}\n"
  },
  {
    "path": "internal/push/offlinepush/jpush/body/pushobj.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage body\n\ntype PushObj struct {\n\tPlatform     any `json:\"platform\"`\n\tAudience     any `json:\"audience\"`\n\tNotification any `json:\"notification,omitempty\"`\n\tMessage      any `json:\"message,omitempty\"`\n\tOptions      any `json:\"options,omitempty\"`\n}\n\nfunc (p *PushObj) SetPlatform(pf *Platform) {\n\tp.Platform = pf.Os\n}\n\nfunc (p *PushObj) SetAudience(ad *Audience) {\n\tp.Audience = ad.Object\n}\n\nfunc (p *PushObj) SetNotification(no *Notification) {\n\tp.Notification = no\n}\n\nfunc (p *PushObj) SetMessage(m *Message) {\n\tp.Message = m\n}\n\nfunc (p *PushObj) SetOptions(o *Options) {\n\tp.Options = o\n}\n"
  },
  {
    "path": "internal/push/offlinepush/jpush/push.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage jpush\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/jpush/body\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/options\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/utils/httputil\"\n)\n\ntype JPush struct {\n\tpushConf   *config.Push\n\thttpClient *httputil.HTTPClient\n}\n\nfunc NewClient(pushConf *config.Push) *JPush {\n\treturn &JPush{pushConf: pushConf,\n\t\thttpClient: httputil.NewHTTPClient(httputil.NewClientConfig()),\n\t}\n}\n\nfunc (j *JPush) Auth(apiKey, secretKey string, timeStamp int64) (token string, err error) {\n\treturn token, nil\n}\n\nfunc (j *JPush) SetAlias(cid, alias string) (resp string, err error) {\n\treturn resp, nil\n}\n\nfunc (j *JPush) getAuthorization(appKey string, masterSecret string) string {\n\tstr := fmt.Sprintf(\"%s:%s\", appKey, masterSecret)\n\tbuf := []byte(str)\n\tAuthorization := fmt.Sprintf(\"Basic %s\", base64.StdEncoding.EncodeToString(buf))\n\treturn Authorization\n}\n\nfunc (j *JPush) Push(ctx context.Context, userIDs []string, title, content string, opts *options.Opts) error {\n\tvar pf body.Platform\n\tpf.SetAll()\n\tvar au body.Audience\n\tau.SetAlias(userIDs)\n\tvar no body.Notification\n\textras := make(map[string]string)\n\textras[\"ex\"] = opts.Ex\n\tif opts.Signal.ClientMsgID != \"\" {\n\t\textras[\"ClientMsgID\"] = opts.Signal.ClientMsgID\n\t}\n\tno.IOSEnableMutableContent()\n\tno.SetExtras(extras)\n\tno.SetAlert(content, title, opts)\n\tno.SetAndroidIntent(j.pushConf)\n\n\tvar msg body.Message\n\tmsg.SetMsgContent(content)\n\tmsg.SetTitle(title)\n\tif opts.Signal.ClientMsgID != \"\" {\n\t\tmsg.SetExtras(\"ClientMsgID\", opts.Signal.ClientMsgID)\n\t}\n\tmsg.SetExtras(\"ex\", opts.Ex)\n\tvar opt body.Options\n\topt.SetApnsProduction(j.pushConf.IOSPush.Production)\n\tvar pushObj body.PushObj\n\tpushObj.SetPlatform(&pf)\n\tpushObj.SetAudience(&au)\n\tpushObj.SetNotification(&no)\n\tpushObj.SetMessage(&msg)\n\tpushObj.SetOptions(&opt)\n\tvar resp map[string]any\n\treturn j.request(ctx, pushObj, &resp, 5)\n}\n\nfunc (j *JPush) request(ctx context.Context, po body.PushObj, resp *map[string]any, timeout int) error {\n\terr := j.httpClient.PostReturn(\n\t\tctx,\n\t\tj.pushConf.JPush.PushURL,\n\t\tmap[string]string{\n\t\t\t\"Authorization\": j.getAuthorization(j.pushConf.JPush.AppKey, j.pushConf.JPush.MasterSecret),\n\t\t},\n\t\tpo,\n\t\tresp,\n\t\ttimeout,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif (*resp)[\"sendno\"] != \"0\" {\n\t\treturn fmt.Errorf(\"jpush push failed %v\", resp)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/push/offlinepush/offlinepusher.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage offlinepush\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/dummy\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/fcm\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/getui\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/jpush\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/options\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"strings\"\n)\n\nconst (\n\tgeTUI    = \"getui\"\n\tfirebase = \"fcm\"\n\tjPush    = \"jpush\"\n)\n\n// OfflinePusher Offline Pusher.\ntype OfflinePusher interface {\n\tPush(ctx context.Context, userIDs []string, title, content string, opts *options.Opts) error\n}\n\nfunc NewOfflinePusher(pushConf *config.Push, cache cache.ThirdCache, fcmConfigPath string) (OfflinePusher, error) {\n\tvar offlinePusher OfflinePusher\n\tpushConf.Enable = strings.ToLower(pushConf.Enable)\n\tswitch pushConf.Enable {\n\tcase geTUI:\n\t\tofflinePusher = getui.NewClient(pushConf, cache)\n\tcase firebase:\n\t\treturn fcm.NewClient(pushConf, cache, fcmConfigPath)\n\tcase jPush:\n\t\tofflinePusher = jpush.NewClient(pushConf)\n\tdefault:\n\t\tofflinePusher = dummy.NewClient()\n\t}\n\treturn offlinePusher, nil\n}\n"
  },
  {
    "path": "internal/push/offlinepush/options/options.go",
    "content": "package options\n\n// Opts opts.\ntype Opts struct {\n\tSignal        *Signal\n\tIOSPushSound  string\n\tIOSBadgeCount bool\n\tEx            string\n}\n\n// Signal message id.\ntype Signal struct {\n\tClientMsgID string\n}\n"
  },
  {
    "path": "internal/push/offlinepush_handler.go",
    "content": "package push\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/options\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbpush \"github.com/openimsdk/protocol/push\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\ntype OfflinePushConsumerHandler struct {\n\tofflinePusher offlinepush.OfflinePusher\n}\n\nfunc NewOfflinePushConsumerHandler(offlinePusher offlinepush.OfflinePusher) *OfflinePushConsumerHandler {\n\treturn &OfflinePushConsumerHandler{\n\t\tofflinePusher: offlinePusher,\n\t}\n}\n\nfunc (o *OfflinePushConsumerHandler) HandleMsg2OfflinePush(ctx context.Context, msg []byte) {\n\tofflinePushMsg := pbpush.PushMsgReq{}\n\tif err := proto.Unmarshal(msg, &offlinePushMsg); err != nil {\n\t\tlog.ZError(ctx, \"offline push Unmarshal msg err\", err, \"msg\", string(msg))\n\t\treturn\n\t}\n\tif offlinePushMsg.MsgData == nil || offlinePushMsg.UserIDs == nil {\n\t\tlog.ZError(ctx, \"offline push msg is empty\", errs.New(\"offlinePushMsg is empty\"), \"userIDs\", offlinePushMsg.UserIDs, \"msg\", offlinePushMsg.MsgData)\n\t\treturn\n\t}\n\tif offlinePushMsg.MsgData.Status == constant.MsgStatusSending {\n\t\tofflinePushMsg.MsgData.Status = constant.MsgStatusSendSuccess\n\t}\n\tlog.ZInfo(ctx, \"receive to OfflinePush MQ\", \"userIDs\", offlinePushMsg.UserIDs, \"msg\", offlinePushMsg.MsgData)\n\n\terr := o.offlinePushMsg(ctx, offlinePushMsg.MsgData, offlinePushMsg.UserIDs)\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"offline push failed\", err, \"msg\", offlinePushMsg.String())\n\t}\n}\n\nfunc (o *OfflinePushConsumerHandler) getOfflinePushInfos(msg *sdkws.MsgData) (title, content string, opts *options.Opts, err error) {\n\ttype AtTextElem struct {\n\t\tText       string   `json:\"text,omitempty\"`\n\t\tAtUserList []string `json:\"atUserList,omitempty\"`\n\t\tIsAtSelf   bool     `json:\"isAtSelf\"`\n\t}\n\n\topts = &options.Opts{Signal: &options.Signal{ClientMsgID: msg.ClientMsgID}}\n\tif msg.OfflinePushInfo != nil {\n\t\topts.IOSBadgeCount = msg.OfflinePushInfo.IOSBadgeCount\n\t\topts.IOSPushSound = msg.OfflinePushInfo.IOSPushSound\n\t\topts.Ex = msg.OfflinePushInfo.Ex\n\t}\n\n\tif msg.OfflinePushInfo != nil {\n\t\ttitle = msg.OfflinePushInfo.Title\n\t\tcontent = msg.OfflinePushInfo.Desc\n\t}\n\tif title == \"\" {\n\t\tswitch msg.ContentType {\n\t\tcase constant.Text:\n\t\t\tfallthrough\n\t\tcase constant.Picture:\n\t\t\tfallthrough\n\t\tcase constant.Voice:\n\t\t\tfallthrough\n\t\tcase constant.Video:\n\t\t\tfallthrough\n\t\tcase constant.File:\n\t\t\ttitle = constant.ContentType2PushContent[int64(msg.ContentType)]\n\t\tcase constant.AtText:\n\t\t\tac := AtTextElem{}\n\t\t\t_ = jsonutil.JsonStringToStruct(string(msg.Content), &ac)\n\t\tcase constant.SignalingNotification:\n\t\t\ttitle = constant.ContentType2PushContent[constant.SignalMsg]\n\t\tdefault:\n\t\t\ttitle = constant.ContentType2PushContent[constant.Common]\n\t\t}\n\t}\n\tif content == \"\" {\n\t\tcontent = title\n\t}\n\treturn\n}\n\nfunc (o *OfflinePushConsumerHandler) offlinePushMsg(ctx context.Context, msg *sdkws.MsgData, offlinePushUserIDs []string) error {\n\ttitle, content, opts, err := o.getOfflinePushInfos(msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = o.offlinePusher.Push(ctx, offlinePushUserIDs, title, content, opts)\n\tif err != nil {\n\t\tprommetrics.MsgOfflinePushFailedCounter.Inc()\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/push/onlinepusher.go",
    "content": "package push\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/openimsdk/protocol/msggateway\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"google.golang.org/grpc\"\n\n\tconf \"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n)\n\ntype OnlinePusher interface {\n\tGetConnsAndOnlinePush(ctx context.Context, msg *sdkws.MsgData,\n\t\tpushToUserIDs []string) (wsResults []*msggateway.SingleMsgToUserResults, err error)\n\tGetOnlinePushFailedUserIDs(ctx context.Context, msg *sdkws.MsgData, wsResults []*msggateway.SingleMsgToUserResults,\n\t\tpushToUserIDs *[]string) []string\n}\n\ntype emptyOnlinePusher struct{}\n\nfunc newEmptyOnlinePusher() *emptyOnlinePusher {\n\treturn &emptyOnlinePusher{}\n}\n\nfunc (emptyOnlinePusher) GetConnsAndOnlinePush(ctx context.Context, msg *sdkws.MsgData, pushToUserIDs []string) (wsResults []*msggateway.SingleMsgToUserResults, err error) {\n\tlog.ZInfo(ctx, \"emptyOnlinePusher GetConnsAndOnlinePush\", nil)\n\treturn nil, nil\n}\nfunc (u emptyOnlinePusher) GetOnlinePushFailedUserIDs(ctx context.Context, msg *sdkws.MsgData, wsResults []*msggateway.SingleMsgToUserResults, pushToUserIDs *[]string) []string {\n\tlog.ZInfo(ctx, \"emptyOnlinePusher GetOnlinePushFailedUserIDs\", nil)\n\treturn nil\n}\n\nfunc NewOnlinePusher(disCov discovery.Conn, config *Config) (OnlinePusher, error) {\n\tif conf.Standalone() {\n\t\treturn NewDefaultAllNode(disCov, config), nil\n\t}\n\tif runtimeenv.RuntimeEnvironment() == conf.KUBERNETES {\n\t\treturn NewDefaultAllNode(disCov, config), nil\n\t}\n\tswitch config.Discovery.Enable {\n\tcase conf.ETCD:\n\t\treturn NewDefaultAllNode(disCov, config), nil\n\tdefault:\n\t\treturn nil, errs.New(fmt.Sprintf(\"unsupported discovery type %s\", config.Discovery.Enable))\n\t}\n}\n\ntype DefaultAllNode struct {\n\tdisCov discovery.Conn\n\tconfig *Config\n}\n\nfunc NewDefaultAllNode(disCov discovery.Conn, config *Config) *DefaultAllNode {\n\treturn &DefaultAllNode{disCov: disCov, config: config}\n}\n\nfunc (d *DefaultAllNode) GetConnsAndOnlinePush(ctx context.Context, msg *sdkws.MsgData,\n\tpushToUserIDs []string) (wsResults []*msggateway.SingleMsgToUserResults, err error) {\n\tconns, err := d.disCov.GetConns(ctx, d.config.Discovery.RpcService.MessageGateway)\n\tif len(conns) == 0 {\n\t\tlog.ZWarn(ctx, \"get gateway conn 0 \", nil)\n\t} else {\n\t\tlog.ZDebug(ctx, \"get gateway conn\", \"conn length\", len(conns))\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar (\n\t\tmu         sync.Mutex\n\t\twg         = errgroup.Group{}\n\t\tinput      = &msggateway.OnlineBatchPushOneMsgReq{MsgData: msg, PushToUserIDs: pushToUserIDs}\n\t\tmaxWorkers = d.config.RpcConfig.MaxConcurrentWorkers\n\t)\n\n\tif maxWorkers < 3 {\n\t\tmaxWorkers = 3\n\t}\n\n\twg.SetLimit(maxWorkers)\n\n\t// Online push message\n\tfor _, conn := range conns {\n\t\tconn := conn // loop var safe\n\t\tctx := ctx\n\t\twg.Go(func() error {\n\t\t\tmsgClient := msggateway.NewMsgGatewayClient(conn)\n\t\t\treply, err := msgClient.SuperGroupOnlineBatchPushOneMsg(ctx, input)\n\t\t\tif err != nil {\n\t\t\t\tlog.ZError(ctx, \"SuperGroupOnlineBatchPushOneMsg \", err, \"req:\", input.String())\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tlog.ZDebug(ctx, \"push result\", \"reply\", reply)\n\t\t\tif reply != nil && reply.SinglePushResult != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\twsResults = append(wsResults, reply.SinglePushResult...)\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t_ = wg.Wait()\n\n\t// always return nil\n\treturn wsResults, nil\n}\n\nfunc (d *DefaultAllNode) GetOnlinePushFailedUserIDs(_ context.Context, msg *sdkws.MsgData,\n\twsResults []*msggateway.SingleMsgToUserResults, pushToUserIDs *[]string) []string {\n\n\tonlineSuccessUserIDs := []string{msg.SendID}\n\tfor _, v := range wsResults {\n\t\t//message sender do not need offline push\n\t\tif msg.SendID == v.UserID {\n\t\t\tcontinue\n\t\t}\n\t\t// mobile online push success\n\t\tif v.OnlinePush {\n\t\t\tonlineSuccessUserIDs = append(onlineSuccessUserIDs, v.UserID)\n\t\t}\n\n\t}\n\n\treturn datautil.SliceSub(*pushToUserIDs, onlineSuccessUserIDs)\n}\n\ntype K8sStaticConsistentHash struct {\n\tdisCov discovery.SvcDiscoveryRegistry\n\tconfig *Config\n}\n\nfunc NewK8sStaticConsistentHash(disCov discovery.SvcDiscoveryRegistry, config *Config) *K8sStaticConsistentHash {\n\treturn &K8sStaticConsistentHash{disCov: disCov, config: config}\n}\n\nfunc (k *K8sStaticConsistentHash) GetConnsAndOnlinePush(ctx context.Context, msg *sdkws.MsgData,\n\tpushToUserIDs []string) (wsResults []*msggateway.SingleMsgToUserResults, err error) {\n\n\tvar usersHost = make(map[string][]string)\n\tfor _, v := range pushToUserIDs {\n\t\ttHost, err := k.disCov.GetUserIdHashGatewayHost(ctx, v)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"get msg gateway hash error\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\ttUsers, tbl := usersHost[tHost]\n\t\tif tbl {\n\t\t\ttUsers = append(tUsers, v)\n\t\t\tusersHost[tHost] = tUsers\n\t\t} else {\n\t\t\tusersHost[tHost] = []string{v}\n\t\t}\n\t}\n\tlog.ZDebug(ctx, \"genUsers send hosts struct:\", \"usersHost\", usersHost)\n\tvar usersConns = make(map[grpc.ClientConnInterface][]string)\n\tfor host, userIds := range usersHost {\n\t\ttconn, _ := k.disCov.GetConn(ctx, host)\n\t\tusersConns[tconn] = userIds\n\t}\n\tvar (\n\t\tmu         sync.Mutex\n\t\twg         = errgroup.Group{}\n\t\tmaxWorkers = k.config.RpcConfig.MaxConcurrentWorkers\n\t)\n\tif maxWorkers < 3 {\n\t\tmaxWorkers = 3\n\t}\n\twg.SetLimit(maxWorkers)\n\tfor conn, userIds := range usersConns {\n\t\ttcon := conn\n\t\ttuserIds := userIds\n\t\twg.Go(func() error {\n\t\t\tinput := &msggateway.OnlineBatchPushOneMsgReq{MsgData: msg, PushToUserIDs: tuserIds}\n\t\t\tmsgClient := msggateway.NewMsgGatewayClient(tcon)\n\t\t\treply, err := msgClient.SuperGroupOnlineBatchPushOneMsg(ctx, input)\n\t\t\tif err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tlog.ZDebug(ctx, \"push result\", \"reply\", reply)\n\t\t\tif reply != nil && reply.SinglePushResult != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\twsResults = append(wsResults, reply.SinglePushResult...)\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\t_ = wg.Wait()\n\treturn wsResults, nil\n}\nfunc (k *K8sStaticConsistentHash) GetOnlinePushFailedUserIDs(_ context.Context, _ *sdkws.MsgData,\n\twsResults []*msggateway.SingleMsgToUserResults, _ *[]string) []string {\n\tvar needOfflinePushUserIDs []string\n\tfor _, v := range wsResults {\n\t\tif !v.OnlinePush {\n\t\t\tneedOfflinePushUserIDs = append(needOfflinePushUserIDs, v.UserID)\n\t\t}\n\t}\n\treturn needOfflinePushUserIDs\n}\n"
  },
  {
    "path": "internal/push/push.go",
    "content": "package push\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/tools/mq\"\n\t\"math/rand\"\n\t\"strconv\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/mqbuild\"\n\tpbpush \"github.com/openimsdk/protocol/push\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"google.golang.org/grpc\"\n)\n\ntype pushServer struct {\n\tpbpush.UnimplementedPushMsgServiceServer\n\tdatabase      controller.PushDatabase\n\tdisCov        discovery.Conn\n\tofflinePusher offlinepush.OfflinePusher\n}\n\ntype Config struct {\n\tRpcConfig          config.Push\n\tRedisConfig        config.Redis\n\tMongoConfig        config.Mongo\n\tKafkaConfig        config.Kafka\n\tNotificationConfig config.Notification\n\tShare              config.Share\n\tWebhooksConfig     config.Webhooks\n\tLocalCacheConfig   config.LocalCache\n\tDiscovery          config.Discovery\n\tFcmConfigPath      config.Path\n}\n\nfunc (p pushServer) DelUserPushToken(ctx context.Context,\n\treq *pbpush.DelUserPushTokenReq) (resp *pbpush.DelUserPushTokenResp, err error) {\n\tif err = p.database.DelFcmToken(ctx, req.UserID, int(req.PlatformID)); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbpush.DelUserPushTokenResp{}, nil\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tdbb := dbbuild.NewBuilder(&config.MongoConfig, &config.RedisConfig)\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar cacheModel cache.ThirdCache\n\tif rdb == nil {\n\t\tmdb, err := dbb.Mongo(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmc, err := mgo.NewCacheMgo(mdb.GetDB())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcacheModel = mcache.NewThirdCache(mc)\n\t} else {\n\t\tcacheModel = redis.NewThirdCache(rdb)\n\t}\n\tofflinePusher, err := offlinepush.NewOfflinePusher(&config.RpcConfig, cacheModel, string(config.FcmConfigPath))\n\tif err != nil {\n\t\treturn err\n\t}\n\tbuilder := mqbuild.NewBuilder(&config.KafkaConfig)\n\n\tofflinePushProducer, err := builder.GetTopicProducer(ctx, config.KafkaConfig.ToOfflinePushTopic)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdatabase := controller.NewPushDatabase(cacheModel, offlinePushProducer)\n\n\tpushConsumer, err := builder.GetTopicConsumer(ctx, config.KafkaConfig.ToPushTopic)\n\tif err != nil {\n\t\treturn err\n\t}\n\tofflinePushConsumer, err := builder.GetTopicConsumer(ctx, config.KafkaConfig.ToOfflinePushTopic)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpushHandler, err := NewConsumerHandler(ctx, config, database, offlinePusher, rdb, client)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tofflineHandler := NewOfflinePushConsumerHandler(offlinePusher)\n\n\tpbpush.RegisterPushMsgServiceServer(server, &pushServer{\n\t\tdatabase:      database,\n\t\tdisCov:        client,\n\t\tofflinePusher: offlinePusher,\n\t})\n\n\tgo func() {\n\t\tpushHandler.WaitCache()\n\t\tfn := func(msg mq.Message) error {\n\t\t\tpushHandler.HandleMs2PsChat(authverify.WithTempAdmin(msg.Context()), msg.Value())\n\t\t\treturn nil\n\t\t}\n\t\tconsumerCtx := mcontext.SetOperationID(context.Background(), \"push_\"+strconv.Itoa(int(rand.Uint32())))\n\t\tlog.ZInfo(consumerCtx, \"begin consume messages\")\n\t\tfor {\n\t\t\tif err := pushConsumer.Subscribe(consumerCtx, fn); err != nil {\n\t\t\t\tlog.ZError(consumerCtx, \"subscribe err\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfn := func(msg mq.Message) error {\n\t\t\tofflineHandler.HandleMsg2OfflinePush(msg.Context(), msg.Value())\n\t\t\treturn nil\n\t\t}\n\t\tconsumerCtx := mcontext.SetOperationID(context.Background(), \"push_\"+strconv.Itoa(int(rand.Uint32())))\n\t\tlog.ZInfo(consumerCtx, \"begin consume messages\")\n\t\tfor {\n\t\t\tif err := offlinePushConsumer.Subscribe(consumerCtx, fn); err != nil {\n\t\t\t\tlog.ZError(consumerCtx, \"subscribe err\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/push/push_handler.go",
    "content": "package push\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush\"\n\t\"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush/options\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpccache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/util/conversationutil\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msggateway\"\n\tpbpush \"github.com/openimsdk/protocol/push\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\ntype ConsumerHandler struct {\n\t//pushConsumerGroup      mq.Consumer\n\tofflinePusher          offlinepush.OfflinePusher\n\tonlinePusher           OnlinePusher\n\tpushDatabase           controller.PushDatabase\n\tonlineCache            rpccache.OnlineCache\n\tgroupLocalCache        *rpccache.GroupLocalCache\n\tconversationLocalCache *rpccache.ConversationLocalCache\n\twebhookClient          *webhook.Client\n\tconfig                 *Config\n\tuserClient             *rpcli.UserClient\n\tgroupClient            *rpcli.GroupClient\n\tmsgClient              *rpcli.MsgClient\n\tconversationClient     *rpcli.ConversationClient\n}\n\nfunc NewConsumerHandler(ctx context.Context, config *Config, database controller.PushDatabase, offlinePusher offlinepush.OfflinePusher, rdb redis.UniversalClient, client discovery.Conn) (*ConsumerHandler, error) {\n\tuserConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmsgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconversationConn, err := client.GetConn(ctx, config.Discovery.RpcService.Conversation)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tonlinePusher, err := NewOnlinePusher(client, config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar consumerHandler ConsumerHandler\n\tconsumerHandler.userClient = rpcli.NewUserClient(userConn)\n\tconsumerHandler.groupClient = rpcli.NewGroupClient(groupConn)\n\tconsumerHandler.msgClient = rpcli.NewMsgClient(msgConn)\n\tconsumerHandler.conversationClient = rpcli.NewConversationClient(conversationConn)\n\n\tconsumerHandler.offlinePusher = offlinePusher\n\tconsumerHandler.onlinePusher = onlinePusher\n\tconsumerHandler.groupLocalCache = rpccache.NewGroupLocalCache(consumerHandler.groupClient, &config.LocalCacheConfig, rdb)\n\tconsumerHandler.conversationLocalCache = rpccache.NewConversationLocalCache(consumerHandler.conversationClient, &config.LocalCacheConfig, rdb)\n\tconsumerHandler.webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)\n\tconsumerHandler.config = config\n\tconsumerHandler.pushDatabase = database\n\tconsumerHandler.onlineCache, err = rpccache.NewOnlineCache(consumerHandler.userClient, consumerHandler.groupLocalCache, rdb, config.RpcConfig.FullUserCache, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &consumerHandler, nil\n}\n\nfunc (c *ConsumerHandler) HandleMs2PsChat(ctx context.Context, msg []byte) {\n\tmsgFromMQ := pbpush.PushMsgReq{}\n\tif err := proto.Unmarshal(msg, &msgFromMQ); err != nil {\n\t\tlog.ZError(ctx, \"push Unmarshal msg err\", err, \"msg\", string(msg))\n\t\treturn\n\t}\n\n\tsec := msgFromMQ.MsgData.SendTime / 1000\n\tnowSec := timeutil.GetCurrentTimestampBySecond()\n\n\tif nowSec-sec > 10 {\n\t\tprommetrics.MsgLoneTimePushCounter.Inc()\n\t\tlog.ZWarn(ctx, \"it’s been a while since the message was sent\", nil, \"msg\", msgFromMQ.String(), \"sec\", sec, \"nowSec\", nowSec, \"nowSec-sec\", nowSec-sec)\n\t}\n\tvar err error\n\n\tswitch msgFromMQ.MsgData.SessionType {\n\tcase constant.ReadGroupChatType:\n\t\terr = c.Push2Group(ctx, msgFromMQ.MsgData.GroupID, msgFromMQ.MsgData)\n\tdefault:\n\t\tvar pushUserIDList []string\n\t\tisSenderSync := datautil.GetSwitchFromOptions(msgFromMQ.MsgData.Options, constant.IsSenderSync)\n\t\tif !isSenderSync || msgFromMQ.MsgData.SendID == msgFromMQ.MsgData.RecvID {\n\t\t\tpushUserIDList = append(pushUserIDList, msgFromMQ.MsgData.RecvID)\n\t\t} else {\n\t\t\tpushUserIDList = append(pushUserIDList, msgFromMQ.MsgData.RecvID, msgFromMQ.MsgData.SendID)\n\t\t}\n\t\terr = c.Push2User(ctx, pushUserIDList, msgFromMQ.MsgData)\n\t}\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"push failed\", err, \"msg\", msgFromMQ.String())\n\t}\n}\n\nfunc (c *ConsumerHandler) WaitCache() {\n\tc.onlineCache.WaitCache()\n}\n\n// Push2User Suitable for two types of conversations, one is SingleChatType and the other is NotificationChatType.\nfunc (c *ConsumerHandler) Push2User(ctx context.Context, userIDs []string, msg *sdkws.MsgData) (err error) {\n\tlog.ZInfo(ctx, \"Get msg from msg_transfer And push msg\", \"userIDs\", userIDs, \"msg\", msg.String())\n\tdefer func(duration time.Time) {\n\t\tt := time.Since(duration)\n\t\tlog.ZInfo(ctx, \"Get msg from msg_transfer And push msg end\", \"msg\", msg.String(), \"time cost\", t)\n\t}(time.Now())\n\tif err := c.webhookBeforeOnlinePush(ctx, &c.config.WebhooksConfig.BeforeOnlinePush, userIDs, msg); err != nil {\n\t\treturn err\n\t}\n\n\twsResults, err := c.GetConnsAndOnlinePush(ctx, msg, userIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.ZDebug(ctx, \"single and notification push result\", \"result\", wsResults, \"msg\", msg, \"push_to_userID\", userIDs)\n\tlog.ZInfo(ctx, \"single and notification push end\")\n\n\tif !c.shouldPushOffline(ctx, msg) {\n\t\treturn nil\n\t}\n\tlog.ZInfo(ctx, \"pushOffline start\")\n\n\tfor _, v := range wsResults {\n\t\t//message sender do not need offline push\n\t\tif msg.SendID == v.UserID {\n\t\t\tcontinue\n\t\t}\n\t\t//receiver online push success\n\t\tif v.OnlinePush {\n\t\t\treturn nil\n\t\t}\n\t}\n\tneedOfflinePushUserID := []string{msg.RecvID}\n\tvar offlinePushUserID []string\n\n\t//receiver offline push\n\tif err = c.webhookBeforeOfflinePush(ctx, &c.config.WebhooksConfig.BeforeOfflinePush, needOfflinePushUserID, msg, &offlinePushUserID); err != nil {\n\t\treturn err\n\t}\n\n\tif len(offlinePushUserID) > 0 {\n\t\tneedOfflinePushUserID = offlinePushUserID\n\t}\n\terr = c.offlinePushMsg(ctx, msg, needOfflinePushUserID)\n\tif err != nil {\n\t\tlog.ZDebug(ctx, \"offlinePushMsg failed\", err, \"needOfflinePushUserID\", needOfflinePushUserID, \"msg\", msg)\n\t\tlog.ZWarn(ctx, \"offlinePushMsg failed\", err, \"needOfflinePushUserID length\", len(needOfflinePushUserID), \"msg\", msg)\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n\nfunc (c *ConsumerHandler) shouldPushOffline(_ context.Context, msg *sdkws.MsgData) bool {\n\tisOfflinePush := datautil.GetSwitchFromOptions(msg.Options, constant.IsOfflinePush)\n\tif !isOfflinePush {\n\t\treturn false\n\t}\n\tswitch msg.ContentType {\n\tcase constant.RoomParticipantsConnectedNotification:\n\t\treturn false\n\tcase constant.RoomParticipantsDisconnectedNotification:\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (c *ConsumerHandler) GetConnsAndOnlinePush(ctx context.Context, msg *sdkws.MsgData, pushToUserIDs []string) ([]*msggateway.SingleMsgToUserResults, error) {\n\tif msg != nil && msg.Status == constant.MsgStatusSending {\n\t\tmsg.Status = constant.MsgStatusSendSuccess\n\t}\n\tonlineUserIDs, offlineUserIDs, err := c.onlineCache.GetUsersOnline(ctx, pushToUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlog.ZDebug(ctx, \"GetConnsAndOnlinePush online cache\", \"sendID\", msg.SendID, \"recvID\", msg.RecvID, \"groupID\", msg.GroupID, \"sessionType\", msg.SessionType, \"clientMsgID\", msg.ClientMsgID, \"serverMsgID\", msg.ServerMsgID, \"offlineUserIDs\", offlineUserIDs, \"onlineUserIDs\", onlineUserIDs)\n\tvar result []*msggateway.SingleMsgToUserResults\n\tif len(onlineUserIDs) > 0 {\n\t\tvar err error\n\t\tresult, err = c.onlinePusher.GetConnsAndOnlinePush(ctx, msg, onlineUserIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tfor _, userID := range offlineUserIDs {\n\t\tresult = append(result, &msggateway.SingleMsgToUserResults{\n\t\t\tUserID: userID,\n\t\t})\n\t}\n\treturn result, nil\n}\n\nfunc (c *ConsumerHandler) Push2Group(ctx context.Context, groupID string, msg *sdkws.MsgData) (err error) {\n\tlog.ZInfo(ctx, \"Get group msg from msg_transfer and push msg\", \"msg\", msg.String(), \"groupID\", groupID)\n\tdefer func(duration time.Time) {\n\t\tt := time.Since(duration)\n\t\tlog.ZInfo(ctx, \"Get group msg from msg_transfer and push msg end\", \"msg\", msg.String(), \"groupID\", groupID, \"time cost\", t)\n\t}(time.Now())\n\tvar pushToUserIDs []string\n\tif err = c.webhookBeforeGroupOnlinePush(ctx, &c.config.WebhooksConfig.BeforeGroupOnlinePush, groupID, msg,\n\t\t&pushToUserIDs); err != nil {\n\t\treturn err\n\t}\n\n\terr = c.groupMessagesHandler(ctx, groupID, &pushToUserIDs, msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twsResults, err := c.GetConnsAndOnlinePush(ctx, msg, pushToUserIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.ZDebug(ctx, \"group push result\", \"result\", wsResults, \"msg\", msg)\n\tlog.ZInfo(ctx, \"online group push end\")\n\n\tif !c.shouldPushOffline(ctx, msg) {\n\t\treturn nil\n\t}\n\tneedOfflinePushUserIDs := c.onlinePusher.GetOnlinePushFailedUserIDs(ctx, msg, wsResults, &pushToUserIDs)\n\t//filter some user, like don not disturb or don't need offline push etc.\n\tneedOfflinePushUserIDs, err = c.filterGroupMessageOfflinePush(ctx, groupID, msg, needOfflinePushUserIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.ZInfo(ctx, \"filterGroupMessageOfflinePush end\")\n\n\t// Use offline push messaging\n\tif len(needOfflinePushUserIDs) > 0 {\n\t\tc.asyncOfflinePush(ctx, needOfflinePushUserIDs, msg)\n\t}\n\n\treturn nil\n}\n\nfunc (c *ConsumerHandler) asyncOfflinePush(ctx context.Context, needOfflinePushUserIDs []string, msg *sdkws.MsgData) {\n\tvar offlinePushUserIDs []string\n\terr := c.webhookBeforeOfflinePush(ctx, &c.config.WebhooksConfig.BeforeOfflinePush, needOfflinePushUserIDs, msg, &offlinePushUserIDs)\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"webhookBeforeOfflinePush failed\", err, \"msg\", msg)\n\t\treturn\n\t}\n\n\tif len(offlinePushUserIDs) > 0 {\n\t\tneedOfflinePushUserIDs = offlinePushUserIDs\n\t}\n\tif err := c.pushDatabase.MsgToOfflinePushMQ(ctx, conversationutil.GenConversationUniqueKeyForSingle(msg.SendID, msg.RecvID), needOfflinePushUserIDs, msg); err != nil {\n\t\tlog.ZDebug(ctx, \"Msg To OfflinePush MQ error\", err, \"needOfflinePushUserIDs\",\n\t\t\tneedOfflinePushUserIDs, \"msg\", msg)\n\t\tlog.ZWarn(ctx, \"Msg To OfflinePush MQ error\", err, \"needOfflinePushUserIDs length\",\n\t\t\tlen(needOfflinePushUserIDs), \"msg\", msg)\n\t\tprommetrics.GroupChatMsgProcessFailedCounter.Inc()\n\t\treturn\n\t}\n}\n\nfunc (c *ConsumerHandler) groupMessagesHandler(ctx context.Context, groupID string, pushToUserIDs *[]string, msg *sdkws.MsgData) (err error) {\n\tif len(*pushToUserIDs) == 0 {\n\t\t*pushToUserIDs, err = c.groupLocalCache.GetGroupMemberIDs(ctx, groupID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitch msg.ContentType {\n\t\tcase constant.MemberQuitNotification:\n\t\t\tvar tips sdkws.MemberQuitTips\n\t\t\tif unmarshalNotificationElem(msg.Content, &tips) != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err = c.DeleteMemberAndSetConversationSeq(ctx, groupID, []string{tips.QuitUser.UserID}); err != nil {\n\t\t\t\tlog.ZError(ctx, \"MemberQuitNotification DeleteMemberAndSetConversationSeq\", err, \"groupID\", groupID, \"userID\", tips.QuitUser.UserID)\n\t\t\t}\n\t\t\t*pushToUserIDs = append(*pushToUserIDs, tips.QuitUser.UserID)\n\t\tcase constant.MemberKickedNotification:\n\t\t\tvar tips sdkws.MemberKickedTips\n\t\t\tif unmarshalNotificationElem(msg.Content, &tips) != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tkickedUsers := datautil.Slice(tips.KickedUserList, func(e *sdkws.GroupMemberFullInfo) string { return e.UserID })\n\t\t\tif err = c.DeleteMemberAndSetConversationSeq(ctx, groupID, kickedUsers); err != nil {\n\t\t\t\tlog.ZError(ctx, \"MemberKickedNotification DeleteMemberAndSetConversationSeq\", err, \"groupID\", groupID, \"userIDs\", kickedUsers)\n\t\t\t}\n\n\t\t\t*pushToUserIDs = append(*pushToUserIDs, kickedUsers...)\n\t\tcase constant.GroupDismissedNotification:\n\t\t\tif msgprocessor.IsNotification(msgprocessor.GetConversationIDByMsg(msg)) {\n\t\t\t\tvar tips sdkws.GroupDismissedTips\n\t\t\t\tif unmarshalNotificationElem(msg.Content, &tips) != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tlog.ZDebug(ctx, \"GroupDismissedNotificationInfo****\", \"groupID\", groupID, \"num\", len(*pushToUserIDs), \"list\", pushToUserIDs)\n\t\t\t\tif len(c.config.Share.IMAdminUser.UserIDs) > 0 {\n\t\t\t\t\tctx = mcontext.WithOpUserIDContext(ctx, c.config.Share.IMAdminUser.UserIDs[0])\n\t\t\t\t}\n\t\t\t\tdefer func(groupID string) {\n\t\t\t\t\tif err := c.groupClient.DismissGroup(ctx, groupID, true); err != nil {\n\t\t\t\t\t\tlog.ZError(ctx, \"DismissGroup Notification clear members\", err, \"groupID\", groupID)\n\t\t\t\t\t}\n\t\t\t\t}(groupID)\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (c *ConsumerHandler) offlinePushMsg(ctx context.Context, msg *sdkws.MsgData, offlinePushUserIDs []string) error {\n\ttitle, content, opts, err := c.getOfflinePushInfos(msg)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"getOfflinePushInfos failed\", err, \"msg\", msg)\n\t\treturn err\n\t}\n\terr = c.offlinePusher.Push(ctx, offlinePushUserIDs, title, content, opts)\n\tif err != nil {\n\t\tprommetrics.MsgOfflinePushFailedCounter.Inc()\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (c *ConsumerHandler) filterGroupMessageOfflinePush(ctx context.Context, groupID string, msg *sdkws.MsgData,\n\tofflinePushUserIDs []string) (userIDs []string, err error) {\n\tneedOfflinePushUserIDs, err := c.conversationClient.GetConversationOfflinePushUserIDs(ctx, conversationutil.GenGroupConversationID(groupID), offlinePushUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn needOfflinePushUserIDs, nil\n}\n\nfunc (c *ConsumerHandler) getOfflinePushInfos(msg *sdkws.MsgData) (title, content string, opts *options.Opts, err error) {\n\ttype AtTextElem struct {\n\t\tText       string   `json:\"text,omitempty\"`\n\t\tAtUserList []string `json:\"atUserList,omitempty\"`\n\t\tIsAtSelf   bool     `json:\"isAtSelf\"`\n\t}\n\n\topts = &options.Opts{Signal: &options.Signal{ClientMsgID: msg.ClientMsgID}}\n\tif msg.OfflinePushInfo != nil {\n\t\topts.IOSBadgeCount = msg.OfflinePushInfo.IOSBadgeCount\n\t\topts.IOSPushSound = msg.OfflinePushInfo.IOSPushSound\n\t\topts.Ex = msg.OfflinePushInfo.Ex\n\t}\n\n\tif msg.OfflinePushInfo != nil {\n\t\ttitle = msg.OfflinePushInfo.Title\n\t\tcontent = msg.OfflinePushInfo.Desc\n\t}\n\tif title == \"\" {\n\t\tswitch msg.ContentType {\n\t\tcase constant.Text:\n\t\t\tfallthrough\n\t\tcase constant.Picture:\n\t\t\tfallthrough\n\t\tcase constant.Voice:\n\t\t\tfallthrough\n\t\tcase constant.Video:\n\t\t\tfallthrough\n\t\tcase constant.File:\n\t\t\ttitle = constant.ContentType2PushContent[int64(msg.ContentType)]\n\t\tcase constant.AtText:\n\t\t\tac := AtTextElem{}\n\t\t\t_ = jsonutil.JsonStringToStruct(string(msg.Content), &ac)\n\t\tcase constant.SignalingNotification:\n\t\t\ttitle = constant.ContentType2PushContent[constant.SignalMsg]\n\t\tdefault:\n\t\t\ttitle = constant.ContentType2PushContent[constant.Common]\n\t\t}\n\t}\n\tif content == \"\" {\n\t\tcontent = title\n\t}\n\treturn\n}\n\nfunc (c *ConsumerHandler) DeleteMemberAndSetConversationSeq(ctx context.Context, groupID string, userIDs []string) error {\n\tconversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID)\n\tmaxSeq, err := c.msgClient.GetConversationMaxSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn c.conversationClient.SetConversationMaxSeq(ctx, conversationID, userIDs, maxSeq)\n}\n\nfunc unmarshalNotificationElem(bytes []byte, t any) error {\n\tvar notification sdkws.NotificationElem\n\tif err := json.Unmarshal(bytes, &notification); err != nil {\n\t\treturn err\n\t}\n\treturn json.Unmarshal([]byte(notification.Detail), t)\n}\n"
  },
  {
    "path": "internal/rpc/auth/auth.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage auth\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpccache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\tredis2 \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\tpbauth \"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msggateway\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/tokenverify\"\n\t\"google.golang.org/grpc\"\n)\n\ntype authServer struct {\n\tpbauth.UnimplementedAuthServer\n\tauthDatabase   controller.AuthDatabase\n\tAuthLocalCache *rpccache.AuthLocalCache\n\tRegisterCenter discovery.Conn\n\tconfig         *Config\n\tuserClient     *rpcli.UserClient\n\tadminUserIDs   []string\n}\n\ntype Config struct {\n\tRpcConfig        config.Auth\n\tRedisConfig      config.Redis\n\tMongoConfig      config.Mongo\n\tShare            config.Share\n\tLocalCacheConfig config.LocalCache\n\tDiscovery        config.Discovery\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tdbb := dbbuild.NewBuilder(&config.MongoConfig, &config.RedisConfig)\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar token cache.TokenModel\n\tif rdb == nil {\n\t\tmdb, err := dbb.Mongo(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmc, err := mgo.NewCacheMgo(mdb.GetDB())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttoken = mcache.NewTokenCacheModel(mc, config.RpcConfig.TokenPolicy.Expire)\n\t} else {\n\t\ttoken = redis2.NewTokenCacheModel(rdb, &config.LocalCacheConfig, config.RpcConfig.TokenPolicy.Expire)\n\t}\n\tuserConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn err\n\t}\n\tauthConn, err := client.GetConn(ctx, config.Discovery.RpcService.Auth)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlocalcache.InitLocalCache(&config.LocalCacheConfig)\n\n\tpbauth.RegisterAuthServer(server, &authServer{\n\t\tRegisterCenter: client,\n\t\tauthDatabase: controller.NewAuthDatabase(\n\t\t\ttoken,\n\t\t\tconfig.Share.Secret,\n\t\t\tconfig.RpcConfig.TokenPolicy.Expire,\n\t\t\tconfig.Share.MultiLogin,\n\t\t\tconfig.Share.IMAdminUser.UserIDs,\n\t\t),\n\t\tAuthLocalCache: rpccache.NewAuthLocalCache(rpcli.NewAuthClient(authConn), &config.LocalCacheConfig, rdb),\n\t\tconfig:         config,\n\t\tuserClient:     rpcli.NewUserClient(userConn),\n\t\tadminUserIDs:   config.Share.IMAdminUser.UserIDs,\n\t})\n\treturn nil\n}\n\nfunc (s *authServer) GetAdminToken(ctx context.Context, req *pbauth.GetAdminTokenReq) (*pbauth.GetAdminTokenResp, error) {\n\tresp := pbauth.GetAdminTokenResp{}\n\tif req.Secret != s.config.Share.Secret {\n\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"secret invalid\")\n\t}\n\n\tif !datautil.Contain(req.UserID, s.adminUserIDs...) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userID is error.\", \"userID\", req.UserID, \"adminUserID\", s.adminUserIDs)\n\n\t}\n\n\tif err := s.userClient.CheckUser(ctx, []string{req.UserID}); err != nil {\n\t\treturn nil, err\n\t}\n\n\ttoken, err := s.authDatabase.CreateToken(ctx, req.UserID, int(constant.AdminPlatformID))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprommetrics.UserLoginCounter.Inc()\n\n\tresp.Token = token\n\tresp.ExpireTimeSeconds = s.config.RpcConfig.TokenPolicy.Expire * 24 * 60 * 60\n\treturn &resp, nil\n}\n\nfunc (s *authServer) GetUserToken(ctx context.Context, req *pbauth.GetUserTokenReq) (*pbauth.GetUserTokenResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif req.PlatformID == constant.AdminPlatformID {\n\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"platformID invalid. platformID must not be adminPlatformID\")\n\t}\n\n\tresp := pbauth.GetUserTokenResp{}\n\n\tif authverify.CheckUserIsAdmin(ctx, req.UserID) {\n\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"don't get Admin token\")\n\t}\n\tuser, err := s.userClient.GetUserInfo(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif user.AppMangerLevel >= constant.AppNotificationAdmin {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"app account can`t get token\")\n\t}\n\ttoken, err := s.authDatabase.CreateToken(ctx, req.UserID, int(req.PlatformID))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp.Token = token\n\tresp.ExpireTimeSeconds = s.config.RpcConfig.TokenPolicy.Expire * 24 * 60 * 60\n\treturn &resp, nil\n}\n\nfunc (s *authServer) GetExistingToken(ctx context.Context, req *pbauth.GetExistingTokenReq) (*pbauth.GetExistingTokenResp, error) {\n\tm, err := s.authDatabase.GetTokensWithoutError(ctx, req.UserID, int(req.PlatformID))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &pbauth.GetExistingTokenResp{\n\t\tTokenStates: convert.TokenMapDB2Pb(m),\n\t}, nil\n}\n\nfunc (s *authServer) parseToken(ctx context.Context, tokensString string) (claims *tokenverify.Claims, err error) {\n\tclaims, err = tokenverify.GetClaimFromToken(tokensString, authverify.Secret(s.config.Share.Secret))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tm, err := s.AuthLocalCache.GetExistingToken(ctx, claims.UserID, claims.PlatformID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(m) == 0 {\n\t\tisAdmin := authverify.CheckUserIsAdmin(ctx, claims.UserID)\n\t\tif isAdmin {\n\t\t\tif err = s.authDatabase.GetTemporaryTokensWithoutError(ctx, claims.UserID, claims.PlatformID, tokensString); err == nil {\n\t\t\t\treturn claims, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, servererrs.ErrTokenNotExist.Wrap()\n\t}\n\tif v, ok := m[tokensString]; ok {\n\t\tswitch v {\n\t\tcase constant.NormalToken:\n\t\t\treturn claims, nil\n\t\tcase constant.KickedToken:\n\t\t\treturn nil, servererrs.ErrTokenKicked.Wrap()\n\t\tdefault:\n\t\t\treturn nil, errs.Wrap(errs.ErrTokenUnknown)\n\t\t}\n\t} else {\n\t\tisAdmin := authverify.CheckUserIsAdmin(ctx, claims.UserID)\n\t\tif isAdmin {\n\t\t\tif err = s.authDatabase.GetTemporaryTokensWithoutError(ctx, claims.UserID, claims.PlatformID, tokensString); err == nil {\n\t\t\t\treturn claims, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, servererrs.ErrTokenNotExist.Wrap()\n}\n\nfunc (s *authServer) ParseToken(ctx context.Context, req *pbauth.ParseTokenReq) (resp *pbauth.ParseTokenResp, err error) {\n\tresp = &pbauth.ParseTokenResp{}\n\tclaims, err := s.parseToken(ctx, req.Token)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp.UserID = claims.UserID\n\tresp.PlatformID = int32(claims.PlatformID)\n\tresp.ExpireTimeSeconds = claims.ExpiresAt.Unix()\n\treturn resp, nil\n}\n\nfunc (s *authServer) ForceLogout(ctx context.Context, req *pbauth.ForceLogoutReq) (*pbauth.ForceLogoutResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := s.forceKickOff(ctx, req.UserID, req.PlatformID); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbauth.ForceLogoutResp{}, nil\n}\n\nfunc (s *authServer) forceKickOff(ctx context.Context, userID string, platformID int32) error {\n\tconns, err := s.RegisterCenter.GetConns(ctx, s.config.Discovery.RpcService.MessageGateway)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, v := range conns {\n\t\tlog.ZDebug(ctx, \"forceKickOff\", \"userID\", userID, \"platformID\", platformID)\n\t\tclient := msggateway.NewMsgGatewayClient(v)\n\t\tkickReq := &msggateway.KickUserOfflineReq{KickUserIDList: []string{userID}, PlatformID: platformID}\n\t\t_, err := client.KickUserOffline(ctx, kickReq)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"forceKickOff\", err, \"kickReq\", kickReq)\n\t\t}\n\t}\n\n\tm, err := s.authDatabase.GetTokensWithoutError(ctx, userID, int(platformID))\n\tif err != nil && !errors.Is(err, redis.Nil) {\n\t\treturn err\n\t}\n\tfor k := range m {\n\t\tm[k] = constant.KickedToken\n\t\tlog.ZDebug(ctx, \"set token map is \", \"token map\", m, \"userID\",\n\t\t\tuserID, \"token\", k)\n\n\t\terr = s.authDatabase.SetTokenMapByUidPid(ctx, userID, int(platformID), m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *authServer) InvalidateToken(ctx context.Context, req *pbauth.InvalidateTokenReq) (*pbauth.InvalidateTokenResp, error) {\n\tm, err := s.authDatabase.GetTokensWithoutError(ctx, req.UserID, int(req.PlatformID))\n\tif err != nil && !errors.Is(err, redis.Nil) {\n\t\treturn nil, err\n\t}\n\tif m == nil {\n\t\treturn nil, errs.New(\"token map is empty\").Wrap()\n\t}\n\tlog.ZDebug(ctx, \"get token from redis\", \"userID\", req.UserID, \"platformID\",\n\t\treq.PlatformID, \"tokenMap\", m)\n\n\tfor k := range m {\n\t\tif k != req.GetPreservedToken() {\n\t\t\tm[k] = constant.KickedToken\n\t\t}\n\t}\n\tlog.ZDebug(ctx, \"set token map is \", \"token map\", m, \"userID\",\n\t\treq.UserID, \"token\", req.GetPreservedToken())\n\terr = s.authDatabase.SetTokenMapByUidPid(ctx, req.UserID, int(req.PlatformID), m)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbauth.InvalidateTokenResp{}, nil\n}\n\nfunc (s *authServer) KickTokens(ctx context.Context, req *pbauth.KickTokensReq) (*pbauth.KickTokensResp, error) {\n\tif err := s.authDatabase.BatchSetTokenMapByUidPid(ctx, req.Tokens); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbauth.KickTokensResp{}, nil\n}\n"
  },
  {
    "path": "internal/rpc/conversation/callback.go",
    "content": "package conversation\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\tdbModel \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc (c *conversationServer) webhookBeforeCreateSingleChatConversations(ctx context.Context, before *config.BeforeConfig, req *dbModel.Conversation) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &callbackstruct.CallbackBeforeCreateSingleChatConversationsReq{\n\t\t\tCallbackCommand:  callbackstruct.CallbackBeforeCreateSingleChatConversationsCommand,\n\t\t\tOwnerUserID:      req.OwnerUserID,\n\t\t\tConversationID:   req.ConversationID,\n\t\t\tConversationType: req.ConversationType,\n\t\t\tUserID:           req.UserID,\n\t\t\tRecvMsgOpt:       req.RecvMsgOpt,\n\t\t\tIsPinned:         req.IsPinned,\n\t\t\tIsPrivateChat:    req.IsPrivateChat,\n\t\t\tBurnDuration:     req.BurnDuration,\n\t\t\tGroupAtType:      req.GroupAtType,\n\t\t\tAttachedInfo:     req.AttachedInfo,\n\t\t\tEx:               req.Ex,\n\t\t}\n\n\t\tresp := &callbackstruct.CallbackBeforeCreateSingleChatConversationsResp{}\n\n\t\tif err := c.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdatautil.NotNilReplace(&req.RecvMsgOpt, resp.RecvMsgOpt)\n\t\tdatautil.NotNilReplace(&req.IsPinned, resp.IsPinned)\n\t\tdatautil.NotNilReplace(&req.IsPrivateChat, resp.IsPrivateChat)\n\t\tdatautil.NotNilReplace(&req.BurnDuration, resp.BurnDuration)\n\t\tdatautil.NotNilReplace(&req.GroupAtType, resp.GroupAtType)\n\t\tdatautil.NotNilReplace(&req.AttachedInfo, resp.AttachedInfo)\n\t\tdatautil.NotNilReplace(&req.Ex, resp.Ex)\n\t\treturn nil\n\t})\n}\n\nfunc (c *conversationServer) webhookAfterCreateSingleChatConversations(ctx context.Context, after *config.AfterConfig, req *dbModel.Conversation) error {\n\tcbReq := &callbackstruct.CallbackAfterCreateSingleChatConversationsReq{\n\t\tCallbackCommand:  callbackstruct.CallbackAfterCreateSingleChatConversationsCommand,\n\t\tOwnerUserID:      req.OwnerUserID,\n\t\tConversationID:   req.ConversationID,\n\t\tConversationType: req.ConversationType,\n\t\tUserID:           req.UserID,\n\t\tRecvMsgOpt:       req.RecvMsgOpt,\n\t\tIsPinned:         req.IsPinned,\n\t\tIsPrivateChat:    req.IsPrivateChat,\n\t\tBurnDuration:     req.BurnDuration,\n\t\tGroupAtType:      req.GroupAtType,\n\t\tAttachedInfo:     req.AttachedInfo,\n\t\tEx:               req.Ex,\n\t}\n\n\tc.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterCreateSingleChatConversationsResp{}, after)\n\treturn nil\n}\n\nfunc (c *conversationServer) webhookBeforeCreateGroupChatConversations(ctx context.Context, before *config.BeforeConfig, req *dbModel.Conversation) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &callbackstruct.CallbackBeforeCreateGroupChatConversationsReq{\n\t\t\tCallbackCommand:  callbackstruct.CallbackBeforeCreateGroupChatConversationsCommand,\n\t\t\tConversationID:   req.ConversationID,\n\t\t\tConversationType: req.ConversationType,\n\t\t\tGroupID:          req.GroupID,\n\t\t\tRecvMsgOpt:       req.RecvMsgOpt,\n\t\t\tIsPinned:         req.IsPinned,\n\t\t\tIsPrivateChat:    req.IsPrivateChat,\n\t\t\tBurnDuration:     req.BurnDuration,\n\t\t\tGroupAtType:      req.GroupAtType,\n\t\t\tAttachedInfo:     req.AttachedInfo,\n\t\t\tEx:               req.Ex,\n\t\t}\n\n\t\tresp := &callbackstruct.CallbackBeforeCreateGroupChatConversationsResp{}\n\n\t\tif err := c.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdatautil.NotNilReplace(&req.RecvMsgOpt, resp.RecvMsgOpt)\n\t\tdatautil.NotNilReplace(&req.IsPinned, resp.IsPinned)\n\t\tdatautil.NotNilReplace(&req.IsPrivateChat, resp.IsPrivateChat)\n\t\tdatautil.NotNilReplace(&req.BurnDuration, resp.BurnDuration)\n\t\tdatautil.NotNilReplace(&req.GroupAtType, resp.GroupAtType)\n\t\tdatautil.NotNilReplace(&req.AttachedInfo, resp.AttachedInfo)\n\t\tdatautil.NotNilReplace(&req.Ex, resp.Ex)\n\t\treturn nil\n\t})\n}\n\nfunc (c *conversationServer) webhookAfterCreateGroupChatConversations(ctx context.Context, after *config.AfterConfig, req *dbModel.Conversation) error {\n\tcbReq := &callbackstruct.CallbackAfterCreateGroupChatConversationsReq{\n\t\tCallbackCommand:  callbackstruct.CallbackAfterCreateGroupChatConversationsCommand,\n\t\tConversationID:   req.ConversationID,\n\t\tConversationType: req.ConversationType,\n\t\tGroupID:          req.GroupID,\n\t\tRecvMsgOpt:       req.RecvMsgOpt,\n\t\tIsPinned:         req.IsPinned,\n\t\tIsPrivateChat:    req.IsPrivateChat,\n\t\tBurnDuration:     req.BurnDuration,\n\t\tGroupAtType:      req.GroupAtType,\n\t\tAttachedInfo:     req.AttachedInfo,\n\t\tEx:               req.Ex,\n\t}\n\n\tc.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterCreateGroupChatConversationsResp{}, after)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/rpc/conversation/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage conversation\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\tdbModel \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbconversation \"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\ntype conversationServer struct {\n\tpbconversation.UnimplementedConversationServer\n\tconversationDatabase controller.ConversationDatabase\n\n\tconversationNotificationSender *ConversationNotificationSender\n\tconfig                         *Config\n\n\twebhookClient *webhook.Client\n\tuserClient    *rpcli.UserClient\n\tmsgClient     *rpcli.MsgClient\n\tgroupClient   *rpcli.GroupClient\n}\n\ntype Config struct {\n\tRpcConfig          config.Conversation\n\tRedisConfig        config.Redis\n\tMongodbConfig      config.Mongo\n\tNotificationConfig config.Notification\n\tShare              config.Share\n\tWebhooksConfig     config.Webhooks\n\tLocalCacheConfig   config.LocalCache\n\tDiscovery          config.Discovery\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tdbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)\n\tmgocli, err := dbb.Mongo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconversationDB, err := mgo.NewConversationMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgroupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmsgClient := rpcli.NewMsgClient(msgConn)\n\n\tcs := conversationServer{\n\t\tconfig:        config,\n\t\twebhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL),\n\t\tuserClient:    rpcli.NewUserClient(userConn),\n\t\tgroupClient:   rpcli.NewGroupClient(groupConn),\n\t\tmsgClient:     msgClient,\n\t}\n\n\tcs.conversationNotificationSender = NewConversationNotificationSender(&config.NotificationConfig, msgClient)\n\tcs.conversationDatabase = controller.NewConversationDatabase(\n\t\tconversationDB,\n\t\tredis.NewConversationRedis(rdb, &config.LocalCacheConfig, conversationDB),\n\t\tmgocli.GetTx())\n\n\tlocalcache.InitLocalCache(&config.LocalCacheConfig)\n\tpbconversation.RegisterConversationServer(server, &cs)\n\treturn nil\n}\n\nfunc (c *conversationServer) GetConversation(ctx context.Context, req *pbconversation.GetConversationReq) (*pbconversation.GetConversationResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversations, err := c.conversationDatabase.FindConversations(ctx, req.OwnerUserID, []string{req.ConversationID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(conversations) < 1 {\n\t\treturn nil, errs.ErrRecordNotFound.WrapMsg(\"conversation not found\")\n\t}\n\tresp := &pbconversation.GetConversationResp{Conversation: &pbconversation.Conversation{}}\n\tresp.Conversation = convert.ConversationDB2Pb(conversations[0])\n\treturn resp, nil\n}\n\n// Deprecated: Use `GetConversations` instead.\nfunc (c *conversationServer) GetSortedConversationList(ctx context.Context, req *pbconversation.GetSortedConversationListReq) (resp *pbconversation.GetSortedConversationListResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tvar conversationIDs []string\n\tif len(req.ConversationIDs) == 0 {\n\t\tconversationIDs, err = c.conversationDatabase.GetConversationIDs(ctx, req.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tconversationIDs = req.ConversationIDs\n\t}\n\n\tconversations, err := c.conversationDatabase.FindConversations(ctx, req.UserID, conversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(conversations) == 0 {\n\t\treturn nil, errs.ErrRecordNotFound.Wrap()\n\t}\n\tmaxSeqs, err := c.msgClient.GetMaxSeqs(ctx, conversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tchatLogs, err := c.msgClient.GetMsgByConversationIDs(ctx, conversationIDs, maxSeqs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconversationMsg, err := c.getConversationInfo(ctx, chatLogs, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thasReadSeqs, err := c.msgClient.GetHasReadSeqs(ctx, conversationIDs, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar unreadTotal int64\n\tconversation_unreadCount := make(map[string]int64)\n\tfor conversationID, maxSeq := range maxSeqs {\n\t\tunreadCount := maxSeq - hasReadSeqs[conversationID]\n\t\tconversation_unreadCount[conversationID] = unreadCount\n\t\tunreadTotal += unreadCount\n\t}\n\n\tconversation_isPinTime := make(map[int64]string)\n\tconversation_notPinTime := make(map[int64]string)\n\n\tfor _, v := range conversations {\n\t\tconversationID := v.ConversationID\n\t\tvar time int64\n\t\tif _, ok := conversationMsg[conversationID]; ok {\n\t\t\ttime = conversationMsg[conversationID].MsgInfo.LatestMsgRecvTime\n\t\t} else {\n\t\t\tconversationMsg[conversationID] = &pbconversation.ConversationElem{\n\t\t\t\tConversationID: conversationID,\n\t\t\t\tIsPinned:       v.IsPinned,\n\t\t\t\tMsgInfo:        nil,\n\t\t\t}\n\t\t\ttime = v.CreateTime.UnixMilli()\n\t\t}\n\n\t\tconversationMsg[conversationID].RecvMsgOpt = v.RecvMsgOpt\n\t\tif v.IsPinned {\n\t\t\tconversationMsg[conversationID].IsPinned = v.IsPinned\n\t\t\tconversation_isPinTime[time] = conversationID\n\t\t\tcontinue\n\t\t}\n\t\tconversation_notPinTime[time] = conversationID\n\t}\n\tresp = &pbconversation.GetSortedConversationListResp{\n\t\tConversationTotal: int64(len(chatLogs)),\n\t\tConversationElems: []*pbconversation.ConversationElem{},\n\t\tUnreadTotal:       unreadTotal,\n\t}\n\n\tc.conversationSort(conversation_isPinTime, resp, conversation_unreadCount, conversationMsg)\n\tc.conversationSort(conversation_notPinTime, resp, conversation_unreadCount, conversationMsg)\n\n\tresp.ConversationElems = datautil.Paginate(resp.ConversationElems, int(req.Pagination.GetPageNumber()), int(req.Pagination.GetShowNumber()))\n\treturn resp, nil\n}\n\nfunc (c *conversationServer) GetAllConversations(ctx context.Context, req *pbconversation.GetAllConversationsReq) (*pbconversation.GetAllConversationsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversations, err := c.conversationDatabase.GetUserAllConversation(ctx, req.OwnerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp := &pbconversation.GetAllConversationsResp{Conversations: []*pbconversation.Conversation{}}\n\tresp.Conversations = convert.ConversationsDB2Pb(conversations)\n\treturn resp, nil\n}\n\nfunc (c *conversationServer) GetConversations(ctx context.Context, req *pbconversation.GetConversationsReq) (*pbconversation.GetConversationsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversations, err := c.getConversations(ctx, req.OwnerUserID, req.ConversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbconversation.GetConversationsResp{\n\t\tConversations: conversations,\n\t}, nil\n}\n\nfunc (c *conversationServer) getConversations(ctx context.Context, ownerUserID string, conversationIDs []string) ([]*pbconversation.Conversation, error) {\n\tconversations, err := c.conversationDatabase.FindConversations(ctx, ownerUserID, conversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp := &pbconversation.GetConversationsResp{Conversations: []*pbconversation.Conversation{}}\n\tresp.Conversations = convert.ConversationsDB2Pb(conversations)\n\treturn convert.ConversationsDB2Pb(conversations), nil\n}\n\n// Deprecated\nfunc (c *conversationServer) SetConversation(ctx context.Context, req *pbconversation.SetConversationReq) (*pbconversation.SetConversationResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.GetConversation().GetUserID()); err != nil {\n\t\treturn nil, err\n\t}\n\tvar conversation dbModel.Conversation\n\tconversation.CreateTime = time.Now()\n\n\tif err := datautil.CopyStructFields(&conversation, req.Conversation); err != nil {\n\t\treturn nil, err\n\t}\n\terr := c.conversationDatabase.SetUserConversations(ctx, req.Conversation.OwnerUserID, []*dbModel.Conversation{&conversation})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc.conversationNotificationSender.ConversationChangeNotification(ctx, req.Conversation.OwnerUserID, []string{req.Conversation.ConversationID})\n\tresp := &pbconversation.SetConversationResp{}\n\treturn resp, nil\n}\n\nfunc (c *conversationServer) SetConversations(ctx context.Context, req *pbconversation.SetConversationsReq) (*pbconversation.SetConversationsResp, error) {\n\tfor _, userID := range req.UserIDs {\n\t\tif err := authverify.CheckAccess(ctx, userID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif req.Conversation.ConversationType == constant.WriteGroupChatType {\n\t\tgroupInfo, err := c.groupClient.GetGroupInfo(ctx, req.Conversation.GroupID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif groupInfo == nil {\n\t\t\treturn nil, servererrs.ErrGroupIDNotFound.WrapMsg(req.Conversation.GroupID)\n\t\t}\n\t\tif groupInfo.Status == constant.GroupStatusDismissed {\n\t\t\treturn nil, servererrs.ErrDismissedAlready.WrapMsg(\"group dismissed\")\n\t\t}\n\t}\n\n\tconversationMap := make(map[string]*dbModel.Conversation)\n\tvar needUpdateUsersList []string\n\n\tfor _, userID := range req.UserIDs {\n\t\tconversationList, err := c.conversationDatabase.FindConversations(ctx, userID, []string{req.Conversation.ConversationID})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(conversationList) != 0 {\n\t\t\tconversationMap[userID] = conversationList[0]\n\t\t} else {\n\t\t\tneedUpdateUsersList = append(needUpdateUsersList, userID)\n\t\t}\n\t}\n\n\tvar conversation dbModel.Conversation\n\tconversation.ConversationID = req.Conversation.ConversationID\n\tconversation.ConversationType = req.Conversation.ConversationType\n\tconversation.UserID = req.Conversation.UserID\n\tconversation.GroupID = req.Conversation.GroupID\n\tconversation.CreateTime = time.Now()\n\n\tm, conversation, err := UpdateConversationsMap(ctx, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor userID := range conversationMap {\n\t\tunequal := UserUpdateCheckMap(ctx, userID, req.Conversation, conversationMap[userID])\n\n\t\tif unequal {\n\t\t\tneedUpdateUsersList = append(needUpdateUsersList, userID)\n\t\t}\n\t}\n\n\tif len(m) != 0 && len(needUpdateUsersList) != 0 {\n\t\tif err := c.conversationDatabase.SetUsersConversationFieldTx(ctx, needUpdateUsersList, &conversation, m); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, userID := range needUpdateUsersList {\n\t\t\tc.conversationNotificationSender.ConversationChangeNotification(ctx, userID, []string{req.Conversation.ConversationID})\n\t\t}\n\t}\n\n\tif req.Conversation.IsPrivateChat != nil && req.Conversation.ConversationType != constant.ReadGroupChatType {\n\t\tvar conversations []*dbModel.Conversation\n\t\tfor _, ownerUserID := range req.UserIDs {\n\t\t\ttransConversation := conversation\n\t\t\ttransConversation.OwnerUserID = ownerUserID\n\t\t\ttransConversation.IsPrivateChat = req.Conversation.IsPrivateChat.Value\n\t\t\tconversations = append(conversations, &transConversation)\n\t\t}\n\n\t\tif err := c.conversationDatabase.SyncPeerUserPrivateConversationTx(ctx, conversations); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, userID := range req.UserIDs {\n\t\t\tc.conversationNotificationSender.ConversationSetPrivateNotification(ctx, userID, req.Conversation.UserID,\n\t\t\t\treq.Conversation.IsPrivateChat.Value, req.Conversation.ConversationID)\n\t\t}\n\t}\n\n\treturn &pbconversation.SetConversationsResp{}, nil\n}\n\nfunc (c *conversationServer) UpdateConversationsByUser(ctx context.Context, req *pbconversation.UpdateConversationsByUserReq) (*pbconversation.UpdateConversationsByUserResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tm := make(map[string]any)\n\tif req.Ex != nil {\n\t\tm[\"ex\"] = req.Ex.Value\n\t}\n\tif len(m) > 0 {\n\t\tif err := c.conversationDatabase.UpdateUserConversations(ctx, req.UserID, m); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &pbconversation.UpdateConversationsByUserResp{}, nil\n}\n\n// create conversation without notification for msg redis transfer.\nfunc (c *conversationServer) CreateSingleChatConversations(ctx context.Context, req *pbconversation.CreateSingleChatConversationsReq) (*pbconversation.CreateSingleChatConversationsResp, error) {\n\tvar conversation dbModel.Conversation\n\tconversation.CreateTime = time.Now()\n\n\tswitch req.ConversationType {\n\tcase constant.SingleChatType:\n\t\t// sendUser create\n\t\tconversation.ConversationID = req.ConversationID\n\t\tconversation.ConversationType = req.ConversationType\n\t\tconversation.OwnerUserID = req.SendID\n\t\tconversation.UserID = req.RecvID\n\n\t\tif err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr := c.conversationDatabase.CreateConversation(ctx, []*dbModel.Conversation{&conversation})\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"create conversation failed\", err, \"conversation\", conversation)\n\t\t}\n\n\t\tc.webhookAfterCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateSingleChatConversations, &conversation)\n\n\t\t// recvUser create\n\t\tconversation2 := conversation\n\t\tconversation2.OwnerUserID = req.RecvID\n\t\tconversation2.UserID = req.SendID\n\n\t\tif err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = c.conversationDatabase.CreateConversation(ctx, []*dbModel.Conversation{&conversation2})\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"create conversation failed\", err, \"conversation2\", conversation)\n\t\t}\n\n\t\tc.webhookAfterCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateSingleChatConversations, &conversation2)\n\tcase constant.NotificationChatType:\n\t\tconversation.ConversationID = req.ConversationID\n\t\tconversation.ConversationType = req.ConversationType\n\t\tconversation.OwnerUserID = req.RecvID\n\t\tconversation.UserID = req.SendID\n\n\t\tif err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr := c.conversationDatabase.CreateConversation(ctx, []*dbModel.Conversation{&conversation})\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"create conversation failed\", err, \"conversation2\", conversation)\n\t\t}\n\n\t\tc.webhookAfterCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateSingleChatConversations, &conversation)\n\t}\n\n\treturn &pbconversation.CreateSingleChatConversationsResp{}, nil\n}\n\nfunc (c *conversationServer) CreateGroupChatConversations(ctx context.Context, req *pbconversation.CreateGroupChatConversationsReq) (*pbconversation.CreateGroupChatConversationsResp, error) {\n\tvar conversation dbModel.Conversation\n\n\tconversation.ConversationID = msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, req.GroupID)\n\tconversation.GroupID = req.GroupID\n\tconversation.ConversationType = constant.ReadGroupChatType\n\tconversation.CreateTime = time.Now()\n\n\tif err := c.webhookBeforeCreateGroupChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateGroupChatConversations, &conversation); err != nil {\n\t\treturn nil, err\n\t}\n\n\terr := c.conversationDatabase.CreateGroupChatConversation(ctx, req.GroupID, req.UserIDs, &conversation)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := c.msgClient.SetUserConversationMaxSeq(ctx, conversation.ConversationID, req.UserIDs, 0); err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.webhookAfterCreateGroupChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateGroupChatConversations, &conversation)\n\treturn &pbconversation.CreateGroupChatConversationsResp{}, nil\n}\n\nfunc (c *conversationServer) SetConversationMaxSeq(ctx context.Context, req *pbconversation.SetConversationMaxSeqReq) (*pbconversation.SetConversationMaxSeqResp, error) {\n\tif err := c.msgClient.SetUserConversationMaxSeq(ctx, req.ConversationID, req.OwnerUserID, req.MaxSeq); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := c.conversationDatabase.UpdateUsersConversationField(ctx, req.OwnerUserID, req.ConversationID,\n\t\tmap[string]any{\"max_seq\": req.MaxSeq}); err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, userID := range req.OwnerUserID {\n\t\tc.conversationNotificationSender.ConversationChangeNotification(ctx, userID, []string{req.ConversationID})\n\t}\n\treturn &pbconversation.SetConversationMaxSeqResp{}, nil\n}\n\nfunc (c *conversationServer) SetConversationMinSeq(ctx context.Context, req *pbconversation.SetConversationMinSeqReq) (*pbconversation.SetConversationMinSeqResp, error) {\n\tif err := c.msgClient.SetUserConversationMin(ctx, req.ConversationID, req.OwnerUserID, req.MinSeq); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := c.conversationDatabase.UpdateUsersConversationField(ctx, req.OwnerUserID, req.ConversationID,\n\t\tmap[string]any{\"min_seq\": req.MinSeq}); err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, userID := range req.OwnerUserID {\n\t\tc.conversationNotificationSender.ConversationChangeNotification(ctx, userID, []string{req.ConversationID})\n\t}\n\treturn &pbconversation.SetConversationMinSeqResp{}, nil\n}\n\nfunc (c *conversationServer) GetConversationIDs(ctx context.Context, req *pbconversation.GetConversationIDsReq) (*pbconversation.GetConversationIDsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversationIDs, err := c.conversationDatabase.GetConversationIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbconversation.GetConversationIDsResp{ConversationIDs: conversationIDs}, nil\n}\n\nfunc (c *conversationServer) GetUserConversationIDsHash(ctx context.Context, req *pbconversation.GetUserConversationIDsHashReq) (*pbconversation.GetUserConversationIDsHashResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\thash, err := c.conversationDatabase.GetUserConversationIDsHash(ctx, req.OwnerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbconversation.GetUserConversationIDsHashResp{Hash: hash}, nil\n}\n\nfunc (c *conversationServer) GetConversationOfflinePushUserIDs(ctx context.Context, req *pbconversation.GetConversationOfflinePushUserIDsReq) (*pbconversation.GetConversationOfflinePushUserIDsResp, error) {\n\tif req.ConversationID == \"\" {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"conversationID is empty\")\n\t}\n\tif len(req.UserIDs) == 0 {\n\t\treturn &pbconversation.GetConversationOfflinePushUserIDsResp{}, nil\n\t}\n\tuserIDs, err := c.conversationDatabase.GetConversationNotReceiveMessageUserIDs(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(userIDs) == 0 {\n\t\treturn &pbconversation.GetConversationOfflinePushUserIDsResp{UserIDs: req.UserIDs}, nil\n\t}\n\tuserIDSet := make(map[string]struct{})\n\tfor _, userID := range req.UserIDs {\n\t\tuserIDSet[userID] = struct{}{}\n\t}\n\tfor _, userID := range userIDs {\n\t\tdelete(userIDSet, userID)\n\t}\n\treturn &pbconversation.GetConversationOfflinePushUserIDsResp{UserIDs: datautil.Keys(userIDSet)}, nil\n}\n\nfunc (c *conversationServer) conversationSort(conversations map[int64]string, resp *pbconversation.GetSortedConversationListResp, conversation_unreadCount map[string]int64, conversationMsg map[string]*pbconversation.ConversationElem) {\n\tkeys := []int64{}\n\tfor key := range conversations {\n\t\tkeys = append(keys, key)\n\t}\n\n\tsort.Slice(keys, func(i, j int) bool {\n\t\treturn keys[i] > keys[j]\n\t})\n\tindex := 0\n\n\tcons := make([]*pbconversation.ConversationElem, len(conversations))\n\tfor _, v := range keys {\n\t\tconversationID := conversations[v]\n\t\tconversationElem := conversationMsg[conversationID]\n\t\tconversationElem.UnreadCount = conversation_unreadCount[conversationID]\n\t\tcons[index] = conversationElem\n\t\tindex++\n\t}\n\tresp.ConversationElems = append(resp.ConversationElems, cons...)\n}\n\nfunc (c *conversationServer) getConversationInfo(ctx context.Context, chatLogs map[string]*sdkws.MsgData, userID string) (map[string]*pbconversation.ConversationElem, error) {\n\tvar (\n\t\tsendIDs         []string\n\t\tgroupIDs        []string\n\t\tsendMap         = make(map[string]*sdkws.UserInfo)\n\t\tgroupMap        = make(map[string]*sdkws.GroupInfo)\n\t\tconversationMsg = make(map[string]*pbconversation.ConversationElem)\n\t)\n\tfor _, chatLog := range chatLogs {\n\t\tswitch chatLog.SessionType {\n\t\tcase constant.SingleChatType:\n\t\t\tif chatLog.SendID == userID {\n\t\t\t\tsendIDs = append(sendIDs, chatLog.RecvID)\n\t\t\t}\n\t\t\tsendIDs = append(sendIDs, chatLog.SendID)\n\t\tcase constant.WriteGroupChatType, constant.ReadGroupChatType:\n\t\t\tgroupIDs = append(groupIDs, chatLog.GroupID)\n\t\t\tsendIDs = append(sendIDs, chatLog.SendID)\n\t\t}\n\t}\n\tif len(sendIDs) != 0 {\n\t\tsendInfos, err := c.userClient.GetUsersInfo(ctx, sendIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, sendInfo := range sendInfos {\n\t\t\tsendMap[sendInfo.UserID] = sendInfo\n\t\t}\n\t}\n\tif len(groupIDs) != 0 {\n\t\tgroupInfos, err := c.groupClient.GetGroupsInfo(ctx, groupIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, groupInfo := range groupInfos {\n\t\t\tgroupMap[groupInfo.GroupID] = groupInfo\n\t\t}\n\t}\n\tfor conversationID, chatLog := range chatLogs {\n\t\tpbchatLog := &pbconversation.ConversationElem{}\n\t\tmsgInfo := &pbconversation.MsgInfo{}\n\t\tif err := datautil.CopyStructFields(msgInfo, chatLog); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch chatLog.SessionType {\n\t\tcase constant.SingleChatType:\n\t\t\tif chatLog.SendID == userID {\n\t\t\t\tif recv, ok := sendMap[chatLog.RecvID]; ok {\n\t\t\t\t\tmsgInfo.FaceURL = recv.FaceURL\n\t\t\t\t\tmsgInfo.SenderName = recv.Nickname\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif send, ok := sendMap[chatLog.SendID]; ok {\n\t\t\t\tmsgInfo.FaceURL = send.FaceURL\n\t\t\t\tmsgInfo.SenderName = send.Nickname\n\t\t\t}\n\t\tcase constant.WriteGroupChatType, constant.ReadGroupChatType:\n\t\t\tmsgInfo.GroupID = chatLog.GroupID\n\t\t\tif group, ok := groupMap[chatLog.GroupID]; ok {\n\t\t\t\tmsgInfo.GroupName = group.GroupName\n\t\t\t\tmsgInfo.GroupFaceURL = group.FaceURL\n\t\t\t\tmsgInfo.GroupMemberCount = group.MemberCount\n\t\t\t\tmsgInfo.GroupType = group.GroupType\n\t\t\t}\n\t\t\tif send, ok := sendMap[chatLog.SendID]; ok {\n\t\t\t\tmsgInfo.SenderName = send.Nickname\n\t\t\t}\n\t\t}\n\t\tpbchatLog.ConversationID = conversationID\n\t\tmsgInfo.LatestMsgRecvTime = chatLog.SendTime\n\t\tpbchatLog.MsgInfo = msgInfo\n\t\tconversationMsg[conversationID] = pbchatLog\n\t}\n\treturn conversationMsg, nil\n}\n\nfunc (c *conversationServer) GetConversationNotReceiveMessageUserIDs(ctx context.Context, req *pbconversation.GetConversationNotReceiveMessageUserIDsReq) (*pbconversation.GetConversationNotReceiveMessageUserIDsResp, error) {\n\tuserIDs, err := c.conversationDatabase.GetConversationNotReceiveMessageUserIDs(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbconversation.GetConversationNotReceiveMessageUserIDsResp{UserIDs: userIDs}, nil\n}\n\nfunc (c *conversationServer) UpdateConversation(ctx context.Context, req *pbconversation.UpdateConversationReq) (*pbconversation.UpdateConversationResp, error) {\n\tfor _, userID := range req.UserIDs {\n\t\tif err := authverify.CheckAccess(ctx, userID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tm := make(map[string]any)\n\tif req.RecvMsgOpt != nil {\n\t\tm[\"recv_msg_opt\"] = req.RecvMsgOpt.Value\n\t}\n\tif req.AttachedInfo != nil {\n\t\tm[\"attached_info\"] = req.AttachedInfo.Value\n\t}\n\tif req.Ex != nil {\n\t\tm[\"ex\"] = req.Ex.Value\n\t}\n\tif req.IsPinned != nil {\n\t\tm[\"is_pinned\"] = req.IsPinned.Value\n\t}\n\tif req.GroupAtType != nil {\n\t\tm[\"group_at_type\"] = req.GroupAtType.Value\n\t}\n\tif req.MsgDestructTime != nil {\n\t\tm[\"msg_destruct_time\"] = req.MsgDestructTime.Value\n\t}\n\tif req.IsMsgDestruct != nil {\n\t\tm[\"is_msg_destruct\"] = req.IsMsgDestruct.Value\n\t}\n\tif req.BurnDuration != nil {\n\t\tm[\"burn_duration\"] = req.BurnDuration.Value\n\t}\n\tif req.IsPrivateChat != nil {\n\t\tm[\"is_private_chat\"] = req.IsPrivateChat.Value\n\t}\n\tif req.MinSeq != nil {\n\t\tm[\"min_seq\"] = req.MinSeq.Value\n\t}\n\tif req.MaxSeq != nil {\n\t\tm[\"max_seq\"] = req.MaxSeq.Value\n\t}\n\tif req.LatestMsgDestructTime != nil {\n\t\tm[\"latest_msg_destruct_time\"] = time.UnixMilli(req.LatestMsgDestructTime.Value)\n\t}\n\tif len(m) > 0 {\n\t\tif err := c.conversationDatabase.UpdateUsersConversationField(ctx, req.UserIDs, req.ConversationID, m); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &pbconversation.UpdateConversationResp{}, nil\n}\n\nfunc (c *conversationServer) GetOwnerConversation(ctx context.Context, req *pbconversation.GetOwnerConversationReq) (*pbconversation.GetOwnerConversationResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, conversations, err := c.conversationDatabase.GetOwnerConversation(ctx, req.UserID, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbconversation.GetOwnerConversationResp{\n\t\tTotal:         total,\n\t\tConversations: convert.ConversationsDB2Pb(conversations),\n\t}, nil\n}\n\nfunc (c *conversationServer) GetNotNotifyConversationIDs(ctx context.Context, req *pbconversation.GetNotNotifyConversationIDsReq) (*pbconversation.GetNotNotifyConversationIDsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversationIDs, err := c.conversationDatabase.GetNotNotifyConversationIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbconversation.GetNotNotifyConversationIDsResp{ConversationIDs: conversationIDs}, nil\n}\n\nfunc (c *conversationServer) GetPinnedConversationIDs(ctx context.Context, req *pbconversation.GetPinnedConversationIDsReq) (*pbconversation.GetPinnedConversationIDsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversationIDs, err := c.conversationDatabase.GetPinnedConversationIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbconversation.GetPinnedConversationIDsResp{ConversationIDs: conversationIDs}, nil\n}\n\nfunc (c *conversationServer) ClearUserConversationMsg(ctx context.Context, req *pbconversation.ClearUserConversationMsgReq) (*pbconversation.ClearUserConversationMsgResp, error) {\n\tconversations, err := c.conversationDatabase.FindRandConversation(ctx, req.Timestamp, int(req.Limit))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlatestMsgDestructTime := time.UnixMilli(req.Timestamp)\n\tfor i, conversation := range conversations {\n\t\tif !conversation.IsMsgDestruct || conversation.MsgDestructTime == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tseq, err := c.msgClient.GetLastMessageSeqByTime(ctx, conversation.ConversationID, req.Timestamp-(conversation.MsgDestructTime*1000))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif seq <= 0 {\n\t\t\tlog.ZDebug(ctx, \"ClearUserConversationMsg GetLastMessageSeqByTime seq <= 0\", \"index\", i, \"conversationID\", conversation.ConversationID, \"ownerUserID\", conversation.OwnerUserID, \"msgDestructTime\", conversation.MsgDestructTime, \"seq\", seq)\n\t\t\tif err := c.setConversationMinSeqAndLatestMsgDestructTime(ctx, conversation.ConversationID, conversation.OwnerUserID, -1, latestMsgDestructTime); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tseq++\n\t\tif err := c.setConversationMinSeqAndLatestMsgDestructTime(ctx, conversation.ConversationID, conversation.OwnerUserID, seq, latestMsgDestructTime); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.ZDebug(ctx, \"ClearUserConversationMsg set min seq\", \"index\", i, \"conversationID\", conversation.ConversationID, \"ownerUserID\", conversation.OwnerUserID, \"seq\", seq, \"msgDestructTime\", conversation.MsgDestructTime)\n\t}\n\treturn &pbconversation.ClearUserConversationMsgResp{Count: int32(len(conversations))}, nil\n}\n\nfunc (c *conversationServer) setConversationMinSeqAndLatestMsgDestructTime(ctx context.Context, conversationID string, ownerUserID string, minSeq int64, latestMsgDestructTime time.Time) error {\n\tupdate := map[string]any{\n\t\t\"latest_msg_destruct_time\": latestMsgDestructTime,\n\t}\n\tif minSeq >= 0 {\n\t\tif err := c.msgClient.SetUserConversationMin(ctx, conversationID, []string{ownerUserID}, minSeq); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tupdate[\"min_seq\"] = minSeq\n\t}\n\n\tif err := c.conversationDatabase.UpdateUsersConversationField(ctx, []string{ownerUserID}, conversationID, update); err != nil {\n\t\treturn err\n\t}\n\tc.conversationNotificationSender.ConversationChangeNotification(ctx, ownerUserID, []string{conversationID})\n\treturn nil\n}\n\nfunc (c *conversationServer) DeleteConversations(ctx context.Context, req *pbconversation.DeleteConversationsReq) (resp *pbconversation.DeleteConversationsResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tif req.NeedDeleteTime == 0 && len(req.ConversationIDs) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"need_delete_time or conversationIDs need be set\")\n\t}\n\n\tif req.NeedDeleteTime != 0 && len(req.ConversationIDs) != 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"need_delete_time and conversationIDs cannot both be set\")\n\t}\n\n\tvar needDeleteConversationIDs []string\n\n\tif len(req.ConversationIDs) == 0 {\n\t\tdeleteTimeThreshold := time.Now().AddDate(0, 0, -int(req.NeedDeleteTime)).UnixMilli()\n\t\tconversationIDs, err := c.conversationDatabase.GetConversationIDs(ctx, req.OwnerUserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlatestMsgs, err := c.msgClient.GetLastMessage(ctx, &msg.GetLastMessageReq{\n\t\t\tUserID:          req.OwnerUserID,\n\t\t\tConversationIDs: conversationIDs,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor conversationID, msg := range latestMsgs.Msgs {\n\t\t\tif msg.SendTime < deleteTimeThreshold {\n\t\t\t\tneedDeleteConversationIDs = append(needDeleteConversationIDs, conversationID)\n\t\t\t}\n\t\t}\n\n\t\tif len(needDeleteConversationIDs) == 0 {\n\t\t\treturn &pbconversation.DeleteConversationsResp{}, nil\n\t\t}\n\t} else {\n\t\tneedDeleteConversationIDs = req.ConversationIDs\n\t}\n\n\tif err := c.conversationDatabase.DeleteUsersConversations(ctx, req.OwnerUserID, needDeleteConversationIDs); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// c.conversationNotificationSender.ConversationDeleteNotification(ctx, req.OwnerUserID, needDeleteConversationIDs)\n\n\treturn &pbconversation.DeleteConversationsResp{}, nil\n}\n"
  },
  {
    "path": "internal/rpc/conversation/db_map.go",
    "content": "package conversation\n\nimport (\n\t\"context\"\n\n\tdbModel \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/conversation\"\n)\n\nfunc UpdateConversationsMap(ctx context.Context, req *conversation.SetConversationsReq) (m map[string]any, conversation dbModel.Conversation, err error) {\n\tm = make(map[string]any)\n\n\tconversation.ConversationID = req.Conversation.ConversationID\n\tconversation.ConversationType = req.Conversation.ConversationType\n\tconversation.UserID = req.Conversation.UserID\n\tconversation.GroupID = req.Conversation.GroupID\n\n\tif req.Conversation.RecvMsgOpt != nil {\n\t\tconversation.RecvMsgOpt = req.Conversation.RecvMsgOpt.Value\n\t\tm[\"recv_msg_opt\"] = req.Conversation.RecvMsgOpt.Value\n\t}\n\n\tif req.Conversation.AttachedInfo != nil {\n\t\tconversation.AttachedInfo = req.Conversation.AttachedInfo.Value\n\t\tm[\"attached_info\"] = req.Conversation.AttachedInfo.Value\n\t}\n\n\tif req.Conversation.Ex != nil {\n\t\tconversation.Ex = req.Conversation.Ex.Value\n\t\tm[\"ex\"] = req.Conversation.Ex.Value\n\t}\n\tif req.Conversation.IsPinned != nil {\n\t\tconversation.IsPinned = req.Conversation.IsPinned.Value\n\t\tm[\"is_pinned\"] = req.Conversation.IsPinned.Value\n\t}\n\tif req.Conversation.GroupAtType != nil {\n\t\tconversation.GroupAtType = req.Conversation.GroupAtType.Value\n\t\tm[\"group_at_type\"] = req.Conversation.GroupAtType.Value\n\t}\n\tif req.Conversation.MsgDestructTime != nil {\n\t\tconversation.MsgDestructTime = req.Conversation.MsgDestructTime.Value\n\t\tm[\"msg_destruct_time\"] = req.Conversation.MsgDestructTime.Value\n\t}\n\tif req.Conversation.IsMsgDestruct != nil {\n\t\tconversation.IsMsgDestruct = req.Conversation.IsMsgDestruct.Value\n\t\tm[\"is_msg_destruct\"] = req.Conversation.IsMsgDestruct.Value\n\t}\n\tif req.Conversation.BurnDuration != nil {\n\t\tconversation.BurnDuration = req.Conversation.BurnDuration.Value\n\t\tm[\"burn_duration\"] = req.Conversation.BurnDuration.Value\n\t}\n\n\treturn m, conversation, nil\n}\n\nfunc UserUpdateCheckMap(ctx context.Context, userID string, req *conversation.ConversationReq, conversation *dbModel.Conversation) (unequal bool) {\n\tunequal = false\n\n\tif req.RecvMsgOpt != nil && conversation.RecvMsgOpt != req.RecvMsgOpt.Value {\n\t\tunequal = true\n\t}\n\tif req.AttachedInfo != nil && conversation.AttachedInfo != req.AttachedInfo.Value {\n\t\tunequal = true\n\t}\n\tif req.Ex != nil && conversation.Ex != req.Ex.Value {\n\t\tunequal = true\n\t}\n\tif req.IsPinned != nil && conversation.IsPinned != req.IsPinned.Value {\n\t\tunequal = true\n\t}\n\tif req.GroupAtType != nil && conversation.GroupAtType != req.GroupAtType.Value {\n\t\tunequal = true\n\t}\n\tif req.MsgDestructTime != nil && conversation.MsgDestructTime != req.MsgDestructTime.Value {\n\t\tunequal = true\n\t}\n\tif req.IsMsgDestruct != nil && conversation.IsMsgDestruct != req.IsMsgDestruct.Value {\n\t\tunequal = true\n\t}\n\tif req.BurnDuration != nil && conversation.BurnDuration != req.BurnDuration.Value {\n\t\tunequal = true\n\t}\n\n\treturn unequal\n}\n"
  },
  {
    "path": "internal/rpc/conversation/notification.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage conversation\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/msg\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\ntype ConversationNotificationSender struct {\n\t*notification.NotificationSender\n}\n\nfunc NewConversationNotificationSender(conf *config.Notification, msgClient *rpcli.MsgClient) *ConversationNotificationSender {\n\treturn &ConversationNotificationSender{notification.NewNotificationSender(conf, notification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {\n\t\treturn msgClient.SendMsg(ctx, req)\n\t}))}\n}\n\n// SetPrivate invote.\nfunc (c *ConversationNotificationSender) ConversationSetPrivateNotification(ctx context.Context, sendID, recvID string,\n\tisPrivateChat bool, conversationID string,\n) {\n\ttips := &sdkws.ConversationSetPrivateTips{\n\t\tRecvID:         recvID,\n\t\tSendID:         sendID,\n\t\tIsPrivate:      isPrivateChat,\n\t\tConversationID: conversationID,\n\t}\n\n\tc.Notification(ctx, sendID, recvID, constant.ConversationPrivateChatNotification, tips)\n}\n\nfunc (c *ConversationNotificationSender) ConversationChangeNotification(ctx context.Context, userID string, conversationIDs []string) {\n\ttips := &sdkws.ConversationUpdateTips{\n\t\tUserID:             userID,\n\t\tConversationIDList: conversationIDs,\n\t}\n\n\tc.Notification(ctx, userID, userID, constant.ConversationChangeNotification, tips)\n}\n\nfunc (c *ConversationNotificationSender) ConversationUnreadChangeNotification(\n\tctx context.Context,\n\tuserID, conversationID string,\n\tunreadCountTime, hasReadSeq int64,\n) {\n\ttips := &sdkws.ConversationHasReadTips{\n\t\tUserID:          userID,\n\t\tConversationID:  conversationID,\n\t\tHasReadSeq:      hasReadSeq,\n\t\tUnreadCountTime: unreadCountTime,\n\t}\n\n\tc.Notification(ctx, userID, userID, constant.ConversationUnreadNotification, tips)\n}\n\nfunc (c *ConversationNotificationSender) ConversationDeleteNotification(ctx context.Context, userID string, conversationIDs []string) {\n\ttips := &sdkws.ConversationDeleteTips{\n\t\tUserID:          userID,\n\t\tConversationIDs: conversationIDs,\n\t}\n\n\tc.Notification(ctx, userID, userID, constant.ConversationDeleteNotification, tips)\n}\n"
  },
  {
    "path": "internal/rpc/conversation/sync.go",
    "content": "package conversation\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/incrversion\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/util/hashutil\"\n\t\"github.com/openimsdk/protocol/conversation\"\n)\n\nfunc (c *conversationServer) GetFullOwnerConversationIDs(ctx context.Context, req *conversation.GetFullOwnerConversationIDsReq) (*conversation.GetFullOwnerConversationIDsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tvl, err := c.conversationDatabase.FindMaxConversationUserVersionCache(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconversationIDs, err := c.conversationDatabase.GetConversationIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tidHash := hashutil.IdHash(conversationIDs)\n\tif req.IdHash == idHash {\n\t\tconversationIDs = nil\n\t}\n\treturn &conversation.GetFullOwnerConversationIDsResp{\n\t\tVersion:         uint64(vl.Version),\n\t\tVersionID:       vl.ID.Hex(),\n\t\tEqual:           req.IdHash == idHash,\n\t\tConversationIDs: conversationIDs,\n\t}, nil\n}\n\nfunc (c *conversationServer) GetIncrementalConversation(ctx context.Context, req *conversation.GetIncrementalConversationReq) (*conversation.GetIncrementalConversationResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\topt := incrversion.Option[*conversation.Conversation, conversation.GetIncrementalConversationResp]{\n\t\tCtx:             ctx,\n\t\tVersionKey:      req.UserID,\n\t\tVersionID:       req.VersionID,\n\t\tVersionNumber:   req.Version,\n\t\tVersion:         c.conversationDatabase.FindConversationUserVersion,\n\t\tCacheMaxVersion: c.conversationDatabase.FindMaxConversationUserVersionCache,\n\t\tFind: func(ctx context.Context, conversationIDs []string) ([]*conversation.Conversation, error) {\n\t\t\treturn c.getConversations(ctx, req.UserID, conversationIDs)\n\t\t},\n\t\tResp: func(version *model.VersionLog, delIDs []string, insertList, updateList []*conversation.Conversation, full bool) *conversation.GetIncrementalConversationResp {\n\t\t\treturn &conversation.GetIncrementalConversationResp{\n\t\t\t\tVersionID: version.ID.Hex(),\n\t\t\t\tVersion:   uint64(version.Version),\n\t\t\t\tFull:      full,\n\t\t\t\tDelete:    delIDs,\n\t\t\t\tInsert:    insertList,\n\t\t\t\tUpdate:    updateList,\n\t\t\t}\n\t\t},\n\t}\n\treturn opt.Build()\n}\n"
  },
  {
    "path": "internal/rpc/group/cache.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage group\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\tpbgroup \"github.com/openimsdk/protocol/group\"\n)\n\n// GetGroupInfoCache get group info from cache.\nfunc (g *groupServer) GetGroupInfoCache(ctx context.Context, req *pbgroup.GetGroupInfoCacheReq) (*pbgroup.GetGroupInfoCacheResp, error) {\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetGroupInfoCacheResp{\n\t\tGroupInfo: convert.Db2PbGroupInfo(group, \"\", 0),\n\t}, nil\n}\n\nfunc (g *groupServer) GetGroupMemberCache(ctx context.Context, req *pbgroup.GetGroupMemberCacheReq) (*pbgroup.GetGroupMemberCacheResp, error) {\n\tif err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\tmembers, err := g.db.TakeGroupMember(ctx, req.GroupID, req.GroupMemberID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetGroupMemberCacheResp{\n\t\tMember: convert.Db2PbGroupMember(members),\n\t}, nil\n}\n"
  },
  {
    "path": "internal/rpc/group/callback.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage group\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/wrapperspb\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\n// CallbackBeforeCreateGroup callback before create group.\nfunc (g *groupServer) webhookBeforeCreateGroup(ctx context.Context, before *config.BeforeConfig, req *group.CreateGroupReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &callbackstruct.CallbackBeforeCreateGroupReq{\n\t\t\tCallbackCommand: callbackstruct.CallbackBeforeCreateGroupCommand,\n\t\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\t\tGroupInfo:       req.GroupInfo,\n\t\t}\n\t\tcbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{\n\t\t\tUserID:    req.OwnerUserID,\n\t\t\tRoleLevel: constant.GroupOwner,\n\t\t})\n\t\tfor _, userID := range req.AdminUserIDs {\n\t\t\tcbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{\n\t\t\t\tUserID:    userID,\n\t\t\t\tRoleLevel: constant.GroupAdmin,\n\t\t\t})\n\t\t}\n\t\tfor _, userID := range req.MemberUserIDs {\n\t\t\tcbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{\n\t\t\t\tUserID:    userID,\n\t\t\t\tRoleLevel: constant.GroupOrdinaryUsers,\n\t\t\t})\n\t\t}\n\t\tresp := &callbackstruct.CallbackBeforeCreateGroupResp{}\n\n\t\tif err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdatautil.NotNilReplace(&req.GroupInfo.GroupID, resp.GroupID)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.GroupName, resp.GroupName)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.Notification, resp.Notification)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.Introduction, resp.Introduction)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.FaceURL, resp.FaceURL)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.OwnerUserID, resp.OwnerUserID)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.Ex, resp.Ex)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.Status, resp.Status)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.CreatorUserID, resp.CreatorUserID)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.GroupType, resp.GroupType)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.NeedVerification, resp.NeedVerification)\n\t\tdatautil.NotNilReplace(&req.GroupInfo.LookMemberInfo, resp.LookMemberInfo)\n\t\treturn nil\n\t})\n}\n\nfunc (g *groupServer) webhookAfterCreateGroup(ctx context.Context, after *config.AfterConfig, req *group.CreateGroupReq) {\n\tcbReq := &callbackstruct.CallbackAfterCreateGroupReq{\n\t\tCallbackCommand: callbackstruct.CallbackAfterCreateGroupCommand,\n\t\tGroupInfo:       req.GroupInfo,\n\t}\n\tcbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{\n\t\tUserID:    req.OwnerUserID,\n\t\tRoleLevel: constant.GroupOwner,\n\t})\n\tfor _, userID := range req.AdminUserIDs {\n\t\tcbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{\n\t\t\tUserID:    userID,\n\t\t\tRoleLevel: constant.GroupAdmin,\n\t\t})\n\t}\n\tfor _, userID := range req.MemberUserIDs {\n\t\tcbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{\n\t\t\tUserID:    userID,\n\t\t\tRoleLevel: constant.GroupOrdinaryUsers,\n\t\t})\n\t}\n\tg.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterCreateGroupResp{}, after)\n}\n\nfunc (g *groupServer) webhookBeforeMembersJoinGroup(ctx context.Context, before *config.BeforeConfig, groupMembers []*model.GroupMember, groupID string, groupEx string) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tgroupMembersMap := datautil.SliceToMap(groupMembers, func(e *model.GroupMember) string {\n\t\t\treturn e.UserID\n\t\t})\n\t\tvar groupMembersCallback []*callbackstruct.CallbackGroupMember\n\n\t\tfor _, member := range groupMembers {\n\t\t\tgroupMembersCallback = append(groupMembersCallback, &callbackstruct.CallbackGroupMember{\n\t\t\t\tUserID: member.UserID,\n\t\t\t\tEx:     member.Ex,\n\t\t\t})\n\t\t}\n\n\t\tcbReq := &callbackstruct.CallbackBeforeMembersJoinGroupReq{\n\t\t\tCallbackCommand: callbackstruct.CallbackBeforeMembersJoinGroupCommand,\n\t\t\tGroupID:         groupID,\n\t\t\tMembersList:     groupMembersCallback,\n\t\t\tGroupEx:         groupEx,\n\t\t}\n\t\tresp := &callbackstruct.CallbackBeforeMembersJoinGroupResp{}\n\n\t\tif err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, memberCallbackResp := range resp.MemberCallbackList {\n\t\t\tif _, ok := groupMembersMap[(*memberCallbackResp.UserID)]; ok {\n\t\t\t\tif memberCallbackResp.MuteEndTime != nil {\n\t\t\t\t\tgroupMembersMap[(*memberCallbackResp.UserID)].MuteEndTime = time.UnixMilli(*memberCallbackResp.MuteEndTime)\n\t\t\t\t}\n\n\t\t\t\tdatautil.NotNilReplace(&groupMembersMap[(*memberCallbackResp.UserID)].FaceURL, memberCallbackResp.FaceURL)\n\t\t\t\tdatautil.NotNilReplace(&groupMembersMap[(*memberCallbackResp.UserID)].Ex, memberCallbackResp.Ex)\n\t\t\t\tdatautil.NotNilReplace(&groupMembersMap[(*memberCallbackResp.UserID)].Nickname, memberCallbackResp.Nickname)\n\t\t\t\tdatautil.NotNilReplace(&groupMembersMap[(*memberCallbackResp.UserID)].RoleLevel, memberCallbackResp.RoleLevel)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc (g *groupServer) webhookBeforeSetGroupMemberInfo(ctx context.Context, before *config.BeforeConfig, req *group.SetGroupMemberInfo) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := callbackstruct.CallbackBeforeSetGroupMemberInfoReq{\n\t\t\tCallbackCommand: callbackstruct.CallbackBeforeSetGroupMemberInfoCommand,\n\t\t\tGroupID:         req.GroupID,\n\t\t\tUserID:          req.UserID,\n\t\t}\n\t\tif req.Nickname != nil {\n\t\t\tcbReq.Nickname = &req.Nickname.Value\n\t\t}\n\t\tif req.FaceURL != nil {\n\t\t\tcbReq.FaceURL = &req.FaceURL.Value\n\t\t}\n\t\tif req.RoleLevel != nil {\n\t\t\tcbReq.RoleLevel = &req.RoleLevel.Value\n\t\t}\n\t\tif req.Ex != nil {\n\t\t\tcbReq.Ex = &req.Ex.Value\n\t\t}\n\t\tresp := &callbackstruct.CallbackBeforeSetGroupMemberInfoResp{}\n\t\tif err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.FaceURL != nil {\n\t\t\treq.FaceURL = wrapperspb.String(*resp.FaceURL)\n\t\t}\n\t\tif resp.Nickname != nil {\n\t\t\treq.Nickname = wrapperspb.String(*resp.Nickname)\n\t\t}\n\t\tif resp.RoleLevel != nil {\n\t\t\treq.RoleLevel = wrapperspb.Int32(*resp.RoleLevel)\n\t\t}\n\t\tif resp.Ex != nil {\n\t\t\treq.Ex = wrapperspb.String(*resp.Ex)\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (g *groupServer) webhookAfterSetGroupMemberInfo(ctx context.Context, after *config.AfterConfig, req *group.SetGroupMemberInfo) {\n\tcbReq := callbackstruct.CallbackAfterSetGroupMemberInfoReq{\n\t\tCallbackCommand: callbackstruct.CallbackAfterSetGroupMemberInfoCommand,\n\t\tGroupID:         req.GroupID,\n\t\tUserID:          req.UserID,\n\t}\n\tif req.Nickname != nil {\n\t\tcbReq.Nickname = &req.Nickname.Value\n\t}\n\tif req.FaceURL != nil {\n\t\tcbReq.FaceURL = &req.FaceURL.Value\n\t}\n\tif req.RoleLevel != nil {\n\t\tcbReq.RoleLevel = &req.RoleLevel.Value\n\t}\n\tif req.Ex != nil {\n\t\tcbReq.Ex = &req.Ex.Value\n\t}\n\tg.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterSetGroupMemberInfoResp{}, after)\n}\n\nfunc (g *groupServer) webhookAfterQuitGroup(ctx context.Context, after *config.AfterConfig, req *group.QuitGroupReq) {\n\tcbReq := &callbackstruct.CallbackQuitGroupReq{\n\t\tCallbackCommand: callbackstruct.CallbackAfterQuitGroupCommand,\n\t\tGroupID:         req.GroupID,\n\t\tUserID:          req.UserID,\n\t}\n\tg.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackQuitGroupResp{}, after)\n}\n\nfunc (g *groupServer) webhookAfterKickGroupMember(ctx context.Context, after *config.AfterConfig, req *group.KickGroupMemberReq) {\n\tcbReq := &callbackstruct.CallbackKillGroupMemberReq{\n\t\tCallbackCommand: callbackstruct.CallbackAfterKickGroupCommand,\n\t\tGroupID:         req.GroupID,\n\t\tKickedUserIDs:   req.KickedUserIDs,\n\t\tReason:          req.Reason,\n\t}\n\tg.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackKillGroupMemberResp{}, after)\n}\n\nfunc (g *groupServer) webhookAfterDismissGroup(ctx context.Context, after *config.AfterConfig, req *callbackstruct.CallbackDisMissGroupReq) {\n\treq.CallbackCommand = callbackstruct.CallbackAfterDisMissGroupCommand\n\tg.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &callbackstruct.CallbackDisMissGroupResp{}, after)\n}\n\nfunc (g *groupServer) webhookBeforeApplyJoinGroup(ctx context.Context, before *config.BeforeConfig, req *callbackstruct.CallbackJoinGroupReq) (err error) {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\treq.CallbackCommand = callbackstruct.CallbackBeforeJoinGroupCommand\n\t\tresp := &callbackstruct.CallbackJoinGroupResp{}\n\t\tif err := g.webhookClient.SyncPost(ctx, req.GetCallbackCommand(), req, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (g *groupServer) webhookAfterTransferGroupOwner(ctx context.Context, after *config.AfterConfig, req *group.TransferGroupOwnerReq) {\n\tcbReq := &callbackstruct.CallbackTransferGroupOwnerReq{\n\t\tCallbackCommand: callbackstruct.CallbackAfterTransferGroupOwnerCommand,\n\t\tGroupID:         req.GroupID,\n\t\tOldOwnerUserID:  req.OldOwnerUserID,\n\t\tNewOwnerUserID:  req.NewOwnerUserID,\n\t}\n\tg.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackTransferGroupOwnerResp{}, after)\n}\n\nfunc (g *groupServer) webhookBeforeInviteUserToGroup(ctx context.Context, before *config.BeforeConfig, req *group.InviteUserToGroupReq) (err error) {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &callbackstruct.CallbackBeforeInviteUserToGroupReq{\n\t\t\tCallbackCommand: callbackstruct.CallbackBeforeInviteJoinGroupCommand,\n\t\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\t\tGroupID:         req.GroupID,\n\t\t\tReason:          req.Reason,\n\t\t\tInvitedUserIDs:  req.InvitedUserIDs,\n\t\t}\n\n\t\tresp := &callbackstruct.CallbackBeforeInviteUserToGroupResp{}\n\t\tif err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Handle the scenario where certain members are refused\n\t\t// You might want to update the req.Members list or handle it as per your business logic\n\n\t\t// if len(resp.RefusedMembersAccount) > 0 {\n\t\t// implement members are refused\n\t\t// }\n\n\t\treturn nil\n\t})\n}\n\nfunc (g *groupServer) webhookAfterJoinGroup(ctx context.Context, after *config.AfterConfig, req *group.JoinGroupReq) {\n\tcbReq := &callbackstruct.CallbackAfterJoinGroupReq{\n\t\tCallbackCommand: callbackstruct.CallbackAfterJoinGroupCommand,\n\t\tOperationID:     mcontext.GetOperationID(ctx),\n\t\tGroupID:         req.GroupID,\n\t\tReqMessage:      req.ReqMessage,\n\t\tJoinSource:      req.JoinSource,\n\t\tInviterUserID:   req.InviterUserID,\n\t}\n\tg.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterJoinGroupResp{}, after)\n}\n\nfunc (g *groupServer) webhookBeforeSetGroupInfo(ctx context.Context, before *config.BeforeConfig, req *group.SetGroupInfoReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &callbackstruct.CallbackBeforeSetGroupInfoReq{\n\t\t\tCallbackCommand: callbackstruct.CallbackBeforeSetGroupInfoCommand,\n\t\t\tGroupID:         req.GroupInfoForSet.GroupID,\n\t\t\tNotification:    req.GroupInfoForSet.Notification,\n\t\t\tIntroduction:    req.GroupInfoForSet.Introduction,\n\t\t\tFaceURL:         req.GroupInfoForSet.FaceURL,\n\t\t\tGroupName:       req.GroupInfoForSet.GroupName,\n\t\t}\n\t\tif req.GroupInfoForSet.Ex != nil {\n\t\t\tcbReq.Ex = req.GroupInfoForSet.Ex.Value\n\t\t}\n\t\tlog.ZDebug(ctx, \"debug CallbackBeforeSetGroupInfo\", \"ex\", cbReq.Ex)\n\t\tif req.GroupInfoForSet.NeedVerification != nil {\n\t\t\tcbReq.NeedVerification = req.GroupInfoForSet.NeedVerification.Value\n\t\t}\n\t\tif req.GroupInfoForSet.LookMemberInfo != nil {\n\t\t\tcbReq.LookMemberInfo = req.GroupInfoForSet.LookMemberInfo.Value\n\t\t}\n\t\tif req.GroupInfoForSet.ApplyMemberFriend != nil {\n\t\t\tcbReq.ApplyMemberFriend = req.GroupInfoForSet.ApplyMemberFriend.Value\n\t\t}\n\t\tresp := &callbackstruct.CallbackBeforeSetGroupInfoResp{}\n\n\t\tif err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif resp.Ex != nil {\n\t\t\treq.GroupInfoForSet.Ex = wrapperspb.String(*resp.Ex)\n\t\t}\n\t\tif resp.NeedVerification != nil {\n\t\t\treq.GroupInfoForSet.NeedVerification = wrapperspb.Int32(*resp.NeedVerification)\n\t\t}\n\t\tif resp.LookMemberInfo != nil {\n\t\t\treq.GroupInfoForSet.LookMemberInfo = wrapperspb.Int32(*resp.LookMemberInfo)\n\t\t}\n\t\tif resp.ApplyMemberFriend != nil {\n\t\t\treq.GroupInfoForSet.ApplyMemberFriend = wrapperspb.Int32(*resp.ApplyMemberFriend)\n\t\t}\n\t\tdatautil.NotNilReplace(&req.GroupInfoForSet.GroupID, &resp.GroupID)\n\t\tdatautil.NotNilReplace(&req.GroupInfoForSet.GroupName, &resp.GroupName)\n\t\tdatautil.NotNilReplace(&req.GroupInfoForSet.FaceURL, &resp.FaceURL)\n\t\tdatautil.NotNilReplace(&req.GroupInfoForSet.Introduction, &resp.Introduction)\n\t\treturn nil\n\t})\n}\n\nfunc (g *groupServer) webhookAfterSetGroupInfo(ctx context.Context, after *config.AfterConfig, req *group.SetGroupInfoReq) {\n\tcbReq := &callbackstruct.CallbackAfterSetGroupInfoReq{\n\t\tCallbackCommand: callbackstruct.CallbackAfterSetGroupInfoCommand,\n\t\tGroupID:         req.GroupInfoForSet.GroupID,\n\t\tNotification:    req.GroupInfoForSet.Notification,\n\t\tIntroduction:    req.GroupInfoForSet.Introduction,\n\t\tFaceURL:         req.GroupInfoForSet.FaceURL,\n\t\tGroupName:       req.GroupInfoForSet.GroupName,\n\t}\n\tif req.GroupInfoForSet.Ex != nil {\n\t\tcbReq.Ex = &req.GroupInfoForSet.Ex.Value\n\t}\n\tif req.GroupInfoForSet.NeedVerification != nil {\n\t\tcbReq.NeedVerification = &req.GroupInfoForSet.NeedVerification.Value\n\t}\n\tif req.GroupInfoForSet.LookMemberInfo != nil {\n\t\tcbReq.LookMemberInfo = &req.GroupInfoForSet.LookMemberInfo.Value\n\t}\n\tif req.GroupInfoForSet.ApplyMemberFriend != nil {\n\t\tcbReq.ApplyMemberFriend = &req.GroupInfoForSet.ApplyMemberFriend.Value\n\t}\n\tg.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterSetGroupInfoResp{}, after)\n}\n\nfunc (g *groupServer) webhookBeforeSetGroupInfoEx(ctx context.Context, before *config.BeforeConfig, req *group.SetGroupInfoExReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &callbackstruct.CallbackBeforeSetGroupInfoExReq{\n\t\t\tCallbackCommand: callbackstruct.CallbackBeforeSetGroupInfoExCommand,\n\t\t\tGroupID:         req.GroupID,\n\t\t\tGroupName:       req.GroupName,\n\t\t\tNotification:    req.Notification,\n\t\t\tIntroduction:    req.Introduction,\n\t\t\tFaceURL:         req.FaceURL,\n\t\t}\n\n\t\tif req.Ex != nil {\n\t\t\tcbReq.Ex = req.Ex\n\t\t}\n\t\tlog.ZDebug(ctx, \"debug CallbackBeforeSetGroupInfoEx\", \"ex\", cbReq.Ex)\n\n\t\tif req.NeedVerification != nil {\n\t\t\tcbReq.NeedVerification = req.NeedVerification\n\t\t}\n\t\tif req.LookMemberInfo != nil {\n\t\t\tcbReq.LookMemberInfo = req.LookMemberInfo\n\t\t}\n\t\tif req.ApplyMemberFriend != nil {\n\t\t\tcbReq.ApplyMemberFriend = req.ApplyMemberFriend\n\t\t}\n\n\t\tresp := &callbackstruct.CallbackBeforeSetGroupInfoExResp{}\n\n\t\tif err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdatautil.NotNilReplace(&req.GroupID, &resp.GroupID)\n\t\tdatautil.NotNilReplace(&req.GroupName, &resp.GroupName)\n\t\tdatautil.NotNilReplace(&req.FaceURL, &resp.FaceURL)\n\t\tdatautil.NotNilReplace(&req.Introduction, &resp.Introduction)\n\t\tdatautil.NotNilReplace(&req.Ex, &resp.Ex)\n\t\tdatautil.NotNilReplace(&req.NeedVerification, &resp.NeedVerification)\n\t\tdatautil.NotNilReplace(&req.LookMemberInfo, &resp.LookMemberInfo)\n\t\tdatautil.NotNilReplace(&req.ApplyMemberFriend, &resp.ApplyMemberFriend)\n\n\t\treturn nil\n\t})\n}\n\nfunc (g *groupServer) webhookAfterSetGroupInfoEx(ctx context.Context, after *config.AfterConfig, req *group.SetGroupInfoExReq) {\n\tcbReq := &callbackstruct.CallbackAfterSetGroupInfoExReq{\n\t\tCallbackCommand: callbackstruct.CallbackAfterSetGroupInfoExCommand,\n\t\tGroupID:         req.GroupID,\n\t\tGroupName:       req.GroupName,\n\t\tNotification:    req.Notification,\n\t\tIntroduction:    req.Introduction,\n\t\tFaceURL:         req.FaceURL,\n\t}\n\n\tif req.Ex != nil {\n\t\tcbReq.Ex = req.Ex\n\t}\n\tif req.NeedVerification != nil {\n\t\tcbReq.NeedVerification = req.NeedVerification\n\t}\n\tif req.LookMemberInfo != nil {\n\t\tcbReq.LookMemberInfo = req.LookMemberInfo\n\t}\n\tif req.ApplyMemberFriend != nil {\n\t\tcbReq.ApplyMemberFriend = req.ApplyMemberFriend\n\t}\n\n\tg.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterSetGroupInfoExResp{}, after)\n}\n"
  },
  {
    "path": "internal/rpc/group/convert.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage group\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\nfunc (g *groupServer) groupDB2PB(group *model.Group, ownerUserID string, memberCount uint32) *sdkws.GroupInfo {\n\treturn &sdkws.GroupInfo{\n\t\tGroupID:                group.GroupID,\n\t\tGroupName:              group.GroupName,\n\t\tNotification:           group.Notification,\n\t\tIntroduction:           group.Introduction,\n\t\tFaceURL:                group.FaceURL,\n\t\tOwnerUserID:            ownerUserID,\n\t\tCreateTime:             group.CreateTime.UnixMilli(),\n\t\tMemberCount:            memberCount,\n\t\tEx:                     group.Ex,\n\t\tStatus:                 group.Status,\n\t\tCreatorUserID:          group.CreatorUserID,\n\t\tGroupType:              group.GroupType,\n\t\tNeedVerification:       group.NeedVerification,\n\t\tLookMemberInfo:         group.LookMemberInfo,\n\t\tApplyMemberFriend:      group.ApplyMemberFriend,\n\t\tNotificationUpdateTime: group.NotificationUpdateTime.UnixMilli(),\n\t\tNotificationUserID:     group.NotificationUserID,\n\t}\n}\n\nfunc (g *groupServer) groupMemberDB2PB(member *model.GroupMember, appMangerLevel int32) *sdkws.GroupMemberFullInfo {\n\treturn &sdkws.GroupMemberFullInfo{\n\t\tGroupID:        member.GroupID,\n\t\tUserID:         member.UserID,\n\t\tRoleLevel:      member.RoleLevel,\n\t\tJoinTime:       member.JoinTime.UnixMilli(),\n\t\tNickname:       member.Nickname,\n\t\tFaceURL:        member.FaceURL,\n\t\tAppMangerLevel: appMangerLevel,\n\t\tJoinSource:     member.JoinSource,\n\t\tOperatorUserID: member.OperatorUserID,\n\t\tEx:             member.Ex,\n\t\tMuteEndTime:    member.MuteEndTime.UnixMilli(),\n\t\tInviterUserID:  member.InviterUserID,\n\t}\n}\n\nfunc (g *groupServer) groupMemberDB2PB2(member *model.GroupMember) *sdkws.GroupMemberFullInfo {\n\treturn g.groupMemberDB2PB(member, 0)\n}\n"
  },
  {
    "path": "internal/rpc/group/db_map.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage group\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\tpbgroup \"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nfunc UpdateGroupInfoMap(ctx context.Context, group *sdkws.GroupInfoForSet) map[string]any {\n\tm := make(map[string]any)\n\tif group.GroupName != \"\" {\n\t\tm[\"group_name\"] = group.GroupName\n\t}\n\tif group.Notification != \"\" {\n\t\tm[\"notification\"] = group.Notification\n\t\tm[\"notification_update_time\"] = time.Now()\n\t\tm[\"notification_user_id\"] = mcontext.GetOpUserID(ctx)\n\t}\n\tif group.Introduction != \"\" {\n\t\tm[\"introduction\"] = group.Introduction\n\t}\n\tif group.FaceURL != \"\" {\n\t\tm[\"face_url\"] = group.FaceURL\n\t}\n\tif group.NeedVerification != nil {\n\t\tm[\"need_verification\"] = group.NeedVerification.Value\n\t}\n\tif group.LookMemberInfo != nil {\n\t\tm[\"look_member_info\"] = group.LookMemberInfo.Value\n\t}\n\tif group.ApplyMemberFriend != nil {\n\t\tm[\"apply_member_friend\"] = group.ApplyMemberFriend.Value\n\t}\n\tif group.Ex != nil {\n\t\tm[\"ex\"] = group.Ex.Value\n\t}\n\treturn m\n}\n\nfunc UpdateGroupInfoExMap(ctx context.Context, group *pbgroup.SetGroupInfoExReq) (m map[string]any, normalFlag, groupNameFlag, notificationFlag bool, err error) {\n\tm = make(map[string]any)\n\n\tif group.GroupName != nil {\n\t\tif strings.TrimSpace(group.GroupName.Value) != \"\" {\n\t\t\tm[\"group_name\"] = group.GroupName.Value\n\t\t\tgroupNameFlag = true\n\t\t} else {\n\t\t\treturn nil, normalFlag, notificationFlag, groupNameFlag, errs.ErrArgs.WrapMsg(\"group name is empty\")\n\t\t}\n\t}\n\n\tif group.Notification != nil {\n\t\tnotificationFlag = true\n\t\tgroup.Notification.Value = strings.TrimSpace(group.Notification.Value) // if Notification only contains spaces, set it to empty string\n\n\t\tm[\"notification\"] = group.Notification.Value\n\t\tm[\"notification_user_id\"] = mcontext.GetOpUserID(ctx)\n\t\tm[\"notification_update_time\"] = time.Now()\n\t}\n\tif group.Introduction != nil {\n\t\tm[\"introduction\"] = group.Introduction.Value\n\t\tnormalFlag = true\n\t}\n\tif group.FaceURL != nil {\n\t\tm[\"face_url\"] = group.FaceURL.Value\n\t\tnormalFlag = true\n\t}\n\tif group.NeedVerification != nil {\n\t\tm[\"need_verification\"] = group.NeedVerification.Value\n\t\tnormalFlag = true\n\t}\n\tif group.LookMemberInfo != nil {\n\t\tm[\"look_member_info\"] = group.LookMemberInfo.Value\n\t\tnormalFlag = true\n\t}\n\tif group.ApplyMemberFriend != nil {\n\t\tm[\"apply_member_friend\"] = group.ApplyMemberFriend.Value\n\t\tnormalFlag = true\n\t}\n\tif group.Ex != nil {\n\t\tm[\"ex\"] = group.Ex.Value\n\t\tnormalFlag = true\n\t}\n\n\treturn m, normalFlag, groupNameFlag, notificationFlag, nil\n}\n\nfunc UpdateGroupStatusMap(status int) map[string]any {\n\treturn map[string]any{\n\t\t\"status\": status,\n\t}\n}\n\nfunc UpdateGroupMemberMutedTimeMap(t time.Time) map[string]any {\n\treturn map[string]any{\n\t\t\"mute_end_time\": t,\n\t}\n}\n\nfunc UpdateGroupMemberMap(req *pbgroup.SetGroupMemberInfo) map[string]any {\n\tm := make(map[string]any)\n\tif req.Nickname != nil {\n\t\tm[\"nickname\"] = req.Nickname.Value\n\t}\n\tif req.FaceURL != nil {\n\t\tm[\"face_url\"] = req.FaceURL.Value\n\t}\n\tif req.RoleLevel != nil {\n\t\tm[\"role_level\"] = req.RoleLevel.Value\n\t}\n\tif req.Ex != nil {\n\t\tm[\"ex\"] = req.Ex.Value\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "internal/rpc/group/fill.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage group\n\nimport (\n\t\"context\"\n\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n)\n\nfunc (g *groupServer) PopulateGroupMember(ctx context.Context, members ...*relationtb.GroupMember) error {\n\treturn g.notification.PopulateGroupMember(ctx, members...)\n}\n"
  },
  {
    "path": "internal/rpc/group/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage group\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/common\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification/grouphash\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbconv \"github.com/openimsdk/protocol/conversation\"\n\tpbgroup \"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/protocol/wrapperspb\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/mw/specialerror\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/encrypt\"\n)\n\ntype groupServer struct {\n\tpbgroup.UnimplementedGroupServer\n\tdb                 controller.GroupDatabase\n\tnotification       *NotificationSender\n\tconfig             *Config\n\twebhookClient      *webhook.Client\n\tuserClient         *rpcli.UserClient\n\tmsgClient          *rpcli.MsgClient\n\tconversationClient *rpcli.ConversationClient\n\tadminUserIDs       []string\n}\n\ntype Config struct {\n\tRpcConfig          config.Group\n\tRedisConfig        config.Redis\n\tMongodbConfig      config.Mongo\n\tNotificationConfig config.Notification\n\tShare              config.Share\n\tWebhooksConfig     config.Webhooks\n\tLocalCacheConfig   config.LocalCache\n\tDiscovery          config.Discovery\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tdbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)\n\tmgocli, err := dbb.Mongo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgroupDB, err := mgo.NewGroupMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tgroupMemberDB, err := mgo.NewGroupMember(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tgroupRequestDB, err := mgo.NewGroupRequestMgo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconversationConn, err := client.GetConn(ctx, config.Discovery.RpcService.Conversation)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgs := groupServer{\n\t\tconfig:             config,\n\t\twebhookClient:      webhook.NewWebhookClient(config.WebhooksConfig.URL),\n\t\tuserClient:         rpcli.NewUserClient(userConn),\n\t\tmsgClient:          rpcli.NewMsgClient(msgConn),\n\t\tconversationClient: rpcli.NewConversationClient(conversationConn),\n\t\tadminUserIDs:       config.Share.IMAdminUser.UserIDs,\n\t}\n\tgs.db = controller.NewGroupDatabase(rdb, &config.LocalCacheConfig, groupDB, groupMemberDB, groupRequestDB, mgocli.GetTx(), grouphash.NewGroupHashFromGroupServer(&gs))\n\tgs.notification = NewNotificationSender(gs.db, config, gs.userClient, gs.msgClient, gs.conversationClient)\n\tlocalcache.InitLocalCache(&config.LocalCacheConfig)\n\tpbgroup.RegisterGroupServer(server, &gs)\n\treturn nil\n}\n\nfunc (g *groupServer) NotificationUserInfoUpdate(ctx context.Context, req *pbgroup.NotificationUserInfoUpdateReq) (*pbgroup.NotificationUserInfoUpdateResp, error) {\n\tmembers, err := g.db.FindGroupMemberUser(ctx, nil, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupIDs := make([]string, 0, len(members))\n\tfor _, member := range members {\n\t\tif member.Nickname != \"\" && member.FaceURL != \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tgroupIDs = append(groupIDs, member.GroupID)\n\t}\n\tfor _, groupID := range groupIDs {\n\t\tif err := g.db.MemberGroupIncrVersion(ctx, groupID, []string{req.UserID}, model.VersionStateUpdate); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tfor _, groupID := range groupIDs {\n\t\tg.notification.GroupMemberInfoSetNotification(ctx, groupID, req.UserID)\n\t}\n\tif err = g.db.DeleteGroupMemberHash(ctx, groupIDs); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.NotificationUserInfoUpdateResp{}, nil\n}\n\nfunc (g *groupServer) CheckGroupAdmin(ctx context.Context, groupID string) error {\n\tif !authverify.IsAdmin(ctx) {\n\t\tmembers, err := g.db.FindGroupMembers(ctx, groupID, []string{mcontext.GetOpUserID(ctx)})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(members) == 0 {\n\t\t\treturn errs.ErrNoPermission.WrapMsg(\"op user not in group\")\n\t\t}\n\t\tgroupMember := members[0]\n\t\tif !(groupMember.RoleLevel == constant.GroupOwner || groupMember.RoleLevel == constant.GroupAdmin) {\n\t\t\treturn errs.ErrNoPermission.WrapMsg(\"no group owner or admin\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (g *groupServer) IsNotFound(err error) bool {\n\treturn errs.ErrRecordNotFound.Is(specialerror.ErrCode(errs.Unwrap(err)))\n}\n\nfunc (g *groupServer) GenGroupID(ctx context.Context, groupID *string) error {\n\tif *groupID != \"\" {\n\t\t_, err := g.db.TakeGroup(ctx, *groupID)\n\t\tif err == nil {\n\t\t\treturn servererrs.ErrGroupIDExisted.WrapMsg(\"group id existed \" + *groupID)\n\t\t} else if g.IsNotFound(err) {\n\t\t\treturn nil\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor i := 0; i < 10; i++ {\n\t\tid := encrypt.Md5(strings.Join([]string{mcontext.GetOperationID(ctx), strconv.FormatInt(time.Now().UnixNano(), 10), strconv.Itoa(rand.Int())}, \",;,\"))\n\t\tbi := big.NewInt(0)\n\t\tbi.SetString(id[0:8], 16)\n\t\tid = bi.String()\n\t\t_, err := g.db.TakeGroup(ctx, id)\n\t\tif err == nil {\n\t\t\tcontinue\n\t\t} else if g.IsNotFound(err) {\n\t\t\t*groupID = id\n\t\t\treturn nil\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn servererrs.ErrData.WrapMsg(\"group id gen error\")\n}\n\nfunc (g *groupServer) CreateGroup(ctx context.Context, req *pbgroup.CreateGroupReq) (*pbgroup.CreateGroupResp, error) {\n\tif req.GroupInfo.GroupType != constant.WorkingGroup {\n\t\treturn nil, errs.ErrArgs.WrapMsg(fmt.Sprintf(\"group type only supports %d\", constant.WorkingGroup))\n\t}\n\tif req.OwnerUserID == \"\" {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"no group owner\")\n\t}\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tuserIDs := append(append(req.MemberUserIDs, req.AdminUserIDs...), req.OwnerUserID)\n\topUserID := mcontext.GetOpUserID(ctx)\n\tif !datautil.Contain(opUserID, userIDs...) {\n\t\tuserIDs = append(userIDs, opUserID)\n\t}\n\n\tif datautil.Duplicate(userIDs) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"group member repeated\")\n\t}\n\n\tuserMap, err := g.userClient.GetUsersInfoMap(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(userMap) != len(userIDs) {\n\t\treturn nil, servererrs.ErrUserIDNotFound.WrapMsg(\"user not found\")\n\t}\n\n\tif err := g.webhookBeforeCreateGroup(ctx, &g.config.WebhooksConfig.BeforeCreateGroup, req); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\tvar groupMembers []*model.GroupMember\n\tgroup := convert.Pb2DBGroupInfo(req.GroupInfo)\n\tif err := g.GenGroupID(ctx, &group.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tjoinGroupFunc := func(userID string, roleLevel int32) {\n\t\tgroupMember := &model.GroupMember{\n\t\t\tGroupID:        group.GroupID,\n\t\t\tUserID:         userID,\n\t\t\tRoleLevel:      roleLevel,\n\t\t\tOperatorUserID: opUserID,\n\t\t\tJoinSource:     constant.JoinByInvitation,\n\t\t\tInviterUserID:  opUserID,\n\t\t\tJoinTime:       time.Now(),\n\t\t\tMuteEndTime:    time.UnixMilli(0),\n\t\t}\n\n\t\tgroupMembers = append(groupMembers, groupMember)\n\t}\n\n\tjoinGroupFunc(req.OwnerUserID, constant.GroupOwner)\n\n\tfor _, userID := range req.AdminUserIDs {\n\t\tjoinGroupFunc(userID, constant.GroupAdmin)\n\t}\n\n\tfor _, userID := range req.MemberUserIDs {\n\t\tjoinGroupFunc(userID, constant.GroupOrdinaryUsers)\n\t}\n\n\tif err := g.webhookBeforeMembersJoinGroup(ctx, &g.config.WebhooksConfig.BeforeMemberJoinGroup, groupMembers, group.GroupID, group.Ex); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\tif err := g.db.CreateGroup(ctx, []*model.Group{group}, groupMembers); err != nil {\n\t\treturn nil, err\n\t}\n\tresp := &pbgroup.CreateGroupResp{GroupInfo: &sdkws.GroupInfo{}}\n\n\tresp.GroupInfo = convert.Db2PbGroupInfo(group, req.OwnerUserID, uint32(len(userIDs)))\n\tresp.GroupInfo.MemberCount = uint32(len(userIDs))\n\ttips := &sdkws.GroupCreatedTips{\n\t\tGroup:          resp.GroupInfo,\n\t\tOperationTime:  group.CreateTime.UnixMilli(),\n\t\tGroupOwnerUser: g.groupMemberDB2PB(groupMembers[0], userMap[groupMembers[0].UserID].AppMangerLevel),\n\t}\n\tfor _, member := range groupMembers {\n\t\tmember.Nickname = userMap[member.UserID].Nickname\n\t\ttips.MemberList = append(tips.MemberList, g.groupMemberDB2PB(member, userMap[member.UserID].AppMangerLevel))\n\t\tif member.UserID == opUserID {\n\t\t\ttips.OpUser = g.groupMemberDB2PB(member, userMap[member.UserID].AppMangerLevel)\n\t\t\tbreak\n\t\t}\n\t}\n\tg.notification.GroupCreatedNotification(ctx, tips, req.SendMessage)\n\n\tif req.GroupInfo.Notification != \"\" {\n\t\tnotificationFlag := true\n\t\tg.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{\n\t\t\tGroup:  tips.Group,\n\t\t\tOpUser: tips.OpUser,\n\t\t}, &notificationFlag)\n\t}\n\n\treqCallBackAfter := &pbgroup.CreateGroupReq{\n\t\tMemberUserIDs: userIDs,\n\t\tGroupInfo:     resp.GroupInfo,\n\t\tOwnerUserID:   req.OwnerUserID,\n\t\tAdminUserIDs:  req.AdminUserIDs,\n\t}\n\n\tg.webhookAfterCreateGroup(ctx, &g.config.WebhooksConfig.AfterCreateGroup, reqCallBackAfter)\n\n\treturn resp, nil\n}\n\nfunc (g *groupServer) GetJoinedGroupList(ctx context.Context, req *pbgroup.GetJoinedGroupListReq) (*pbgroup.GetJoinedGroupListResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.FromUserID); err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, members, err := g.db.PageGetJoinGroup(ctx, req.FromUserID, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar resp pbgroup.GetJoinedGroupListResp\n\tresp.Total = uint32(total)\n\tif len(members) == 0 {\n\t\treturn &resp, nil\n\t}\n\tgroupIDs := datautil.Slice(members, func(e *model.GroupMember) string {\n\t\treturn e.GroupID\n\t})\n\tgroups, err := g.db.FindGroup(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupMemberNum, err := g.db.MapGroupMemberNum(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\towners, err := g.db.FindGroupsOwner(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\townerMap := datautil.SliceToMap(owners, func(e *model.GroupMember) string {\n\t\treturn e.GroupID\n\t})\n\tresp.Groups = datautil.Slice(datautil.Order(groupIDs, groups, func(group *model.Group) string {\n\t\treturn group.GroupID\n\t}), func(group *model.Group) *sdkws.GroupInfo {\n\t\tvar userID string\n\t\tif user := ownerMap[group.GroupID]; user != nil {\n\t\t\tuserID = user.UserID\n\t\t}\n\t\treturn convert.Db2PbGroupInfo(group, userID, groupMemberNum[group.GroupID])\n\t})\n\treturn &resp, nil\n}\n\nfunc (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.InviteUserToGroupReq) (*pbgroup.InviteUserToGroupResp, error) {\n\tif len(req.InvitedUserIDs) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"user empty\")\n\t}\n\tif datautil.Duplicate(req.InvitedUserIDs) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userID duplicate\")\n\t}\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif group.Status == constant.GroupStatusDismissed {\n\t\treturn nil, servererrs.ErrDismissedAlready.WrapMsg(\"group dismissed checking group status found it dismissed\")\n\t}\n\n\tif err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserMap, err := g.userClient.GetUsersInfoMap(ctx, req.InvitedUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(userMap) != len(req.InvitedUserIDs) {\n\t\treturn nil, errs.ErrRecordNotFound.WrapMsg(\"user not found\")\n\t}\n\n\tvar groupMember *model.GroupMember\n\topUserID := mcontext.GetOpUserID(ctx)\n\n\tif !authverify.IsAdmin(ctx) {\n\t\tvar err error\n\t\tgroupMember, err = g.db.TakeGroupMember(ctx, req.GroupID, opUserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := g.PopulateGroupMember(ctx, groupMember); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif err := g.webhookBeforeInviteUserToGroup(ctx, &g.config.WebhooksConfig.BeforeInviteUserToGroup, req); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\tif group.NeedVerification == constant.AllNeedVerification {\n\t\tif !authverify.IsAdmin(ctx) {\n\t\t\tif !(groupMember.RoleLevel == constant.GroupOwner || groupMember.RoleLevel == constant.GroupAdmin) {\n\t\t\t\tvar requests []*model.GroupRequest\n\t\t\t\tfor _, userID := range req.InvitedUserIDs {\n\t\t\t\t\trequests = append(requests, &model.GroupRequest{\n\t\t\t\t\t\tUserID:        userID,\n\t\t\t\t\t\tGroupID:       req.GroupID,\n\t\t\t\t\t\tJoinSource:    constant.JoinByInvitation,\n\t\t\t\t\t\tInviterUserID: opUserID,\n\t\t\t\t\t\tReqTime:       time.Now(),\n\t\t\t\t\t\tHandledTime:   time.Unix(0, 0),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tif err := g.db.CreateGroupRequest(ctx, requests); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tfor _, request := range requests {\n\t\t\t\t\tg.notification.JoinGroupApplicationNotification(ctx, &pbgroup.JoinGroupReq{\n\t\t\t\t\t\tGroupID:       request.GroupID,\n\t\t\t\t\t\tReqMessage:    request.ReqMsg,\n\t\t\t\t\t\tJoinSource:    request.JoinSource,\n\t\t\t\t\t\tInviterUserID: request.InviterUserID,\n\t\t\t\t\t}, request)\n\t\t\t\t}\n\t\t\t\treturn &pbgroup.InviteUserToGroupResp{}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tvar groupMembers []*model.GroupMember\n\tfor _, userID := range req.InvitedUserIDs {\n\t\tmember := &model.GroupMember{\n\t\t\tGroupID:        req.GroupID,\n\t\t\tUserID:         userID,\n\t\t\tRoleLevel:      constant.GroupOrdinaryUsers,\n\t\t\tOperatorUserID: opUserID,\n\t\t\tInviterUserID:  opUserID,\n\t\t\tJoinSource:     constant.JoinByInvitation,\n\t\t\tJoinTime:       time.Now(),\n\t\t\tMuteEndTime:    time.UnixMilli(0),\n\t\t}\n\n\t\tgroupMembers = append(groupMembers, member)\n\t}\n\n\tif err := g.webhookBeforeMembersJoinGroup(ctx, &g.config.WebhooksConfig.BeforeMemberJoinGroup, groupMembers, group.GroupID, group.Ex); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\tconst singleQuantity = 50\n\tfor start := 0; start < len(groupMembers); start += singleQuantity {\n\t\tend := min(start+singleQuantity, len(groupMembers))\n\t\tcurrentMembers := groupMembers[start:end]\n\n\t\tif err := g.db.CreateGroup(ctx, nil, currentMembers); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tuserIDs := datautil.Slice(currentMembers, func(e *model.GroupMember) string {\n\t\t\treturn e.UserID\n\t\t})\n\n\t\tif len(userIDs) != 0 {\n\t\t\tg.notification.GroupApplicationAgreeMemberEnterNotification(ctx, req.GroupID, req.SendMessage, opUserID, userIDs...)\n\t\t}\n\t}\n\tif err := g.setMemberJoinSeq(ctx, req.GroupID, req.InvitedUserIDs); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.InviteUserToGroupResp{}, nil\n}\n\nfunc (g *groupServer) GetGroupAllMember(ctx context.Context, req *pbgroup.GetGroupAllMemberReq) (*pbgroup.GetGroupAllMemberResp, error) {\n\tmembers, err := g.db.FindGroupMemberAll(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !authverify.IsAdmin(ctx) {\n\t\tvar inGroup bool\n\t\topUserID := mcontext.GetOpUserID(ctx)\n\t\tfor _, member := range members {\n\t\t\tif member.UserID == opUserID {\n\t\t\t\tinGroup = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !inGroup {\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"opuser not in group\")\n\t\t}\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\tvar resp pbgroup.GetGroupAllMemberResp\n\tresp.Members = datautil.Slice(members, func(e *model.GroupMember) *sdkws.GroupMemberFullInfo {\n\t\treturn convert.Db2PbGroupMember(e)\n\t})\n\treturn &resp, nil\n}\n\nfunc (g *groupServer) checkAdminOrInGroup(ctx context.Context, groupID string) error {\n\tif authverify.IsAdmin(ctx) {\n\t\treturn nil\n\t}\n\topUserID := mcontext.GetOpUserID(ctx)\n\tmembers, err := g.db.FindGroupMembers(ctx, groupID, []string{opUserID})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(members) == 0 {\n\t\treturn errs.ErrNoPermission.WrapMsg(\"op user not in group\")\n\t}\n\treturn nil\n}\n\nfunc (g *groupServer) GetGroupMemberList(ctx context.Context, req *pbgroup.GetGroupMemberListReq) (*pbgroup.GetGroupMemberListResp, error) {\n\tif err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\tvar (\n\t\ttotal   int64\n\t\tmembers []*model.GroupMember\n\t\terr     error\n\t)\n\tif req.Keyword == \"\" {\n\t\ttotal, members, err = g.db.PageGetGroupMember(ctx, req.GroupID, req.Pagination)\n\t} else {\n\t\ttotal, members, err = g.db.SearchGroupMember(ctx, req.GroupID, req.Keyword, req.Pagination)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetGroupMemberListResp{\n\t\tTotal:   uint32(total),\n\t\tMembers: datautil.Batch(convert.Db2PbGroupMember, members),\n\t}, nil\n}\n\nfunc (g *groupServer) KickGroupMember(ctx context.Context, req *pbgroup.KickGroupMemberReq) (*pbgroup.KickGroupMemberResp, error) {\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(req.KickedUserIDs) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"KickedUserIDs empty\")\n\t}\n\tif datautil.Duplicate(req.KickedUserIDs) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"KickedUserIDs duplicate\")\n\t}\n\topUserID := mcontext.GetOpUserID(ctx)\n\tif datautil.Contain(opUserID, req.KickedUserIDs...) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"opUserID in KickedUserIDs\")\n\t}\n\towner, err := g.db.TakeGroupOwner(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif datautil.Contain(owner.UserID, req.KickedUserIDs...) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"ownerUID can not Kick\")\n\t}\n\n\tmembers, err := g.db.FindGroupMembers(ctx, req.GroupID, append(req.KickedUserIDs, opUserID))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\tmemberMap := make(map[string]*model.GroupMember)\n\tfor i, member := range members {\n\t\tmemberMap[member.UserID] = members[i]\n\t}\n\tisAppManagerUid := authverify.IsAdmin(ctx)\n\topMember := memberMap[opUserID]\n\tfor _, userID := range req.KickedUserIDs {\n\t\tmember, ok := memberMap[userID]\n\t\tif !ok {\n\t\t\treturn nil, servererrs.ErrUserIDNotFound.WrapMsg(userID)\n\t\t}\n\t\tif !isAppManagerUid {\n\t\t\tif opMember == nil {\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"opUserID no in group\")\n\t\t\t}\n\t\t\tswitch opMember.RoleLevel {\n\t\t\tcase constant.GroupOwner:\n\t\t\tcase constant.GroupAdmin:\n\t\t\t\tif member.RoleLevel == constant.GroupOwner || member.RoleLevel == constant.GroupAdmin {\n\t\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"group admins cannot remove the group owner and other admins\")\n\t\t\t\t}\n\t\t\tcase constant.GroupOrdinaryUsers:\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"opUserID no permission\")\n\t\t\tdefault:\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"opUserID roleLevel unknown\")\n\t\t\t}\n\t\t}\n\t}\n\townerUserIDs, err := g.db.GetGroupRoleLevelMemberIDs(ctx, req.GroupID, constant.GroupOwner)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar ownerUserID string\n\tif len(ownerUserIDs) > 0 {\n\t\townerUserID = ownerUserIDs[0]\n\t}\n\tif err := g.db.DeleteGroupMember(ctx, group.GroupID, req.KickedUserIDs); err != nil {\n\t\treturn nil, err\n\t}\n\tnum, err := g.db.FindGroupMemberNum(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttips := &sdkws.MemberKickedTips{\n\t\tGroup: &sdkws.GroupInfo{\n\t\t\tGroupID:                group.GroupID,\n\t\t\tGroupName:              group.GroupName,\n\t\t\tNotification:           group.Notification,\n\t\t\tIntroduction:           group.Introduction,\n\t\t\tFaceURL:                group.FaceURL,\n\t\t\tOwnerUserID:            ownerUserID,\n\t\t\tCreateTime:             group.CreateTime.UnixMilli(),\n\t\t\tMemberCount:            num,\n\t\t\tEx:                     group.Ex,\n\t\t\tStatus:                 group.Status,\n\t\t\tCreatorUserID:          group.CreatorUserID,\n\t\t\tGroupType:              group.GroupType,\n\t\t\tNeedVerification:       group.NeedVerification,\n\t\t\tLookMemberInfo:         group.LookMemberInfo,\n\t\t\tApplyMemberFriend:      group.ApplyMemberFriend,\n\t\t\tNotificationUpdateTime: group.NotificationUpdateTime.UnixMilli(),\n\t\t\tNotificationUserID:     group.NotificationUserID,\n\t\t},\n\t\tKickedUserList: []*sdkws.GroupMemberFullInfo{},\n\t}\n\tif opMember, ok := memberMap[opUserID]; ok {\n\t\ttips.OpUser = convert.Db2PbGroupMember(opMember)\n\t}\n\tfor _, userID := range req.KickedUserIDs {\n\t\ttips.KickedUserList = append(tips.KickedUserList, convert.Db2PbGroupMember(memberMap[userID]))\n\t}\n\tg.notification.MemberKickedNotification(ctx, tips, req.SendMessage)\n\tif err := g.deleteMemberAndSetConversationSeq(ctx, req.GroupID, req.KickedUserIDs); err != nil {\n\t\treturn nil, err\n\t}\n\tg.webhookAfterKickGroupMember(ctx, &g.config.WebhooksConfig.AfterKickGroupMember, req)\n\n\treturn &pbgroup.KickGroupMemberResp{}, nil\n}\n\nfunc (g *groupServer) GetGroupMembersInfo(ctx context.Context, req *pbgroup.GetGroupMembersInfoReq) (*pbgroup.GetGroupMembersInfoResp, error) {\n\tif len(req.UserIDs) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userIDs empty\")\n\t}\n\tif req.GroupID == \"\" {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"groupID empty\")\n\t}\n\tif err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\tmembers, err := g.getGroupMembersInfo(ctx, req.GroupID, req.UserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetGroupMembersInfoResp{\n\t\tMembers: members,\n\t}, nil\n}\n\nfunc (g *groupServer) getGroupMembersInfo(ctx context.Context, groupID string, userIDs []string) ([]*sdkws.GroupMemberFullInfo, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tmembers, err := g.db.FindGroupMembers(ctx, groupID, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\treturn datautil.Slice(members, func(e *model.GroupMember) *sdkws.GroupMemberFullInfo {\n\t\treturn convert.Db2PbGroupMember(e)\n\t}), nil\n}\n\n// GetGroupApplicationList handles functions that get a list of group requests.\nfunc (g *groupServer) GetGroupApplicationList(ctx context.Context, req *pbgroup.GetGroupApplicationListReq) (*pbgroup.GetGroupApplicationListResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.FromUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tvar (\n\t\tgroupIDs []string\n\t\terr      error\n\t)\n\tif len(req.GroupIDs) == 0 {\n\t\tgroupIDs, err = g.db.FindUserManagedGroupID(ctx, req.FromUserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\treq.GroupIDs = datautil.Distinct(req.GroupIDs)\n\t\tif !authverify.IsAdmin(ctx) {\n\t\t\tfor _, groupID := range req.GroupIDs {\n\t\t\t\tif err := g.CheckGroupAdmin(ctx, groupID); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tgroupIDs = req.GroupIDs\n\t}\n\tresp := &pbgroup.GetGroupApplicationListResp{}\n\tif len(groupIDs) == 0 {\n\t\treturn resp, nil\n\t}\n\thandleResults := datautil.Slice(req.HandleResults, func(e int32) int {\n\t\treturn int(e)\n\t})\n\ttotal, groupRequests, err := g.db.PageGroupRequest(ctx, groupIDs, handleResults, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp.Total = uint32(total)\n\tif len(groupRequests) == 0 {\n\t\treturn resp, nil\n\t}\n\tvar userIDs []string\n\n\tfor _, gr := range groupRequests {\n\t\tuserIDs = append(userIDs, gr.UserID)\n\t}\n\tuserIDs = datautil.Distinct(userIDs)\n\tuserMap, err := g.userClient.GetUsersInfoMap(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroups, err := g.db.FindGroup(ctx, datautil.Distinct(groupIDs))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupMap := datautil.SliceToMap(groups, func(e *model.Group) string {\n\t\treturn e.GroupID\n\t})\n\tif ids := datautil.Single(datautil.Keys(groupMap), groupIDs); len(ids) > 0 {\n\t\treturn nil, servererrs.ErrGroupIDNotFound.WrapMsg(strings.Join(ids, \",\"))\n\t}\n\tgroupMemberNumMap, err := g.db.MapGroupMemberNum(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\towners, err := g.db.FindGroupsOwner(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, owners...); err != nil {\n\t\treturn nil, err\n\t}\n\townerMap := datautil.SliceToMap(owners, func(e *model.GroupMember) string {\n\t\treturn e.GroupID\n\t})\n\tresp.GroupRequests = datautil.Slice(groupRequests, func(e *model.GroupRequest) *sdkws.GroupRequest {\n\t\tvar ownerUserID string\n\t\tif owner, ok := ownerMap[e.GroupID]; ok {\n\t\t\townerUserID = owner.UserID\n\t\t}\n\t\treturn convert.Db2PbGroupRequest(e, userMap[e.UserID], convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerUserID, groupMemberNumMap[e.GroupID]))\n\t})\n\treturn resp, nil\n}\n\nfunc (g *groupServer) GetGroupsInfo(ctx context.Context, req *pbgroup.GetGroupsInfoReq) (*pbgroup.GetGroupsInfoResp, error) {\n\tif len(req.GroupIDs) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"groupID is empty\")\n\t}\n\tgroups, err := g.getGroupsInfo(ctx, req.GroupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetGroupsInfoResp{\n\t\tGroupInfos: groups,\n\t}, nil\n}\n\nfunc (g *groupServer) GetGroupApplicationUnhandledCount(ctx context.Context, req *pbgroup.GetGroupApplicationUnhandledCountReq) (*pbgroup.GetGroupApplicationUnhandledCountResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tgroupIDs, err := g.db.FindUserManagedGroupID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcount, err := g.db.GetGroupApplicationUnhandledCount(ctx, groupIDs, req.Time)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetGroupApplicationUnhandledCountResp{\n\t\tCount: count,\n\t}, nil\n}\n\nfunc (g *groupServer) getGroupsInfo(ctx context.Context, groupIDs []string) ([]*sdkws.GroupInfo, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tgroups, err := g.db.FindGroup(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupMemberNumMap, err := g.db.MapGroupMemberNum(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\towners, err := g.db.FindGroupsOwner(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, owners...); err != nil {\n\t\treturn nil, err\n\t}\n\townerMap := datautil.SliceToMap(owners, func(e *model.GroupMember) string {\n\t\treturn e.GroupID\n\t})\n\treturn datautil.Slice(groups, func(e *model.Group) *sdkws.GroupInfo {\n\t\tvar ownerUserID string\n\t\tif owner, ok := ownerMap[e.GroupID]; ok {\n\t\t\townerUserID = owner.UserID\n\t\t}\n\t\treturn convert.Db2PbGroupInfo(e, ownerUserID, groupMemberNumMap[e.GroupID])\n\t}), nil\n}\n\nfunc (g *groupServer) GroupApplicationResponse(ctx context.Context, req *pbgroup.GroupApplicationResponseReq) (*pbgroup.GroupApplicationResponseResp, error) {\n\tif !datautil.Contain(req.HandleResult, constant.GroupResponseAgree, constant.GroupResponseRefuse) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"HandleResult unknown\")\n\t}\n\tif !authverify.IsAdmin(ctx) {\n\t\tgroupMember, err := g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !(groupMember.RoleLevel == constant.GroupOwner || groupMember.RoleLevel == constant.GroupAdmin) {\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"no group owner or admin\")\n\t\t}\n\t}\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupRequest, err := g.db.TakeGroupRequest(ctx, req.GroupID, req.FromUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif groupRequest.HandleResult != 0 {\n\t\treturn nil, servererrs.ErrGroupRequestHandled.WrapMsg(\"group request already processed\")\n\t}\n\tvar inGroup bool\n\tif _, err := g.db.TakeGroupMember(ctx, req.GroupID, req.FromUserID); err == nil {\n\t\tinGroup = true // Already in group\n\t} else if !g.IsNotFound(err) {\n\t\treturn nil, err\n\t}\n\tif err := g.userClient.CheckUser(ctx, []string{req.FromUserID}); err != nil {\n\t\treturn nil, err\n\t}\n\tvar member *model.GroupMember\n\tif (!inGroup) && req.HandleResult == constant.GroupResponseAgree {\n\t\tmember = &model.GroupMember{\n\t\t\tGroupID:        req.GroupID,\n\t\t\tUserID:         req.FromUserID,\n\t\t\tNickname:       \"\",\n\t\t\tFaceURL:        \"\",\n\t\t\tRoleLevel:      constant.GroupOrdinaryUsers,\n\t\t\tJoinTime:       time.Now(),\n\t\t\tJoinSource:     groupRequest.JoinSource,\n\t\t\tMuteEndTime:    time.Unix(0, 0),\n\t\t\tInviterUserID:  groupRequest.InviterUserID,\n\t\t\tOperatorUserID: mcontext.GetOpUserID(ctx),\n\t\t}\n\n\t\tif err := g.webhookBeforeMembersJoinGroup(ctx, &g.config.WebhooksConfig.BeforeMemberJoinGroup, []*model.GroupMember{member}, group.GroupID, group.Ex); err != nil && err != servererrs.ErrCallbackContinue {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tlog.ZDebug(ctx, \"GroupApplicationResponse\", \"inGroup\", inGroup, \"HandleResult\", req.HandleResult, \"member\", member)\n\tif err := g.db.HandlerGroupRequest(ctx, req.GroupID, req.FromUserID, req.HandledMsg, req.HandleResult, member); err != nil {\n\t\treturn nil, err\n\t}\n\tswitch req.HandleResult {\n\tcase constant.GroupResponseAgree:\n\t\tg.notification.GroupApplicationAcceptedNotification(ctx, req)\n\t\tif member == nil {\n\t\t\tlog.ZDebug(ctx, \"GroupApplicationResponse\", \"member is nil\")\n\t\t} else {\n\t\t\tif groupRequest.InviterUserID == \"\" {\n\t\t\t\tif err = g.notification.MemberEnterNotification(ctx, req.GroupID, req.FromUserID); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err = g.notification.GroupApplicationAgreeMemberEnterNotification(ctx, req.GroupID, nil, groupRequest.InviterUserID, req.FromUserID); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := g.setMemberJoinSeq(ctx, req.GroupID, []string{req.FromUserID}); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\tcase constant.GroupResponseRefuse:\n\t\tg.notification.GroupApplicationRejectedNotification(ctx, req)\n\t}\n\n\treturn &pbgroup.GroupApplicationResponseResp{}, nil\n}\n\nfunc (g *groupServer) JoinGroup(ctx context.Context, req *pbgroup.JoinGroupReq) (*pbgroup.JoinGroupResp, error) {\n\tuser, err := g.userClient.GetUserInfo(ctx, req.InviterUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif group.Status == constant.GroupStatusDismissed {\n\t\treturn nil, servererrs.ErrDismissedAlready.Wrap()\n\t}\n\n\treqCall := &callbackstruct.CallbackJoinGroupReq{\n\t\tGroupID:    req.GroupID,\n\t\tGroupType:  string(group.GroupType),\n\t\tApplyID:    req.InviterUserID,\n\t\tReqMessage: req.ReqMessage,\n\t\tEx:         req.Ex,\n\t}\n\n\tif err := g.webhookBeforeApplyJoinGroup(ctx, &g.config.WebhooksConfig.BeforeApplyJoinGroup, reqCall); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\t_, err = g.db.TakeGroupMember(ctx, req.GroupID, req.InviterUserID)\n\tif err == nil {\n\t\treturn nil, errs.ErrArgs.Wrap()\n\t} else if !g.IsNotFound(err) && errs.Unwrap(err) != errs.ErrRecordNotFound {\n\t\treturn nil, err\n\t}\n\tlog.ZDebug(ctx, \"JoinGroup.groupInfo\", \"group\", group, \"eq\", group.NeedVerification == constant.Directly)\n\tif group.NeedVerification == constant.Directly {\n\t\tgroupMember := &model.GroupMember{\n\t\t\tGroupID:        group.GroupID,\n\t\t\tUserID:         user.UserID,\n\t\t\tRoleLevel:      constant.GroupOrdinaryUsers,\n\t\t\tOperatorUserID: mcontext.GetOpUserID(ctx),\n\t\t\tInviterUserID:  req.InviterUserID,\n\t\t\tJoinTime:       time.Now(),\n\t\t\tMuteEndTime:    time.UnixMilli(0),\n\t\t}\n\n\t\tif err := g.webhookBeforeMembersJoinGroup(ctx, &g.config.WebhooksConfig.BeforeMemberJoinGroup, []*model.GroupMember{groupMember}, group.GroupID, group.Ex); err != nil && err != servererrs.ErrCallbackContinue {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif err := g.db.CreateGroup(ctx, nil, []*model.GroupMember{groupMember}); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif err = g.notification.MemberEnterNotification(ctx, req.GroupID, req.InviterUserID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := g.setMemberJoinSeq(ctx, req.GroupID, []string{req.InviterUserID}); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tg.webhookAfterJoinGroup(ctx, &g.config.WebhooksConfig.AfterJoinGroup, req)\n\n\t\treturn &pbgroup.JoinGroupResp{}, nil\n\t}\n\n\tgroupRequest := model.GroupRequest{\n\t\tUserID:      req.InviterUserID,\n\t\tReqMsg:      req.ReqMessage,\n\t\tGroupID:     req.GroupID,\n\t\tJoinSource:  req.JoinSource,\n\t\tReqTime:     time.Now(),\n\t\tHandledTime: time.Unix(0, 0),\n\t\tEx:          req.Ex,\n\t}\n\tif err = g.db.CreateGroupRequest(ctx, []*model.GroupRequest{&groupRequest}); err != nil {\n\t\treturn nil, err\n\t}\n\tg.notification.JoinGroupApplicationNotification(ctx, req, &groupRequest)\n\treturn &pbgroup.JoinGroupResp{}, nil\n}\n\nfunc (g *groupServer) QuitGroup(ctx context.Context, req *pbgroup.QuitGroupReq) (*pbgroup.QuitGroupResp, error) {\n\tif req.UserID == \"\" {\n\t\treq.UserID = mcontext.GetOpUserID(ctx)\n\t} else {\n\t\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tmember, err := g.db.TakeGroupMember(ctx, req.GroupID, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif member.RoleLevel == constant.GroupOwner {\n\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"group owner can't quit\")\n\t}\n\tif err := g.PopulateGroupMember(ctx, member); err != nil {\n\t\treturn nil, err\n\t}\n\terr = g.db.DeleteGroupMember(ctx, req.GroupID, []string{req.UserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tg.notification.MemberQuitNotification(ctx, g.groupMemberDB2PB(member, 0))\n\tif err := g.deleteMemberAndSetConversationSeq(ctx, req.GroupID, []string{req.UserID}); err != nil {\n\t\treturn nil, err\n\t}\n\tg.webhookAfterQuitGroup(ctx, &g.config.WebhooksConfig.AfterQuitGroup, req)\n\n\treturn &pbgroup.QuitGroupResp{}, nil\n}\n\nfunc (g *groupServer) deleteMemberAndSetConversationSeq(ctx context.Context, groupID string, userIDs []string) error {\n\tconversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID)\n\tmaxSeq, err := g.msgClient.GetConversationMaxSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn g.conversationClient.SetConversationMaxSeq(ctx, conversationID, userIDs, maxSeq)\n}\n\nfunc (g *groupServer) setMemberJoinSeq(ctx context.Context, groupID string, userIDs []string) error {\n\tconversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID)\n\treturn g.conversationClient.SetConversationMaxSeq(ctx, conversationID, userIDs, 0)\n}\n\nfunc (g *groupServer) SetGroupInfo(ctx context.Context, req *pbgroup.SetGroupInfoReq) (*pbgroup.SetGroupInfoResp, error) {\n\tvar opMember *model.GroupMember\n\tif !authverify.IsAdmin(ctx) {\n\t\tvar err error\n\t\topMember, err = g.db.TakeGroupMember(ctx, req.GroupInfoForSet.GroupID, mcontext.GetOpUserID(ctx))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !(opMember.RoleLevel == constant.GroupOwner || opMember.RoleLevel == constant.GroupAdmin) {\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"no group owner or admin\")\n\t\t}\n\t\tif err := g.PopulateGroupMember(ctx, opMember); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif err := g.webhookBeforeSetGroupInfo(ctx, &g.config.WebhooksConfig.BeforeSetGroupInfo, req); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupInfoForSet.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif group.Status == constant.GroupStatusDismissed {\n\t\treturn nil, servererrs.ErrDismissedAlready.Wrap()\n\t}\n\n\tcount, err := g.db.FindGroupMemberNum(ctx, group.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\towner, err := g.db.TakeGroupOwner(ctx, group.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, owner); err != nil {\n\t\treturn nil, err\n\t}\n\tupdate := UpdateGroupInfoMap(ctx, req.GroupInfoForSet)\n\tif len(update) == 0 {\n\t\treturn &pbgroup.SetGroupInfoResp{}, nil\n\t}\n\tif err := g.db.UpdateGroup(ctx, group.GroupID, update); err != nil {\n\t\treturn nil, err\n\t}\n\tgroup, err = g.db.TakeGroup(ctx, req.GroupInfoForSet.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttips := &sdkws.GroupInfoSetTips{\n\t\tGroup:    g.groupDB2PB(group, owner.UserID, count),\n\t\tMuteTime: 0,\n\t\tOpUser:   &sdkws.GroupMemberFullInfo{},\n\t}\n\tif opMember != nil {\n\t\ttips.OpUser = g.groupMemberDB2PB(opMember, 0)\n\t}\n\tnum := len(update)\n\tif req.GroupInfoForSet.Notification != \"\" {\n\t\tnum -= 3\n\t\tfunc() {\n\t\t\tconversation := &pbconv.ConversationReq{\n\t\t\t\tConversationID:   msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, req.GroupInfoForSet.GroupID),\n\t\t\t\tConversationType: constant.ReadGroupChatType,\n\t\t\t\tGroupID:          req.GroupInfoForSet.GroupID,\n\t\t\t}\n\t\t\tresp, err := g.GetGroupMemberUserIDs(ctx, &pbgroup.GetGroupMemberUserIDsReq{GroupID: req.GroupInfoForSet.GroupID})\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"GetGroupMemberIDs is failed.\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.GroupNotification}\n\t\t\tif err := g.conversationClient.SetConversations(ctx, resp.UserIDs, conversation); err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"SetConversations\", err, \"UserIDs\", resp.UserIDs, \"conversation\", conversation)\n\t\t\t}\n\t\t}()\n\t\tnotficationFlag := true\n\t\tg.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{Group: tips.Group, OpUser: tips.OpUser}, &notficationFlag)\n\t}\n\tif req.GroupInfoForSet.GroupName != \"\" {\n\t\tnum--\n\t\tg.notification.GroupInfoSetNameNotification(ctx, &sdkws.GroupInfoSetNameTips{Group: tips.Group, OpUser: tips.OpUser})\n\t}\n\tif num > 0 {\n\t\tg.notification.GroupInfoSetNotification(ctx, tips)\n\t}\n\n\tg.webhookAfterSetGroupInfo(ctx, &g.config.WebhooksConfig.AfterSetGroupInfo, req)\n\n\treturn &pbgroup.SetGroupInfoResp{}, nil\n}\n\nfunc (g *groupServer) SetGroupInfoEx(ctx context.Context, req *pbgroup.SetGroupInfoExReq) (*pbgroup.SetGroupInfoExResp, error) {\n\tvar opMember *model.GroupMember\n\n\tif !authverify.IsAdmin(ctx) {\n\t\tvar err error\n\n\t\topMember, err = g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif !(opMember.RoleLevel == constant.GroupOwner || opMember.RoleLevel == constant.GroupAdmin) {\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"no group owner or admin\")\n\t\t}\n\n\t\tif err := g.PopulateGroupMember(ctx, opMember); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif err := g.webhookBeforeSetGroupInfoEx(ctx, &g.config.WebhooksConfig.BeforeSetGroupInfoEx, req); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif group.Status == constant.GroupStatusDismissed {\n\t\treturn nil, servererrs.ErrDismissedAlready.Wrap()\n\t}\n\n\tcount, err := g.db.FindGroupMemberNum(ctx, group.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\towner, err := g.db.TakeGroupOwner(ctx, group.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := g.PopulateGroupMember(ctx, owner); err != nil {\n\t\treturn nil, err\n\t}\n\n\tupdatedData, normalFlag, groupNameFlag, notificationFlag, err := UpdateGroupInfoExMap(ctx, req)\n\tif len(updatedData) == 0 {\n\t\treturn &pbgroup.SetGroupInfoExResp{}, nil\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := g.db.UpdateGroup(ctx, group.GroupID, updatedData); err != nil {\n\t\treturn nil, err\n\t}\n\n\tgroup, err = g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttips := &sdkws.GroupInfoSetTips{\n\t\tGroup:    g.groupDB2PB(group, owner.UserID, count),\n\t\tMuteTime: 0,\n\t\tOpUser:   &sdkws.GroupMemberFullInfo{},\n\t}\n\n\tif opMember != nil {\n\t\ttips.OpUser = g.groupMemberDB2PB(opMember, 0)\n\t}\n\n\tif notificationFlag {\n\t\tif req.Notification.Value != \"\" {\n\t\t\tconversation := &pbconv.ConversationReq{\n\t\t\t\tConversationID:   msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, req.GroupID),\n\t\t\t\tConversationType: constant.ReadGroupChatType,\n\t\t\t\tGroupID:          req.GroupID,\n\t\t\t}\n\n\t\t\tresp, err := g.GetGroupMemberUserIDs(ctx, &pbgroup.GetGroupMemberUserIDsReq{GroupID: req.GroupID})\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"GetGroupMemberIDs is failed.\", err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tconversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.GroupNotification}\n\t\t\tif err := g.conversationClient.SetConversations(ctx, resp.UserIDs, conversation); err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"SetConversations\", err, \"UserIDs\", resp.UserIDs, \"conversation\", conversation)\n\t\t\t}\n\n\t\t\tg.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{Group: tips.Group, OpUser: tips.OpUser}, &notificationFlag)\n\t\t} else {\n\t\t\tnotificationFlag = false\n\t\t\tg.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{Group: tips.Group, OpUser: tips.OpUser}, &notificationFlag)\n\t\t}\n\t}\n\n\tif groupNameFlag {\n\t\tg.notification.GroupInfoSetNameNotification(ctx, &sdkws.GroupInfoSetNameTips{Group: tips.Group, OpUser: tips.OpUser})\n\t}\n\n\t// if updatedData > 0, send the normal notification\n\tif normalFlag {\n\t\tg.notification.GroupInfoSetNotification(ctx, tips)\n\t}\n\n\tg.webhookAfterSetGroupInfoEx(ctx, &g.config.WebhooksConfig.AfterSetGroupInfoEx, req)\n\n\treturn &pbgroup.SetGroupInfoExResp{}, nil\n}\n\nfunc (g *groupServer) TransferGroupOwner(ctx context.Context, req *pbgroup.TransferGroupOwnerReq) (*pbgroup.TransferGroupOwnerResp, error) {\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif group.Status == constant.GroupStatusDismissed {\n\t\treturn nil, servererrs.ErrDismissedAlready.Wrap()\n\t}\n\n\tif req.OldOwnerUserID == req.NewOwnerUserID {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"OldOwnerUserID == NewOwnerUserID\")\n\t}\n\n\tmembers, err := g.db.FindGroupMembers(ctx, req.GroupID, []string{req.OldOwnerUserID, req.NewOwnerUserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmemberMap := datautil.SliceToMap(members, func(e *model.GroupMember) string { return e.UserID })\n\tif ids := datautil.Single([]string{req.OldOwnerUserID, req.NewOwnerUserID}, datautil.Keys(memberMap)); len(ids) > 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"user not in group \" + strings.Join(ids, \",\"))\n\t}\n\n\toldOwner := memberMap[req.OldOwnerUserID]\n\tif oldOwner == nil {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"OldOwnerUserID not in group \" + req.NewOwnerUserID)\n\t}\n\n\tnewOwner := memberMap[req.NewOwnerUserID]\n\tif newOwner == nil {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"NewOwnerUser not in group \" + req.NewOwnerUserID)\n\t}\n\n\tif !authverify.IsAdmin(ctx) {\n\t\tif !(mcontext.GetOpUserID(ctx) == oldOwner.UserID && oldOwner.RoleLevel == constant.GroupOwner) {\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"no permission transfer group owner\")\n\t\t}\n\t}\n\n\tif newOwner.MuteEndTime.After(time.Now()) {\n\t\tif _, err := g.CancelMuteGroupMember(ctx, &pbgroup.CancelMuteGroupMemberReq{\n\t\t\tGroupID: group.GroupID,\n\t\t\tUserID:  req.NewOwnerUserID}); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif err := g.db.TransferGroupOwner(ctx, req.GroupID, req.OldOwnerUserID, req.NewOwnerUserID, newOwner.RoleLevel); err != nil {\n\t\treturn nil, err\n\t}\n\n\tg.webhookAfterTransferGroupOwner(ctx, &g.config.WebhooksConfig.AfterTransferGroupOwner, req)\n\n\tg.notification.GroupOwnerTransferredNotification(ctx, req)\n\n\treturn &pbgroup.TransferGroupOwnerResp{}, nil\n}\n\nfunc (g *groupServer) GetGroups(ctx context.Context, req *pbgroup.GetGroupsReq) (*pbgroup.GetGroupsResp, error) {\n\tvar (\n\t\tgroup []*model.Group\n\t\terr   error\n\t)\n\tvar resp pbgroup.GetGroupsResp\n\tif req.GroupID != \"\" {\n\t\tgroup, err = g.db.FindGroup(ctx, []string{req.GroupID})\n\t\tresp.Total = uint32(len(group))\n\t} else {\n\t\tvar total int64\n\t\ttotal, group, err = g.db.SearchGroup(ctx, req.GroupName, req.Pagination)\n\t\tresp.Total = uint32(total)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgroupIDs := datautil.Slice(group, func(e *model.Group) string {\n\t\treturn e.GroupID\n\t})\n\n\townerMembers, err := g.db.FindGroupsOwner(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\townerMemberMap := datautil.SliceToMap(ownerMembers, func(e *model.GroupMember) string {\n\t\treturn e.GroupID\n\t})\n\tgroupMemberNumMap, err := g.db.MapGroupMemberNum(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp.Groups = datautil.Slice(group, func(group *model.Group) *pbgroup.CMSGroup {\n\t\tvar (\n\t\t\tuserID   string\n\t\t\tusername string\n\t\t)\n\t\tif member, ok := ownerMemberMap[group.GroupID]; ok {\n\t\t\tuserID = member.UserID\n\t\t\tusername = member.Nickname\n\t\t}\n\t\treturn convert.Db2PbCMSGroup(group, userID, username, groupMemberNumMap[group.GroupID])\n\t})\n\treturn &resp, nil\n}\n\nfunc (g *groupServer) GetGroupMembersCMS(ctx context.Context, req *pbgroup.GetGroupMembersCMSReq) (*pbgroup.GetGroupMembersCMSResp, error) {\n\tif err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, members, err := g.db.SearchGroupMember(ctx, req.UserName, req.GroupID, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar resp pbgroup.GetGroupMembersCMSResp\n\tresp.Total = uint32(total)\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\tresp.Members = datautil.Slice(members, func(e *model.GroupMember) *sdkws.GroupMemberFullInfo {\n\t\treturn convert.Db2PbGroupMember(e)\n\t})\n\treturn &resp, nil\n}\n\nfunc (g *groupServer) GetUserReqApplicationList(ctx context.Context, req *pbgroup.GetUserReqApplicationListReq) (*pbgroup.GetUserReqApplicationListResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tuser, err := g.userClient.GetUserInfo(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thandleResults := datautil.Slice(req.HandleResults, func(e int32) int {\n\t\treturn int(e)\n\t})\n\ttotal, requests, err := g.db.PageGroupRequestUser(ctx, req.UserID, req.GroupIDs, handleResults, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(requests) == 0 {\n\t\treturn &pbgroup.GetUserReqApplicationListResp{Total: uint32(total)}, nil\n\t}\n\tgroupIDs := datautil.Distinct(datautil.Slice(requests, func(e *model.GroupRequest) string {\n\t\treturn e.GroupID\n\t}))\n\tgroups, err := g.db.FindGroup(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupMap := datautil.SliceToMap(groups, func(e *model.Group) string {\n\t\treturn e.GroupID\n\t})\n\towners, err := g.db.FindGroupsOwner(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, owners...); err != nil {\n\t\treturn nil, err\n\t}\n\townerMap := datautil.SliceToMap(owners, func(e *model.GroupMember) string {\n\t\treturn e.GroupID\n\t})\n\tgroupMemberNum, err := g.db.MapGroupMemberNum(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetUserReqApplicationListResp{\n\t\tTotal: uint32(total),\n\t\tGroupRequests: datautil.Slice(requests, func(e *model.GroupRequest) *sdkws.GroupRequest {\n\t\t\tvar ownerUserID string\n\t\t\tif owner, ok := ownerMap[e.GroupID]; ok {\n\t\t\t\townerUserID = owner.UserID\n\t\t\t}\n\t\t\treturn convert.Db2PbGroupRequest(e, user, convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerUserID, groupMemberNum[e.GroupID]))\n\t\t}),\n\t}, nil\n}\n\nfunc (g *groupServer) DismissGroup(ctx context.Context, req *pbgroup.DismissGroupReq) (*pbgroup.DismissGroupResp, error) {\n\towner, err := g.db.TakeGroupOwner(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !authverify.IsAdmin(ctx) {\n\t\tif owner.UserID != mcontext.GetOpUserID(ctx) {\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"not group owner\")\n\t\t}\n\t}\n\tif err := g.PopulateGroupMember(ctx, owner); err != nil {\n\t\treturn nil, err\n\t}\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !req.DeleteMember && group.Status == constant.GroupStatusDismissed {\n\t\treturn nil, servererrs.ErrDismissedAlready.WrapMsg(\"group status is dismissed\")\n\t}\n\tif err := g.db.DismissGroup(ctx, req.GroupID, req.DeleteMember); err != nil {\n\t\treturn nil, err\n\t}\n\tif !req.DeleteMember {\n\t\tnum, err := g.db.FindGroupMemberNum(ctx, req.GroupID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tgroup.Status = constant.GroupStatusDismissed\n\t\ttips := &sdkws.GroupDismissedTips{\n\t\t\tGroup:  g.groupDB2PB(group, owner.UserID, num),\n\t\t\tOpUser: &sdkws.GroupMemberFullInfo{},\n\t\t}\n\t\tif mcontext.GetOpUserID(ctx) == owner.UserID {\n\t\t\ttips.OpUser = g.groupMemberDB2PB(owner, 0)\n\t\t}\n\t\tg.notification.GroupDismissedNotification(ctx, tips, req.SendMessage)\n\t}\n\tmembersID, err := g.db.FindGroupMemberUserID(ctx, group.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcbReq := &callbackstruct.CallbackDisMissGroupReq{\n\t\tGroupID:   req.GroupID,\n\t\tOwnerID:   owner.UserID,\n\t\tMembersID: membersID,\n\t\tGroupType: string(group.GroupType),\n\t}\n\n\tg.webhookAfterDismissGroup(ctx, &g.config.WebhooksConfig.AfterDismissGroup, cbReq)\n\n\treturn &pbgroup.DismissGroupResp{}, nil\n}\n\nfunc (g *groupServer) MuteGroupMember(ctx context.Context, req *pbgroup.MuteGroupMemberReq) (*pbgroup.MuteGroupMemberResp, error) {\n\tmember, err := g.db.TakeGroupMember(ctx, req.GroupID, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, member); err != nil {\n\t\treturn nil, err\n\t}\n\tif !authverify.IsAdmin(ctx) {\n\t\topMember, err := g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tswitch member.RoleLevel {\n\t\tcase constant.GroupOwner:\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"set group owner mute\")\n\t\tcase constant.GroupAdmin:\n\t\t\tif opMember.RoleLevel != constant.GroupOwner {\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"set group admin mute\")\n\t\t\t}\n\t\tcase constant.GroupOrdinaryUsers:\n\t\t\tif !(opMember.RoleLevel == constant.GroupAdmin || opMember.RoleLevel == constant.GroupOwner) {\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"set group ordinary users mute\")\n\t\t\t}\n\t\t}\n\t}\n\tdata := UpdateGroupMemberMutedTimeMap(time.Now().Add(time.Second * time.Duration(req.MutedSeconds)))\n\tif err := g.db.UpdateGroupMember(ctx, member.GroupID, member.UserID, data); err != nil {\n\t\treturn nil, err\n\t}\n\tg.notification.GroupMemberMutedNotification(ctx, req.GroupID, req.UserID, req.MutedSeconds)\n\treturn &pbgroup.MuteGroupMemberResp{}, nil\n}\n\nfunc (g *groupServer) CancelMuteGroupMember(ctx context.Context, req *pbgroup.CancelMuteGroupMemberReq) (*pbgroup.CancelMuteGroupMemberResp, error) {\n\tmember, err := g.db.TakeGroupMember(ctx, req.GroupID, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := g.PopulateGroupMember(ctx, member); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !authverify.IsAdmin(ctx) {\n\t\topMember, err := g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch member.RoleLevel {\n\t\tcase constant.GroupOwner:\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"Can not set group owner unmute\")\n\t\tcase constant.GroupAdmin:\n\t\t\tif opMember.RoleLevel != constant.GroupOwner {\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"Can not set group admin unmute\")\n\t\t\t}\n\t\tcase constant.GroupOrdinaryUsers:\n\t\t\tif !(opMember.RoleLevel == constant.GroupAdmin || opMember.RoleLevel == constant.GroupOwner) {\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"Can not set group ordinary users unmute\")\n\t\t\t}\n\t\t}\n\t}\n\n\tdata := UpdateGroupMemberMutedTimeMap(time.Unix(0, 0))\n\tif err := g.db.UpdateGroupMember(ctx, member.GroupID, member.UserID, data); err != nil {\n\t\treturn nil, err\n\t}\n\n\tg.notification.GroupMemberCancelMutedNotification(ctx, req.GroupID, req.UserID)\n\n\treturn &pbgroup.CancelMuteGroupMemberResp{}, nil\n}\n\nfunc (g *groupServer) MuteGroup(ctx context.Context, req *pbgroup.MuteGroupReq) (*pbgroup.MuteGroupResp, error) {\n\tif err := g.CheckGroupAdmin(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.db.UpdateGroup(ctx, req.GroupID, UpdateGroupStatusMap(constant.GroupStatusMuted)); err != nil {\n\t\treturn nil, err\n\t}\n\tg.notification.GroupMutedNotification(ctx, req.GroupID)\n\treturn &pbgroup.MuteGroupResp{}, nil\n}\n\nfunc (g *groupServer) CancelMuteGroup(ctx context.Context, req *pbgroup.CancelMuteGroupReq) (*pbgroup.CancelMuteGroupResp, error) {\n\tif err := g.CheckGroupAdmin(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.db.UpdateGroup(ctx, req.GroupID, UpdateGroupStatusMap(constant.GroupOk)); err != nil {\n\t\treturn nil, err\n\t}\n\tg.notification.GroupCancelMutedNotification(ctx, req.GroupID)\n\treturn &pbgroup.CancelMuteGroupResp{}, nil\n}\n\nfunc (g *groupServer) SetGroupMemberInfo(ctx context.Context, req *pbgroup.SetGroupMemberInfoReq) (*pbgroup.SetGroupMemberInfoResp, error) {\n\tif len(req.Members) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"members empty\")\n\t}\n\topUserID := mcontext.GetOpUserID(ctx)\n\tif opUserID == \"\" {\n\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"no op user id\")\n\t}\n\tisAppManagerUid := authverify.IsAdmin(ctx)\n\tgroupMembers := make(map[string][]*pbgroup.SetGroupMemberInfo)\n\tfor i, member := range req.Members {\n\t\tif member.RoleLevel != nil {\n\t\t\tswitch member.RoleLevel.Value {\n\t\t\tcase constant.GroupOwner:\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"cannot set ungroup owner\")\n\t\t\tcase constant.GroupAdmin, constant.GroupOrdinaryUsers:\n\t\t\tdefault:\n\t\t\t\treturn nil, errs.ErrArgs.WrapMsg(\"invalid role level\")\n\t\t\t}\n\t\t}\n\t\tgroupMembers[member.GroupID] = append(groupMembers[member.GroupID], req.Members[i])\n\t}\n\tfor groupID, members := range groupMembers {\n\t\ttemp := make(map[string]struct{})\n\t\tuserIDs := make([]string, 0, len(members)+1)\n\t\tfor _, member := range members {\n\t\t\tif _, ok := temp[member.UserID]; ok {\n\t\t\t\treturn nil, errs.ErrArgs.WrapMsg(fmt.Sprintf(\"repeat group %s user %s\", member.GroupID, member.UserID))\n\t\t\t}\n\t\t\ttemp[member.UserID] = struct{}{}\n\t\t\tuserIDs = append(userIDs, member.UserID)\n\t\t}\n\t\tif _, ok := temp[opUserID]; !ok {\n\t\t\tuserIDs = append(userIDs, opUserID)\n\t\t}\n\t\tdbMembers, err := g.db.FindGroupMembers(ctx, groupID, userIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\topUserIndex := -1\n\t\tfor i, member := range dbMembers {\n\t\t\tif member.UserID == opUserID {\n\t\t\t\topUserIndex = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tswitch len(userIDs) - len(dbMembers) {\n\t\tcase 0:\n\t\t\tif !isAppManagerUid {\n\t\t\t\troleLevel := dbMembers[opUserIndex].RoleLevel\n\t\t\t\tvar (\n\t\t\t\t\tdbSelf  = &model.GroupMember{}\n\t\t\t\t\treqSelf *pbgroup.SetGroupMemberInfo\n\t\t\t\t)\n\t\t\t\tswitch roleLevel {\n\t\t\t\tcase constant.GroupOwner:\n\t\t\t\t\tfor _, member := range dbMembers {\n\t\t\t\t\t\tif member.UserID == opUserID {\n\t\t\t\t\t\t\tdbSelf = member\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase constant.GroupAdmin:\n\t\t\t\t\tfor _, member := range dbMembers {\n\t\t\t\t\t\tif member.UserID == opUserID {\n\t\t\t\t\t\t\tdbSelf = member\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif member.RoleLevel == constant.GroupOwner {\n\t\t\t\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"admin can not change group owner\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif member.RoleLevel == constant.GroupAdmin && member.UserID != opUserID {\n\t\t\t\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"admin can not change other group admin\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase constant.GroupOrdinaryUsers:\n\t\t\t\t\tfor _, member := range dbMembers {\n\t\t\t\t\t\tif member.UserID == opUserID {\n\t\t\t\t\t\t\tdbSelf = member\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif !(member.RoleLevel == constant.GroupOrdinaryUsers && member.UserID == opUserID) {\n\t\t\t\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"ordinary users can not change other role level\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tfor _, member := range dbMembers {\n\t\t\t\t\t\tif member.UserID == opUserID {\n\t\t\t\t\t\t\tdbSelf = member\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif member.RoleLevel >= roleLevel {\n\t\t\t\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"can not change higher role level\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor _, member := range req.Members {\n\t\t\t\t\tif member.UserID == opUserID {\n\t\t\t\t\t\treqSelf = member\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif reqSelf != nil && reqSelf.RoleLevel != nil {\n\t\t\t\t\tif reqSelf.RoleLevel.GetValue() > dbSelf.RoleLevel {\n\t\t\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"can not improve role level by self\")\n\t\t\t\t\t}\n\t\t\t\t\tif roleLevel == constant.GroupOwner {\n\t\t\t\t\t\treturn nil, errs.ErrArgs.WrapMsg(\"group owner can not change own role level\") // Prevent the absence of a group owner\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase 1:\n\t\t\tif opUserIndex >= 0 {\n\t\t\t\treturn nil, errs.ErrArgs.WrapMsg(\"user not in group\")\n\t\t\t}\n\t\t\tif !isAppManagerUid {\n\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"user not in group\")\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, errs.ErrArgs.WrapMsg(\"user not in group\")\n\t\t}\n\t}\n\n\tfor i := 0; i < len(req.Members); i++ {\n\n\t\tif err := g.webhookBeforeSetGroupMemberInfo(ctx, &g.config.WebhooksConfig.BeforeSetGroupMemberInfo, req.Members[i]); err != nil && err != servererrs.ErrCallbackContinue {\n\t\t\treturn nil, err\n\t\t}\n\n\t}\n\tif err := g.db.UpdateGroupMembers(ctx, datautil.Slice(req.Members, func(e *pbgroup.SetGroupMemberInfo) *common.BatchUpdateGroupMember {\n\t\treturn &common.BatchUpdateGroupMember{\n\t\t\tGroupID: e.GroupID,\n\t\t\tUserID:  e.UserID,\n\t\t\tMap:     UpdateGroupMemberMap(e),\n\t\t}\n\t})); err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, member := range req.Members {\n\t\tif member.RoleLevel != nil {\n\t\t\tswitch member.RoleLevel.Value {\n\t\t\tcase constant.GroupAdmin:\n\t\t\t\tg.notification.GroupMemberSetToAdminNotification(ctx, member.GroupID, member.UserID)\n\t\t\tcase constant.GroupOrdinaryUsers:\n\t\t\t\tg.notification.GroupMemberSetToOrdinaryUserNotification(ctx, member.GroupID, member.UserID)\n\t\t\t}\n\t\t}\n\t\tif member.Nickname != nil || member.FaceURL != nil || member.Ex != nil {\n\t\t\tg.notification.GroupMemberInfoSetNotification(ctx, member.GroupID, member.UserID)\n\t\t}\n\t}\n\tfor i := 0; i < len(req.Members); i++ {\n\t\tg.webhookAfterSetGroupMemberInfo(ctx, &g.config.WebhooksConfig.AfterSetGroupMemberInfo, req.Members[i])\n\t}\n\n\treturn &pbgroup.SetGroupMemberInfoResp{}, nil\n}\n\nfunc (g *groupServer) GetGroupAbstractInfo(ctx context.Context, req *pbgroup.GetGroupAbstractInfoReq) (*pbgroup.GetGroupAbstractInfoResp, error) {\n\tif len(req.GroupIDs) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"groupIDs empty\")\n\t}\n\tif datautil.Duplicate(req.GroupIDs) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"groupIDs duplicate\")\n\t}\n\tfor _, groupID := range req.GroupIDs {\n\t\tif err := g.checkAdminOrInGroup(ctx, groupID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tgroups, err := g.db.FindGroup(ctx, req.GroupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif ids := datautil.Single(req.GroupIDs, datautil.Slice(groups, func(group *model.Group) string {\n\t\treturn group.GroupID\n\t})); len(ids) > 0 {\n\t\treturn nil, servererrs.ErrGroupIDNotFound.WrapMsg(\"not found group \" + strings.Join(ids, \",\"))\n\t}\n\tgroupUserMap, err := g.db.MapGroupMemberUserID(ctx, req.GroupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif ids := datautil.Single(req.GroupIDs, datautil.Keys(groupUserMap)); len(ids) > 0 {\n\t\treturn nil, servererrs.ErrGroupIDNotFound.WrapMsg(fmt.Sprintf(\"group %s not found member\", strings.Join(ids, \",\")))\n\t}\n\treturn &pbgroup.GetGroupAbstractInfoResp{\n\t\tGroupAbstractInfos: datautil.Slice(groups, func(group *model.Group) *pbgroup.GroupAbstractInfo {\n\t\t\tusers := groupUserMap[group.GroupID]\n\t\t\treturn convert.Db2PbGroupAbstractInfo(group.GroupID, users.MemberNum, users.Hash)\n\t\t}),\n\t}, nil\n}\n\nfunc (g *groupServer) GetUserInGroupMembers(ctx context.Context, req *pbgroup.GetUserInGroupMembersReq) (*pbgroup.GetUserInGroupMembersResp, error) {\n\tif len(req.GroupIDs) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"groupIDs empty\")\n\t}\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tmembers, err := g.db.FindGroupMemberUser(ctx, req.GroupIDs, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetUserInGroupMembersResp{\n\t\tMembers: datautil.Slice(members, func(e *model.GroupMember) *sdkws.GroupMemberFullInfo {\n\t\t\treturn convert.Db2PbGroupMember(e)\n\t\t}),\n\t}, nil\n}\n\nfunc (g *groupServer) GetGroupMemberUserIDs(ctx context.Context, req *pbgroup.GetGroupMemberUserIDsReq) (*pbgroup.GetGroupMemberUserIDsResp, error) {\n\tuserIDs, err := g.db.FindGroupMemberUserID(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := authverify.CheckAccessIn(ctx, userIDs...); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetGroupMemberUserIDsResp{\n\t\tUserIDs: userIDs,\n\t}, nil\n}\n\nfunc (g *groupServer) GetGroupMemberRoleLevel(ctx context.Context, req *pbgroup.GetGroupMemberRoleLevelReq) (*pbgroup.GetGroupMemberRoleLevelResp, error) {\n\tif len(req.RoleLevels) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"RoleLevels empty\")\n\t}\n\tif err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\tmembers, err := g.db.FindGroupMemberRoleLevels(ctx, req.GroupID, req.RoleLevels)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbgroup.GetGroupMemberRoleLevelResp{\n\t\tMembers: datautil.Slice(members, func(e *model.GroupMember) *sdkws.GroupMemberFullInfo {\n\t\t\treturn convert.Db2PbGroupMember(e)\n\t\t}),\n\t}, nil\n}\n\nfunc (g *groupServer) GetGroupUsersReqApplicationList(ctx context.Context, req *pbgroup.GetGroupUsersReqApplicationListReq) (*pbgroup.GetGroupUsersReqApplicationListResp, error) {\n\tif err := g.CheckGroupAdmin(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\trequests, err := g.db.FindGroupRequests(ctx, req.GroupID, req.UserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(requests) == 0 {\n\t\treturn &pbgroup.GetGroupUsersReqApplicationListResp{}, nil\n\t}\n\n\tgroupIDs := datautil.Distinct(datautil.Slice(requests, func(e *model.GroupRequest) string {\n\t\treturn e.GroupID\n\t}))\n\n\tgroups, err := g.db.FindGroup(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgroupMap := datautil.SliceToMap(groups, func(e *model.Group) string {\n\t\treturn e.GroupID\n\t})\n\n\tif ids := datautil.Single(groupIDs, datautil.Keys(groupMap)); len(ids) > 0 {\n\t\treturn nil, servererrs.ErrGroupIDNotFound.WrapMsg(strings.Join(ids, \",\"))\n\t}\n\n\tuserMap, err := g.userClient.GetUsersInfoMap(ctx, req.UserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\towners, err := g.db.FindGroupsOwner(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := g.PopulateGroupMember(ctx, owners...); err != nil {\n\t\treturn nil, err\n\t}\n\n\townerMap := datautil.SliceToMap(owners, func(e *model.GroupMember) string {\n\t\treturn e.GroupID\n\t})\n\n\tgroupMemberNum, err := g.db.MapGroupMemberNum(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &pbgroup.GetGroupUsersReqApplicationListResp{\n\t\tTotal: int64(len(requests)),\n\t\tGroupRequests: datautil.Slice(requests, func(e *model.GroupRequest) *sdkws.GroupRequest {\n\t\t\tvar ownerUserID string\n\t\t\tif owner, ok := ownerMap[e.GroupID]; ok {\n\t\t\t\townerUserID = owner.UserID\n\t\t\t}\n\n\t\t\tvar userInfo *sdkws.UserInfo\n\t\t\tif user, ok := userMap[e.UserID]; !ok {\n\t\t\t\tuserInfo = user\n\t\t\t}\n\n\t\t\treturn convert.Db2PbGroupRequest(e, userInfo, convert.Db2PbGroupInfo(groupMap[e.GroupID], ownerUserID, groupMemberNum[e.GroupID]))\n\t\t}),\n\t}, nil\n}\n\nfunc (g *groupServer) GetSpecifiedUserGroupRequestInfo(ctx context.Context, req *pbgroup.GetSpecifiedUserGroupRequestInfoReq) (*pbgroup.GetSpecifiedUserGroupRequestInfoResp, error) {\n\topUserID := mcontext.GetOpUserID(ctx)\n\n\towners, err := g.db.FindGroupsOwner(ctx, []string{req.GroupID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif req.UserID != opUserID {\n\t\tadminIDs, err := g.db.GetGroupRoleLevelMemberIDs(ctx, req.GroupID, constant.GroupAdmin)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tadminIDs = append(adminIDs, owners[0].UserID)\n\t\tadminIDs = append(adminIDs, g.adminUserIDs...)\n\n\t\tif !datautil.Contain(opUserID, adminIDs...) {\n\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"opUser no permission\")\n\t\t}\n\t}\n\n\trequests, err := g.db.FindGroupRequests(ctx, req.GroupID, []string{req.UserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(requests) == 0 {\n\t\treturn &pbgroup.GetSpecifiedUserGroupRequestInfoResp{}, nil\n\t}\n\n\tgroups, err := g.db.FindGroup(ctx, []string{req.GroupID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserInfos, err := g.userClient.GetUsersInfo(ctx, []string{req.UserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgroupMemberNum, err := g.db.MapGroupMemberNum(ctx, []string{req.GroupID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp := &pbgroup.GetSpecifiedUserGroupRequestInfoResp{\n\t\tGroupRequests: make([]*sdkws.GroupRequest, 0, len(requests)),\n\t}\n\n\tfor _, request := range requests {\n\t\tresp.GroupRequests = append(resp.GroupRequests, convert.Db2PbGroupRequest(request, userInfos[0], convert.Db2PbGroupInfo(groups[0], owners[0].UserID, groupMemberNum[groups[0].GroupID])))\n\t}\n\n\tresp.Total = uint32(len(requests))\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/rpc/group/notification.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage group\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/versionctx\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification/common_user\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbgroup \"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/stringutil\"\n)\n\n// GroupApplicationReceiver\nconst (\n\tapplicantReceiver = iota\n\tadminReceiver\n)\n\nfunc NewNotificationSender(db controller.GroupDatabase, config *Config, userClient *rpcli.UserClient, msgClient *rpcli.MsgClient, conversationClient *rpcli.ConversationClient) *NotificationSender {\n\treturn &NotificationSender{\n\t\tNotificationSender: notification.NewNotificationSender(&config.NotificationConfig,\n\t\t\tnotification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {\n\t\t\t\treturn msgClient.SendMsg(ctx, req)\n\t\t\t}),\n\t\t\tnotification.WithUserRpcClient(userClient.GetUserInfo),\n\t\t),\n\t\tgetUsersInfo: func(ctx context.Context, userIDs []string) ([]common_user.CommonUser, error) {\n\t\t\tusers, err := userClient.GetUsersInfo(ctx, userIDs)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn datautil.Slice(users, func(e *sdkws.UserInfo) common_user.CommonUser { return e }), nil\n\t\t},\n\t\tdb:                 db,\n\t\tconfig:             config,\n\t\tmsgClient:          msgClient,\n\t\tconversationClient: conversationClient,\n\t}\n}\n\ntype NotificationSender struct {\n\t*notification.NotificationSender\n\tgetUsersInfo       func(ctx context.Context, userIDs []string) ([]common_user.CommonUser, error)\n\tdb                 controller.GroupDatabase\n\tconfig             *Config\n\tmsgClient          *rpcli.MsgClient\n\tconversationClient *rpcli.ConversationClient\n}\n\nfunc (g *NotificationSender) PopulateGroupMember(ctx context.Context, members ...*model.GroupMember) error {\n\tif len(members) == 0 {\n\t\treturn nil\n\t}\n\temptyUserIDs := make(map[string]struct{})\n\tfor _, member := range members {\n\t\tif member.Nickname == \"\" || member.FaceURL == \"\" {\n\t\t\temptyUserIDs[member.UserID] = struct{}{}\n\t\t}\n\t}\n\tif len(emptyUserIDs) > 0 {\n\t\tusers, err := g.getUsersInfo(ctx, datautil.Keys(emptyUserIDs))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuserMap := make(map[string]common_user.CommonUser)\n\t\tfor i, user := range users {\n\t\t\tuserMap[user.GetUserID()] = users[i]\n\t\t}\n\t\tfor i, member := range members {\n\t\t\tuser, ok := userMap[member.UserID]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif member.Nickname == \"\" {\n\t\t\t\tmembers[i].Nickname = user.GetNickname()\n\t\t\t}\n\t\t\tif member.FaceURL == \"\" {\n\t\t\t\tmembers[i].FaceURL = user.GetFaceURL()\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (g *NotificationSender) getUser(ctx context.Context, userID string) (*sdkws.PublicUserInfo, error) {\n\tusers, err := g.getUsersInfo(ctx, []string{userID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(users) == 0 {\n\t\treturn nil, servererrs.ErrUserIDNotFound.WrapMsg(fmt.Sprintf(\"user %s not found\", userID))\n\t}\n\treturn &sdkws.PublicUserInfo{\n\t\tUserID:   users[0].GetUserID(),\n\t\tNickname: users[0].GetNickname(),\n\t\tFaceURL:  users[0].GetFaceURL(),\n\t\tEx:       users[0].GetEx(),\n\t}, nil\n}\n\nfunc (g *NotificationSender) getGroupInfo(ctx context.Context, groupID string) (*sdkws.GroupInfo, error) {\n\tgm, err := g.db.TakeGroup(ctx, groupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnum, err := g.db.FindGroupMemberNum(ctx, groupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\townerUserIDs, err := g.db.GetGroupRoleLevelMemberIDs(ctx, groupID, constant.GroupOwner)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar ownerUserID string\n\tif len(ownerUserIDs) > 0 {\n\t\townerUserID = ownerUserIDs[0]\n\t}\n\n\treturn convert.Db2PbGroupInfo(gm, ownerUserID, num), nil\n}\n\nfunc (g *NotificationSender) getGroupMembers(ctx context.Context, groupID string, userIDs []string) ([]*sdkws.GroupMemberFullInfo, error) {\n\tmembers, err := g.db.FindGroupMembers(ctx, groupID, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\tlog.ZDebug(ctx, \"getGroupMembers\", \"members\", members)\n\tres := make([]*sdkws.GroupMemberFullInfo, 0, len(members))\n\tfor _, member := range members {\n\t\tres = append(res, g.groupMemberDB2PB(member, 0))\n\t}\n\treturn res, nil\n}\n\nfunc (g *NotificationSender) getGroupMemberMap(ctx context.Context, groupID string, userIDs []string) (map[string]*sdkws.GroupMemberFullInfo, error) {\n\tmembers, err := g.getGroupMembers(ctx, groupID, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tm := make(map[string]*sdkws.GroupMemberFullInfo)\n\tfor i, member := range members {\n\t\tm[member.UserID] = members[i]\n\t}\n\treturn m, nil\n}\n\nfunc (g *NotificationSender) getGroupMember(ctx context.Context, groupID string, userID string) (*sdkws.GroupMemberFullInfo, error) {\n\tmembers, err := g.getGroupMembers(ctx, groupID, []string{userID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(members) == 0 {\n\t\treturn nil, errs.ErrInternalServer.WrapMsg(fmt.Sprintf(\"group %s member %s not found\", groupID, userID))\n\t}\n\treturn members[0], nil\n}\n\nfunc (g *NotificationSender) getGroupOwnerAndAdminUserID(ctx context.Context, groupID string) ([]string, error) {\n\tmembers, err := g.db.FindGroupMemberRoleLevels(ctx, groupID, []int32{constant.GroupOwner, constant.GroupAdmin})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := g.PopulateGroupMember(ctx, members...); err != nil {\n\t\treturn nil, err\n\t}\n\tfn := func(e *model.GroupMember) string { return e.UserID }\n\treturn datautil.Slice(members, fn), nil\n}\n\nfunc (g *NotificationSender) groupMemberDB2PB(member *model.GroupMember, appMangerLevel int32) *sdkws.GroupMemberFullInfo {\n\treturn &sdkws.GroupMemberFullInfo{\n\t\tGroupID:        member.GroupID,\n\t\tUserID:         member.UserID,\n\t\tRoleLevel:      member.RoleLevel,\n\t\tJoinTime:       member.JoinTime.UnixMilli(),\n\t\tNickname:       member.Nickname,\n\t\tFaceURL:        member.FaceURL,\n\t\tAppMangerLevel: appMangerLevel,\n\t\tJoinSource:     member.JoinSource,\n\t\tOperatorUserID: member.OperatorUserID,\n\t\tEx:             member.Ex,\n\t\tMuteEndTime:    member.MuteEndTime.UnixMilli(),\n\t\tInviterUserID:  member.InviterUserID,\n\t}\n}\n\n/* func (g *NotificationSender) getUsersInfoMap(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error) {\n\tusers, err := g.getUsersInfo(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make(map[string]*sdkws.UserInfo)\n\tfor _, user := range users {\n\t\tresult[user.GetUserID()] = user.(*sdkws.UserInfo)\n\t}\n\treturn result, nil\n} */\n\nfunc (g *NotificationSender) fillOpUser(ctx context.Context, targetUser **sdkws.GroupMemberFullInfo, groupID string) (err error) {\n\treturn g.fillUserByUserID(ctx, mcontext.GetOpUserID(ctx), targetUser, groupID)\n}\n\nfunc (g *NotificationSender) fillUserByUserID(ctx context.Context, userID string, targetUser **sdkws.GroupMemberFullInfo, groupID string) error {\n\tif targetUser == nil {\n\t\treturn errs.ErrInternalServer.WrapMsg(\"**sdkws.GroupMemberFullInfo is nil\")\n\t}\n\tif groupID != \"\" {\n\t\tif authverify.CheckUserIsAdmin(ctx, userID) {\n\t\t\t*targetUser = &sdkws.GroupMemberFullInfo{\n\t\t\t\tGroupID:        groupID,\n\t\t\t\tUserID:         userID,\n\t\t\t\tRoleLevel:      constant.GroupAdmin,\n\t\t\t\tAppMangerLevel: constant.AppAdmin,\n\t\t\t}\n\t\t} else {\n\t\t\tmember, err := g.db.TakeGroupMember(ctx, groupID, userID)\n\t\t\tif err == nil {\n\t\t\t\t*targetUser = g.groupMemberDB2PB(member, 0)\n\t\t\t} else if !(errors.Is(err, mongo.ErrNoDocuments) || errs.ErrRecordNotFound.Is(err)) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tuser, err := g.getUser(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif *targetUser == nil {\n\t\t*targetUser = &sdkws.GroupMemberFullInfo{\n\t\t\tGroupID:        groupID,\n\t\t\tUserID:         userID,\n\t\t\tNickname:       user.Nickname,\n\t\t\tFaceURL:        user.FaceURL,\n\t\t\tOperatorUserID: userID,\n\t\t}\n\t} else {\n\t\tif (*targetUser).Nickname == \"\" {\n\t\t\t(*targetUser).Nickname = user.Nickname\n\t\t}\n\t\tif (*targetUser).FaceURL == \"\" {\n\t\t\t(*targetUser).FaceURL = user.FaceURL\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (g *NotificationSender) setVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string) {\n\tversions := versionctx.GetVersionLog(ctx).Get()\n\tfor i := len(versions) - 1; i >= 0; i-- {\n\t\tcoll := versions[i]\n\t\tif coll.Name == collName && coll.Doc.DID == id {\n\t\t\t*version = uint64(coll.Doc.Version)\n\t\t\t*versionID = coll.Doc.ID.Hex()\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (g *NotificationSender) setSortVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string, sortVersion *uint64) {\n\tversions := versionctx.GetVersionLog(ctx).Get()\n\tfor _, coll := range versions {\n\t\tif coll.Name == collName && coll.Doc.DID == id {\n\t\t\t*version = uint64(coll.Doc.Version)\n\t\t\t*versionID = coll.Doc.ID.Hex()\n\t\t\tfor _, elem := range coll.Doc.Logs {\n\t\t\t\tif elem.EID == model.VersionSortChangeID {\n\t\t\t\t\t*sortVersion = uint64(elem.Version)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (g *NotificationSender) GroupCreatedNotification(ctx context.Context, tips *sdkws.GroupCreatedTips, SendMessage *bool) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupCreatedNotification, tips, notification.WithSendMessage(SendMessage))\n}\n\nfunc (g *NotificationSender) GroupInfoSetNotification(ctx context.Context, tips *sdkws.GroupInfoSetTips) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetNotification, tips, notification.WithRpcGetUserName())\n}\n\nfunc (g *NotificationSender) GroupInfoSetNameNotification(ctx context.Context, tips *sdkws.GroupInfoSetNameTips) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetNameNotification, tips)\n}\n\nfunc (g *NotificationSender) GroupInfoSetAnnouncementNotification(ctx context.Context, tips *sdkws.GroupInfoSetAnnouncementTips, sendMessage *bool) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetAnnouncementNotification, tips, notification.WithRpcGetUserName(), notification.WithSendMessage(sendMessage))\n}\n\nfunc (g *NotificationSender) uuid() string {\n\treturn uuid.New().String()\n}\n\nfunc (g *NotificationSender) getGroupRequest(ctx context.Context, groupID string, userID string) (*sdkws.GroupRequest, error) {\n\trequest, err := g.db.TakeGroupRequest(ctx, groupID, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tusers, err := g.getUsersInfo(ctx, []string{userID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(users) == 0 {\n\t\treturn nil, servererrs.ErrUserIDNotFound.WrapMsg(fmt.Sprintf(\"user %s not found\", userID))\n\t}\n\tinfo, ok := users[0].(*sdkws.UserInfo)\n\tif !ok {\n\t\tinfo = &sdkws.UserInfo{\n\t\t\tUserID:   users[0].GetUserID(),\n\t\t\tNickname: users[0].GetNickname(),\n\t\t\tFaceURL:  users[0].GetFaceURL(),\n\t\t\tEx:       users[0].GetEx(),\n\t\t}\n\t}\n\treturn convert.Db2PbGroupRequest(request, info, nil), nil\n}\n\nfunc (g *NotificationSender) JoinGroupApplicationNotification(ctx context.Context, req *pbgroup.JoinGroupReq, dbReq *model.GroupRequest) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\trequest, err := g.getGroupRequest(ctx, dbReq.GroupID, dbReq.UserID)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"JoinGroupApplicationNotification getGroupRequest\", err, \"dbReq\", dbReq)\n\t\treturn\n\t}\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar user *sdkws.PublicUserInfo\n\tuser, err = g.getUser(ctx, req.InviterUserID)\n\tif err != nil {\n\t\treturn\n\t}\n\tuserIDs, err := g.getGroupOwnerAndAdminUserID(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tuserIDs = append(userIDs, req.InviterUserID, mcontext.GetOpUserID(ctx))\n\ttips := &sdkws.JoinGroupApplicationTips{\n\t\tGroup:     group,\n\t\tApplicant: user,\n\t\tReqMsg:    req.ReqMessage,\n\t\tUuid:      g.uuid(),\n\t\tRequest:   request,\n\t}\n\tfor _, userID := range datautil.Distinct(userIDs) {\n\t\tg.Notification(ctx, mcontext.GetOpUserID(ctx), userID, constant.JoinGroupApplicationNotification, tips)\n\t}\n}\n\nfunc (g *NotificationSender) MemberQuitNotification(ctx context.Context, member *sdkws.GroupMemberFullInfo) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, member.GroupID)\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.MemberQuitTips{Group: group, QuitUser: member}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, member.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), member.GroupID, constant.MemberQuitNotification, tips)\n}\n\nfunc (g *NotificationSender) GroupApplicationAcceptedNotification(ctx context.Context, req *pbgroup.GroupApplicationResponseReq) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\trequest, err := g.getGroupRequest(ctx, req.GroupID, req.FromUserID)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"GroupApplicationAcceptedNotification getGroupRequest\", err, \"req\", req)\n\t\treturn\n\t}\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar userIDs []string\n\tuserIDs, err = g.getGroupOwnerAndAdminUserID(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar opUser *sdkws.GroupMemberFullInfo\n\tif err = g.fillOpUser(ctx, &opUser, group.GroupID); err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupApplicationAcceptedTips{\n\t\tGroup:     group,\n\t\tOpUser:    opUser,\n\t\tHandleMsg: req.HandledMsg,\n\t\tUuid:      g.uuid(),\n\t\tRequest:   request,\n\t}\n\tfor _, userID := range append(userIDs, req.FromUserID) {\n\t\tif userID == req.FromUserID {\n\t\t\ttips.ReceiverAs = applicantReceiver\n\t\t} else {\n\t\t\ttips.ReceiverAs = adminReceiver\n\t\t}\n\t\tg.Notification(ctx, mcontext.GetOpUserID(ctx), userID, constant.GroupApplicationAcceptedNotification, tips)\n\t}\n}\n\nfunc (g *NotificationSender) GroupApplicationRejectedNotification(ctx context.Context, req *pbgroup.GroupApplicationResponseReq) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\trequest, err := g.getGroupRequest(ctx, req.GroupID, req.FromUserID)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"GroupApplicationAcceptedNotification getGroupRequest\", err, \"req\", req)\n\t\treturn\n\t}\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar userIDs []string\n\tuserIDs, err = g.getGroupOwnerAndAdminUserID(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar opUser *sdkws.GroupMemberFullInfo\n\tif err = g.fillOpUser(ctx, &opUser, group.GroupID); err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupApplicationRejectedTips{\n\t\tGroup:     group,\n\t\tOpUser:    opUser,\n\t\tHandleMsg: req.HandledMsg,\n\t\tUuid:      g.uuid(),\n\t\tRequest:   request,\n\t}\n\tfor _, userID := range append(userIDs, req.FromUserID) {\n\t\tif userID == req.FromUserID {\n\t\t\ttips.ReceiverAs = applicantReceiver\n\t\t} else {\n\t\t\ttips.ReceiverAs = adminReceiver\n\t\t}\n\t\tg.Notification(ctx, mcontext.GetOpUserID(ctx), userID, constant.GroupApplicationRejectedNotification, tips)\n\t}\n}\n\nfunc (g *NotificationSender) GroupOwnerTransferredNotification(ctx context.Context, req *pbgroup.TransferGroupOwnerReq) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn\n\t}\n\topUserID := mcontext.GetOpUserID(ctx)\n\tvar member map[string]*sdkws.GroupMemberFullInfo\n\tmember, err = g.getGroupMemberMap(ctx, req.GroupID, []string{opUserID, req.NewOwnerUserID, req.OldOwnerUserID})\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupOwnerTransferredTips{\n\t\tGroup:             group,\n\t\tOpUser:            member[opUserID],\n\t\tNewGroupOwner:     member[req.NewOwnerUserID],\n\t\tOldGroupOwnerInfo: member[req.OldOwnerUserID],\n\t}\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, req.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupOwnerTransferredNotification, tips)\n}\n\nfunc (g *NotificationSender) MemberKickedNotification(ctx context.Context, tips *sdkws.MemberKickedTips, SendMessage *bool) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.MemberKickedNotification, tips, notification.WithSendMessage(SendMessage))\n}\n\nfunc (g *NotificationSender) GroupApplicationAgreeMemberEnterNotification(ctx context.Context, groupID string, SendMessage *bool, invitedOpUserID string, entrantUserID ...string) error {\n\treturn g.groupApplicationAgreeMemberEnterNotification(ctx, groupID, SendMessage, invitedOpUserID, entrantUserID...)\n}\n\nfunc (g *NotificationSender) groupApplicationAgreeMemberEnterNotification(ctx context.Context, groupID string, SendMessage *bool, invitedOpUserID string, entrantUserID ...string) error {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\n\tif !g.config.RpcConfig.EnableHistoryForNewMembers {\n\t\tconversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID)\n\t\tmaxSeq, err := g.msgClient.GetConversationMaxSeq(ctx, conversationID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := g.msgClient.SetUserConversationsMinSeq(ctx, conversationID, entrantUserID, maxSeq+1); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := g.conversationClient.CreateGroupChatConversations(ctx, groupID, entrantUserID); err != nil {\n\t\treturn err\n\t}\n\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tusers, err := g.getGroupMembers(ctx, groupID, entrantUserID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttips := &sdkws.MemberInvitedTips{\n\t\tGroup:           group,\n\t\tInvitedUserList: users,\n\t}\n\topUserID := mcontext.GetOpUserID(ctx)\n\tif err = g.fillUserByUserID(ctx, opUserID, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn nil\n\t}\n\tif invitedOpUserID == opUserID {\n\t\ttips.InviterUser = tips.OpUser\n\t} else {\n\t\tif err = g.fillUserByUserID(ctx, invitedOpUserID, &tips.InviterUser, tips.Group.GroupID); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.MemberInvitedNotification, tips, notification.WithSendMessage(SendMessage))\n\treturn nil\n}\n\nfunc (g *NotificationSender) MemberEnterNotification(ctx context.Context, groupID string, entrantUserID string) error {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\n\tif !g.config.RpcConfig.EnableHistoryForNewMembers {\n\t\tconversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID)\n\t\tmaxSeq, err := g.msgClient.GetConversationMaxSeq(ctx, conversationID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := g.msgClient.SetUserConversationsMinSeq(ctx, conversationID, []string{entrantUserID}, maxSeq+1); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := g.conversationClient.CreateGroupChatConversations(ctx, groupID, []string{entrantUserID}); err != nil {\n\t\treturn err\n\t}\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuser, err := g.getGroupMember(ctx, groupID, entrantUserID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttips := &sdkws.MemberEnterTips{\n\t\tGroup:         group,\n\t\tEntrantUser:   user,\n\t\tOperationTime: time.Now().UnixMilli(),\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.MemberEnterNotification, tips)\n\treturn nil\n}\n\nfunc (g *NotificationSender) GroupDismissedNotification(ctx context.Context, tips *sdkws.GroupDismissedTips, SendMessage *bool) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupDismissedNotification, tips, notification.WithSendMessage(SendMessage))\n}\n\nfunc (g *NotificationSender) GroupMemberMutedNotification(ctx context.Context, groupID, groupMemberUserID string, mutedSeconds uint32) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar user map[string]*sdkws.GroupMemberFullInfo\n\tuser, err = g.getGroupMemberMap(ctx, groupID, []string{mcontext.GetOpUserID(ctx), groupMemberUserID})\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupMemberMutedTips{\n\t\tGroup: group, MutedSeconds: mutedSeconds,\n\t\tOpUser: user[mcontext.GetOpUserID(ctx)], MutedUser: user[groupMemberUserID],\n\t}\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberMutedNotification, tips)\n}\n\nfunc (g *NotificationSender) GroupMemberCancelMutedNotification(ctx context.Context, groupID, groupMemberUserID string) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar user map[string]*sdkws.GroupMemberFullInfo\n\tuser, err = g.getGroupMemberMap(ctx, groupID, []string{mcontext.GetOpUserID(ctx), groupMemberUserID})\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupMemberCancelMutedTips{Group: group, OpUser: user[mcontext.GetOpUserID(ctx)], MutedUser: user[groupMemberUserID]}\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberCancelMutedNotification, tips)\n}\n\nfunc (g *NotificationSender) GroupMutedNotification(ctx context.Context, groupID string) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar users []*sdkws.GroupMemberFullInfo\n\tusers, err = g.getGroupMembers(ctx, groupID, []string{mcontext.GetOpUserID(ctx)})\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupMutedTips{Group: group}\n\tif len(users) > 0 {\n\t\ttips.OpUser = users[0]\n\t}\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, groupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMutedNotification, tips)\n}\n\nfunc (g *NotificationSender) GroupCancelMutedNotification(ctx context.Context, groupID string) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar users []*sdkws.GroupMemberFullInfo\n\tusers, err = g.getGroupMembers(ctx, groupID, []string{mcontext.GetOpUserID(ctx)})\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupCancelMutedTips{Group: group}\n\tif len(users) > 0 {\n\t\ttips.OpUser = users[0]\n\t}\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, groupID)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupCancelMutedNotification, tips)\n}\n\nfunc (g *NotificationSender) GroupMemberInfoSetNotification(ctx context.Context, groupID, groupMemberUserID string) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar user map[string]*sdkws.GroupMemberFullInfo\n\tuser, err = g.getGroupMemberMap(ctx, groupID, []string{groupMemberUserID})\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupMemberInfoSetTips{Group: group, OpUser: user[mcontext.GetOpUserID(ctx)], ChangedUser: user[groupMemberUserID]}\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setSortVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID, &tips.GroupSortVersion)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberInfoSetNotification, tips)\n}\n\nfunc (g *NotificationSender) GroupMemberSetToAdminNotification(ctx context.Context, groupID, groupMemberUserID string) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tuser, err := g.getGroupMemberMap(ctx, groupID, []string{mcontext.GetOpUserID(ctx), groupMemberUserID})\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupMemberInfoSetTips{Group: group, OpUser: user[mcontext.GetOpUserID(ctx)], ChangedUser: user[groupMemberUserID]}\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setSortVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID, &tips.GroupSortVersion)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberSetToAdminNotification, tips)\n}\n\nfunc (g *NotificationSender) GroupMemberSetToOrdinaryUserNotification(ctx context.Context, groupID, groupMemberUserID string) {\n\tvar err error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, stringutil.GetFuncName(1)+\" failed\", err)\n\t\t}\n\t}()\n\tvar group *sdkws.GroupInfo\n\tgroup, err = g.getGroupInfo(ctx, groupID)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar user map[string]*sdkws.GroupMemberFullInfo\n\tuser, err = g.getGroupMemberMap(ctx, groupID, []string{mcontext.GetOpUserID(ctx), groupMemberUserID})\n\tif err != nil {\n\t\treturn\n\t}\n\ttips := &sdkws.GroupMemberInfoSetTips{Group: group, OpUser: user[mcontext.GetOpUserID(ctx)], ChangedUser: user[groupMemberUserID]}\n\tif err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {\n\t\treturn\n\t}\n\tg.setSortVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID, &tips.GroupSortVersion)\n\tg.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberSetToOrdinaryUserNotification, tips)\n}\n"
  },
  {
    "path": "internal/rpc/group/statistics.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage group\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nfunc (g *groupServer) GroupCreateCount(ctx context.Context, req *group.GroupCreateCountReq) (*group.GroupCreateCountResp, error) {\n\tif req.Start > req.End {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"start > end: %d > %d\", req.Start, req.End)\n\t}\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, err := g.db.CountTotal(ctx, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstart := time.UnixMilli(req.Start)\n\tbefore, err := g.db.CountTotal(ctx, &start)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcount, err := g.db.CountRangeEverydayTotal(ctx, start, time.UnixMilli(req.End))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &group.GroupCreateCountResp{Total: total, Before: before, Count: count}, nil\n}\n"
  },
  {
    "path": "internal/rpc/group/sync.go",
    "content": "package group\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/incrversion\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/util/hashutil\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbgroup \"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/log\"\n)\n\nconst versionSyncLimit = 500\n\nfunc (g *groupServer) GetFullGroupMemberUserIDs(ctx context.Context, req *pbgroup.GetFullGroupMemberUserIDsReq) (*pbgroup.GetFullGroupMemberUserIDsResp, error) {\n\tuserIDs, err := g.db.FindGroupMemberUserID(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := authverify.CheckAccessIn(ctx, userIDs...); err != nil {\n\t\treturn nil, err\n\t}\n\tvl, err := g.db.FindMaxGroupMemberVersionCache(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tidHash := hashutil.IdHash(userIDs)\n\tif req.IdHash == idHash {\n\t\tuserIDs = nil\n\t}\n\treturn &pbgroup.GetFullGroupMemberUserIDsResp{\n\t\tVersion:   uint64(vl.Version),\n\t\tVersionID: vl.ID.Hex(),\n\t\tEqual:     req.IdHash == idHash,\n\t\tUserIDs:   userIDs,\n\t}, nil\n}\n\nfunc (g *groupServer) GetFullJoinGroupIDs(ctx context.Context, req *pbgroup.GetFullJoinGroupIDsReq) (*pbgroup.GetFullJoinGroupIDsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tvl, err := g.db.FindMaxJoinGroupVersionCache(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroupIDs, err := g.db.FindJoinGroupID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tidHash := hashutil.IdHash(groupIDs)\n\tif req.IdHash == idHash {\n\t\tgroupIDs = nil\n\t}\n\treturn &pbgroup.GetFullJoinGroupIDsResp{\n\t\tVersion:   uint64(vl.Version),\n\t\tVersionID: vl.ID.Hex(),\n\t\tEqual:     req.IdHash == idHash,\n\t\tGroupIDs:  groupIDs,\n\t}, nil\n}\n\nfunc (g *groupServer) GetIncrementalGroupMember(ctx context.Context, req *pbgroup.GetIncrementalGroupMemberReq) (*pbgroup.GetIncrementalGroupMemberResp, error) {\n\tif err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil {\n\t\treturn nil, err\n\t}\n\tgroup, err := g.db.TakeGroup(ctx, req.GroupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif group.Status == constant.GroupStatusDismissed {\n\t\treturn nil, servererrs.ErrDismissedAlready.Wrap()\n\t}\n\tvar (\n\t\thasGroupUpdate bool\n\t\tsortVersion    uint64\n\t)\n\topt := incrversion.Option[*sdkws.GroupMemberFullInfo, pbgroup.GetIncrementalGroupMemberResp]{\n\t\tCtx:           ctx,\n\t\tVersionKey:    req.GroupID,\n\t\tVersionID:     req.VersionID,\n\t\tVersionNumber: req.Version,\n\t\tVersion: func(ctx context.Context, groupID string, version uint, limit int) (*model.VersionLog, error) {\n\t\t\tvl, err := g.db.FindMemberIncrVersion(ctx, groupID, version, limit)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tlogs := make([]model.VersionLogElem, 0, len(vl.Logs))\n\t\t\tfor i, log := range vl.Logs {\n\t\t\t\tswitch log.EID {\n\t\t\t\tcase model.VersionGroupChangeID:\n\t\t\t\t\tvl.LogLen--\n\t\t\t\t\thasGroupUpdate = true\n\t\t\t\tcase model.VersionSortChangeID:\n\t\t\t\t\tvl.LogLen--\n\t\t\t\t\tsortVersion = uint64(log.Version)\n\t\t\t\tdefault:\n\t\t\t\t\tlogs = append(logs, vl.Logs[i])\n\t\t\t\t}\n\t\t\t}\n\t\t\tvl.Logs = logs\n\t\t\tif vl.LogLen > 0 {\n\t\t\t\thasGroupUpdate = true\n\t\t\t}\n\t\t\treturn vl, nil\n\t\t},\n\t\tCacheMaxVersion: g.db.FindMaxGroupMemberVersionCache,\n\t\tFind: func(ctx context.Context, ids []string) ([]*sdkws.GroupMemberFullInfo, error) {\n\t\t\treturn g.getGroupMembersInfo(ctx, req.GroupID, ids)\n\t\t},\n\t\tResp: func(version *model.VersionLog, delIDs []string, insertList, updateList []*sdkws.GroupMemberFullInfo, full bool) *pbgroup.GetIncrementalGroupMemberResp {\n\t\t\treturn &pbgroup.GetIncrementalGroupMemberResp{\n\t\t\t\tVersionID:   version.ID.Hex(),\n\t\t\t\tVersion:     uint64(version.Version),\n\t\t\t\tFull:        full,\n\t\t\t\tDelete:      delIDs,\n\t\t\t\tInsert:      insertList,\n\t\t\t\tUpdate:      updateList,\n\t\t\t\tSortVersion: sortVersion,\n\t\t\t}\n\t\t},\n\t}\n\tresp, err := opt.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Full || hasGroupUpdate {\n\t\tcount, err := g.db.FindGroupMemberNum(ctx, group.GroupID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\towner, err := g.db.TakeGroupOwner(ctx, group.GroupID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp.Group = g.groupDB2PB(group, owner.UserID, count)\n\t}\n\treturn resp, nil\n}\n\nfunc (g *groupServer) GetIncrementalJoinGroup(ctx context.Context, req *pbgroup.GetIncrementalJoinGroupReq) (*pbgroup.GetIncrementalJoinGroupResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\topt := incrversion.Option[*sdkws.GroupInfo, pbgroup.GetIncrementalJoinGroupResp]{\n\t\tCtx:             ctx,\n\t\tVersionKey:      req.UserID,\n\t\tVersionID:       req.VersionID,\n\t\tVersionNumber:   req.Version,\n\t\tVersion:         g.db.FindJoinIncrVersion,\n\t\tCacheMaxVersion: g.db.FindMaxJoinGroupVersionCache,\n\t\tFind:            g.getGroupsInfo,\n\t\tResp: func(version *model.VersionLog, delIDs []string, insertList, updateList []*sdkws.GroupInfo, full bool) *pbgroup.GetIncrementalJoinGroupResp {\n\t\t\treturn &pbgroup.GetIncrementalJoinGroupResp{\n\t\t\t\tVersionID: version.ID.Hex(),\n\t\t\t\tVersion:   uint64(version.Version),\n\t\t\t\tFull:      full,\n\t\t\t\tDelete:    delIDs,\n\t\t\t\tInsert:    insertList,\n\t\t\t\tUpdate:    updateList,\n\t\t\t}\n\t\t},\n\t}\n\treturn opt.Build()\n}\n\nfunc (g *groupServer) BatchGetIncrementalGroupMember(ctx context.Context, req *pbgroup.BatchGetIncrementalGroupMemberReq) (*pbgroup.BatchGetIncrementalGroupMemberResp, error) {\n\tvar num int\n\tresp := make(map[string]*pbgroup.GetIncrementalGroupMemberResp)\n\n\tfor _, memberReq := range req.ReqList {\n\t\tif _, ok := resp[memberReq.GroupID]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tmemberResp, err := g.GetIncrementalGroupMember(ctx, memberReq)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, servererrs.ErrDismissedAlready) {\n\t\t\t\tlog.ZWarn(ctx, \"Failed to get incremental group member\", err, \"groupID\", memberReq.GroupID, \"request\", memberReq)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresp[memberReq.GroupID] = memberResp\n\t\tnum += len(memberResp.Insert) + len(memberResp.Update) + len(memberResp.Delete)\n\t\tif num >= versionSyncLimit {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn &pbgroup.BatchGetIncrementalGroupMemberResp{RespList: resp}, nil\n}\n"
  },
  {
    "path": "internal/rpc/incrversion/batch_option.go",
    "content": "package incrversion\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n)\n\ntype BatchOption[A, B any] struct {\n\tCtx            context.Context\n\tTargetKeys     []string\n\tVersionIDs     []string\n\tVersionNumbers []uint64\n\t//SyncLimit       int\n\tVersions         func(ctx context.Context, dIds []string, versions []uint64, limits []int) (map[string]*model.VersionLog, error)\n\tCacheMaxVersions func(ctx context.Context, dIds []string) (map[string]*model.VersionLog, error)\n\tFind             func(ctx context.Context, dId string, ids []string) (A, error)\n\tResp             func(versionsMap map[string]*model.VersionLog, deleteIdsMap map[string][]string, insertListMap, updateListMap map[string]A, fullMap map[string]bool) *B\n}\n\nfunc (o *BatchOption[A, B]) newError(msg string) error {\n\treturn errs.ErrInternalServer.WrapMsg(msg)\n}\n\nfunc (o *BatchOption[A, B]) check() error {\n\tif o.Ctx == nil {\n\t\treturn o.newError(\"opt ctx is nil\")\n\t}\n\tif len(o.TargetKeys) == 0 {\n\t\treturn o.newError(\"targetKeys is empty\")\n\t}\n\tif o.Versions == nil {\n\t\treturn o.newError(\"func versions is nil\")\n\t}\n\tif o.Find == nil {\n\t\treturn o.newError(\"func find is nil\")\n\t}\n\tif o.Resp == nil {\n\t\treturn o.newError(\"func resp is nil\")\n\t}\n\treturn nil\n}\n\nfunc (o *BatchOption[A, B]) validVersions() []bool {\n\tvalids := make([]bool, len(o.VersionIDs))\n\tfor i, versionID := range o.VersionIDs {\n\t\tobjID, err := primitive.ObjectIDFromHex(versionID)\n\t\tvalids[i] = (err == nil && (!objID.IsZero()) && o.VersionNumbers[i] > 0)\n\t}\n\treturn valids\n}\n\nfunc (o *BatchOption[A, B]) equalIDs(objIDs []primitive.ObjectID) []bool {\n\tequals := make([]bool, len(o.VersionIDs))\n\tfor i, versionID := range o.VersionIDs {\n\t\tequals[i] = versionID == objIDs[i].Hex()\n\t}\n\treturn equals\n}\n\nfunc (o *BatchOption[A, B]) getVersions(tags *[]int) (versions map[string]*model.VersionLog, err error) {\n\tvar dIDs []string\n\tvar versionNums []uint64\n\tvar limits []int\n\n\tvalids := o.validVersions()\n\n\tif o.CacheMaxVersions == nil {\n\t\tfor i, valid := range valids {\n\t\t\tif valid {\n\t\t\t\t(*tags)[i] = tagQuery\n\t\t\t\tdIDs = append(dIDs, o.TargetKeys[i])\n\t\t\t\tversionNums = append(versionNums, o.VersionNumbers[i])\n\t\t\t\tlimits = append(limits, syncLimit)\n\t\t\t} else {\n\t\t\t\t(*tags)[i] = tagFull\n\t\t\t\tdIDs = append(dIDs, o.TargetKeys[i])\n\t\t\t\tversionNums = append(versionNums, 0)\n\t\t\t\tlimits = append(limits, 0)\n\t\t\t}\n\t\t}\n\n\t\tversions, err = o.Versions(o.Ctx, dIDs, versionNums, limits)\n\t\tif err != nil {\n\t\t\treturn nil, errs.Wrap(err)\n\t\t}\n\t\treturn versions, nil\n\n\t} else {\n\t\tcaches, err := o.CacheMaxVersions(o.Ctx, o.TargetKeys)\n\t\tif err != nil {\n\t\t\treturn nil, errs.Wrap(err)\n\t\t}\n\n\t\tobjIDs := make([]primitive.ObjectID, len(o.VersionIDs))\n\n\t\tfor i, versionID := range o.VersionIDs {\n\t\t\tobjID, _ := primitive.ObjectIDFromHex(versionID)\n\t\t\tobjIDs[i] = objID\n\t\t}\n\n\t\tequals := o.equalIDs(objIDs)\n\t\tfor i, valid := range valids {\n\t\t\tif !valid {\n\t\t\t\t(*tags)[i] = tagFull\n\t\t\t} else if !equals[i] {\n\t\t\t\t(*tags)[i] = tagFull\n\t\t\t} else if o.VersionNumbers[i] == uint64(caches[o.TargetKeys[i]].Version) {\n\t\t\t\t(*tags)[i] = tagEqual\n\t\t\t} else {\n\t\t\t\t(*tags)[i] = tagQuery\n\t\t\t\tdIDs = append(dIDs, o.TargetKeys[i])\n\t\t\t\tversionNums = append(versionNums, o.VersionNumbers[i])\n\t\t\t\tlimits = append(limits, syncLimit)\n\n\t\t\t\tdelete(caches, o.TargetKeys[i])\n\t\t\t}\n\t\t}\n\n\t\tif dIDs != nil {\n\t\t\tversionMap, err := o.Versions(o.Ctx, dIDs, versionNums, limits)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errs.Wrap(err)\n\t\t\t}\n\n\t\t\tfor k, v := range versionMap {\n\t\t\t\tcaches[k] = v\n\t\t\t}\n\t\t}\n\n\t\tversions = caches\n\t}\n\treturn versions, nil\n}\n\nfunc (o *BatchOption[A, B]) Build() (*B, error) {\n\tif err := o.check(); err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\n\ttags := make([]int, len(o.TargetKeys))\n\tversions, err := o.getVersions(&tags)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\n\tfullMap := make(map[string]bool)\n\tfor i, tag := range tags {\n\t\tswitch tag {\n\t\tcase tagQuery:\n\t\t\tvLog := versions[o.TargetKeys[i]]\n\t\t\tfullMap[o.TargetKeys[i]] = vLog.ID.Hex() != o.VersionIDs[i] || uint64(vLog.Version) < o.VersionNumbers[i] || len(vLog.Logs) != vLog.LogLen\n\t\tcase tagFull:\n\t\t\tfullMap[o.TargetKeys[i]] = true\n\t\tcase tagEqual:\n\t\t\tfullMap[o.TargetKeys[i]] = false\n\t\tdefault:\n\t\t\tpanic(fmt.Errorf(\"undefined tag %d\", tag))\n\t\t}\n\t}\n\n\tvar (\n\t\tinsertIdsMap = make(map[string][]string)\n\t\tdeleteIdsMap = make(map[string][]string)\n\t\tupdateIdsMap = make(map[string][]string)\n\t)\n\n\tfor _, targetKey := range o.TargetKeys {\n\t\tif !fullMap[targetKey] {\n\t\t\tversion := versions[targetKey]\n\t\t\tinsertIds, deleteIds, updateIds := version.DeleteAndChangeIDs()\n\t\t\tinsertIdsMap[targetKey] = insertIds\n\t\t\tdeleteIdsMap[targetKey] = deleteIds\n\t\t\tupdateIdsMap[targetKey] = updateIds\n\t\t}\n\t}\n\n\tvar (\n\t\tinsertListMap = make(map[string]A)\n\t\tupdateListMap = make(map[string]A)\n\t)\n\n\tfor targetKey, insertIds := range insertIdsMap {\n\t\tif len(insertIds) > 0 {\n\t\t\tinsertList, err := o.Find(o.Ctx, targetKey, insertIds)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errs.Wrap(err)\n\t\t\t}\n\t\t\tinsertListMap[targetKey] = insertList\n\t\t}\n\t}\n\n\tfor targetKey, updateIds := range updateIdsMap {\n\t\tif len(updateIds) > 0 {\n\t\t\tupdateList, err := o.Find(o.Ctx, targetKey, updateIds)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errs.Wrap(err)\n\t\t\t}\n\t\t\tupdateListMap[targetKey] = updateList\n\t\t}\n\t}\n\n\treturn o.Resp(versions, deleteIdsMap, insertListMap, updateListMap, fullMap), nil\n}\n"
  },
  {
    "path": "internal/rpc/incrversion/option.go",
    "content": "package incrversion\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n)\n\n//func Limit(maxSync int, version uint64) int {\n//\tif version == 0 {\n//\t\treturn 0\n//\t}\n//\treturn maxSync\n//}\n\nconst syncLimit = 200\n\nconst (\n\ttagQuery = iota + 1\n\ttagFull\n\ttagEqual\n)\n\ntype Option[A, B any] struct {\n\tCtx           context.Context\n\tVersionKey    string\n\tVersionID     string\n\tVersionNumber uint64\n\t//SyncLimit       int\n\tCacheMaxVersion func(ctx context.Context, dId string) (*model.VersionLog, error)\n\tVersion         func(ctx context.Context, dId string, version uint, limit int) (*model.VersionLog, error)\n\t//SortID          func(ctx context.Context, dId string) ([]string, error)\n\tFind func(ctx context.Context, ids []string) ([]A, error)\n\tResp func(version *model.VersionLog, deleteIds []string, insertList, updateList []A, full bool) *B\n}\n\nfunc (o *Option[A, B]) newError(msg string) error {\n\treturn errs.ErrInternalServer.WrapMsg(msg)\n}\n\nfunc (o *Option[A, B]) check() error {\n\tif o.Ctx == nil {\n\t\treturn o.newError(\"opt ctx is nil\")\n\t}\n\tif o.VersionKey == \"\" {\n\t\treturn o.newError(\"versionKey is empty\")\n\t}\n\t//if o.SyncLimit <= 0 {\n\t//\treturn o.newError(\"invalid synchronization quantity\")\n\t//}\n\tif o.Version == nil {\n\t\treturn o.newError(\"func version is nil\")\n\t}\n\t//if o.SortID == nil {\n\t//\treturn o.newError(\"func allID is nil\")\n\t//}\n\tif o.Find == nil {\n\t\treturn o.newError(\"func find is nil\")\n\t}\n\tif o.Resp == nil {\n\t\treturn o.newError(\"func resp is nil\")\n\t}\n\treturn nil\n}\n\nfunc (o *Option[A, B]) validVersion() bool {\n\tobjID, err := primitive.ObjectIDFromHex(o.VersionID)\n\treturn err == nil && (!objID.IsZero()) && o.VersionNumber > 0\n}\n\nfunc (o *Option[A, B]) equalID(objID primitive.ObjectID) bool {\n\treturn o.VersionID == objID.Hex()\n}\n\nfunc (o *Option[A, B]) getVersion(tag *int) (*model.VersionLog, error) {\n\tif o.CacheMaxVersion == nil {\n\t\tif o.validVersion() {\n\t\t\t*tag = tagQuery\n\t\t\treturn o.Version(o.Ctx, o.VersionKey, uint(o.VersionNumber), syncLimit)\n\t\t}\n\t\t*tag = tagFull\n\t\treturn o.Version(o.Ctx, o.VersionKey, 0, 0)\n\t} else {\n\t\tcache, err := o.CacheMaxVersion(o.Ctx, o.VersionKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !o.validVersion() {\n\t\t\t*tag = tagFull\n\t\t\treturn cache, nil\n\t\t}\n\t\tif !o.equalID(cache.ID) {\n\t\t\t*tag = tagFull\n\t\t\treturn cache, nil\n\t\t}\n\t\tif o.VersionNumber == uint64(cache.Version) {\n\t\t\t*tag = tagEqual\n\t\t\treturn cache, nil\n\t\t}\n\t\t*tag = tagQuery\n\t\treturn o.Version(o.Ctx, o.VersionKey, uint(o.VersionNumber), syncLimit)\n\t}\n}\n\nfunc (o *Option[A, B]) Build() (*B, error) {\n\tif err := o.check(); err != nil {\n\t\treturn nil, err\n\t}\n\tvar tag int\n\tversion, err := o.getVersion(&tag)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar full bool\n\tswitch tag {\n\tcase tagQuery:\n\t\tfull = version.ID.Hex() != o.VersionID || uint64(version.Version) < o.VersionNumber || len(version.Logs) != version.LogLen\n\tcase tagFull:\n\t\tfull = true\n\tcase tagEqual:\n\t\tfull = false\n\tdefault:\n\t\tpanic(fmt.Errorf(\"undefined tag %d\", tag))\n\t}\n\tvar (\n\t\tinsertIds []string\n\t\tdeleteIds []string\n\t\tupdateIds []string\n\t)\n\tif !full {\n\t\tinsertIds, deleteIds, updateIds = version.DeleteAndChangeIDs()\n\t}\n\tvar (\n\t\tinsertList []A\n\t\tupdateList []A\n\t)\n\tif len(insertIds) > 0 {\n\t\tinsertList, err = o.Find(o.Ctx, insertIds)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif len(updateIds) > 0 {\n\t\tupdateList, err = o.Find(o.Ctx, updateIds)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn o.Resp(version, deleteIds, insertList, updateList, full), nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/as_read.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\tcbapi \"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc (m *msgServer) GetConversationsHasReadAndMaxSeq(ctx context.Context, req *msg.GetConversationsHasReadAndMaxSeqReq) (*msg.GetConversationsHasReadAndMaxSeqResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tvar conversationIDs []string\n\tif len(req.ConversationIDs) == 0 {\n\t\tvar err error\n\t\tconversationIDs, err = m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tconversationIDs = req.ConversationIDs\n\t}\n\n\thasReadSeqs, err := m.MsgDatabase.GetHasReadSeqs(ctx, req.UserID, conversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconversations, err := m.ConversationLocalCache.GetConversations(ctx, req.UserID, conversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconversationMaxSeqMap := make(map[string]int64)\n\tfor _, conversation := range conversations {\n\t\tif conversation.MaxSeq != 0 {\n\t\t\tconversationMaxSeqMap[conversation.ConversationID] = conversation.MaxSeq\n\t\t}\n\t}\n\tmaxSeqs, err := m.MsgDatabase.GetMaxSeqsWithTime(ctx, conversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp := &msg.GetConversationsHasReadAndMaxSeqResp{Seqs: make(map[string]*msg.Seqs)}\n\tif req.ReturnPinned {\n\t\tpinnedConversationIDs, err := m.ConversationLocalCache.GetPinnedConversationIDs(ctx, req.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp.PinnedConversationIDs = pinnedConversationIDs\n\t}\n\tfor conversationID, maxSeq := range maxSeqs {\n\t\tresp.Seqs[conversationID] = &msg.Seqs{\n\t\t\tHasReadSeq: hasReadSeqs[conversationID],\n\t\t\tMaxSeq:     maxSeq.Seq,\n\t\t\tMaxSeqTime: maxSeq.Time,\n\t\t}\n\t\tif v, ok := conversationMaxSeqMap[conversationID]; ok {\n\t\t\tresp.Seqs[conversationID].MaxSeq = v\n\t\t}\n\t}\n\treturn resp, nil\n}\n\nfunc (m *msgServer) SetConversationHasReadSeq(ctx context.Context, req *msg.SetConversationHasReadSeqReq) (*msg.SetConversationHasReadSeqResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tmaxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif req.HasReadSeq > maxSeq {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"hasReadSeq must not be bigger than maxSeq\")\n\t}\n\tif err := m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq); err != nil {\n\t\treturn nil, err\n\t}\n\tm.sendMarkAsReadNotification(ctx, req.ConversationID, constant.SingleChatType, req.UserID, req.UserID, nil, req.HasReadSeq)\n\treturn &msg.SetConversationHasReadSeqResp{}, nil\n}\n\nfunc (m *msgServer) MarkMsgsAsRead(ctx context.Context, req *msg.MarkMsgsAsReadReq) (*msg.MarkMsgsAsReadResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tmaxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thasReadSeq := req.Seqs[len(req.Seqs)-1]\n\tif hasReadSeq > maxSeq {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"hasReadSeq must not be bigger than maxSeq\")\n\t}\n\tconversation, err := m.ConversationLocalCache.GetConversation(ctx, req.UserID, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := m.MsgDatabase.MarkSingleChatMsgsAsRead(ctx, req.UserID, req.ConversationID, req.Seqs); err != nil {\n\t\treturn nil, err\n\t}\n\tcurrentHasReadSeq, err := m.MsgDatabase.GetHasReadSeq(ctx, req.UserID, req.ConversationID)\n\tif err != nil && !errors.Is(err, redis.Nil) {\n\t\treturn nil, err\n\t}\n\tif hasReadSeq > currentHasReadSeq {\n\t\terr = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, hasReadSeq)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treqCallback := &cbapi.CallbackSingleMsgReadReq{\n\t\tConversationID: conversation.ConversationID,\n\t\tUserID:         req.UserID,\n\t\tSeqs:           req.Seqs,\n\t\tContentType:    conversation.ConversationType,\n\t}\n\tm.webhookAfterSingleMsgRead(ctx, &m.config.WebhooksConfig.AfterSingleMsgRead, reqCallback)\n\tm.sendMarkAsReadNotification(ctx, req.ConversationID, conversation.ConversationType, req.UserID,\n\t\tm.conversationAndGetRecvID(conversation, req.UserID), req.Seqs, hasReadSeq)\n\treturn &msg.MarkMsgsAsReadResp{}, nil\n}\n\nfunc (m *msgServer) MarkConversationAsRead(ctx context.Context, req *msg.MarkConversationAsReadReq) (*msg.MarkConversationAsReadResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversation, err := m.ConversationLocalCache.GetConversation(ctx, req.UserID, req.ConversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thasReadSeq, err := m.MsgDatabase.GetHasReadSeq(ctx, req.UserID, req.ConversationID)\n\tif err != nil && !errors.Is(err, redis.Nil) {\n\t\treturn nil, err\n\t}\n\tvar seqs []int64\n\n\tlog.ZDebug(ctx, \"MarkConversationAsRead\", \"hasReadSeq\", hasReadSeq, \"req.HasReadSeq\", req.HasReadSeq)\n\tif conversation.ConversationType == constant.SingleChatType {\n\t\tfor i := hasReadSeq + 1; i <= req.HasReadSeq; i++ {\n\t\t\tseqs = append(seqs, i)\n\t\t}\n\t\t// avoid client missed call MarkConversationMessageAsRead by order\n\t\tfor _, val := range req.Seqs {\n\t\t\tif !datautil.Contain(val, seqs...) {\n\t\t\t\tseqs = append(seqs, val)\n\t\t\t}\n\t\t}\n\t\tif len(seqs) > 0 {\n\t\t\tlog.ZDebug(ctx, \"MarkConversationAsRead\", \"seqs\", seqs, \"conversationID\", req.ConversationID)\n\t\t\tif err = m.MsgDatabase.MarkSingleChatMsgsAsRead(ctx, req.UserID, req.ConversationID, seqs); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif req.HasReadSeq > hasReadSeq {\n\t\t\terr = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\thasReadSeq = req.HasReadSeq\n\t\t}\n\t\tm.sendMarkAsReadNotification(ctx, req.ConversationID, conversation.ConversationType, req.UserID,\n\t\t\tm.conversationAndGetRecvID(conversation, req.UserID), seqs, hasReadSeq)\n\t} else if conversation.ConversationType == constant.ReadGroupChatType ||\n\t\tconversation.ConversationType == constant.NotificationChatType {\n\t\tif req.HasReadSeq > hasReadSeq {\n\t\t\terr = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\thasReadSeq = req.HasReadSeq\n\t\t}\n\t\tm.sendMarkAsReadNotification(ctx, req.ConversationID, constant.SingleChatType, req.UserID,\n\t\t\treq.UserID, seqs, hasReadSeq)\n\t}\n\n\tif conversation.ConversationType == constant.SingleChatType {\n\t\treqCall := &cbapi.CallbackSingleMsgReadReq{\n\t\t\tConversationID: conversation.ConversationID,\n\t\t\tUserID:         conversation.OwnerUserID,\n\t\t\tSeqs:           req.Seqs,\n\t\t\tContentType:    conversation.ConversationType,\n\t\t}\n\t\tm.webhookAfterSingleMsgRead(ctx, &m.config.WebhooksConfig.AfterSingleMsgRead, reqCall)\n\t} else if conversation.ConversationType == constant.ReadGroupChatType {\n\t\treqCall := &cbapi.CallbackGroupMsgReadReq{\n\t\t\tSendID:       conversation.OwnerUserID,\n\t\t\tReceiveID:    req.UserID,\n\t\t\tUnreadMsgNum: req.HasReadSeq,\n\t\t\tContentType:  int64(conversation.ConversationType),\n\t\t}\n\t\tm.webhookAfterGroupMsgRead(ctx, &m.config.WebhooksConfig.AfterGroupMsgRead, reqCall)\n\t}\n\treturn &msg.MarkConversationAsReadResp{}, nil\n}\n\nfunc (m *msgServer) sendMarkAsReadNotification(ctx context.Context, conversationID string, sessionType int32, sendID, recvID string, seqs []int64, hasReadSeq int64) {\n\ttips := &sdkws.MarkAsReadTips{\n\t\tMarkAsReadUserID: sendID,\n\t\tConversationID:   conversationID,\n\t\tSeqs:             seqs,\n\t\tHasReadSeq:       hasReadSeq,\n\t}\n\tm.notificationSender.NotificationWithSessionType(ctx, sendID, recvID, constant.HasReadReceipt, sessionType, tips)\n}\n"
  },
  {
    "path": "internal/rpc/msg/callback.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/tools/errs\"\n\n\tcbapi \"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbchat \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/stringutil\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\nfunc toCommonCallback(ctx context.Context, msg *pbchat.SendMsgReq, command string) cbapi.CommonCallbackReq {\n\treturn cbapi.CommonCallbackReq{\n\t\tSendID:           msg.MsgData.SendID,\n\t\tServerMsgID:      msg.MsgData.ServerMsgID,\n\t\tCallbackCommand:  command,\n\t\tClientMsgID:      msg.MsgData.ClientMsgID,\n\t\tOperationID:      mcontext.GetOperationID(ctx),\n\t\tSenderPlatformID: msg.MsgData.SenderPlatformID,\n\t\tSenderNickname:   msg.MsgData.SenderNickname,\n\t\tSessionType:      msg.MsgData.SessionType,\n\t\tMsgFrom:          msg.MsgData.MsgFrom,\n\t\tContentType:      msg.MsgData.ContentType,\n\t\tStatus:           msg.MsgData.Status,\n\t\tSendTime:         msg.MsgData.SendTime,\n\t\tCreateTime:       msg.MsgData.CreateTime,\n\t\tAtUserIDList:     msg.MsgData.AtUserIDList,\n\t\tSenderFaceURL:    msg.MsgData.SenderFaceURL,\n\t\tContent:          GetContent(msg.MsgData),\n\t\tSeq:              uint32(msg.MsgData.Seq),\n\t\tEx:               msg.MsgData.Ex,\n\t}\n}\n\nfunc GetContent(msg *sdkws.MsgData) string {\n\tif msg.ContentType >= constant.NotificationBegin && msg.ContentType <= constant.NotificationEnd {\n\t\tvar tips sdkws.TipsComm\n\t\t_ = proto.Unmarshal(msg.Content, &tips)\n\t\tcontent := tips.JsonDetail\n\t\treturn content\n\t} else {\n\t\treturn string(msg.Content)\n\t}\n}\n\nfunc (m *msgServer) webhookBeforeSendSingleMsg(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tif msg.MsgData.ContentType == constant.Typing {\n\t\t\treturn nil\n\t\t}\n\t\tif !filterBeforeMsg(msg, before) {\n\t\t\treturn nil\n\t\t}\n\t\tcbReq := &cbapi.CallbackBeforeSendSingleMsgReq{\n\t\t\tCommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeSendSingleMsgCommand),\n\t\t\tRecvID:            msg.MsgData.RecvID,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeSendSingleMsgResp{}\n\t\tif err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// Move to msgtransfer\nfunc (m *msgServer) webhookAfterSendSingleMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) {\n\tif msg.MsgData.ContentType == constant.Typing {\n\t\treturn\n\t}\n\tif !filterAfterMsg(msg, after) {\n\t\treturn\n\t}\n\tcbReq := &cbapi.CallbackAfterSendSingleMsgReq{\n\t\tCommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendSingleMsgCommand),\n\t\tRecvID:            msg.MsgData.RecvID,\n\t}\n\tm.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendSingleMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData))\n}\n\nfunc (m *msgServer) webhookBeforeSendGroupMsg(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tif !filterBeforeMsg(msg, before) {\n\t\t\treturn nil\n\t\t}\n\t\tif msg.MsgData.ContentType == constant.Typing {\n\t\t\treturn nil\n\t\t}\n\t\tcbReq := &cbapi.CallbackBeforeSendGroupMsgReq{\n\t\t\tCommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeSendGroupMsgCommand),\n\t\t\tGroupID:           msg.MsgData.GroupID,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeSendGroupMsgResp{}\n\t\tif err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (m *msgServer) webhookAfterSendGroupMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) {\n\tif msg.MsgData.ContentType == constant.Typing {\n\t\treturn\n\t}\n\tif !filterAfterMsg(msg, after) {\n\t\treturn\n\t}\n\tcbReq := &cbapi.CallbackAfterSendGroupMsgReq{\n\t\tCommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendGroupMsgCommand),\n\t\tGroupID:           msg.MsgData.GroupID,\n\t}\n\n\tm.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendGroupMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData))\n}\n\nfunc (m *msgServer) webhookBeforeMsgModify(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq, beforeMsgData **sdkws.MsgData) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\t//if msg.MsgData.ContentType != constant.Text {\n\t\t//\treturn nil\n\t\t//}\n\t\tif !filterBeforeMsg(msg, before) {\n\t\t\treturn nil\n\t\t}\n\t\tcbReq := &cbapi.CallbackMsgModifyCommandReq{\n\t\t\tCommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeMsgModifyCommand),\n\t\t}\n\t\tresp := &cbapi.CallbackMsgModifyCommandResp{}\n\t\tif err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif beforeMsgData != nil {\n\t\t\t*beforeMsgData = proto.Clone(msg.MsgData).(*sdkws.MsgData)\n\t\t}\n\t\tif resp.Content != nil {\n\t\t\tmsg.MsgData.Content = []byte(*resp.Content)\n\t\t\tif err := json.Unmarshal(msg.MsgData.Content, &struct{}{}); err != nil {\n\t\t\t\treturn errs.ErrArgs.WrapMsg(\"webhook msg modify content is not json\", \"content\", string(msg.MsgData.Content))\n\t\t\t}\n\t\t}\n\t\tdatautil.NotNilReplace(msg.MsgData.OfflinePushInfo, resp.OfflinePushInfo)\n\t\tdatautil.NotNilReplace(&msg.MsgData.RecvID, resp.RecvID)\n\t\tdatautil.NotNilReplace(&msg.MsgData.GroupID, resp.GroupID)\n\t\tdatautil.NotNilReplace(&msg.MsgData.ClientMsgID, resp.ClientMsgID)\n\t\tdatautil.NotNilReplace(&msg.MsgData.ServerMsgID, resp.ServerMsgID)\n\t\tdatautil.NotNilReplace(&msg.MsgData.SenderPlatformID, resp.SenderPlatformID)\n\t\tdatautil.NotNilReplace(&msg.MsgData.SenderNickname, resp.SenderNickname)\n\t\tdatautil.NotNilReplace(&msg.MsgData.SenderFaceURL, resp.SenderFaceURL)\n\t\tdatautil.NotNilReplace(&msg.MsgData.SessionType, resp.SessionType)\n\t\tdatautil.NotNilReplace(&msg.MsgData.MsgFrom, resp.MsgFrom)\n\t\tdatautil.NotNilReplace(&msg.MsgData.ContentType, resp.ContentType)\n\t\tdatautil.NotNilReplace(&msg.MsgData.Status, resp.Status)\n\t\tdatautil.NotNilReplace(&msg.MsgData.Options, resp.Options)\n\t\tdatautil.NotNilReplace(&msg.MsgData.AtUserIDList, resp.AtUserIDList)\n\t\tdatautil.NotNilReplace(&msg.MsgData.AttachedInfo, resp.AttachedInfo)\n\t\tdatautil.NotNilReplace(&msg.MsgData.Ex, resp.Ex)\n\t\treturn nil\n\t})\n}\n\nfunc (m *msgServer) webhookAfterGroupMsgRead(ctx context.Context, after *config.AfterConfig, req *cbapi.CallbackGroupMsgReadReq) {\n\treq.CallbackCommand = cbapi.CallbackAfterGroupMsgReadCommand\n\tm.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CallbackGroupMsgReadResp{}, after)\n}\n\nfunc (m *msgServer) webhookAfterSingleMsgRead(ctx context.Context, after *config.AfterConfig, req *cbapi.CallbackSingleMsgReadReq) {\n\n\treq.CallbackCommand = cbapi.CallbackAfterSingleMsgReadCommand\n\n\tm.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CallbackSingleMsgReadResp{}, after)\n\n}\n\nfunc (m *msgServer) webhookAfterRevokeMsg(ctx context.Context, after *config.AfterConfig, req *pbchat.RevokeMsgReq) {\n\tcallbackReq := &cbapi.CallbackAfterRevokeMsgReq{\n\t\tCallbackCommand: cbapi.CallbackAfterRevokeMsgCommand,\n\t\tConversationID:  req.ConversationID,\n\t\tSeq:             req.Seq,\n\t\tUserID:          req.UserID,\n\t}\n\tm.webhookClient.AsyncPost(ctx, callbackReq.GetCallbackCommand(), callbackReq, &cbapi.CallbackAfterRevokeMsgResp{}, after)\n}\n\nfunc buildKeyMsgDataQuery(msg *sdkws.MsgData) map[string]string {\n\tkeyMsgData := apistruct.KeyMsgData{\n\t\tSendID:  msg.SendID,\n\t\tRecvID:  msg.RecvID,\n\t\tGroupID: msg.GroupID,\n\t}\n\n\treturn map[string]string{\n\t\twebhook.Key: base64.StdEncoding.EncodeToString(stringutil.StructToJsonBytes(keyMsgData)),\n\t}\n}\n"
  },
  {
    "path": "internal/rpc/msg/clear.go",
    "content": "package msg\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/tools/log\"\n)\n\n// DestructMsgs hard delete in Database.\nfunc (m *msgServer) DestructMsgs(ctx context.Context, req *msg.DestructMsgsReq) (*msg.DestructMsgsResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tdocs, err := m.MsgDatabase.GetRandBeforeMsg(ctx, req.Timestamp, int(req.Limit))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor i, doc := range docs {\n\t\tif err := m.MsgDatabase.DeleteDoc(ctx, doc.DocID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.ZDebug(ctx, \"DestructMsgs delete doc\", \"index\", i, \"docID\", doc.DocID)\n\t\tindex := strings.LastIndex(doc.DocID, \":\")\n\t\tif index < 0 {\n\t\t\tcontinue\n\t\t}\n\t\tvar minSeq int64\n\t\tfor _, model := range doc.Msg {\n\t\t\tif model.Msg == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif model.Msg.Seq > minSeq {\n\t\t\t\tminSeq = model.Msg.Seq\n\t\t\t}\n\t\t}\n\t\tif minSeq <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tconversationID := doc.DocID[:index]\n\t\tif conversationID == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tminSeq++\n\t\tif err := m.MsgDatabase.SetMinSeq(ctx, conversationID, minSeq); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.ZDebug(ctx, \"DestructMsgs delete doc set min seq\", \"index\", i, \"docID\", doc.DocID, \"conversationID\", conversationID, \"setMinSeq\", minSeq)\n\t}\n\treturn &msg.DestructMsgsResp{Count: int32(len(docs))}, nil\n}\n\nfunc (m *msgServer) GetLastMessageSeqByTime(ctx context.Context, req *msg.GetLastMessageSeqByTimeReq) (*msg.GetLastMessageSeqByTimeResp, error) {\n\tseq, err := m.MsgDatabase.GetLastMessageSeqByTime(ctx, req.ConversationID, req.Time)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &msg.GetLastMessageSeqByTimeResp{Seq: seq}, nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/delete.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n)\n\nfunc (m *msgServer) getMinSeqs(maxSeqs map[string]int64) map[string]int64 {\n\tminSeqs := make(map[string]int64)\n\tfor k, v := range maxSeqs {\n\t\tminSeqs[k] = v + 1\n\t}\n\treturn minSeqs\n}\n\nfunc (m *msgServer) validateDeleteSyncOpt(opt *msg.DeleteSyncOpt) (isSyncSelf, isSyncOther bool) {\n\tif opt == nil {\n\t\treturn\n\t}\n\treturn opt.IsSyncSelf, opt.IsSyncOther\n}\n\nfunc (m *msgServer) ClearConversationsMsg(ctx context.Context, req *msg.ClearConversationsMsgReq) (*msg.ClearConversationsMsgResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := m.clearConversation(ctx, req.ConversationIDs, req.UserID, req.DeleteSyncOpt); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &msg.ClearConversationsMsgResp{}, nil\n}\n\nfunc (m *msgServer) UserClearAllMsg(ctx context.Context, req *msg.UserClearAllMsgReq) (*msg.UserClearAllMsgResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversationIDs, err := m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := m.clearConversation(ctx, conversationIDs, req.UserID, req.DeleteSyncOpt); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &msg.UserClearAllMsgResp{}, nil\n}\n\nfunc (m *msgServer) DeleteMsgs(ctx context.Context, req *msg.DeleteMsgsReq) (*msg.DeleteMsgsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tisSyncSelf, isSyncOther := m.validateDeleteSyncOpt(req.DeleteSyncOpt)\n\tif isSyncOther {\n\t\tif err := m.MsgDatabase.DeleteMsgsPhysicalBySeqs(ctx, req.ConversationID, req.Seqs); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tconv, err := m.conversationClient.GetConversation(ctx, req.ConversationID, req.UserID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttips := &sdkws.DeleteMsgsTips{UserID: req.UserID, ConversationID: req.ConversationID, Seqs: req.Seqs}\n\t\tm.notificationSender.NotificationWithSessionType(ctx, req.UserID, m.conversationAndGetRecvID(conv, req.UserID),\n\t\t\tconstant.DeleteMsgsNotification, conv.ConversationType, tips)\n\t} else {\n\t\tif err := m.MsgDatabase.DeleteUserMsgsBySeqs(ctx, req.UserID, req.ConversationID, req.Seqs); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif isSyncSelf {\n\t\t\ttips := &sdkws.DeleteMsgsTips{UserID: req.UserID, ConversationID: req.ConversationID, Seqs: req.Seqs}\n\t\t\tm.notificationSender.NotificationWithSessionType(ctx, req.UserID, req.UserID, constant.DeleteMsgsNotification, constant.SingleChatType, tips)\n\t\t}\n\t}\n\treturn &msg.DeleteMsgsResp{}, nil\n}\n\nfunc (m *msgServer) DeleteMsgPhysicalBySeq(ctx context.Context, req *msg.DeleteMsgPhysicalBySeqReq) (*msg.DeleteMsgPhysicalBySeqResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\terr := m.MsgDatabase.DeleteMsgsPhysicalBySeqs(ctx, req.ConversationID, req.Seqs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &msg.DeleteMsgPhysicalBySeqResp{}, nil\n}\n\nfunc (m *msgServer) DeleteMsgPhysical(ctx context.Context, req *msg.DeleteMsgPhysicalReq) (*msg.DeleteMsgPhysicalResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tremainTime := timeutil.GetCurrentTimestampBySecond() - req.Timestamp\n\tif _, err := m.DestructMsgs(ctx, &msg.DestructMsgsReq{Timestamp: remainTime, Limit: 9999}); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &msg.DeleteMsgPhysicalResp{}, nil\n}\n\nfunc (m *msgServer) clearConversation(ctx context.Context, conversationIDs []string, userID string, deleteSyncOpt *msg.DeleteSyncOpt) error {\n\tconversations, err := m.conversationClient.GetConversations(ctx, conversationIDs, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar existConversationIDs []string\n\tfor _, conversation := range conversations {\n\t\texistConversationIDs = append(existConversationIDs, conversation.ConversationID)\n\t}\n\tlog.ZDebug(ctx, \"ClearConversationsMsg\", \"existConversationIDs\", existConversationIDs)\n\tmaxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, existConversationIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tisSyncSelf, isSyncOther := m.validateDeleteSyncOpt(deleteSyncOpt)\n\tif !isSyncOther {\n\t\tsetSeqs := m.getMinSeqs(maxSeqs)\n\t\tif err := m.MsgDatabase.SetUserConversationsMinSeqs(ctx, userID, setSeqs); err != nil {\n\t\t\treturn err\n\t\t}\n\t\townerUserIDs := []string{userID}\n\t\tfor conversationID, seq := range setSeqs {\n\t\t\tif err := m.conversationClient.SetConversationMinSeq(ctx, conversationID, ownerUserIDs, seq); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t// notification 2 self\n\t\tif isSyncSelf {\n\t\t\ttips := &sdkws.ClearConversationTips{UserID: userID, ConversationIDs: existConversationIDs}\n\t\t\tm.notificationSender.NotificationWithSessionType(ctx, userID, userID, constant.ClearConversationNotification, constant.SingleChatType, tips)\n\t\t}\n\t} else {\n\t\tif err := m.MsgDatabase.SetMinSeqs(ctx, m.getMinSeqs(maxSeqs)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, conversation := range conversations {\n\t\t\ttips := &sdkws.ClearConversationTips{UserID: userID, ConversationIDs: []string{conversation.ConversationID}}\n\t\t\tm.notificationSender.NotificationWithSessionType(ctx, userID, m.conversationAndGetRecvID(conversation, userID), constant.ClearConversationNotification, conversation.ConversationType, tips)\n\t\t}\n\t}\n\tif err := m.MsgDatabase.UserSetHasReadSeqs(ctx, userID, maxSeqs); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/filter.go",
    "content": "package msg\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbchat \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nconst (\n\tseparator = \"-\"\n)\n\nfunc filterAfterMsg(msg *pbchat.SendMsgReq, after *config.AfterConfig) bool {\n\treturn filterMsg(msg, after.AttentionIds, after.DeniedTypes)\n}\n\nfunc filterBeforeMsg(msg *pbchat.SendMsgReq, before *config.BeforeConfig) bool {\n\treturn filterMsg(msg, nil, before.DeniedTypes)\n}\n\nfunc filterMsg(msg *pbchat.SendMsgReq, attentionIds []string, deniedTypes []int32) bool {\n\t// According to the attentionIds configuration, only some users are sent\n\tif len(attentionIds) != 0 && !datautil.Contain(msg.MsgData.RecvID, attentionIds...) {\n\t\treturn false\n\t}\n\n\tif defaultDeniedTypes(msg.MsgData.ContentType) {\n\t\treturn false\n\t}\n\n\tif len(deniedTypes) != 0 && datautil.Contain(msg.MsgData.ContentType, deniedTypes...) {\n\t\treturn false\n\t}\n\t//if len(allowedTypes) != 0 && !isInInterval(msg.MsgData.ContentType, allowedTypes) {\n\t//\treturn false\n\t//}\n\t//if len(deniedTypes) != 0 && isInInterval(msg.MsgData.ContentType, deniedTypes) {\n\t//\treturn false\n\t//}\n\treturn true\n}\n\nfunc defaultDeniedTypes(contentType int32) bool {\n\tif contentType >= constant.NotificationBegin && contentType <= constant.NotificationEnd {\n\t\treturn true\n\t}\n\tif contentType == constant.Typing {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// isInInterval if data is in interval\n// Supports two formats: a single type or a range. The range is defined by the lower and upper bounds connected with a hyphen (\"-\")\n// e.g. [1, 100, 200-500, 600-700] means that only data within the range\n// {1, 100} ∪ [200, 500] ∪ [600, 700] will return true.\nfunc isInInterval(data int32, interval []string) bool {\n\tfor _, v := range interval {\n\t\tif strings.Contains(v, separator) {\n\t\t\t// is interval\n\t\t\tbounds := strings.Split(v, separator)\n\t\t\tif len(bounds) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbottom, err := strconv.Atoi(bounds[0])\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttop, err := strconv.Atoi(bounds[1])\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif datautil.BetweenEq(int(data), bottom, top) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t} else {\n\t\t\tiv, err := strconv.Atoi(v)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif int(data) == iv {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/rpc/msg/msg_status.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbmsg \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nfunc (m *msgServer) SetSendMsgStatus(ctx context.Context, req *pbmsg.SetSendMsgStatusReq) (*pbmsg.SetSendMsgStatusResp, error) {\n\tresp := &pbmsg.SetSendMsgStatusResp{}\n\tif err := m.MsgDatabase.SetSendMsgStatus(ctx, mcontext.GetOperationID(ctx), req.Status); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (m *msgServer) GetSendMsgStatus(ctx context.Context, req *pbmsg.GetSendMsgStatusReq) (*pbmsg.GetSendMsgStatusResp, error) {\n\tresp := &pbmsg.GetSendMsgStatusResp{}\n\tstatus, err := m.MsgDatabase.GetSendMsgStatus(ctx, mcontext.GetOperationID(ctx))\n\tif IsNotFound(err) {\n\t\tresp.Status = constant.MsgStatusNotExist\n\t\treturn resp, nil\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\tresp.Status = status\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/notification.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\ntype MsgNotificationSender struct {\n\t*notification.NotificationSender\n}\n\nfunc NewMsgNotificationSender(config *Config, opts ...notification.NotificationSenderOptions) *MsgNotificationSender {\n\treturn &MsgNotificationSender{notification.NewNotificationSender(&config.NotificationConfig, opts...)}\n}\n\nfunc (m *MsgNotificationSender) UserDeleteMsgsNotification(ctx context.Context, userID, conversationID string, seqs []int64) {\n\ttips := sdkws.DeleteMsgsTips{\n\t\tUserID:         userID,\n\t\tConversationID: conversationID,\n\t\tSeqs:           seqs,\n\t}\n\tm.Notification(ctx, userID, userID, constant.DeleteMsgsNotification, &tips)\n}\n\nfunc (m *MsgNotificationSender) MarkAsReadNotification(ctx context.Context, conversationID string, sessionType int32, sendID, recvID string, seqs []int64, hasReadSeq int64) {\n\ttips := &sdkws.MarkAsReadTips{\n\t\tMarkAsReadUserID: sendID,\n\t\tConversationID:   conversationID,\n\t\tSeqs:             seqs,\n\t\tHasReadSeq:       hasReadSeq,\n\t}\n\tm.NotificationWithSessionType(ctx, sendID, recvID, constant.HasReadReceipt, sessionType, tips)\n}\n"
  },
  {
    "path": "internal/rpc/msg/revoke.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc (m *msgServer) RevokeMsg(ctx context.Context, req *msg.RevokeMsgReq) (*msg.RevokeMsgResp, error) {\n\tif req.UserID == \"\" {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"user_id is empty\")\n\t}\n\tif req.ConversationID == \"\" {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"conversation_id is empty\")\n\t}\n\tif req.Seq < 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"seq is invalid\")\n\t}\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tuser, err := m.UserLocalCache.GetUserInfo(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, _, msgs, err := m.MsgDatabase.GetMsgBySeqs(ctx, req.UserID, req.ConversationID, []int64{req.Seq})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(msgs) == 0 || msgs[0] == nil {\n\t\treturn nil, errs.ErrRecordNotFound.WrapMsg(\"msg not found\")\n\t}\n\tif msgs[0].ContentType == constant.MsgRevokeNotification {\n\t\treturn nil, servererrs.ErrMsgAlreadyRevoke.WrapMsg(\"msg already revoke\")\n\t}\n\n\tdata, _ := json.Marshal(msgs[0])\n\tlog.ZDebug(ctx, \"GetMsgBySeqs\", \"conversationID\", req.ConversationID, \"seq\", req.Seq, \"msg\", string(data))\n\tvar role int32\n\tif !authverify.IsAdmin(ctx) {\n\t\tsessionType := msgs[0].SessionType\n\t\tswitch sessionType {\n\t\tcase constant.SingleChatType:\n\t\t\tif err := authverify.CheckAccess(ctx, msgs[0].SendID); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trole = user.AppMangerLevel\n\t\tcase constant.ReadGroupChatType:\n\t\t\tmembers, err := m.GroupLocalCache.GetGroupMemberInfoMap(ctx, msgs[0].GroupID, datautil.Distinct([]string{req.UserID, msgs[0].SendID}))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif req.UserID != msgs[0].SendID {\n\t\t\t\tswitch members[req.UserID].RoleLevel {\n\t\t\t\tcase constant.GroupOwner:\n\t\t\t\tcase constant.GroupAdmin:\n\t\t\t\t\tif sendMember, ok := members[msgs[0].SendID]; ok {\n\t\t\t\t\t\tif sendMember.RoleLevel != constant.GroupOrdinaryUsers {\n\t\t\t\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"no permission\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, errs.ErrNoPermission.WrapMsg(\"no permission\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tif member := members[req.UserID]; member != nil {\n\t\t\t\trole = member.RoleLevel\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, errs.ErrInternalServer.WrapMsg(\"msg sessionType not supported\", \"sessionType\", sessionType)\n\t\t}\n\t}\n\tnow := time.Now().UnixMilli()\n\terr = m.MsgDatabase.RevokeMsg(ctx, req.ConversationID, req.Seq, &model.RevokeModel{\n\t\tRole:     role,\n\t\tUserID:   req.UserID,\n\t\tNickname: user.Nickname,\n\t\tTime:     now,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trevokerUserID := mcontext.GetOpUserID(ctx)\n\tvar flag bool\n\n\tif len(m.config.Share.IMAdminUser.UserIDs) > 0 {\n\t\tflag = datautil.Contain(revokerUserID, m.adminUserIDs...)\n\t}\n\ttips := sdkws.RevokeMsgTips{\n\t\tRevokerUserID:  revokerUserID,\n\t\tClientMsgID:    msgs[0].ClientMsgID,\n\t\tRevokeTime:     now,\n\t\tSeq:            req.Seq,\n\t\tSesstionType:   msgs[0].SessionType,\n\t\tConversationID: req.ConversationID,\n\t\tIsAdminRevoke:  flag,\n\t}\n\tvar recvID string\n\tif msgs[0].SessionType == constant.ReadGroupChatType {\n\t\trecvID = msgs[0].GroupID\n\t} else {\n\t\trecvID = msgs[0].RecvID\n\t}\n\tm.notificationSender.NotificationWithSessionType(ctx, req.UserID, recvID, constant.MsgRevokeNotification, msgs[0].SessionType, &tips)\n\tm.webhookAfterRevokeMsg(ctx, &m.config.WebhooksConfig.AfterRevokeMsg, req)\n\treturn &msg.RevokeMsgResp{}, nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/send.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/util/conversationutil\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbconv \"github.com/openimsdk/protocol/conversation\"\n\tpbmsg \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/protocol/wrapperspb\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (*pbmsg.SendMsgResp, error) {\n\tif req.MsgData == nil {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"msgData is nil\")\n\t}\n\tif err := authverify.CheckAccess(ctx, req.MsgData.SendID); err != nil {\n\t\treturn nil, err\n\t}\n\tbefore := new(*sdkws.MsgData)\n\tresp, err := m.sendMsg(ctx, req, before)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif *before != nil && proto.Equal(*before, req.MsgData) == false {\n\t\tresp.Modify = req.MsgData\n\t}\n\treturn resp, nil\n}\n\nfunc (m *msgServer) sendMsg(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (*pbmsg.SendMsgResp, error) {\n\tm.encapsulateMsgData(req.MsgData)\n\tswitch req.MsgData.SessionType {\n\tcase constant.SingleChatType:\n\t\treturn m.sendMsgSingleChat(ctx, req, before)\n\tcase constant.NotificationChatType:\n\t\treturn m.sendMsgNotification(ctx, req, before)\n\tcase constant.ReadGroupChatType:\n\t\treturn m.sendMsgGroupChat(ctx, req, before)\n\tdefault:\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"unknown sessionType\")\n\t}\n}\n\nfunc (m *msgServer) sendMsgGroupChat(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) {\n\tif err = m.messageVerification(ctx, req); err != nil {\n\t\tprommetrics.GroupChatMsgProcessFailedCounter.Inc()\n\t\treturn nil, err\n\t}\n\n\tif err = m.webhookBeforeSendGroupMsg(ctx, &m.config.WebhooksConfig.BeforeSendGroupMsg, req); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := m.webhookBeforeMsgModify(ctx, &m.config.WebhooksConfig.BeforeMsgModify, req, before); err != nil {\n\t\treturn nil, err\n\t}\n\terr = m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForGroup(req.MsgData.GroupID), req.MsgData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif req.MsgData.ContentType == constant.AtText {\n\t\tgo m.setConversationAtInfo(ctx, req.MsgData)\n\t}\n\n\tm.webhookAfterSendGroupMsg(ctx, &m.config.WebhooksConfig.AfterSendGroupMsg, req)\n\n\tprommetrics.GroupChatMsgProcessSuccessCounter.Inc()\n\tresp = &pbmsg.SendMsgResp{}\n\tresp.SendTime = req.MsgData.SendTime\n\tresp.ServerMsgID = req.MsgData.ServerMsgID\n\tresp.ClientMsgID = req.MsgData.ClientMsgID\n\treturn resp, nil\n}\n\nfunc (m *msgServer) setConversationAtInfo(nctx context.Context, msg *sdkws.MsgData) {\n\n\tlog.ZDebug(nctx, \"setConversationAtInfo\", \"msg\", msg)\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.ZPanic(nctx, \"setConversationAtInfo Panic\", errs.ErrPanic(r))\n\t\t}\n\t}()\n\n\tctx := mcontext.NewCtx(\"@@@\" + mcontext.GetOperationID(nctx))\n\n\tvar atUserID []string\n\n\tconversation := &pbconv.ConversationReq{\n\t\tConversationID:   msgprocessor.GetConversationIDByMsg(msg),\n\t\tConversationType: msg.SessionType,\n\t\tGroupID:          msg.GroupID,\n\t}\n\tmemberUserIDList, err := m.GroupLocalCache.GetGroupMemberIDs(ctx, msg.GroupID)\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"GetGroupMemberIDs\", err)\n\t\treturn\n\t}\n\n\ttagAll := datautil.Contain(constant.AtAllString, msg.AtUserIDList...)\n\tif tagAll {\n\n\t\tmemberUserIDList = datautil.DeleteElems(memberUserIDList, msg.SendID)\n\n\t\tatUserID = datautil.Single([]string{constant.AtAllString}, msg.AtUserIDList)\n\n\t\tif len(atUserID) == 0 { // just @everyone\n\t\t\tconversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAll}\n\t\t} else { // @Everyone and @other people\n\t\t\tconversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAllAtMe}\n\t\t\tatUserID = datautil.SliceIntersectFuncs(atUserID, memberUserIDList, func(a string) string { return a }, func(b string) string {\n\t\t\t\treturn b\n\t\t\t})\n\t\t\tif err := m.conversationClient.SetConversations(ctx, atUserID, conversation); err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"SetConversations\", err, \"userID\", atUserID, \"conversation\", conversation)\n\t\t\t}\n\t\t\tmemberUserIDList = datautil.Single(atUserID, memberUserIDList)\n\t\t}\n\n\t\tconversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAll}\n\t\tif err := m.conversationClient.SetConversations(ctx, memberUserIDList, conversation); err != nil {\n\t\t\tlog.ZWarn(ctx, \"SetConversations\", err, \"userID\", memberUserIDList, \"conversation\", conversation)\n\t\t}\n\n\t\treturn\n\t}\n\tatUserID = datautil.SliceIntersectFuncs(msg.AtUserIDList, memberUserIDList, func(a string) string { return a }, func(b string) string {\n\t\treturn b\n\t})\n\tconversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtMe}\n\n\tif err := m.conversationClient.SetConversations(ctx, atUserID, conversation); err != nil {\n\t\tlog.ZWarn(ctx, \"SetConversations\", err, atUserID, conversation)\n\t}\n}\n\nfunc (m *msgServer) sendMsgNotification(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) {\n\tif err := m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &pbmsg.SendMsgResp{\n\t\tServerMsgID: req.MsgData.ServerMsgID,\n\t\tClientMsgID: req.MsgData.ClientMsgID,\n\t\tSendTime:    req.MsgData.SendTime,\n\t}\n\treturn resp, nil\n}\n\nfunc (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) {\n\tif err := m.messageVerification(ctx, req); err != nil {\n\t\treturn nil, err\n\t}\n\tisSend := true\n\tisNotification := msgprocessor.IsNotificationByMsg(req.MsgData)\n\tif !isNotification {\n\t\tisSend, err = m.modifyMessageByUserMessageReceiveOpt(authverify.WithTempAdmin(ctx), req.MsgData.RecvID, conversationutil.GenConversationIDForSingle(req.MsgData.SendID, req.MsgData.RecvID), constant.SingleChatType, req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif !isSend {\n\t\tprommetrics.SingleChatMsgProcessFailedCounter.Inc()\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"message is not sent\")\n\t} else {\n\t\tif err := m.webhookBeforeMsgModify(ctx, &m.config.WebhooksConfig.BeforeMsgModify, req, before); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil {\n\t\t\tprommetrics.SingleChatMsgProcessFailedCounter.Inc()\n\t\t\treturn nil, err\n\t\t}\n\n\t\tm.webhookAfterSendSingleMsg(ctx, &m.config.WebhooksConfig.AfterSendSingleMsg, req)\n\t\tprommetrics.SingleChatMsgProcessSuccessCounter.Inc()\n\t\treturn &pbmsg.SendMsgResp{\n\t\t\tServerMsgID: req.MsgData.ServerMsgID,\n\t\t\tClientMsgID: req.MsgData.ClientMsgID,\n\t\t\tSendTime:    req.MsgData.SendTime,\n\t\t}, nil\n\t}\n}\n\nfunc (m *msgServer) SendSimpleMsg(ctx context.Context, req *pbmsg.SendSimpleMsgReq) (*pbmsg.SendSimpleMsgResp, error) {\n\tif req.MsgData == nil {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"msg data is nil\")\n\t}\n\tsender, err := m.UserLocalCache.GetUserInfo(ctx, req.MsgData.SendID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.MsgData.SenderFaceURL = sender.FaceURL\n\treq.MsgData.SenderNickname = sender.Nickname\n\tresp, err := m.SendMsg(ctx, &pbmsg.SendMsgReq{MsgData: req.MsgData})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbmsg.SendSimpleMsgResp{\n\t\tServerMsgID: resp.ServerMsgID,\n\t\tClientMsgID: resp.ClientMsgID,\n\t\tSendTime:    resp.SendTime,\n\t\tModify:      resp.Modify,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/seq.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sort\"\n\n\tpbmsg \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc (m *msgServer) GetConversationMaxSeq(ctx context.Context, req *pbmsg.GetConversationMaxSeqReq) (*pbmsg.GetConversationMaxSeqResp, error) {\n\tmaxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID)\n\tif err != nil && !errors.Is(err, redis.Nil) {\n\t\treturn nil, err\n\t}\n\treturn &pbmsg.GetConversationMaxSeqResp{MaxSeq: maxSeq}, nil\n}\n\nfunc (m *msgServer) GetMaxSeqs(ctx context.Context, req *pbmsg.GetMaxSeqsReq) (*pbmsg.SeqsInfoResp, error) {\n\tmaxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, req.ConversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbmsg.SeqsInfoResp{MaxSeqs: maxSeqs}, nil\n}\n\nfunc (m *msgServer) GetHasReadSeqs(ctx context.Context, req *pbmsg.GetHasReadSeqsReq) (*pbmsg.SeqsInfoResp, error) {\n\thasReadSeqs, err := m.MsgDatabase.GetHasReadSeqs(ctx, req.UserID, req.ConversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbmsg.SeqsInfoResp{MaxSeqs: hasReadSeqs}, nil\n}\n\nfunc (m *msgServer) GetMsgByConversationIDs(ctx context.Context, req *pbmsg.GetMsgByConversationIDsReq) (*pbmsg.GetMsgByConversationIDsResp, error) {\n\tMsgs, err := m.MsgDatabase.FindOneByDocIDs(ctx, req.ConversationIDs, req.MaxSeqs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbmsg.GetMsgByConversationIDsResp{MsgDatas: Msgs}, nil\n}\n\nfunc (m *msgServer) SetUserConversationsMinSeq(ctx context.Context, req *pbmsg.SetUserConversationsMinSeqReq) (*pbmsg.SetUserConversationsMinSeqResp, error) {\n\tfor _, userID := range req.UserIDs {\n\t\tif err := m.MsgDatabase.SetUserConversationsMinSeqs(ctx, userID, map[string]int64{req.ConversationID: req.Seq}); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &pbmsg.SetUserConversationsMinSeqResp{}, nil\n}\n\nfunc (m *msgServer) GetActiveConversation(ctx context.Context, req *pbmsg.GetActiveConversationReq) (*pbmsg.GetActiveConversationResp, error) {\n\tres, err := m.MsgDatabase.GetCacheMaxSeqWithTime(ctx, req.ConversationIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconversations := make([]*pbmsg.ActiveConversation, 0, len(res))\n\tfor conversationID, val := range res {\n\t\tconversations = append(conversations, &pbmsg.ActiveConversation{\n\t\t\tMaxSeq:         val.Seq,\n\t\t\tLastTime:       val.Time,\n\t\t\tConversationID: conversationID,\n\t\t})\n\t}\n\tif req.Limit > 0 {\n\t\tsort.Sort(activeConversations(conversations))\n\t\tif len(conversations) > int(req.Limit) {\n\t\t\tconversations = conversations[:req.Limit]\n\t\t}\n\t}\n\treturn &pbmsg.GetActiveConversationResp{Conversations: conversations}, nil\n}\n\nfunc (m *msgServer) SetUserConversationMaxSeq(ctx context.Context, req *pbmsg.SetUserConversationMaxSeqReq) (*pbmsg.SetUserConversationMaxSeqResp, error) {\n\tfor _, userID := range req.OwnerUserID {\n\t\tif err := m.MsgDatabase.SetUserConversationsMaxSeq(ctx, req.ConversationID, userID, req.MaxSeq); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &pbmsg.SetUserConversationMaxSeqResp{}, nil\n}\n\nfunc (m *msgServer) SetUserConversationMinSeq(ctx context.Context, req *pbmsg.SetUserConversationMinSeqReq) (*pbmsg.SetUserConversationMinSeqResp, error) {\n\tfor _, userID := range req.OwnerUserID {\n\t\tif err := m.MsgDatabase.SetUserConversationsMinSeq(ctx, req.ConversationID, userID, req.MinSeq); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &pbmsg.SetUserConversationMinSeqResp{}, nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/server.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/mqbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpccache\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/discovery\"\n)\n\ntype MessageInterceptorFunc func(ctx context.Context, globalConfig *Config, req *msg.SendMsgReq) (*sdkws.MsgData, error)\n\n// MessageInterceptorChain defines a chain of message interceptor functions.\ntype MessageInterceptorChain []MessageInterceptorFunc\n\ntype Config struct {\n\tRpcConfig          config.Msg\n\tRedisConfig        config.Redis\n\tMongodbConfig      config.Mongo\n\tKafkaConfig        config.Kafka\n\tNotificationConfig config.Notification\n\tShare              config.Share\n\tWebhooksConfig     config.Webhooks\n\tLocalCacheConfig   config.LocalCache\n\tDiscovery          config.Discovery\n}\n\n// MsgServer encapsulates dependencies required for message handling.\ntype msgServer struct {\n\tmsg.UnimplementedMsgServer\n\tRegisterCenter         discovery.Conn                   // Service discovery registry for service registration.\n\tMsgDatabase            controller.CommonMsgDatabase     // Interface for message database operations.\n\tUserLocalCache         *rpccache.UserLocalCache         // Local cache for user data.\n\tFriendLocalCache       *rpccache.FriendLocalCache       // Local cache for friend data.\n\tGroupLocalCache        *rpccache.GroupLocalCache        // Local cache for group data.\n\tConversationLocalCache *rpccache.ConversationLocalCache // Local cache for conversation data.\n\tHandlers               MessageInterceptorChain          // Chain of handlers for processing messages.\n\tnotificationSender     *notification.NotificationSender // RPC client for sending notifications.\n\tmsgNotificationSender  *MsgNotificationSender           // RPC client for sending msg notifications.\n\tconfig                 *Config                          // Global configuration settings.\n\twebhookClient          *webhook.Client\n\tconversationClient     *rpcli.ConversationClient\n\n\tadminUserIDs []string\n}\n\nfunc (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) {\n\tm.Handlers = append(m.Handlers, interceptorFunc...)\n\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tbuilder := mqbuild.NewBuilder(&config.KafkaConfig)\n\tredisProducer, err := builder.GetTopicProducer(ctx, config.KafkaConfig.ToRedisTopic)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)\n\tmgocli, err := dbb.Mongo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsgDocModel, err := mgo.NewMsgMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar msgModel cache.MsgCache\n\tif rdb == nil {\n\t\tcm, err := mgo.NewCacheMgo(mgocli.GetDB())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmsgModel = mcache.NewMsgCache(cm, msgDocModel)\n\t} else {\n\t\tmsgModel = redis.NewMsgCache(rdb, msgDocModel)\n\t}\n\tseqConversation, err := mgo.NewSeqConversationMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tseqConversationCache := redis.NewSeqConversationCacheRedis(rdb, seqConversation)\n\tseqUser, err := mgo.NewSeqUserMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tseqUserCache := redis.NewSeqUserCacheRedis(rdb, seqUser)\n\tuserConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgroupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfriendConn, err := client.GetConn(ctx, config.Discovery.RpcService.Friend)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconversationConn, err := client.GetConn(ctx, config.Discovery.RpcService.Conversation)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconversationClient := rpcli.NewConversationClient(conversationConn)\n\tmsgDatabase := controller.NewCommonMsgDatabase(msgDocModel, msgModel, seqUserCache, seqConversationCache, redisProducer)\n\ts := &msgServer{\n\t\tMsgDatabase:            msgDatabase,\n\t\tRegisterCenter:         client,\n\t\tUserLocalCache:         rpccache.NewUserLocalCache(rpcli.NewUserClient(userConn), &config.LocalCacheConfig, rdb),\n\t\tGroupLocalCache:        rpccache.NewGroupLocalCache(rpcli.NewGroupClient(groupConn), &config.LocalCacheConfig, rdb),\n\t\tConversationLocalCache: rpccache.NewConversationLocalCache(conversationClient, &config.LocalCacheConfig, rdb),\n\t\tFriendLocalCache:       rpccache.NewFriendLocalCache(rpcli.NewRelationClient(friendConn), &config.LocalCacheConfig, rdb),\n\t\tconfig:                 config,\n\t\twebhookClient:          webhook.NewWebhookClient(config.WebhooksConfig.URL),\n\t\tconversationClient:     conversationClient,\n\t\tadminUserIDs:           config.Share.IMAdminUser.UserIDs,\n\t}\n\n\ts.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg))\n\ts.msgNotificationSender = NewMsgNotificationSender(config, notification.WithLocalSendMsg(s.SendMsg))\n\n\tmsg.RegisterMsgServer(server, s)\n\n\treturn nil\n}\n\nfunc (m *msgServer) conversationAndGetRecvID(conversation *conversation.Conversation, userID string) string {\n\tif conversation.ConversationType == constant.SingleChatType ||\n\t\tconversation.ConversationType == constant.NotificationChatType {\n\t\tif userID == conversation.OwnerUserID {\n\t\t\treturn conversation.UserID\n\t\t} else {\n\t\t\treturn conversation.OwnerUserID\n\t\t}\n\t} else if conversation.ConversationType == constant.ReadGroupChatType {\n\t\treturn conversation.GroupID\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/rpc/msg/statistics.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc (m *msgServer) GetActiveUser(ctx context.Context, req *msg.GetActiveUserReq) (*msg.GetActiveUserResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tmsgCount, userCount, users, dateCount, err := m.MsgDatabase.RangeUserSendCount(ctx, time.UnixMilli(req.Start), time.UnixMilli(req.End), req.Group, req.Ase, req.Pagination.PageNumber, req.Pagination.ShowNumber)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar pbUsers []*msg.ActiveUser\n\tif len(users) > 0 {\n\t\tuserIDs := datautil.Slice(users, func(e *model.UserCount) string { return e.UserID })\n\t\tuserMap, err := m.UserLocalCache.GetUsersInfoMap(ctx, userIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpbUsers = make([]*msg.ActiveUser, 0, len(users))\n\t\tfor _, user := range users {\n\t\t\tpbUser := userMap[user.UserID]\n\t\t\tif pbUser == nil {\n\t\t\t\tpbUser = &sdkws.UserInfo{\n\t\t\t\t\tUserID:   user.UserID,\n\t\t\t\t\tNickname: user.UserID,\n\t\t\t\t}\n\t\t\t}\n\t\t\tpbUsers = append(pbUsers, &msg.ActiveUser{\n\t\t\t\tUser:  pbUser,\n\t\t\t\tCount: user.Count,\n\t\t\t})\n\t\t}\n\t}\n\treturn &msg.GetActiveUserResp{\n\t\tMsgCount:  msgCount,\n\t\tUserCount: userCount,\n\t\tDateCount: dateCount,\n\t\tUsers:     pbUsers,\n\t}, nil\n}\n\nfunc (m *msgServer) GetActiveGroup(ctx context.Context, req *msg.GetActiveGroupReq) (*msg.GetActiveGroupResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tmsgCount, groupCount, groups, dateCount, err := m.MsgDatabase.RangeGroupSendCount(ctx, time.UnixMilli(req.Start), time.UnixMilli(req.End), req.Ase, req.Pagination.PageNumber, req.Pagination.ShowNumber)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar pbgroups []*msg.ActiveGroup\n\tif len(groups) > 0 {\n\t\tgroupIDs := datautil.Slice(groups, func(e *model.GroupCount) string { return e.GroupID })\n\t\tresp, err := m.GroupLocalCache.GetGroupInfos(ctx, groupIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tgroupMap := make(map[string]*sdkws.GroupInfo, len(groups))\n\t\tfor i, group := range groups {\n\t\t\tgroupMap[group.GroupID] = resp[i]\n\t\t}\n\t\tpbgroups = make([]*msg.ActiveGroup, 0, len(groups))\n\t\tfor _, group := range groups {\n\t\t\tpbgroup := groupMap[group.GroupID]\n\t\t\tif pbgroup == nil {\n\t\t\t\tpbgroup = &sdkws.GroupInfo{\n\t\t\t\t\tGroupID:   group.GroupID,\n\t\t\t\t\tGroupName: group.GroupID,\n\t\t\t\t}\n\t\t\t}\n\t\t\tpbgroups = append(pbgroups, &msg.ActiveGroup{\n\t\t\t\tGroup: pbgroup,\n\t\t\t\tCount: group.Count,\n\t\t\t})\n\t\t}\n\t}\n\treturn &msg.GetActiveGroupResp{\n\t\tMsgCount:   msgCount,\n\t\tGroupCount: groupCount,\n\t\tDateCount:  dateCount,\n\t\tGroups:     pbgroups,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/sync_msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/util/conversationutil\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n)\n\nfunc (m *msgServer) PullMessageBySeqs(ctx context.Context, req *sdkws.PullMessageBySeqsReq) (*sdkws.PullMessageBySeqsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tresp := &sdkws.PullMessageBySeqsResp{}\n\tresp.Msgs = make(map[string]*sdkws.PullMsgs)\n\tresp.NotificationMsgs = make(map[string]*sdkws.PullMsgs)\n\tfor _, seq := range req.SeqRanges {\n\t\tif !msgprocessor.IsNotification(seq.ConversationID) {\n\t\t\tconversation, err := m.ConversationLocalCache.GetConversation(ctx, req.UserID, seq.ConversationID)\n\t\t\tif err != nil {\n\t\t\t\tlog.ZError(ctx, \"GetConversation error\", err, \"conversationID\", seq.ConversationID)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tminSeq, maxSeq, msgs, err := m.MsgDatabase.GetMsgBySeqsRange(ctx, req.UserID, seq.ConversationID,\n\t\t\t\tseq.Begin, seq.End, seq.Num, conversation.MaxSeq)\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"GetMsgBySeqsRange error\", err, \"conversationID\", seq.ConversationID, \"seq\", seq)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar isEnd bool\n\t\t\tswitch req.Order {\n\t\t\tcase sdkws.PullOrder_PullOrderAsc:\n\t\t\t\tisEnd = maxSeq <= seq.End\n\t\t\tcase sdkws.PullOrder_PullOrderDesc:\n\t\t\t\tisEnd = seq.Begin <= minSeq\n\t\t\t}\n\t\t\tif len(msgs) == 0 {\n\t\t\t\tlog.ZWarn(ctx, \"not have msgs\", nil, \"conversationID\", seq.ConversationID, \"seq\", seq)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresp.Msgs[seq.ConversationID] = &sdkws.PullMsgs{Msgs: msgs, IsEnd: isEnd}\n\t\t} else {\n\t\t\tvar seqs []int64\n\t\t\tfor i := seq.Begin; i <= seq.End; i++ {\n\t\t\t\tseqs = append(seqs, i)\n\t\t\t}\n\t\t\tminSeq, maxSeq, notificationMsgs, err := m.MsgDatabase.GetMsgBySeqs(ctx, req.UserID, seq.ConversationID, seqs)\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"GetMsgBySeqs error\", err, \"conversationID\", seq.ConversationID, \"seq\", seq)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar isEnd bool\n\t\t\tswitch req.Order {\n\t\t\tcase sdkws.PullOrder_PullOrderAsc:\n\t\t\t\tisEnd = maxSeq <= seq.End\n\t\t\tcase sdkws.PullOrder_PullOrderDesc:\n\t\t\t\tisEnd = seq.Begin <= minSeq\n\t\t\t}\n\t\t\tif len(notificationMsgs) == 0 {\n\t\t\t\tlog.ZWarn(ctx, \"not have notificationMsgs\", nil, \"conversationID\", seq.ConversationID, \"seq\", seq)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresp.NotificationMsgs[seq.ConversationID] = &sdkws.PullMsgs{Msgs: notificationMsgs, IsEnd: isEnd}\n\t\t}\n\t}\n\treturn resp, nil\n}\n\nfunc (m *msgServer) GetSeqMessage(ctx context.Context, req *msg.GetSeqMessageReq) (*msg.GetSeqMessageResp, error) {\n\tresp := &msg.GetSeqMessageResp{\n\t\tMsgs:             make(map[string]*sdkws.PullMsgs),\n\t\tNotificationMsgs: make(map[string]*sdkws.PullMsgs),\n\t}\n\tfor _, conv := range req.Conversations {\n\t\tisEnd, endSeq, msgs, err := m.MsgDatabase.GetMessagesBySeqWithBounds(ctx, req.UserID, conv.ConversationID, conv.Seqs, req.GetOrder())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar pullMsgs *sdkws.PullMsgs\n\t\tif ok := false; conversationutil.IsNotificationConversationID(conv.ConversationID) {\n\t\t\tpullMsgs, ok = resp.NotificationMsgs[conv.ConversationID]\n\t\t\tif !ok {\n\t\t\t\tpullMsgs = &sdkws.PullMsgs{}\n\t\t\t\tresp.NotificationMsgs[conv.ConversationID] = pullMsgs\n\t\t\t}\n\t\t} else {\n\t\t\tpullMsgs, ok = resp.Msgs[conv.ConversationID]\n\t\t\tif !ok {\n\t\t\t\tpullMsgs = &sdkws.PullMsgs{}\n\t\t\t\tresp.Msgs[conv.ConversationID] = pullMsgs\n\t\t\t}\n\t\t}\n\t\tpullMsgs.Msgs = append(pullMsgs.Msgs, msgs...)\n\t\tpullMsgs.IsEnd = isEnd\n\t\tpullMsgs.EndSeq = endSeq\n\t}\n\treturn resp, nil\n}\n\nfunc (m *msgServer) GetMaxSeq(ctx context.Context, req *sdkws.GetMaxSeqReq) (*sdkws.GetMaxSeqResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tconversationIDs, err := m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, conversationID := range conversationIDs {\n\t\tconversationIDs = append(conversationIDs, conversationutil.GetNotificationConversationIDByConversationID(conversationID))\n\t}\n\tconversationIDs = append(conversationIDs, conversationutil.GetSelfNotificationConversationID(req.UserID))\n\tlog.ZDebug(ctx, \"GetMaxSeq\", \"conversationIDs\", conversationIDs)\n\tmaxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, conversationIDs)\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"GetMaxSeqs error\", err, \"conversationIDs\", conversationIDs, \"maxSeqs\", maxSeqs)\n\t\treturn nil, err\n\t}\n\t// avoid pulling messages from sessions with a large number of max seq values of 0\n\tfor conversationID, seq := range maxSeqs {\n\t\tif seq == 0 {\n\t\t\tdelete(maxSeqs, conversationID)\n\t\t}\n\t}\n\tresp := new(sdkws.GetMaxSeqResp)\n\tresp.MaxSeqs = maxSeqs\n\treturn resp, nil\n}\n\nfunc (m *msgServer) SearchMessage(ctx context.Context, req *msg.SearchMessageReq) (resp *msg.SearchMessageResp, err error) {\n\t// var chatLogs []*sdkws.MsgData\n\tvar chatLogs []*msg.SearchedMsgData\n\tvar total int64\n\tresp = &msg.SearchMessageResp{}\n\tif total, chatLogs, err = m.MsgDatabase.SearchMessage(ctx, req); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar (\n\t\tsendIDs  []string\n\t\trecvIDs  []string\n\t\tgroupIDs []string\n\t\tsendMap  = make(map[string]string)\n\t\trecvMap  = make(map[string]string)\n\t\tgroupMap = make(map[string]*sdkws.GroupInfo)\n\t)\n\n\tfor _, chatLog := range chatLogs {\n\t\tif chatLog.MsgData.SenderNickname == \"\" {\n\t\t\tsendIDs = append(sendIDs, chatLog.MsgData.SendID)\n\t\t}\n\t\tswitch chatLog.MsgData.SessionType {\n\t\tcase constant.SingleChatType, constant.NotificationChatType:\n\t\t\trecvIDs = append(recvIDs, chatLog.MsgData.RecvID)\n\t\tcase constant.WriteGroupChatType, constant.ReadGroupChatType:\n\t\t\tgroupIDs = append(groupIDs, chatLog.MsgData.GroupID)\n\t\t}\n\t}\n\n\t// Retrieve sender and receiver information\n\tif len(sendIDs) != 0 {\n\t\tsendInfos, err := m.UserLocalCache.GetUsersInfo(ctx, sendIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, sendInfo := range sendInfos {\n\t\t\tsendMap[sendInfo.UserID] = sendInfo.Nickname\n\t\t}\n\t}\n\n\tif len(recvIDs) != 0 {\n\t\trecvInfos, err := m.UserLocalCache.GetUsersInfo(ctx, recvIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, recvInfo := range recvInfos {\n\t\t\trecvMap[recvInfo.UserID] = recvInfo.Nickname\n\t\t}\n\t}\n\n\t// Retrieve group information including member counts\n\tif len(groupIDs) != 0 {\n\t\tgroupInfos, err := m.GroupLocalCache.GetGroupInfos(ctx, groupIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, groupInfo := range groupInfos {\n\t\t\tgroupMap[groupInfo.GroupID] = groupInfo\n\t\t\t// Get actual member count\n\t\t\tmemberIDs, err := m.GroupLocalCache.GetGroupMemberIDs(ctx, groupInfo.GroupID)\n\t\t\tif err == nil {\n\t\t\t\tgroupInfo.MemberCount = uint32(len(memberIDs)) // Update the member count with actual number\n\t\t\t}\n\t\t}\n\t}\n\n\t// Construct response with updated information\n\tfor _, chatLog := range chatLogs {\n\t\tpbchatLog := &msg.ChatLog{}\n\t\tdatautil.CopyStructFields(pbchatLog, chatLog.MsgData)\n\t\tpbchatLog.SendTime = chatLog.MsgData.SendTime\n\t\tpbchatLog.CreateTime = chatLog.MsgData.CreateTime\n\t\tif chatLog.MsgData.SenderNickname == \"\" {\n\t\t\tpbchatLog.SenderNickname = sendMap[chatLog.MsgData.SendID]\n\t\t}\n\t\tswitch chatLog.MsgData.SessionType {\n\t\tcase constant.SingleChatType, constant.NotificationChatType:\n\t\t\tpbchatLog.RecvNickname = recvMap[chatLog.MsgData.RecvID]\n\t\tcase constant.ReadGroupChatType:\n\t\t\tgroupInfo := groupMap[chatLog.MsgData.GroupID]\n\t\t\tpbchatLog.SenderFaceURL = groupInfo.FaceURL\n\t\t\tpbchatLog.GroupMemberCount = groupInfo.MemberCount // Reflects actual member count\n\t\t\tpbchatLog.RecvID = groupInfo.GroupID\n\t\t\tpbchatLog.GroupName = groupInfo.GroupName\n\t\t\tpbchatLog.GroupOwner = groupInfo.OwnerUserID\n\t\t\tpbchatLog.GroupType = groupInfo.GroupType\n\t\t}\n\t\tsearchChatLog := &msg.SearchChatLog{ChatLog: pbchatLog, IsRevoked: chatLog.IsRevoked}\n\n\t\tresp.ChatLogs = append(resp.ChatLogs, searchChatLog)\n\t}\n\tresp.ChatLogsNum = int32(total)\n\treturn resp, nil\n}\n\nfunc (m *msgServer) GetServerTime(ctx context.Context, _ *msg.GetServerTimeReq) (*msg.GetServerTimeResp, error) {\n\treturn &msg.GetServerTimeResp{ServerTime: timeutil.GetCurrentTimestampByMill()}, nil\n}\n\nfunc (m *msgServer) GetLastMessage(ctx context.Context, req *msg.GetLastMessageReq) (*msg.GetLastMessageResp, error) {\n\tmsgs, err := m.MsgDatabase.GetLastMessage(ctx, req.ConversationIDs, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &msg.GetLastMessageResp{Msgs: msgs}, nil\n}\n"
  },
  {
    "path": "internal/rpc/msg/utils.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n)\n\nfunc IsNotFound(err error) bool {\n\tswitch errs.Unwrap(err) {\n\tcase redis.Nil, mongo.ErrNoDocuments:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\ntype activeConversations []*msg.ActiveConversation\n\nfunc (s activeConversations) Len() int {\n\treturn len(s)\n}\n\nfunc (s activeConversations) Less(i, j int) bool {\n\treturn s[i].LastTime > s[j].LastTime\n}\n\nfunc (s activeConversations) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\n//type seqTime struct {\n//\tConversationID string\n//\tSeq            int64\n//\tTime           int64\n//\tUnread         int64\n//\tPinned         bool\n//}\n//\n//func (s seqTime) String() string {\n//\treturn fmt.Sprintf(\"<Time_%d,Unread_%d,Pinned_%t>\", s.Time, s.Unread, s.Pinned)\n//}\n//\n//type seqTimes []seqTime\n//\n//func (s seqTimes) Len() int {\n//\treturn len(s)\n//}\n//\n//// Less sticky priority, unread priority, time descending\n//func (s seqTimes) Less(i, j int) bool {\n//\tiv, jv := s[i], s[j]\n//\tif iv.Pinned && (!jv.Pinned) {\n//\t\treturn true\n//\t}\n//\tif jv.Pinned && (!iv.Pinned) {\n//\t\treturn false\n//\t}\n//\tif iv.Unread > 0 && jv.Unread == 0 {\n//\t\treturn true\n//\t}\n//\tif jv.Unread > 0 && iv.Unread == 0 {\n//\t\treturn false\n//\t}\n//\treturn iv.Time > jv.Time\n//}\n//\n//func (s seqTimes) Swap(i, j int) {\n//\ts[i], s[j] = s[j], s[i]\n//}\n//\n//type conversationStatus struct {\n//\tConversationID string\n//\tPinned         bool\n//\tRecv           bool\n//}\n"
  },
  {
    "path": "internal/rpc/msg/verify.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msg\n\nimport (\n\t\"context\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/encrypt\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nvar ExcludeContentType = []int{constant.HasReadReceipt}\n\ntype Validator interface {\n\tvalidate(pb *msg.SendMsgReq) (bool, int32, string)\n}\n\ntype MessageRevoked struct {\n\tRevokerID                   string `json:\"revokerID\"`\n\tRevokerRole                 int32  `json:\"revokerRole\"`\n\tClientMsgID                 string `json:\"clientMsgID\"`\n\tRevokerNickname             string `json:\"revokerNickname\"`\n\tRevokeTime                  int64  `json:\"revokeTime\"`\n\tSourceMessageSendTime       int64  `json:\"sourceMessageSendTime\"`\n\tSourceMessageSendID         string `json:\"sourceMessageSendID\"`\n\tSourceMessageSenderNickname string `json:\"sourceMessageSenderNickname\"`\n\tSessionType                 int32  `json:\"sessionType\"`\n\tSeq                         uint32 `json:\"seq\"`\n}\n\nfunc (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgReq) error {\n\tswitch data.MsgData.SessionType {\n\tcase constant.SingleChatType:\n\t\tif datautil.Contain(data.MsgData.SendID, m.adminUserIDs...) {\n\t\t\treturn nil\n\t\t}\n\t\tif data.MsgData.ContentType <= constant.NotificationEnd &&\n\t\t\tdata.MsgData.ContentType >= constant.NotificationBegin {\n\t\t\treturn nil\n\t\t}\n\t\tif err := m.webhookBeforeSendSingleMsg(ctx, &m.config.WebhooksConfig.BeforeSendSingleMsg, data); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tu, err := m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif authverify.CheckSystemAccount(ctx, u.AppMangerLevel) {\n\t\t\treturn nil\n\t\t}\n\t\tblack, err := m.FriendLocalCache.IsBlack(ctx, data.MsgData.SendID, data.MsgData.RecvID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif black {\n\t\t\treturn servererrs.ErrBlockedByPeer.Wrap()\n\t\t}\n\t\tif m.config.RpcConfig.FriendVerify {\n\t\t\tfriend, err := m.FriendLocalCache.IsFriend(ctx, data.MsgData.SendID, data.MsgData.RecvID)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !friend {\n\t\t\t\treturn servererrs.ErrNotPeersFriend.Wrap()\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\treturn nil\n\tcase constant.ReadGroupChatType:\n\t\tgroupInfo, err := m.GroupLocalCache.GetGroupInfo(ctx, data.MsgData.GroupID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif groupInfo.Status == constant.GroupStatusDismissed &&\n\t\t\tdata.MsgData.ContentType != constant.GroupDismissedNotification {\n\t\t\treturn servererrs.ErrDismissedAlready.Wrap()\n\t\t}\n\t\tif groupInfo.GroupType == constant.SuperGroup {\n\t\t\treturn nil\n\t\t}\n\n\t\tif datautil.Contain(data.MsgData.SendID, m.adminUserIDs...) {\n\t\t\treturn nil\n\t\t}\n\t\tif data.MsgData.ContentType <= constant.NotificationEnd &&\n\t\t\tdata.MsgData.ContentType >= constant.NotificationBegin {\n\t\t\treturn nil\n\t\t}\n\t\tmemberIDs, err := m.GroupLocalCache.GetGroupMemberIDMap(ctx, data.MsgData.GroupID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, ok := memberIDs[data.MsgData.SendID]; !ok {\n\t\t\treturn servererrs.ErrNotInGroupYet.Wrap()\n\t\t}\n\n\t\tgroupMemberInfo, err := m.GroupLocalCache.GetGroupMember(ctx, data.MsgData.GroupID, data.MsgData.SendID)\n\t\tif err != nil {\n\t\t\tif errs.ErrRecordNotFound.Is(err) {\n\t\t\t\treturn servererrs.ErrNotInGroupYet.WrapMsg(err.Error())\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tif groupMemberInfo.RoleLevel == constant.GroupOwner {\n\t\t\treturn nil\n\t\t} else {\n\t\t\tif groupMemberInfo.MuteEndTime >= time.Now().UnixMilli() {\n\t\t\t\treturn servererrs.ErrMutedInGroup.Wrap()\n\t\t\t}\n\t\t\tif groupInfo.Status == constant.GroupStatusMuted && groupMemberInfo.RoleLevel != constant.GroupAdmin {\n\t\t\t\treturn servererrs.ErrMutedGroup.Wrap()\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (m *msgServer) encapsulateMsgData(msg *sdkws.MsgData) {\n\tmsg.ServerMsgID = GetMsgID(msg.SendID)\n\tif msg.SendTime == 0 {\n\t\tmsg.SendTime = timeutil.GetCurrentTimestampByMill()\n\t}\n\tswitch msg.ContentType {\n\tcase constant.Text, constant.Picture, constant.Voice, constant.Video,\n\t\tconstant.File, constant.AtText, constant.Merger, constant.Card,\n\t\tconstant.Location, constant.Custom, constant.Quote, constant.AdvancedText, constant.MarkdownText:\n\tcase constant.Revoke:\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)\n\tcase constant.HasReadReceipt:\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsConversationUpdate, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsSenderConversationUpdate, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)\n\tcase constant.Typing:\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsHistory, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsPersistent, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsSenderSync, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsConversationUpdate, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsSenderConversationUpdate, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)\n\t\tdatautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)\n\t}\n}\n\nfunc GetMsgID(sendID string) string {\n\tt := timeutil.GetCurrentTimeFormatted()\n\treturn encrypt.Md5(t + \"-\" + sendID + \"-\" + strconv.Itoa(rand.Int()))\n}\n\nfunc (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, userID, conversationID string, sessionType int, pb *msg.SendMsgReq) (bool, error) {\n\topt, err := m.UserLocalCache.GetUserGlobalMsgRecvOpt(ctx, userID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tswitch opt {\n\tcase constant.ReceiveMessage:\n\tcase constant.NotReceiveMessage:\n\t\treturn false, nil\n\tcase constant.ReceiveNotNotifyMessage:\n\t\tif pb.MsgData.Options == nil {\n\t\t\tpb.MsgData.Options = make(map[string]bool, 10)\n\t\t}\n\t\tdatautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)\n\t\treturn true, nil\n\t}\n\tsingleOpt, err := m.ConversationLocalCache.GetSingleConversationRecvMsgOpt(ctx, userID, conversationID)\n\tif errs.ErrRecordNotFound.Is(err) {\n\t\treturn true, nil\n\t} else if err != nil {\n\t\treturn false, err\n\t}\n\tswitch singleOpt {\n\tcase constant.ReceiveMessage:\n\t\treturn true, nil\n\tcase constant.NotReceiveMessage:\n\t\tif datautil.Contain(int(pb.MsgData.ContentType), ExcludeContentType...) {\n\t\t\treturn true, nil\n\t\t}\n\t\treturn false, nil\n\tcase constant.ReceiveNotNotifyMessage:\n\t\tif pb.MsgData.Options == nil {\n\t\t\tpb.MsgData.Options = make(map[string]bool, 10)\n\t\t}\n\t\tdatautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)\n\t\treturn true, nil\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "internal/rpc/relation/black.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage relation\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/relation\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc (s *friendServer) GetPaginationBlacks(ctx context.Context, req *relation.GetPaginationBlacksReq) (resp *relation.GetPaginationBlacksResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, blacks, err := s.blackDatabase.FindOwnerBlacks(ctx, req.UserID, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &relation.GetPaginationBlacksResp{}\n\tresp.Blacks, err = convert.BlackDB2Pb(ctx, blacks, s.userClient.GetUsersInfoMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp.Total = int32(total)\n\treturn resp, nil\n}\n\nfunc (s *friendServer) IsBlack(ctx context.Context, req *relation.IsBlackReq) (*relation.IsBlackResp, error) {\n\tif err := authverify.CheckAccessIn(ctx, req.UserID1, req.UserID2); err != nil {\n\t\treturn nil, err\n\t}\n\tin1, in2, err := s.blackDatabase.CheckIn(ctx, req.UserID1, req.UserID2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp := &relation.IsBlackResp{}\n\tresp.InUser1Blacks = in1\n\tresp.InUser2Blacks = in2\n\treturn resp, nil\n}\n\nfunc (s *friendServer) RemoveBlack(ctx context.Context, req *relation.RemoveBlackReq) (*relation.RemoveBlackResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := s.blackDatabase.Delete(ctx, []*model.Black{{OwnerUserID: req.OwnerUserID, BlockUserID: req.BlackUserID}}); err != nil {\n\t\treturn nil, err\n\t}\n\n\ts.notificationSender.BlackDeletedNotification(ctx, req)\n\ts.webhookAfterRemoveBlack(ctx, &s.config.WebhooksConfig.AfterRemoveBlack, req)\n\n\treturn &relation.RemoveBlackResp{}, nil\n}\n\nfunc (s *friendServer) AddBlack(ctx context.Context, req *relation.AddBlackReq) (*relation.AddBlackResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := s.webhookBeforeAddBlack(ctx, &s.config.WebhooksConfig.BeforeAddBlack, req); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := s.userClient.CheckUser(ctx, []string{req.OwnerUserID, req.BlackUserID}); err != nil {\n\t\treturn nil, err\n\t}\n\tblack := model.Black{\n\t\tOwnerUserID:    req.OwnerUserID,\n\t\tBlockUserID:    req.BlackUserID,\n\t\tOperatorUserID: mcontext.GetOpUserID(ctx),\n\t\tCreateTime:     time.Now(),\n\t\tEx:             req.Ex,\n\t}\n\n\tif err := s.blackDatabase.Create(ctx, []*model.Black{&black}); err != nil {\n\t\treturn nil, err\n\t}\n\ts.notificationSender.BlackAddedNotification(ctx, req)\n\treturn &relation.AddBlackResp{}, nil\n}\n\nfunc (s *friendServer) GetSpecifiedBlacks(ctx context.Context, req *relation.GetSpecifiedBlacksReq) (*relation.GetSpecifiedBlacksResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(req.UserIDList) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userIDList is empty\")\n\t}\n\n\tif datautil.Duplicate(req.UserIDList) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userIDList repeated\")\n\t}\n\n\tuserMap, err := s.userClient.GetUsersInfoMap(ctx, req.UserIDList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblacks, err := s.blackDatabase.FindBlackInfos(ctx, req.OwnerUserID, req.UserIDList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblackMap := datautil.SliceToMap(blacks, func(e *model.Black) string {\n\t\treturn e.BlockUserID\n\t})\n\n\tresp := &relation.GetSpecifiedBlacksResp{\n\t\tBlacks: make([]*sdkws.BlackInfo, 0, len(req.UserIDList)),\n\t}\n\n\ttoPublcUser := func(userID string) *sdkws.PublicUserInfo {\n\t\tv, ok := userMap[userID]\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\treturn &sdkws.PublicUserInfo{\n\t\t\tUserID:   v.UserID,\n\t\t\tNickname: v.Nickname,\n\t\t\tFaceURL:  v.FaceURL,\n\t\t\tEx:       v.Ex,\n\t\t}\n\t}\n\n\tfor _, userID := range req.UserIDList {\n\t\tif black := blackMap[userID]; black != nil {\n\t\t\tresp.Blacks = append(resp.Blacks,\n\t\t\t\t&sdkws.BlackInfo{\n\t\t\t\t\tOwnerUserID:    black.OwnerUserID,\n\t\t\t\t\tCreateTime:     black.CreateTime.UnixMilli(),\n\t\t\t\t\tBlackUserInfo:  toPublcUser(userID),\n\t\t\t\t\tAddSource:      black.AddSource,\n\t\t\t\t\tOperatorUserID: black.OperatorUserID,\n\t\t\t\t\tEx:             black.Ex,\n\t\t\t\t})\n\t\t}\n\t}\n\n\tresp.Total = int32(len(resp.Blacks))\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/rpc/relation/callback.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage relation\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\n\tcbapi \"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/relation\"\n)\n\nfunc (s *friendServer) webhookAfterDeleteFriend(ctx context.Context, after *config.AfterConfig, req *relation.DeleteFriendReq) {\n\tcbReq := &cbapi.CallbackAfterDeleteFriendReq{\n\t\tCallbackCommand: cbapi.CallbackAfterDeleteFriendCommand,\n\t\tOwnerUserID:     req.OwnerUserID,\n\t\tFriendUserID:    req.FriendUserID,\n\t}\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterDeleteFriendResp{}, after)\n}\n\nfunc (s *friendServer) webhookBeforeAddFriend(ctx context.Context, before *config.BeforeConfig, req *relation.ApplyToAddFriendReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &cbapi.CallbackBeforeAddFriendReq{\n\t\t\tCallbackCommand: cbapi.CallbackBeforeAddFriendCommand,\n\t\t\tFromUserID:      req.FromUserID,\n\t\t\tToUserID:        req.ToUserID,\n\t\t\tReqMsg:          req.ReqMsg,\n\t\t\tEx:              req.Ex,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeAddFriendResp{}\n\n\t\tif err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (s *friendServer) webhookAfterAddFriend(ctx context.Context, after *config.AfterConfig, req *relation.ApplyToAddFriendReq) {\n\tcbReq := &cbapi.CallbackAfterAddFriendReq{\n\t\tCallbackCommand: cbapi.CallbackAfterAddFriendCommand,\n\t\tFromUserID:      req.FromUserID,\n\t\tToUserID:        req.ToUserID,\n\t\tReqMsg:          req.ReqMsg,\n\t}\n\tresp := &cbapi.CallbackAfterAddFriendResp{}\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)\n}\n\nfunc (s *friendServer) webhookAfterSetFriendRemark(ctx context.Context, after *config.AfterConfig, req *relation.SetFriendRemarkReq) {\n\tcbReq := &cbapi.CallbackAfterSetFriendRemarkReq{\n\t\tCallbackCommand: cbapi.CallbackAfterSetFriendRemarkCommand,\n\t\tOwnerUserID:     req.OwnerUserID,\n\t\tFriendUserID:    req.FriendUserID,\n\t\tRemark:          req.Remark,\n\t}\n\tresp := &cbapi.CallbackAfterSetFriendRemarkResp{}\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)\n}\n\nfunc (s *friendServer) webhookAfterImportFriends(ctx context.Context, after *config.AfterConfig, req *relation.ImportFriendReq) {\n\tcbReq := &cbapi.CallbackAfterImportFriendsReq{\n\t\tCallbackCommand: cbapi.CallbackAfterImportFriendsCommand,\n\t\tOwnerUserID:     req.OwnerUserID,\n\t\tFriendUserIDs:   req.FriendUserIDs,\n\t}\n\tresp := &cbapi.CallbackAfterImportFriendsResp{}\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)\n}\n\nfunc (s *friendServer) webhookAfterRemoveBlack(ctx context.Context, after *config.AfterConfig, req *relation.RemoveBlackReq) {\n\tcbReq := &cbapi.CallbackAfterRemoveBlackReq{\n\t\tCallbackCommand: cbapi.CallbackAfterRemoveBlackCommand,\n\t\tOwnerUserID:     req.OwnerUserID,\n\t\tBlackUserID:     req.BlackUserID,\n\t}\n\tresp := &cbapi.CallbackAfterRemoveBlackResp{}\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)\n}\n\nfunc (s *friendServer) webhookBeforeSetFriendRemark(ctx context.Context, before *config.BeforeConfig, req *relation.SetFriendRemarkReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &cbapi.CallbackBeforeSetFriendRemarkReq{\n\t\t\tCallbackCommand: cbapi.CallbackBeforeSetFriendRemarkCommand,\n\t\t\tOwnerUserID:     req.OwnerUserID,\n\t\t\tFriendUserID:    req.FriendUserID,\n\t\t\tRemark:          req.Remark,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeSetFriendRemarkResp{}\n\t\tif err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif resp.Remark != \"\" {\n\t\t\treq.Remark = resp.Remark\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (s *friendServer) webhookBeforeAddBlack(ctx context.Context, before *config.BeforeConfig, req *relation.AddBlackReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &cbapi.CallbackBeforeAddBlackReq{\n\t\t\tCallbackCommand: cbapi.CallbackBeforeAddBlackCommand,\n\t\t\tOwnerUserID:     req.OwnerUserID,\n\t\t\tBlackUserID:     req.BlackUserID,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeAddBlackResp{}\n\t\treturn s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before)\n\t})\n}\n\nfunc (s *friendServer) webhookBeforeAddFriendAgree(ctx context.Context, before *config.BeforeConfig, req *relation.RespondFriendApplyReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &cbapi.CallbackBeforeAddFriendAgreeReq{\n\t\t\tCallbackCommand: cbapi.CallbackBeforeAddFriendAgreeCommand,\n\t\t\tFromUserID:      req.FromUserID,\n\t\t\tToUserID:        req.ToUserID,\n\t\t\tHandleMsg:       req.HandleMsg,\n\t\t\tHandleResult:    req.HandleResult,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeAddFriendAgreeResp{}\n\t\treturn s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before)\n\t})\n}\n\nfunc (s *friendServer) webhookAfterAddFriendAgree(ctx context.Context, after *config.AfterConfig, req *relation.RespondFriendApplyReq) {\n\tcbReq := &cbapi.CallbackAfterAddFriendAgreeReq{\n\t\tCallbackCommand: cbapi.CallbackAfterAddFriendAgreeCommand,\n\t\tFromUserID:      req.FromUserID,\n\t\tToUserID:        req.ToUserID,\n\t\tHandleMsg:       req.HandleMsg,\n\t\tHandleResult:    req.HandleResult,\n\t}\n\tresp := &cbapi.CallbackAfterAddFriendAgreeResp{}\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)\n}\n\nfunc (s *friendServer) webhookBeforeImportFriends(ctx context.Context, before *config.BeforeConfig, req *relation.ImportFriendReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &cbapi.CallbackBeforeImportFriendsReq{\n\t\t\tCallbackCommand: cbapi.CallbackBeforeImportFriendsCommand,\n\t\t\tOwnerUserID:     req.OwnerUserID,\n\t\t\tFriendUserIDs:   req.FriendUserIDs,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeImportFriendsResp{}\n\t\tif err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(resp.FriendUserIDs) > 0 {\n\t\t\treq.FriendUserIDs = resp.FriendUserIDs\n\t\t}\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "internal/rpc/relation/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage relation\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification/common_user\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\n\t\"github.com/openimsdk/tools/mq/memamq\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/relation\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"google.golang.org/grpc\"\n)\n\ntype friendServer struct {\n\trelation.UnimplementedFriendServer\n\tdb                 controller.FriendDatabase\n\tblackDatabase      controller.BlackDatabase\n\tnotificationSender *FriendNotificationSender\n\tRegisterCenter     discovery.Conn\n\tconfig             *Config\n\twebhookClient      *webhook.Client\n\tqueue              *memamq.MemoryQueue\n\tuserClient         *rpcli.UserClient\n}\n\ntype Config struct {\n\tRpcConfig     config.Friend\n\tRedisConfig   config.Redis\n\tMongodbConfig config.Mongo\n\t// ZookeeperConfig    config.ZooKeeper\n\tNotificationConfig config.Notification\n\tShare              config.Share\n\tWebhooksConfig     config.Webhooks\n\tLocalCacheConfig   config.LocalCache\n\tDiscovery          config.Discovery\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tdbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)\n\tmgocli, err := dbb.Mongo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfriendMongoDB, err := mgo.NewFriendMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfriendRequestMongoDB, err := mgo.NewFriendRequestMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tblackMongoDB, err := mgo.NewBlackMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserClient := rpcli.NewUserClient(userConn)\n\tdatabase := controller.NewFriendDatabase(\n\t\tfriendMongoDB,\n\t\tfriendRequestMongoDB,\n\t\tredis.NewFriendCacheRedis(rdb, &config.LocalCacheConfig, friendMongoDB),\n\t\tmgocli.GetTx(),\n\t)\n\t// Initialize notification sender\n\tnotificationSender := NewFriendNotificationSender(\n\t\t&config.NotificationConfig,\n\t\trpcli.NewMsgClient(msgConn),\n\t\tWithRpcFunc(userClient.GetUsersInfo),\n\t\tWithFriendDB(database),\n\t)\n\tlocalcache.InitLocalCache(&config.LocalCacheConfig)\n\n\t// Register Friend server with refactored MongoDB and Redis integrations\n\trelation.RegisterFriendServer(server, &friendServer{\n\t\tdb: database,\n\t\tblackDatabase: controller.NewBlackDatabase(\n\t\t\tblackMongoDB,\n\t\t\tredis.NewBlackCacheRedis(rdb, &config.LocalCacheConfig, blackMongoDB),\n\t\t),\n\t\tnotificationSender: notificationSender,\n\t\tRegisterCenter:     client,\n\t\tconfig:             config,\n\t\twebhookClient:      webhook.NewWebhookClient(config.WebhooksConfig.URL),\n\t\tqueue:              memamq.NewMemoryQueue(16, 1024*1024),\n\t\tuserClient:         userClient,\n\t})\n\treturn nil\n}\n\n// ok.\nfunc (s *friendServer) ApplyToAddFriend(ctx context.Context, req *relation.ApplyToAddFriendReq) (resp *relation.ApplyToAddFriendResp, err error) {\n\tresp = &relation.ApplyToAddFriendResp{}\n\tif err := authverify.CheckAccess(ctx, req.FromUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tif req.ToUserID == req.FromUserID {\n\t\treturn nil, servererrs.ErrCanNotAddYourself.WrapMsg(\"req.ToUserID\", req.ToUserID)\n\t}\n\tif err = s.webhookBeforeAddFriend(ctx, &s.config.WebhooksConfig.BeforeAddFriend, req); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\tif err := s.userClient.CheckUser(ctx, []string{req.ToUserID, req.FromUserID}); err != nil {\n\t\treturn nil, err\n\t}\n\n\tin1, in2, err := s.db.CheckIn(ctx, req.FromUserID, req.ToUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif in1 && in2 {\n\t\treturn nil, servererrs.ErrRelationshipAlready.WrapMsg(\"already friends has f\")\n\t}\n\tif err = s.db.AddFriendRequest(ctx, req.FromUserID, req.ToUserID, req.ReqMsg, req.Ex); err != nil {\n\t\treturn nil, err\n\t}\n\ts.notificationSender.FriendApplicationAddNotification(ctx, req)\n\ts.webhookAfterAddFriend(ctx, &s.config.WebhooksConfig.AfterAddFriend, req)\n\treturn resp, nil\n}\n\n// ok.\nfunc (s *friendServer) ImportFriends(ctx context.Context, req *relation.ImportFriendReq) (resp *relation.ImportFriendResp, err error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := s.userClient.CheckUser(ctx, append([]string{req.OwnerUserID}, req.FriendUserIDs...)); err != nil {\n\t\treturn nil, err\n\t}\n\tif datautil.Contain(req.OwnerUserID, req.FriendUserIDs...) {\n\t\treturn nil, servererrs.ErrCanNotAddYourself.WrapMsg(\"can not add yourself\")\n\t}\n\tif datautil.Duplicate(req.FriendUserIDs) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"friend userID repeated\")\n\t}\n\n\tif err := s.webhookBeforeImportFriends(ctx, &s.config.WebhooksConfig.BeforeImportFriends, req); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\tif err := s.db.BecomeFriends(ctx, req.OwnerUserID, req.FriendUserIDs, constant.BecomeFriendByImport); err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, userID := range req.FriendUserIDs {\n\t\ts.notificationSender.FriendApplicationAgreedNotification(ctx, &relation.RespondFriendApplyReq{\n\t\t\tFromUserID:   req.OwnerUserID,\n\t\t\tToUserID:     userID,\n\t\t\tHandleResult: constant.FriendResponseAgree,\n\t\t}, false)\n\t}\n\n\ts.webhookAfterImportFriends(ctx, &s.config.WebhooksConfig.AfterImportFriends, req)\n\treturn &relation.ImportFriendResp{}, nil\n}\n\n// ok.\nfunc (s *friendServer) RespondFriendApply(ctx context.Context, req *relation.RespondFriendApplyReq) (resp *relation.RespondFriendApplyResp, err error) {\n\tresp = &relation.RespondFriendApplyResp{}\n\tif err := authverify.CheckAccess(ctx, req.ToUserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfriendRequest := model.FriendRequest{\n\t\tFromUserID:   req.FromUserID,\n\t\tToUserID:     req.ToUserID,\n\t\tHandleMsg:    req.HandleMsg,\n\t\tHandleResult: req.HandleResult,\n\t}\n\tif req.HandleResult == constant.FriendResponseAgree {\n\t\tif err := s.webhookBeforeAddFriendAgree(ctx, &s.config.WebhooksConfig.BeforeAddFriendAgree, req); err != nil && err != servererrs.ErrCallbackContinue {\n\t\t\treturn nil, err\n\t\t}\n\t\terr := s.db.AgreeFriendRequest(ctx, &friendRequest)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ts.webhookAfterAddFriendAgree(ctx, &s.config.WebhooksConfig.AfterAddFriendAgree, req)\n\t\ts.notificationSender.FriendApplicationAgreedNotification(ctx, req, true)\n\t\treturn resp, nil\n\t}\n\tif req.HandleResult == constant.FriendResponseRefuse {\n\t\terr := s.db.RefuseFriendRequest(ctx, &friendRequest)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ts.notificationSender.FriendApplicationRefusedNotification(ctx, req)\n\t\treturn resp, nil\n\t}\n\treturn nil, errs.ErrArgs.WrapMsg(\"req.HandleResult != -1/1\")\n}\n\n// ok.\nfunc (s *friendServer) DeleteFriend(ctx context.Context, req *relation.DeleteFriendReq) (resp *relation.DeleteFriendResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\t_, err = s.db.FindFriendsWithError(ctx, req.OwnerUserID, []string{req.FriendUserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := s.db.Delete(ctx, req.OwnerUserID, []string{req.FriendUserID}); err != nil {\n\t\treturn nil, err\n\t}\n\n\ts.notificationSender.FriendDeletedNotification(ctx, req)\n\ts.webhookAfterDeleteFriend(ctx, &s.config.WebhooksConfig.AfterDeleteFriend, req)\n\n\treturn &relation.DeleteFriendResp{}, nil\n}\n\n// ok.\nfunc (s *friendServer) SetFriendRemark(ctx context.Context, req *relation.SetFriendRemarkReq) (resp *relation.SetFriendRemarkResp, err error) {\n\tif err = s.webhookBeforeSetFriendRemark(ctx, &s.config.WebhooksConfig.BeforeSetFriendRemark, req); err != nil && err != servererrs.ErrCallbackContinue {\n\t\treturn nil, err\n\t}\n\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\t_, err = s.db.FindFriendsWithError(ctx, req.OwnerUserID, []string{req.FriendUserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := s.db.UpdateRemark(ctx, req.OwnerUserID, req.FriendUserID, req.Remark); err != nil {\n\t\treturn nil, err\n\t}\n\n\ts.webhookAfterSetFriendRemark(ctx, &s.config.WebhooksConfig.AfterSetFriendRemark, req)\n\ts.notificationSender.FriendRemarkSetNotification(ctx, req.OwnerUserID, req.FriendUserID)\n\n\treturn &relation.SetFriendRemarkResp{}, nil\n}\n\nfunc (s *friendServer) GetFriendInfo(ctx context.Context, req *relation.GetFriendInfoReq) (*relation.GetFriendInfoResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tfriends, err := s.db.FindFriendsWithError(ctx, req.OwnerUserID, req.FriendUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &relation.GetFriendInfoResp{FriendInfos: convert.FriendOnlyDB2PbOnly(friends)}, nil\n}\n\nfunc (s *friendServer) GetDesignatedFriends(ctx context.Context, req *relation.GetDesignatedFriendsReq) (resp *relation.GetDesignatedFriendsResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &relation.GetDesignatedFriendsResp{}\n\tif datautil.Duplicate(req.FriendUserIDs) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"friend userID repeated\")\n\t}\n\tfriends, err := s.getFriend(ctx, req.OwnerUserID, req.FriendUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &relation.GetDesignatedFriendsResp{\n\t\tFriendsInfo: friends,\n\t}, nil\n}\n\nfunc (s *friendServer) getFriend(ctx context.Context, ownerUserID string, friendUserIDs []string) ([]*sdkws.FriendInfo, error) {\n\tif len(friendUserIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tfriends, err := s.db.FindFriendsWithError(ctx, ownerUserID, friendUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn convert.FriendsDB2Pb(ctx, friends, s.userClient.GetUsersInfoMap)\n}\n\n// Get the list of friend requests sent out proactively.\nfunc (s *friendServer) GetDesignatedFriendsApply(ctx context.Context, req *relation.GetDesignatedFriendsApplyReq) (resp *relation.GetDesignatedFriendsApplyResp, err error) {\n\tif err := authverify.CheckAccessIn(ctx, req.FromUserID, req.ToUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tfriendRequests, err := s.db.FindBothFriendRequests(ctx, req.FromUserID, req.ToUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &relation.GetDesignatedFriendsApplyResp{}\n\tresp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.getCommonUserMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\n// Get received friend requests (i.e., those initiated by others).\nfunc (s *friendServer) GetPaginationFriendsApplyTo(ctx context.Context, req *relation.GetPaginationFriendsApplyToReq) (resp *relation.GetPaginationFriendsApplyToResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\thandleResults := datautil.Slice(req.HandleResults, func(e int32) int {\n\t\treturn int(e)\n\t})\n\ttotal, friendRequests, err := s.db.PageFriendRequestToMe(ctx, req.UserID, handleResults, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp = &relation.GetPaginationFriendsApplyToResp{}\n\tresp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.getCommonUserMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp.Total = int32(total)\n\n\treturn resp, nil\n}\n\nfunc (s *friendServer) GetPaginationFriendsApplyFrom(ctx context.Context, req *relation.GetPaginationFriendsApplyFromReq) (resp *relation.GetPaginationFriendsApplyFromResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\thandleResults := datautil.Slice(req.HandleResults, func(e int32) int {\n\t\treturn int(e)\n\t})\n\ttotal, friendRequests, err := s.db.PageFriendRequestFromMe(ctx, req.UserID, handleResults, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp = &relation.GetPaginationFriendsApplyFromResp{}\n\tresp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.getCommonUserMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp.Total = int32(total)\n\n\treturn resp, nil\n}\n\n// ok.\nfunc (s *friendServer) IsFriend(ctx context.Context, req *relation.IsFriendReq) (resp *relation.IsFriendResp, err error) {\n\tif err := authverify.CheckAccessIn(ctx, req.UserID1, req.UserID2); err != nil {\n\t\treturn nil, err\n\t}\n\tresp = &relation.IsFriendResp{}\n\tresp.InUser1Friends, resp.InUser2Friends, err = s.db.CheckIn(ctx, req.UserID1, req.UserID2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (s *friendServer) GetPaginationFriends(ctx context.Context, req *relation.GetPaginationFriendsReq) (resp *relation.GetPaginationFriendsResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\ttotal, friends, err := s.db.PageOwnerFriends(ctx, req.UserID, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp = &relation.GetPaginationFriendsResp{}\n\tresp.FriendsInfo, err = convert.FriendsDB2Pb(ctx, friends, s.userClient.GetUsersInfoMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp.Total = int32(total)\n\n\treturn resp, nil\n}\n\nfunc (s *friendServer) GetFriendIDs(ctx context.Context, req *relation.GetFriendIDsReq) (resp *relation.GetFriendIDsResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp = &relation.GetFriendIDsResp{}\n\tresp.FriendIDs, err = s.db.FindFriendUserIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp, nil\n}\n\nfunc (s *friendServer) GetSpecifiedFriendsInfo(ctx context.Context, req *relation.GetSpecifiedFriendsInfoReq) (*relation.GetSpecifiedFriendsInfoResp, error) {\n\tif len(req.UserIDList) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userIDList is empty\")\n\t}\n\n\tif datautil.Duplicate(req.UserIDList) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userIDList repeated\")\n\t}\n\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\tuserMap, err := s.userClient.GetUsersInfoMap(ctx, req.UserIDList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfriends, err := s.db.FindFriendsWithError(ctx, req.OwnerUserID, req.UserIDList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblacks, err := s.blackDatabase.FindBlackInfos(ctx, req.OwnerUserID, req.UserIDList)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfriendMap := datautil.SliceToMap(friends, func(e *model.Friend) string {\n\t\treturn e.FriendUserID\n\t})\n\n\tblackMap := datautil.SliceToMap(blacks, func(e *model.Black) string {\n\t\treturn e.BlockUserID\n\t})\n\n\tresp := &relation.GetSpecifiedFriendsInfoResp{\n\t\tInfos: make([]*relation.GetSpecifiedFriendsInfoInfo, 0, len(req.UserIDList)),\n\t}\n\n\tfor _, userID := range req.UserIDList {\n\t\tuser := userMap[userID]\n\n\t\tif user == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar friendInfo *sdkws.FriendInfo\n\t\tif friend := friendMap[userID]; friend != nil {\n\t\t\tfriendInfo = &sdkws.FriendInfo{\n\t\t\t\tOwnerUserID:    friend.OwnerUserID,\n\t\t\t\tRemark:         friend.Remark,\n\t\t\t\tCreateTime:     friend.CreateTime.UnixMilli(),\n\t\t\t\tAddSource:      friend.AddSource,\n\t\t\t\tOperatorUserID: friend.OperatorUserID,\n\t\t\t\tEx:             friend.Ex,\n\t\t\t\tIsPinned:       friend.IsPinned,\n\t\t\t}\n\t\t}\n\n\t\tvar blackInfo *sdkws.BlackInfo\n\t\tif black := blackMap[userID]; black != nil {\n\t\t\tblackInfo = &sdkws.BlackInfo{\n\t\t\t\tOwnerUserID:    black.OwnerUserID,\n\t\t\t\tCreateTime:     black.CreateTime.UnixMilli(),\n\t\t\t\tAddSource:      black.AddSource,\n\t\t\t\tOperatorUserID: black.OperatorUserID,\n\t\t\t\tEx:             black.Ex,\n\t\t\t}\n\t\t}\n\n\t\tresp.Infos = append(resp.Infos, &relation.GetSpecifiedFriendsInfoInfo{\n\t\t\tUserInfo:   user,\n\t\t\tFriendInfo: friendInfo,\n\t\t\tBlackInfo:  blackInfo,\n\t\t})\n\t}\n\n\treturn resp, nil\n}\n\nfunc (s *friendServer) UpdateFriends(ctx context.Context, req *relation.UpdateFriendsReq) (*relation.UpdateFriendsResp, error) {\n\tif len(req.FriendUserIDs) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"friendIDList is empty\")\n\t}\n\tif datautil.Duplicate(req.FriendUserIDs) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"friendIDList repeated\")\n\t}\n\n\tif err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\t_, err := s.db.FindFriendsWithError(ctx, req.OwnerUserID, req.FriendUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tval := make(map[string]any)\n\n\tif req.IsPinned != nil {\n\t\tval[\"is_pinned\"] = req.IsPinned.Value\n\t}\n\tif req.Remark != nil {\n\t\tval[\"remark\"] = req.Remark.Value\n\t}\n\tif req.Ex != nil {\n\t\tval[\"ex\"] = req.Ex.Value\n\t}\n\tif err = s.db.UpdateFriends(ctx, req.OwnerUserID, req.FriendUserIDs, val); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp := &relation.UpdateFriendsResp{}\n\n\ts.notificationSender.FriendsInfoUpdateNotification(ctx, req.OwnerUserID, req.FriendUserIDs)\n\treturn resp, nil\n}\n\nfunc (s *friendServer) GetSelfUnhandledApplyCount(ctx context.Context, req *relation.GetSelfUnhandledApplyCountReq) (*relation.GetSelfUnhandledApplyCountResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcount, err := s.db.GetUnhandledCount(ctx, req.UserID, req.Time)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &relation.GetSelfUnhandledApplyCountResp{\n\t\tCount: count,\n\t}, nil\n}\n\nfunc (s *friendServer) getCommonUserMap(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error) {\n\tusers, err := s.userClient.GetUsersInfo(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn datautil.SliceToMapAny(users, func(e *sdkws.UserInfo) (string, common_user.CommonUser) {\n\t\treturn e.UserID, e\n\t}), nil\n}\n"
  },
  {
    "path": "internal/rpc/relation/notification.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage relation\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/versionctx\"\n\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification/common_user\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/relation\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\ntype FriendNotificationSender struct {\n\t*notification.NotificationSender\n\t// Target not found err\n\tgetUsersInfo func(ctx context.Context, userIDs []string) ([]common_user.CommonUser, error)\n\t// db controller\n\tdb controller.FriendDatabase\n}\n\ntype friendNotificationSenderOptions func(*FriendNotificationSender)\n\nfunc WithFriendDB(db controller.FriendDatabase) friendNotificationSenderOptions {\n\treturn func(s *FriendNotificationSender) {\n\t\ts.db = db\n\t}\n}\n\nfunc WithDBFunc(fn func(ctx context.Context, userIDs []string) (users []*relationtb.User, err error)) friendNotificationSenderOptions {\n\treturn func(s *FriendNotificationSender) {\n\t\tf := func(ctx context.Context, userIDs []string) (result []common_user.CommonUser, err error) {\n\t\t\tusers, err := fn(ctx, userIDs)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfor _, user := range users {\n\t\t\t\tresult = append(result, user)\n\t\t\t}\n\t\t\treturn result, nil\n\t\t}\n\t\ts.getUsersInfo = f\n\t}\n}\n\nfunc WithRpcFunc(fn func(ctx context.Context, userIDs []string) ([]*sdkws.UserInfo, error)) friendNotificationSenderOptions {\n\treturn func(s *FriendNotificationSender) {\n\t\tf := func(ctx context.Context, userIDs []string) (result []common_user.CommonUser, err error) {\n\t\t\tusers, err := fn(ctx, userIDs)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfor _, user := range users {\n\t\t\t\tresult = append(result, user)\n\t\t\t}\n\t\t\treturn result, err\n\t\t}\n\t\ts.getUsersInfo = f\n\t}\n}\n\nfunc NewFriendNotificationSender(conf *config.Notification, msgClient *rpcli.MsgClient, opts ...friendNotificationSenderOptions) *FriendNotificationSender {\n\tf := &FriendNotificationSender{\n\t\tNotificationSender: notification.NewNotificationSender(conf, notification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {\n\t\t\treturn msgClient.SendMsg(ctx, req)\n\t\t})),\n\t}\n\tfor _, opt := range opts {\n\t\topt(f)\n\t}\n\treturn f\n}\n\nfunc (f *FriendNotificationSender) getUsersInfoMap(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error) {\n\tusers, err := f.getUsersInfo(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make(map[string]*sdkws.UserInfo)\n\tfor _, user := range users {\n\t\tresult[user.GetUserID()] = user.(*sdkws.UserInfo)\n\t}\n\treturn result, nil\n}\n\n//nolint:unused\nfunc (f *FriendNotificationSender) getFromToUserNickname(ctx context.Context, fromUserID, toUserID string) (string, string, error) {\n\tusers, err := f.getUsersInfoMap(ctx, []string{fromUserID, toUserID})\n\tif err != nil {\n\t\treturn \"\", \"\", nil\n\t}\n\treturn users[fromUserID].Nickname, users[toUserID].Nickname, nil\n}\n\nfunc (f *FriendNotificationSender) UserInfoUpdatedNotification(ctx context.Context, changedUserID string) {\n\ttips := sdkws.UserInfoUpdatedTips{UserID: changedUserID}\n\tf.Notification(ctx, mcontext.GetOpUserID(ctx), changedUserID, constant.UserInfoUpdatedNotification, &tips)\n}\n\nfunc (f *FriendNotificationSender) getCommonUserMap(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error) {\n\tusers, err := f.getUsersInfo(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn datautil.SliceToMap(users, func(e common_user.CommonUser) string {\n\t\treturn e.GetUserID()\n\t}), nil\n}\n\nfunc (f *FriendNotificationSender) getFriendRequests(ctx context.Context, fromUserID, toUserID string) (*sdkws.FriendRequest, error) {\n\tif f.db == nil {\n\t\treturn nil, errs.ErrInternalServer.WithDetail(\"db is nil\")\n\t}\n\tfriendRequests, err := f.db.FindBothFriendRequests(ctx, fromUserID, toUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trequests, err := convert.FriendRequestDB2Pb(ctx, friendRequests, f.getCommonUserMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, request := range requests {\n\t\tif request.FromUserID == fromUserID && request.ToUserID == toUserID {\n\t\t\treturn request, nil\n\t\t}\n\t}\n\treturn nil, errs.ErrRecordNotFound.WrapMsg(\"friend request not found\", \"fromUserID\", fromUserID, \"toUserID\", toUserID)\n}\n\nfunc (f *FriendNotificationSender) FriendApplicationAddNotification(ctx context.Context, req *relation.ApplyToAddFriendReq) {\n\trequest, err := f.getFriendRequests(ctx, req.FromUserID, req.ToUserID)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"FriendApplicationAddNotification get friend request\", err, \"fromUserID\", req.FromUserID, \"toUserID\", req.ToUserID)\n\t\treturn\n\t}\n\ttips := sdkws.FriendApplicationTips{\n\t\tFromToUserID: &sdkws.FromToUserID{\n\t\t\tFromUserID: req.FromUserID,\n\t\t\tToUserID:   req.ToUserID,\n\t\t},\n\t\tRequest: request,\n\t}\n\tf.Notification(ctx, req.FromUserID, req.ToUserID, constant.FriendApplicationNotification, &tips)\n}\n\nfunc (f *FriendNotificationSender) FriendApplicationAgreedNotification(ctx context.Context, req *relation.RespondFriendApplyReq, checkReq bool) {\n\tvar (\n\t\trequest *sdkws.FriendRequest\n\t\terr     error\n\t)\n\tif checkReq {\n\t\trequest, err = f.getFriendRequests(ctx, req.FromUserID, req.ToUserID)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"FriendApplicationAgreedNotification get friend request\", err, \"fromUserID\", req.FromUserID, \"toUserID\", req.ToUserID)\n\t\t\treturn\n\t\t}\n\t}\n\ttips := sdkws.FriendApplicationApprovedTips{\n\t\tFromToUserID: &sdkws.FromToUserID{\n\t\t\tFromUserID: req.FromUserID,\n\t\t\tToUserID:   req.ToUserID,\n\t\t},\n\t\tHandleMsg: req.HandleMsg,\n\t\tRequest:   request,\n\t}\n\tf.Notification(ctx, req.ToUserID, req.FromUserID, constant.FriendApplicationApprovedNotification, &tips)\n}\n\nfunc (f *FriendNotificationSender) FriendApplicationRefusedNotification(ctx context.Context, req *relation.RespondFriendApplyReq) {\n\trequest, err := f.getFriendRequests(ctx, req.FromUserID, req.ToUserID)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"FriendApplicationRefusedNotification get friend request\", err, \"fromUserID\", req.FromUserID, \"toUserID\", req.ToUserID)\n\t\treturn\n\t}\n\ttips := sdkws.FriendApplicationRejectedTips{\n\t\tFromToUserID: &sdkws.FromToUserID{\n\t\t\tFromUserID: req.FromUserID,\n\t\t\tToUserID:   req.ToUserID,\n\t\t},\n\t\tHandleMsg: req.HandleMsg,\n\t\tRequest:   request,\n\t}\n\tf.Notification(ctx, req.ToUserID, req.FromUserID, constant.FriendApplicationRejectedNotification, &tips)\n}\n\n//func (f *FriendNotificationSender) FriendAddedNotification(ctx context.Context, operationID, opUserID, fromUserID, toUserID string) error {\n//\ttips := sdkws.FriendAddedTips{Friend: &sdkws.FriendInfo{}, OpUser: &sdkws.PublicUserInfo{}}\n//\tuser, err := f.getUsersInfo(ctx, []string{opUserID})\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\ttips.OpUser.UserID = user[0].GetUserID()\n//\ttips.OpUser.Ex = user[0].GetEx()\n//\ttips.OpUser.Nickname = user[0].GetNickname()\n//\ttips.OpUser.FaceURL = user[0].GetFaceURL()\n//\tfriends, err := f.db.FindFriendsWithError(ctx, fromUserID, []string{toUserID})\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\ttips.Friend, err = convert.FriendDB2Pb(ctx, friends[0], f.getUsersInfoMap)\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\tf.Notification(ctx, fromUserID, toUserID, constant.FriendAddedNotification, &tips)\n//\treturn nil\n//}\n\nfunc (f *FriendNotificationSender) FriendDeletedNotification(ctx context.Context, req *relation.DeleteFriendReq) {\n\ttips := sdkws.FriendDeletedTips{FromToUserID: &sdkws.FromToUserID{\n\t\tFromUserID: req.OwnerUserID,\n\t\tToUserID:   req.FriendUserID,\n\t}}\n\tf.Notification(ctx, req.OwnerUserID, req.FriendUserID, constant.FriendDeletedNotification, &tips)\n}\n\nfunc (f *FriendNotificationSender) setVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string) {\n\tversions := versionctx.GetVersionLog(ctx).Get()\n\tfor _, coll := range versions {\n\t\tif coll.Name == collName && coll.Doc.DID == id {\n\t\t\t*version = uint64(coll.Doc.Version)\n\t\t\t*versionID = coll.Doc.ID.Hex()\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (f *FriendNotificationSender) setSortVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string, sortVersion *uint64) {\n\tversions := versionctx.GetVersionLog(ctx).Get()\n\tfor _, coll := range versions {\n\t\tif coll.Name == collName && coll.Doc.DID == id {\n\t\t\t*version = uint64(coll.Doc.Version)\n\t\t\t*versionID = coll.Doc.ID.Hex()\n\t\t\tfor _, elem := range coll.Doc.Logs {\n\t\t\t\tif elem.EID == relationtb.VersionSortChangeID {\n\t\t\t\t\t*sortVersion = uint64(elem.Version)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (f *FriendNotificationSender) FriendRemarkSetNotification(ctx context.Context, fromUserID, toUserID string) {\n\ttips := sdkws.FriendInfoChangedTips{FromToUserID: &sdkws.FromToUserID{}}\n\ttips.FromToUserID.FromUserID = fromUserID\n\ttips.FromToUserID.ToUserID = toUserID\n\tf.setSortVersion(ctx, &tips.FriendVersion, &tips.FriendVersionID, database.FriendVersionName, toUserID, &tips.FriendSortVersion)\n\tf.Notification(ctx, fromUserID, toUserID, constant.FriendRemarkSetNotification, &tips)\n}\n\nfunc (f *FriendNotificationSender) FriendsInfoUpdateNotification(ctx context.Context, toUserID string, friendIDs []string) {\n\ttips := sdkws.FriendsInfoUpdateTips{FromToUserID: &sdkws.FromToUserID{}}\n\ttips.FromToUserID.ToUserID = toUserID\n\ttips.FriendIDs = friendIDs\n\tf.Notification(ctx, toUserID, toUserID, constant.FriendsInfoUpdateNotification, &tips)\n}\n\nfunc (f *FriendNotificationSender) BlackAddedNotification(ctx context.Context, req *relation.AddBlackReq) {\n\ttips := sdkws.BlackAddedTips{FromToUserID: &sdkws.FromToUserID{}}\n\ttips.FromToUserID.FromUserID = req.OwnerUserID\n\ttips.FromToUserID.ToUserID = req.BlackUserID\n\tf.Notification(ctx, req.OwnerUserID, req.BlackUserID, constant.BlackAddedNotification, &tips)\n}\n\nfunc (f *FriendNotificationSender) BlackDeletedNotification(ctx context.Context, req *relation.RemoveBlackReq) {\n\tblackDeletedTips := sdkws.BlackDeletedTips{FromToUserID: &sdkws.FromToUserID{\n\t\tFromUserID: req.OwnerUserID,\n\t\tToUserID:   req.BlackUserID,\n\t}}\n\tf.Notification(ctx, req.OwnerUserID, req.BlackUserID, constant.BlackDeletedNotification, &blackDeletedTips)\n}\n\nfunc (f *FriendNotificationSender) FriendInfoUpdatedNotification(ctx context.Context, changedUserID string, needNotifiedUserID string) {\n\ttips := sdkws.UserInfoUpdatedTips{UserID: changedUserID}\n\tf.Notification(ctx, mcontext.GetOpUserID(ctx), needNotifiedUserID, constant.FriendInfoUpdatedNotification, &tips)\n}\n"
  },
  {
    "path": "internal/rpc/relation/sync.go",
    "content": "package relation\n\nimport (\n\t\"context\"\n\t\"slices\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/util/hashutil\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/log\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/incrversion\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/relation\"\n)\n\nfunc (s *friendServer) NotificationUserInfoUpdate(ctx context.Context, req *relation.NotificationUserInfoUpdateReq) (*relation.NotificationUserInfoUpdateResp, error) {\n\tuserIDs, err := s.db.FindFriendUserIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(userIDs) > 0 {\n\t\tfriendUserIDs := []string{req.UserID}\n\t\tnoCancelCtx := context.WithoutCancel(ctx)\n\t\terr := s.queue.PushCtx(ctx, func() {\n\t\t\tfor _, userID := range userIDs {\n\t\t\t\tif err := s.db.OwnerIncrVersion(noCancelCtx, userID, friendUserIDs, model.VersionStateUpdate); err != nil {\n\t\t\t\t\tlog.ZError(ctx, \"OwnerIncrVersion\", err, \"userID\", userID, \"friendUserIDs\", friendUserIDs)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor _, userID := range userIDs {\n\t\t\t\ts.notificationSender.FriendInfoUpdatedNotification(noCancelCtx, req.UserID, userID)\n\t\t\t}\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"NotificationUserInfoUpdate timeout\", err, \"userID\", req.UserID)\n\t\t}\n\t}\n\treturn &relation.NotificationUserInfoUpdateResp{}, nil\n}\n\nfunc (s *friendServer) GetFullFriendUserIDs(ctx context.Context, req *relation.GetFullFriendUserIDsReq) (*relation.GetFullFriendUserIDsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tvl, err := s.db.FindMaxFriendVersionCache(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserIDs, err := s.db.FindFriendUserIDs(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tidHash := hashutil.IdHash(userIDs)\n\tif req.IdHash == idHash {\n\t\tuserIDs = nil\n\t}\n\treturn &relation.GetFullFriendUserIDsResp{\n\t\tVersion:   uint64(vl.Version),\n\t\tVersionID: vl.ID.Hex(),\n\t\tEqual:     req.IdHash == idHash,\n\t\tUserIDs:   userIDs,\n\t}, nil\n}\n\nfunc (s *friendServer) GetIncrementalFriends(ctx context.Context, req *relation.GetIncrementalFriendsReq) (*relation.GetIncrementalFriendsResp, error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\tvar sortVersion uint64\n\topt := incrversion.Option[*sdkws.FriendInfo, relation.GetIncrementalFriendsResp]{\n\t\tCtx:           ctx,\n\t\tVersionKey:    req.UserID,\n\t\tVersionID:     req.VersionID,\n\t\tVersionNumber: req.Version,\n\t\tVersion: func(ctx context.Context, ownerUserID string, version uint, limit int) (*model.VersionLog, error) {\n\t\t\tvl, err := s.db.FindFriendIncrVersion(ctx, ownerUserID, version, limit)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tvl.Logs = slices.DeleteFunc(vl.Logs, func(elem model.VersionLogElem) bool {\n\t\t\t\tif elem.EID == model.VersionSortChangeID {\n\t\t\t\t\tvl.LogLen--\n\t\t\t\t\tsortVersion = uint64(elem.Version)\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t})\n\t\t\treturn vl, nil\n\t\t},\n\t\tCacheMaxVersion: s.db.FindMaxFriendVersionCache,\n\t\tFind: func(ctx context.Context, ids []string) ([]*sdkws.FriendInfo, error) {\n\t\t\treturn s.getFriend(ctx, req.UserID, ids)\n\t\t},\n\t\tResp: func(version *model.VersionLog, deleteIds []string, insertList, updateList []*sdkws.FriendInfo, full bool) *relation.GetIncrementalFriendsResp {\n\t\t\treturn &relation.GetIncrementalFriendsResp{\n\t\t\t\tVersionID:   version.ID.Hex(),\n\t\t\t\tVersion:     uint64(version.Version),\n\t\t\t\tFull:        full,\n\t\t\t\tDelete:      deleteIds,\n\t\t\t\tInsert:      insertList,\n\t\t\t\tUpdate:      updateList,\n\t\t\t\tSortVersion: sortVersion,\n\t\t\t}\n\t\t},\n\t}\n\treturn opt.Build()\n}\n"
  },
  {
    "path": "internal/rpc/third/log.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage third\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc genLogID() string {\n\tconst dataLen = 10\n\tdata := make([]byte, dataLen)\n\trand.Read(data)\n\tchars := []byte(\"0123456789\")\n\tfor i := 0; i < len(data); i++ {\n\t\tif i == 0 {\n\t\t\tdata[i] = chars[1:][data[i]%9]\n\t\t} else {\n\t\t\tdata[i] = chars[data[i]%10]\n\t\t}\n\t}\n\treturn string(data)\n}\n\nfunc (t *thirdServer) UploadLogs(ctx context.Context, req *third.UploadLogsReq) (*third.UploadLogsResp, error) {\n\tvar dbLogs []*relationtb.Log\n\tuserID := mcontext.GetOpUserID(ctx)\n\tplatform := constant.PlatformID2Name[int(req.Platform)]\n\tfor _, fileURL := range req.FileURLs {\n\t\tlog := relationtb.Log{\n\t\t\tPlatform:     platform,\n\t\t\tUserID:       userID,\n\t\t\tCreateTime:   time.Now(),\n\t\t\tUrl:          fileURL.URL,\n\t\t\tFileName:     fileURL.Filename,\n\t\t\tAppFramework: req.AppFramework,\n\t\t\tVersion:      req.Version,\n\t\t\tEx:           req.Ex,\n\t\t}\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tid := genLogID()\n\t\t\tlogs, err := t.thirdDatabase.GetLogs(ctx, []string{id}, \"\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif len(logs) == 0 {\n\t\t\t\tlog.LogID = id\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif log.LogID == \"\" {\n\t\t\treturn nil, servererrs.ErrData.WrapMsg(\"Log id gen error\")\n\t\t}\n\t\tdbLogs = append(dbLogs, &log)\n\t}\n\terr := t.thirdDatabase.UploadLogs(ctx, dbLogs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.UploadLogsResp{}, nil\n}\n\nfunc (t *thirdServer) DeleteLogs(ctx context.Context, req *third.DeleteLogsReq) (*third.DeleteLogsResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tuserID := \"\"\n\tlogs, err := t.thirdDatabase.GetLogs(ctx, req.LogIDs, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar logIDs []string\n\tfor _, log := range logs {\n\t\tlogIDs = append(logIDs, log.LogID)\n\t}\n\tif ids := datautil.Single(req.LogIDs, logIDs); len(ids) > 0 {\n\t\treturn nil, errs.ErrRecordNotFound.WrapMsg(\"logIDs not found\", \"logIDs\", ids)\n\t}\n\terr = t.thirdDatabase.DeleteLogs(ctx, req.LogIDs, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &third.DeleteLogsResp{}, nil\n}\n\nfunc dbToPbLogInfos(logs []*relationtb.Log) []*third.LogInfo {\n\tdb2pbForLogInfo := func(log *relationtb.Log) *third.LogInfo {\n\t\treturn &third.LogInfo{\n\t\t\tFilename:   log.FileName,\n\t\t\tUserID:     log.UserID,\n\t\t\tPlatform:   log.Platform,\n\t\t\tUrl:        log.Url,\n\t\t\tCreateTime: log.CreateTime.UnixMilli(),\n\t\t\tLogID:      log.LogID,\n\t\t\tSystemType: log.SystemType,\n\t\t\tVersion:    log.Version,\n\t\t\tEx:         log.Ex,\n\t\t}\n\t}\n\treturn datautil.Slice(logs, db2pbForLogInfo)\n}\n\nfunc (t *thirdServer) SearchLogs(ctx context.Context, req *third.SearchLogsReq) (*third.SearchLogsResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tvar (\n\t\tresp    third.SearchLogsResp\n\t\tuserIDs []string\n\t)\n\tif req.StartTime > req.EndTime {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"startTime>endTime\")\n\t}\n\tif req.StartTime == 0 && req.EndTime == 0 {\n\t\tt := time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC)\n\t\ttimestampMills := t.UnixNano() / int64(time.Millisecond)\n\t\treq.StartTime = timestampMills\n\t\treq.EndTime = time.Now().UnixNano() / int64(time.Millisecond)\n\t}\n\n\ttotal, logs, err := t.thirdDatabase.SearchLogs(ctx, req.Keyword, time.UnixMilli(req.StartTime), time.UnixMilli(req.EndTime), req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpbLogs := dbToPbLogInfos(logs)\n\tfor _, log := range logs {\n\t\tuserIDs = append(userIDs, log.UserID)\n\t}\n\tuserMap, err := t.userClient.GetUsersInfoMap(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, pbLog := range pbLogs {\n\t\tif user, ok := userMap[pbLog.UserID]; ok {\n\t\t\tpbLog.Nickname = user.Nickname\n\t\t}\n\t}\n\tresp.LogsInfos = pbLogs\n\tresp.Total = uint32(total)\n\treturn &resp, nil\n}\n"
  },
  {
    "path": "internal/rpc/third/s3.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage third\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"path\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/s3\"\n\t\"github.com/openimsdk/tools/s3/cont\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc (t *thirdServer) PartLimit(ctx context.Context, req *third.PartLimitReq) (*third.PartLimitResp, error) {\n\tlimit, err := t.s3dataBase.PartLimit()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.PartLimitResp{\n\t\tMinPartSize: limit.MinPartSize,\n\t\tMaxPartSize: limit.MaxPartSize,\n\t\tMaxNumSize:  int32(limit.MaxNumSize),\n\t}, nil\n}\n\nfunc (t *thirdServer) PartSize(ctx context.Context, req *third.PartSizeReq) (*third.PartSizeResp, error) {\n\tsize, err := t.s3dataBase.PartSize(ctx, req.Size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.PartSizeResp{Size: size}, nil\n}\n\nfunc (t *thirdServer) InitiateMultipartUpload(ctx context.Context, req *third.InitiateMultipartUploadReq) (*third.InitiateMultipartUploadResp, error) {\n\tif err := t.checkUploadName(ctx, req.Name); err != nil {\n\t\treturn nil, err\n\t}\n\texpireTime := time.Now().Add(t.defaultExpire)\n\tresult, err := t.s3dataBase.InitiateMultipartUpload(ctx, req.Hash, req.Size, t.defaultExpire, int(req.MaxParts), req.ContentType)\n\tif err != nil {\n\t\tif haErr, ok := errs.Unwrap(err).(*cont.HashAlreadyExistsError); ok {\n\t\t\tobj := &model.Object{\n\t\t\t\tName:        req.Name,\n\t\t\t\tUserID:      mcontext.GetOpUserID(ctx),\n\t\t\t\tHash:        req.Hash,\n\t\t\t\tKey:         haErr.Object.Key,\n\t\t\t\tSize:        haErr.Object.Size,\n\t\t\t\tContentType: req.ContentType,\n\t\t\t\tGroup:       req.Cause,\n\t\t\t\tCreateTime:  time.Now(),\n\t\t\t}\n\t\t\tif err := t.s3dataBase.SetObject(ctx, obj); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &third.InitiateMultipartUploadResp{\n\t\t\t\tUrl: t.apiAddress(req.UrlPrefix, obj.Name),\n\t\t\t}, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tvar sign *third.AuthSignParts\n\tif result.Sign != nil && len(result.Sign.Parts) > 0 {\n\t\tsign = &third.AuthSignParts{\n\t\t\tUrl:    result.Sign.URL,\n\t\t\tQuery:  toPbMapArray(result.Sign.Query),\n\t\t\tHeader: toPbMapArray(result.Sign.Header),\n\t\t\tParts:  make([]*third.SignPart, len(result.Sign.Parts)),\n\t\t}\n\t\tfor i, part := range result.Sign.Parts {\n\t\t\tsign.Parts[i] = &third.SignPart{\n\t\t\t\tPartNumber: int32(part.PartNumber),\n\t\t\t\tUrl:        part.URL,\n\t\t\t\tQuery:      toPbMapArray(part.Query),\n\t\t\t\tHeader:     toPbMapArray(part.Header),\n\t\t\t}\n\t\t}\n\t}\n\treturn &third.InitiateMultipartUploadResp{\n\t\tUpload: &third.UploadInfo{\n\t\t\tUploadID:   result.UploadID,\n\t\t\tPartSize:   result.PartSize,\n\t\t\tSign:       sign,\n\t\t\tExpireTime: expireTime.UnixMilli(),\n\t\t},\n\t}, nil\n}\n\nfunc (t *thirdServer) AuthSign(ctx context.Context, req *third.AuthSignReq) (*third.AuthSignResp, error) {\n\tpartNumbers := datautil.Slice(req.PartNumbers, func(partNumber int32) int { return int(partNumber) })\n\tresult, err := t.s3dataBase.AuthSign(ctx, req.UploadID, partNumbers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp := &third.AuthSignResp{\n\t\tUrl:    result.URL,\n\t\tQuery:  toPbMapArray(result.Query),\n\t\tHeader: toPbMapArray(result.Header),\n\t\tParts:  make([]*third.SignPart, len(result.Parts)),\n\t}\n\tfor i, part := range result.Parts {\n\t\tresp.Parts[i] = &third.SignPart{\n\t\t\tPartNumber: int32(part.PartNumber),\n\t\t\tUrl:        part.URL,\n\t\t\tQuery:      toPbMapArray(part.Query),\n\t\t\tHeader:     toPbMapArray(part.Header),\n\t\t}\n\t}\n\treturn resp, nil\n}\n\nfunc (t *thirdServer) CompleteMultipartUpload(ctx context.Context, req *third.CompleteMultipartUploadReq) (*third.CompleteMultipartUploadResp, error) {\n\tif err := t.checkUploadName(ctx, req.Name); err != nil {\n\t\treturn nil, err\n\t}\n\tresult, err := t.s3dataBase.CompleteMultipartUpload(ctx, req.UploadID, req.Parts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tobj := &model.Object{\n\t\tName:        req.Name,\n\t\tUserID:      mcontext.GetOpUserID(ctx),\n\t\tHash:        result.Hash,\n\t\tKey:         result.Key,\n\t\tSize:        result.Size,\n\t\tContentType: req.ContentType,\n\t\tGroup:       req.Cause,\n\t\tCreateTime:  time.Now(),\n\t}\n\tif err := t.s3dataBase.SetObject(ctx, obj); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.CompleteMultipartUploadResp{\n\t\tUrl: t.apiAddress(req.UrlPrefix, obj.Name),\n\t}, nil\n}\n\nfunc (t *thirdServer) AccessURL(ctx context.Context, req *third.AccessURLReq) (*third.AccessURLResp, error) {\n\topt := &s3.AccessURLOption{}\n\tif len(req.Query) > 0 {\n\t\tswitch req.Query[\"type\"] {\n\t\tcase \"\":\n\t\tcase \"image\":\n\t\t\topt.Image = &s3.Image{}\n\t\t\topt.Image.Format = req.Query[\"format\"]\n\t\t\topt.Image.Width, _ = strconv.Atoi(req.Query[\"width\"])\n\t\t\topt.Image.Height, _ = strconv.Atoi(req.Query[\"height\"])\n\t\t\tlog.ZDebug(ctx, \"AccessURL image\", \"name\", req.Name, \"option\", opt.Image)\n\t\tdefault:\n\t\t\treturn nil, errs.ErrArgs.WrapMsg(\"invalid query type\")\n\t\t}\n\t}\n\texpireTime, rawURL, err := t.s3dataBase.AccessURL(ctx, req.Name, t.defaultExpire, opt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.AccessURLResp{\n\t\tUrl:        rawURL,\n\t\tExpireTime: expireTime.UnixMilli(),\n\t}, nil\n}\n\nfunc (t *thirdServer) InitiateFormData(ctx context.Context, req *third.InitiateFormDataReq) (*third.InitiateFormDataResp, error) {\n\tif req.Name == \"\" {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"name is empty\")\n\t}\n\tif req.Size <= 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"size must be greater than 0\")\n\t}\n\tif err := t.checkUploadName(ctx, req.Name); err != nil {\n\t\treturn nil, err\n\t}\n\tvar duration time.Duration\n\topUserID := mcontext.GetOpUserID(ctx)\n\tvar key string\n\tif authverify.CheckUserIsAdmin(ctx, opUserID) {\n\t\tif req.Millisecond <= 0 {\n\t\t\tduration = time.Minute * 10\n\t\t} else {\n\t\t\tduration = time.Millisecond * time.Duration(req.Millisecond)\n\t\t}\n\t\tif req.Absolute {\n\t\t\tkey = req.Name\n\t\t}\n\t} else {\n\t\tduration = time.Minute * 10\n\t}\n\tuid, err := uuid.NewRandom()\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"uuid NewRandom failed\")\n\t}\n\tif key == \"\" {\n\t\tdate := time.Now().Format(\"20060102\")\n\t\tkey = path.Join(cont.DirectPath, date, opUserID, hex.EncodeToString(uid[:])+path.Ext(req.Name))\n\t}\n\tmate := FormDataMate{\n\t\tName:        req.Name,\n\t\tSize:        req.Size,\n\t\tContentType: req.ContentType,\n\t\tGroup:       req.Group,\n\t\tKey:         key,\n\t}\n\tmateData, err := json.Marshal(&mate)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"marshal failed\")\n\t}\n\tresp, err := t.s3dataBase.FormData(ctx, key, req.Size, req.ContentType, duration)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.InitiateFormDataResp{\n\t\tId:       base64.RawStdEncoding.EncodeToString(mateData),\n\t\tUrl:      resp.URL,\n\t\tFile:     resp.File,\n\t\tHeader:   toPbMapArray(resp.Header),\n\t\tFormData: resp.FormData,\n\t\tExpires:  resp.Expires.UnixMilli(),\n\t\tSuccessCodes: datautil.Slice(resp.SuccessCodes, func(code int) int32 {\n\t\t\treturn int32(code)\n\t\t}),\n\t}, nil\n}\n\nfunc (t *thirdServer) CompleteFormData(ctx context.Context, req *third.CompleteFormDataReq) (*third.CompleteFormDataResp, error) {\n\tif req.Id == \"\" {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"id is empty\")\n\t}\n\tdata, err := base64.RawStdEncoding.DecodeString(req.Id)\n\tif err != nil {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"invalid id \" + err.Error())\n\t}\n\tvar mate FormDataMate\n\tif err := json.Unmarshal(data, &mate); err != nil {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"invalid id \" + err.Error())\n\t}\n\tif err := t.checkUploadName(ctx, mate.Name); err != nil {\n\t\treturn nil, err\n\t}\n\tinfo, err := t.s3dataBase.StatObject(ctx, mate.Key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif info.Size > 0 && info.Size != mate.Size {\n\t\treturn nil, servererrs.ErrData.WrapMsg(\"file size mismatch\")\n\t}\n\tobj := &model.Object{\n\t\tName:        mate.Name,\n\t\tUserID:      mcontext.GetOpUserID(ctx),\n\t\tHash:        \"etag_\" + info.ETag,\n\t\tKey:         info.Key,\n\t\tSize:        info.Size,\n\t\tContentType: mate.ContentType,\n\t\tGroup:       mate.Group,\n\t\tCreateTime:  time.Now(),\n\t}\n\tif err := t.s3dataBase.SetObject(ctx, obj); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.CompleteFormDataResp{Url: t.apiAddress(req.UrlPrefix, mate.Name)}, nil\n}\n\nfunc (t *thirdServer) apiAddress(prefix, name string) string {\n\treturn prefix + name\n}\n\nfunc (t *thirdServer) DeleteOutdatedData(ctx context.Context, req *third.DeleteOutdatedDataReq) (*third.DeleteOutdatedDataResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tengine := t.config.RpcConfig.Object.Enable\n\texpireTime := time.UnixMilli(req.ExpireTime)\n\t// Find all expired data in S3 database\n\tmodels, err := t.s3dataBase.FindExpirationObject(ctx, engine, expireTime, req.ObjectGroup, int64(req.Limit))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor i, obj := range models {\n\t\tif err := t.s3dataBase.DeleteSpecifiedData(ctx, engine, []string{obj.Name}); err != nil {\n\t\t\treturn nil, errs.Wrap(err)\n\t\t}\n\t\tif err := t.s3dataBase.DelS3Key(ctx, engine, obj.Name); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcount, err := t.s3dataBase.GetKeyCount(ctx, engine, obj.Key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.ZDebug(ctx, \"delete s3 object record\", \"index\", i, \"s3\", obj, \"count\", count)\n\t\tif count == 0 {\n\t\t\tif err := t.s3.DeleteObject(ctx, obj.Key); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\treturn &third.DeleteOutdatedDataResp{Count: int32(len(models))}, nil\n}\n\ntype FormDataMate struct {\n\tName        string `json:\"name\"`\n\tSize        int64  `json:\"size\"`\n\tContentType string `json:\"contentType\"`\n\tGroup       string `json:\"group\"`\n\tKey         string `json:\"key\"`\n}\n"
  },
  {
    "path": "internal/rpc/third/third.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage third\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/tools/s3/disable\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/tools/s3/aws\"\n\t\"github.com/openimsdk/tools/s3/kodo\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/s3\"\n\t\"github.com/openimsdk/tools/s3/cos\"\n\t\"github.com/openimsdk/tools/s3/minio\"\n\t\"github.com/openimsdk/tools/s3/oss\"\n\t\"google.golang.org/grpc\"\n)\n\ntype thirdServer struct {\n\tthird.UnimplementedThirdServer\n\tthirdDatabase controller.ThirdDatabase\n\ts3dataBase    controller.S3Database\n\tdefaultExpire time.Duration\n\tconfig        *Config\n\ts3            s3.Interface\n\tuserClient    *rpcli.UserClient\n}\n\ntype Config struct {\n\tRpcConfig          config.Third\n\tRedisConfig        config.Redis\n\tMongodbConfig      config.Mongo\n\tNotificationConfig config.Notification\n\tShare              config.Share\n\tMinioConfig        config.Minio\n\tLocalCacheConfig   config.LocalCache\n\tDiscovery          config.Discovery\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tdbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)\n\tmgocli, err := dbb.Mongo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogdb, err := mgo.NewLogMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\ts3db, err := mgo.NewS3Mongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar thirdCache cache.ThirdCache\n\tif rdb == nil {\n\t\ttc, err := mgo.NewCacheMgo(mgocli.GetDB())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tthirdCache = mcache.NewThirdCache(tc)\n\t} else {\n\t\tthirdCache = redis.NewThirdCache(rdb)\n\t}\n\t// Select the oss method according to the profile policy\n\tvar o s3.Interface\n\tswitch enable := config.RpcConfig.Object.Enable; enable {\n\tcase \"minio\":\n\t\tvar minioCache minio.Cache\n\t\tif rdb == nil {\n\t\t\tmc, err := mgo.NewCacheMgo(mgocli.GetDB())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tminioCache = mcache.NewMinioCache(mc)\n\t\t} else {\n\t\t\tminioCache = redis.NewMinioCache(rdb)\n\t\t}\n\t\to, err = minio.NewMinio(ctx, minioCache, *config.MinioConfig.Build())\n\tcase \"cos\":\n\t\to, err = cos.NewCos(*config.RpcConfig.Object.Cos.Build())\n\tcase \"oss\":\n\t\to, err = oss.NewOSS(*config.RpcConfig.Object.Oss.Build())\n\tcase \"kodo\":\n\t\to, err = kodo.NewKodo(*config.RpcConfig.Object.Kodo.Build())\n\tcase \"aws\":\n\t\to, err = aws.NewAws(*config.RpcConfig.Object.Aws.Build())\n\tcase \"\":\n\t\to = disable.NewDisable()\n\tdefault:\n\t\terr = fmt.Errorf(\"invalid object enable: %s\", enable)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tuserConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlocalcache.InitLocalCache(&config.LocalCacheConfig)\n\tthird.RegisterThirdServer(server, &thirdServer{\n\t\tthirdDatabase: controller.NewThirdDatabase(thirdCache, logdb),\n\t\ts3dataBase:    controller.NewS3Database(rdb, o, s3db),\n\t\tdefaultExpire: time.Hour * 24 * 7,\n\t\tconfig:        config,\n\t\ts3:            o,\n\t\tuserClient:    rpcli.NewUserClient(userConn),\n\t})\n\treturn nil\n}\n\nfunc (t *thirdServer) FcmUpdateToken(ctx context.Context, req *third.FcmUpdateTokenReq) (resp *third.FcmUpdateTokenResp, err error) {\n\terr = t.thirdDatabase.FcmUpdateToken(ctx, req.Account, int(req.PlatformID), req.FcmToken, req.ExpireTime)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.FcmUpdateTokenResp{}, nil\n}\n\nfunc (t *thirdServer) SetAppBadge(ctx context.Context, req *third.SetAppBadgeReq) (resp *third.SetAppBadgeResp, err error) {\n\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\treturn nil, err\n\t}\n\terr = t.thirdDatabase.SetAppBadge(ctx, req.UserID, int(req.AppUnreadCount))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &third.SetAppBadgeResp{}, nil\n}\n"
  },
  {
    "path": "internal/rpc/third/tool.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage third\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nfunc toPbMapArray(m map[string][]string) []*third.KeyValues {\n\tif len(m) == 0 {\n\t\treturn nil\n\t}\n\tres := make([]*third.KeyValues, 0, len(m))\n\tfor key := range m {\n\t\tres = append(res, &third.KeyValues{\n\t\t\tKey:    key,\n\t\t\tValues: m[key],\n\t\t})\n\t}\n\treturn res\n}\n\nfunc (t *thirdServer) checkUploadName(ctx context.Context, name string) error {\n\tif name == \"\" {\n\t\treturn errs.ErrArgs.WrapMsg(\"name is empty\")\n\t}\n\tif name[0] == '/' {\n\t\treturn errs.ErrArgs.WrapMsg(\"name cannot start with `/`\")\n\t}\n\tif err := checkValidObjectName(name); err != nil {\n\t\treturn errs.ErrArgs.WrapMsg(err.Error())\n\t}\n\topUserID := mcontext.GetOpUserID(ctx)\n\tif opUserID == \"\" {\n\t\treturn errs.ErrNoPermission.WrapMsg(\"opUserID is empty\")\n\t}\n\tif !authverify.CheckUserIsAdmin(ctx, opUserID) {\n\t\tif !strings.HasPrefix(name, opUserID+\"/\") {\n\t\t\treturn errs.ErrNoPermission.WrapMsg(fmt.Sprintf(\"name must start with `%s/`\", opUserID))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkValidObjectNamePrefix(objectName string) error {\n\tif len(objectName) > 1024 {\n\t\treturn errs.New(\"object name cannot be longer than 1024 characters\")\n\t}\n\tif !utf8.ValidString(objectName) {\n\t\treturn errs.New(\"object name with non UTF-8 strings are not supported\")\n\t}\n\treturn nil\n}\n\nfunc checkValidObjectName(objectName string) error {\n\tif strings.TrimSpace(objectName) == \"\" {\n\t\treturn errs.New(\"object name cannot be empty\")\n\t}\n\treturn checkValidObjectNamePrefix(objectName)\n}\n\nfunc putUpdate[T any](update map[string]any, name string, val interface{ GetValuePtr() *T }) {\n\tptrVal := val.GetValuePtr()\n\tif ptrVal == nil {\n\t\treturn\n\t}\n\tupdate[name] = *ptrVal\n}\n"
  },
  {
    "path": "internal/rpc/user/callback.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage user\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\n\tcbapi \"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\tpbuser \"github.com/openimsdk/protocol/user\"\n)\n\nfunc (s *userServer) webhookBeforeUpdateUserInfo(ctx context.Context, before *config.BeforeConfig, req *pbuser.UpdateUserInfoReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &cbapi.CallbackBeforeUpdateUserInfoReq{\n\t\t\tCallbackCommand: cbapi.CallbackBeforeUpdateUserInfoCommand,\n\t\t\tUserID:          req.UserInfo.UserID,\n\t\t\tFaceURL:         &req.UserInfo.FaceURL,\n\t\t\tNickname:        &req.UserInfo.Nickname,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeUpdateUserInfoResp{}\n\t\tif err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdatautil.NotNilReplace(&req.UserInfo.FaceURL, resp.FaceURL)\n\t\tdatautil.NotNilReplace(&req.UserInfo.Ex, resp.Ex)\n\t\tdatautil.NotNilReplace(&req.UserInfo.Nickname, resp.Nickname)\n\t\treturn nil\n\t})\n}\n\nfunc (s *userServer) webhookAfterUpdateUserInfo(ctx context.Context, after *config.AfterConfig, req *pbuser.UpdateUserInfoReq) {\n\tcbReq := &cbapi.CallbackAfterUpdateUserInfoReq{\n\t\tCallbackCommand: cbapi.CallbackAfterUpdateUserInfoCommand,\n\t\tUserID:          req.UserInfo.UserID,\n\t\tFaceURL:         req.UserInfo.FaceURL,\n\t\tNickname:        req.UserInfo.Nickname,\n\t}\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterUpdateUserInfoResp{}, after)\n}\n\nfunc (s *userServer) webhookBeforeUpdateUserInfoEx(ctx context.Context, before *config.BeforeConfig, req *pbuser.UpdateUserInfoExReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &cbapi.CallbackBeforeUpdateUserInfoExReq{\n\t\t\tCallbackCommand: cbapi.CallbackBeforeUpdateUserInfoExCommand,\n\t\t\tUserID:          req.UserInfo.UserID,\n\t\t\tFaceURL:         req.UserInfo.FaceURL,\n\t\t\tNickname:        req.UserInfo.Nickname,\n\t\t}\n\t\tresp := &cbapi.CallbackBeforeUpdateUserInfoExResp{}\n\t\tif err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdatautil.NotNilReplace(req.UserInfo.FaceURL, resp.FaceURL)\n\t\tdatautil.NotNilReplace(req.UserInfo.Ex, resp.Ex)\n\t\tdatautil.NotNilReplace(req.UserInfo.Nickname, resp.Nickname)\n\t\treturn nil\n\t})\n}\n\nfunc (s *userServer) webhookAfterUpdateUserInfoEx(ctx context.Context, after *config.AfterConfig, req *pbuser.UpdateUserInfoExReq) {\n\tcbReq := &cbapi.CallbackAfterUpdateUserInfoExReq{\n\t\tCallbackCommand: cbapi.CallbackAfterUpdateUserInfoExCommand,\n\t\tUserID:          req.UserInfo.UserID,\n\t\tFaceURL:         req.UserInfo.FaceURL,\n\t\tNickname:        req.UserInfo.Nickname,\n\t}\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterUpdateUserInfoExResp{}, after)\n}\n\nfunc (s *userServer) webhookBeforeUserRegister(ctx context.Context, before *config.BeforeConfig, req *pbuser.UserRegisterReq) error {\n\treturn webhook.WithCondition(ctx, before, func(ctx context.Context) error {\n\t\tcbReq := &cbapi.CallbackBeforeUserRegisterReq{\n\t\t\tCallbackCommand: cbapi.CallbackBeforeUserRegisterCommand,\n\t\t\tUsers:           req.Users,\n\t\t}\n\n\t\tresp := &cbapi.CallbackBeforeUserRegisterResp{}\n\n\t\tif err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(resp.Users) != 0 {\n\t\t\treq.Users = resp.Users\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (s *userServer) webhookAfterUserRegister(ctx context.Context, after *config.AfterConfig, req *pbuser.UserRegisterReq) {\n\tcbReq := &cbapi.CallbackAfterUserRegisterReq{\n\t\tCallbackCommand: cbapi.CallbackAfterUserRegisterCommand,\n\t\tUsers:           req.Users,\n\t}\n\n\ts.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterUserRegisterResp{}, after)\n}\n"
  },
  {
    "path": "internal/rpc/user/config.go",
    "content": "package user\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\tpbuser \"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc (s *userServer) GetUserClientConfig(ctx context.Context, req *pbuser.GetUserClientConfigReq) (*pbuser.GetUserClientConfigResp, error) {\n\tif req.UserID != \"\" {\n\t\tif err := authverify.CheckAccess(ctx, req.UserID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif _, err := s.db.GetUserByID(ctx, req.UserID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tres, err := s.clientConfig.GetUserConfig(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.GetUserClientConfigResp{Configs: res}, nil\n}\n\nfunc (s *userServer) SetUserClientConfig(ctx context.Context, req *pbuser.SetUserClientConfigReq) (*pbuser.SetUserClientConfigResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tif req.UserID != \"\" {\n\t\tif _, err := s.db.GetUserByID(ctx, req.UserID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif err := s.clientConfig.SetUserConfig(ctx, req.UserID, req.Configs); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.SetUserClientConfigResp{}, nil\n}\n\nfunc (s *userServer) DelUserClientConfig(ctx context.Context, req *pbuser.DelUserClientConfigReq) (*pbuser.DelUserClientConfigResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := s.clientConfig.DelUserConfig(ctx, req.UserID, req.Keys); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.DelUserClientConfigResp{}, nil\n}\n\nfunc (s *userServer) PageUserClientConfig(ctx context.Context, req *pbuser.PageUserClientConfigReq) (*pbuser.PageUserClientConfigResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\ttotal, res, err := s.clientConfig.GetUserConfigPage(ctx, req.UserID, req.Key, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.PageUserClientConfigResp{\n\t\tTotal: total,\n\t\tConfigs: datautil.Slice(res, func(e *model.ClientConfig) *pbuser.ClientConfig {\n\t\t\treturn &pbuser.ClientConfig{\n\t\t\t\tUserID: e.UserID,\n\t\t\t\tKey:    e.Key,\n\t\t\t\tValue:  e.Value,\n\t\t\t}\n\t\t}),\n\t}, nil\n}\n"
  },
  {
    "path": "internal/rpc/user/notification.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage user\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/msg\"\n\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification/common_user\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\ntype UserNotificationSender struct {\n\t*notification.NotificationSender\n\tgetUsersInfo func(ctx context.Context, userIDs []string) ([]common_user.CommonUser, error)\n\t// db controller\n\tdb controller.UserDatabase\n}\n\ntype userNotificationSenderOptions func(*UserNotificationSender)\n\nfunc WithUserDB(db controller.UserDatabase) userNotificationSenderOptions {\n\treturn func(u *UserNotificationSender) {\n\t\tu.db = db\n\t}\n}\n\nfunc WithUserFunc(\n\tfn func(ctx context.Context, userIDs []string) (users []*relationtb.User, err error),\n) userNotificationSenderOptions {\n\treturn func(u *UserNotificationSender) {\n\t\tf := func(ctx context.Context, userIDs []string) (result []common_user.CommonUser, err error) {\n\t\t\tusers, err := fn(ctx, userIDs)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfor _, user := range users {\n\t\t\t\tresult = append(result, user)\n\t\t\t}\n\t\t\treturn result, nil\n\t\t}\n\t\tu.getUsersInfo = f\n\t}\n}\n\nfunc NewUserNotificationSender(config *Config, msgClient *rpcli.MsgClient, opts ...userNotificationSenderOptions) *UserNotificationSender {\n\tf := &UserNotificationSender{\n\t\tNotificationSender: notification.NewNotificationSender(&config.NotificationConfig, notification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {\n\t\t\treturn msgClient.SendMsg(ctx, req)\n\t\t})),\n\t}\n\tfor _, opt := range opts {\n\t\topt(f)\n\t}\n\treturn f\n}\n\n/* func (u *UserNotificationSender) getUsersInfoMap(\n\tctx context.Context,\n\tuserIDs []string,\n) (map[string]*sdkws.UserInfo, error) {\n\tusers, err := u.getUsersInfo(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make(map[string]*sdkws.UserInfo)\n\tfor _, user := range users {\n\t\tresult[user.GetUserID()] = user.(*sdkws.UserInfo)\n\t}\n\treturn result, nil\n} */\n\n/* func (u *UserNotificationSender) getFromToUserNickname(\n\tctx context.Context,\n\tfromUserID, toUserID string,\n) (string, string, error) {\n\tusers, err := u.getUsersInfoMap(ctx, []string{fromUserID, toUserID})\n\tif err != nil {\n\t\treturn \"\", \"\", nil\n\t}\n\treturn users[fromUserID].Nickname, users[toUserID].Nickname, nil\n} */\n\nfunc (u *UserNotificationSender) UserStatusChangeNotification(\n\tctx context.Context,\n\ttips *sdkws.UserStatusChangeTips,\n) {\n\tu.Notification(ctx, tips.FromUserID, tips.ToUserID, constant.UserStatusChangeNotification, tips)\n}\nfunc (u *UserNotificationSender) UserCommandUpdateNotification(\n\tctx context.Context,\n\ttips *sdkws.UserCommandUpdateTips,\n) {\n\tu.Notification(ctx, tips.FromUserID, tips.ToUserID, constant.UserCommandUpdateNotification, tips)\n}\nfunc (u *UserNotificationSender) UserCommandAddNotification(\n\tctx context.Context,\n\ttips *sdkws.UserCommandAddTips,\n) {\n\tu.Notification(ctx, tips.FromUserID, tips.ToUserID, constant.UserCommandAddNotification, tips)\n}\nfunc (u *UserNotificationSender) UserCommandDeleteNotification(\n\tctx context.Context,\n\ttips *sdkws.UserCommandDeleteTips,\n) {\n\tu.Notification(ctx, tips.FromUserID, tips.ToUserID, constant.UserCommandDeleteNotification, tips)\n}\n"
  },
  {
    "path": "internal/rpc/user/online.go",
    "content": "package user\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbuser \"github.com/openimsdk/protocol/user\"\n)\n\nfunc (s *userServer) getUserOnlineStatus(ctx context.Context, userID string) (*pbuser.OnlineStatus, error) {\n\tplatformIDs, err := s.online.GetOnline(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstatus := pbuser.OnlineStatus{\n\t\tUserID:      userID,\n\t\tPlatformIDs: platformIDs,\n\t}\n\tif len(platformIDs) > 0 {\n\t\tstatus.Status = constant.Online\n\t} else {\n\t\tstatus.Status = constant.Offline\n\t}\n\treturn &status, nil\n}\n\nfunc (s *userServer) getUsersOnlineStatus(ctx context.Context, userIDs []string) ([]*pbuser.OnlineStatus, error) {\n\tres := make([]*pbuser.OnlineStatus, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tstatus, err := s.getUserOnlineStatus(ctx, userID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres = append(res, status)\n\t}\n\treturn res, nil\n}\n\n// SubscribeOrCancelUsersStatus Subscribe online or cancel online users.\nfunc (s *userServer) SubscribeOrCancelUsersStatus(ctx context.Context, req *pbuser.SubscribeOrCancelUsersStatusReq) (*pbuser.SubscribeOrCancelUsersStatusResp, error) {\n\treturn &pbuser.SubscribeOrCancelUsersStatusResp{}, nil\n}\n\n// GetUserStatus Get the online status of the user.\nfunc (s *userServer) GetUserStatus(ctx context.Context, req *pbuser.GetUserStatusReq) (*pbuser.GetUserStatusResp, error) {\n\tres, err := s.getUsersOnlineStatus(ctx, req.UserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.GetUserStatusResp{StatusList: res}, nil\n}\n\n// SetUserStatus Synchronize user's online status.\nfunc (s *userServer) SetUserStatus(ctx context.Context, req *pbuser.SetUserStatusReq) (*pbuser.SetUserStatusResp, error) {\n\tvar (\n\t\tonline  []int32\n\t\toffline []int32\n\t)\n\tswitch req.Status {\n\tcase constant.Online:\n\t\tonline = []int32{req.PlatformID}\n\tcase constant.Offline:\n\t\toffline = []int32{req.PlatformID}\n\t}\n\tif err := s.online.SetUserOnline(ctx, req.UserID, online, offline); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.SetUserStatusResp{}, nil\n}\n\n// GetSubscribeUsersStatus Get the online status of subscribers.\nfunc (s *userServer) GetSubscribeUsersStatus(ctx context.Context, req *pbuser.GetSubscribeUsersStatusReq) (*pbuser.GetSubscribeUsersStatusResp, error) {\n\treturn &pbuser.GetSubscribeUsersStatusResp{}, nil\n}\n\nfunc (s *userServer) SetUserOnlineStatus(ctx context.Context, req *pbuser.SetUserOnlineStatusReq) (*pbuser.SetUserOnlineStatusResp, error) {\n\tfor _, status := range req.Status {\n\t\tif err := s.online.SetUserOnline(ctx, status.UserID, status.Online, status.Offline); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &pbuser.SetUserOnlineStatusResp{}, nil\n}\n\nfunc (s *userServer) GetAllOnlineUsers(ctx context.Context, req *pbuser.GetAllOnlineUsersReq) (*pbuser.GetAllOnlineUsersResp, error) {\n\tresMap, nextCursor, err := s.online.GetAllOnlineUsers(ctx, req.Cursor)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp := &pbuser.GetAllOnlineUsersResp{\n\t\tStatusList: make([]*pbuser.OnlineStatus, 0, len(resMap)),\n\t\tNextCursor: nextCursor,\n\t}\n\tfor userID, plats := range resMap {\n\t\tresp.StatusList = append(resp.StatusList, &pbuser.OnlineStatus{\n\t\t\tUserID:      userID,\n\t\t\tStatus:      int32(datautil.If(len(plats) > 0, constant.Online, constant.Offline)),\n\t\t\tPlatformIDs: plats,\n\t\t})\n\t}\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/rpc/user/statistics.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage user\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\tpbuser \"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nfunc (s *userServer) UserRegisterCount(ctx context.Context, req *pbuser.UserRegisterCountReq) (*pbuser.UserRegisterCountResp, error) {\n\tif req.Start > req.End {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"start > end\")\n\t}\n\ttotal, err := s.db.CountTotal(ctx, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstart := time.UnixMilli(req.Start)\n\tbefore, err := s.db.CountTotal(ctx, &start)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcount, err := s.db.CountRangeEverydayTotal(ctx, start, time.UnixMilli(req.End))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.UserRegisterCountResp{Total: total, Before: before, Count: count}, nil\n}\n"
  },
  {
    "path": "internal/rpc/user/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage user\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/relation\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\ttablerelation \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/dbbuild\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/group\"\n\tfriendpb \"github.com/openimsdk/protocol/relation\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\tpbuser \"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"google.golang.org/grpc\"\n)\n\nconst (\n\tdefaultSecret = \"openIM123\"\n)\n\ntype userServer struct {\n\tpbuser.UnimplementedUserServer\n\tonline                   cache.OnlineCache\n\tdb                       controller.UserDatabase\n\tfriendNotificationSender *relation.FriendNotificationSender\n\tuserNotificationSender   *UserNotificationSender\n\tRegisterCenter           discovery.Conn\n\tconfig                   *Config\n\twebhookClient            *webhook.Client\n\tgroupClient              *rpcli.GroupClient\n\trelationClient           *rpcli.RelationClient\n\tclientConfig             controller.ClientConfigDatabase\n\n\tadminUserIDs []string\n}\n\ntype Config struct {\n\tRpcConfig          config.User\n\tRedisConfig        config.Redis\n\tMongodbConfig      config.Mongo\n\tKafkaConfig        config.Kafka\n\tNotificationConfig config.Notification\n\tShare              config.Share\n\tWebhooksConfig     config.Webhooks\n\tLocalCacheConfig   config.LocalCache\n\tDiscovery          config.Discovery\n}\n\nfunc Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error {\n\tdbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)\n\tmgocli, err := dbb.Mongo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\trdb, err := dbb.Redis(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tusers := make([]*tablerelation.User, 0)\n\n\tfor i := range config.Share.IMAdminUser.UserIDs {\n\t\tusers = append(users, &tablerelation.User{\n\t\t\tUserID:         config.Share.IMAdminUser.UserIDs[i],\n\t\t\tNickname:       config.Share.IMAdminUser.Nicknames[i],\n\t\t\tAppMangerLevel: constant.AppAdmin,\n\t\t})\n\t}\n\tuserDB, err := mgo.NewUserMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tclientConfigDB, err := mgo.NewClientConfig(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgroupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfriendConn, err := client.GetConn(ctx, config.Discovery.RpcService.Friend)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmsgClient := rpcli.NewMsgClient(msgConn)\n\tuserCache := redis.NewUserCacheRedis(rdb, &config.LocalCacheConfig, userDB, redis.GetRocksCacheOptions())\n\tdatabase := controller.NewUserDatabase(userDB, userCache, mgocli.GetTx())\n\tlocalcache.InitLocalCache(&config.LocalCacheConfig)\n\tu := &userServer{\n\t\tonline:                   redis.NewUserOnline(rdb),\n\t\tdb:                       database,\n\t\tRegisterCenter:           client,\n\t\tfriendNotificationSender: relation.NewFriendNotificationSender(&config.NotificationConfig, msgClient, relation.WithDBFunc(database.FindWithError)),\n\t\tuserNotificationSender:   NewUserNotificationSender(config, msgClient, WithUserFunc(database.FindWithError)),\n\t\tconfig:                   config,\n\t\twebhookClient:            webhook.NewWebhookClient(config.WebhooksConfig.URL),\n\t\tclientConfig:             controller.NewClientConfigDatabase(clientConfigDB, redis.NewClientConfigCache(rdb, clientConfigDB), mgocli.GetTx()),\n\t\tgroupClient:              rpcli.NewGroupClient(groupConn),\n\t\trelationClient:           rpcli.NewRelationClient(friendConn),\n\t\tadminUserIDs:             config.Share.IMAdminUser.UserIDs,\n\t}\n\tpbuser.RegisterUserServer(server, u)\n\treturn u.db.InitOnce(context.Background(), users)\n}\n\nfunc (s *userServer) GetDesignateUsers(ctx context.Context, req *pbuser.GetDesignateUsersReq) (resp *pbuser.GetDesignateUsersResp, err error) {\n\tresp = &pbuser.GetDesignateUsersResp{}\n\tusers, err := s.db.Find(ctx, req.UserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp.UsersInfo = convert.UsersDB2Pb(users)\n\treturn resp, nil\n}\n\n// deprecated:\n// UpdateUserInfo\nfunc (s *userServer) UpdateUserInfo(ctx context.Context, req *pbuser.UpdateUserInfoReq) (resp *pbuser.UpdateUserInfoResp, err error) {\n\tresp = &pbuser.UpdateUserInfoResp{}\n\terr = authverify.CheckAccess(ctx, req.UserInfo.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := s.webhookBeforeUpdateUserInfo(ctx, &s.config.WebhooksConfig.BeforeUpdateUserInfo, req); err != nil {\n\t\treturn nil, err\n\t}\n\tdata := convert.UserPb2DBMap(req.UserInfo)\n\toldUser, err := s.db.GetUserByID(ctx, req.UserInfo.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := s.db.UpdateByMap(ctx, req.UserInfo.UserID, data); err != nil {\n\t\treturn nil, err\n\t}\n\ts.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserInfo.UserID)\n\n\ts.webhookAfterUpdateUserInfo(ctx, &s.config.WebhooksConfig.AfterUpdateUserInfo, req)\n\tif err = s.NotificationUserInfoUpdate(ctx, req.UserInfo.UserID, oldUser); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (s *userServer) UpdateUserInfoEx(ctx context.Context, req *pbuser.UpdateUserInfoExReq) (resp *pbuser.UpdateUserInfoExResp, err error) {\n\tresp = &pbuser.UpdateUserInfoExResp{}\n\terr = authverify.CheckAccess(ctx, req.UserInfo.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = s.webhookBeforeUpdateUserInfoEx(ctx, &s.config.WebhooksConfig.BeforeUpdateUserInfoEx, req); err != nil {\n\t\treturn nil, err\n\t}\n\n\toldUser, err := s.db.GetUserByID(ctx, req.UserInfo.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata := convert.UserPb2DBMapEx(req.UserInfo)\n\tif err = s.db.UpdateByMap(ctx, req.UserInfo.UserID, data); err != nil {\n\t\treturn nil, err\n\t}\n\n\ts.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserInfo.UserID)\n\n\t//friends, err := s.friendRpcClient.GetFriendIDs(ctx, req.UserInfo.UserID)\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\t//if req.UserInfo.Nickname != nil || req.UserInfo.FaceURL != nil {\n\t//\tif err := s.NotificationUserInfoUpdate(ctx, req.UserInfo.UserID); err != nil {\n\t//\t\treturn nil, err\n\t//\t}\n\t//}\n\t//for _, friendID := range friends {\n\t//\ts.friendNotificationSender.FriendInfoUpdatedNotification(ctx, req.UserInfo.UserID, friendID)\n\t//}\n\n\ts.webhookAfterUpdateUserInfoEx(ctx, &s.config.WebhooksConfig.AfterUpdateUserInfoEx, req)\n\tif err := s.NotificationUserInfoUpdate(ctx, req.UserInfo.UserID, oldUser); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp, nil\n}\nfunc (s *userServer) SetGlobalRecvMessageOpt(ctx context.Context, req *pbuser.SetGlobalRecvMessageOptReq) (resp *pbuser.SetGlobalRecvMessageOptResp, err error) {\n\tresp = &pbuser.SetGlobalRecvMessageOptResp{}\n\tif _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil {\n\t\treturn nil, err\n\t}\n\tm := make(map[string]any, 1)\n\tm[\"global_recv_msg_opt\"] = req.GlobalRecvMsgOpt\n\tif err := s.db.UpdateByMap(ctx, req.UserID, m); err != nil {\n\t\treturn nil, err\n\t}\n\ts.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID)\n\treturn resp, nil\n}\n\nfunc (s *userServer) AccountCheck(ctx context.Context, req *pbuser.AccountCheckReq) (resp *pbuser.AccountCheckResp, err error) {\n\tresp = &pbuser.AccountCheckResp{}\n\tif datautil.Duplicate(req.CheckUserIDs) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userID repeated\")\n\t}\n\tif err = authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tusers, err := s.db.Find(ctx, req.CheckUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserIDs := make(map[string]any, 0)\n\tfor _, v := range users {\n\t\tuserIDs[v.UserID] = nil\n\t}\n\tfor _, v := range req.CheckUserIDs {\n\t\ttemp := &pbuser.AccountCheckRespSingleUserStatus{UserID: v}\n\t\tif _, ok := userIDs[v]; ok {\n\t\t\ttemp.AccountStatus = constant.Registered\n\t\t} else {\n\t\t\ttemp.AccountStatus = constant.UnRegistered\n\t\t}\n\t\tresp.Results = append(resp.Results, temp)\n\t}\n\treturn resp, nil\n}\n\nfunc (s *userServer) GetPaginationUsers(ctx context.Context, req *pbuser.GetPaginationUsersReq) (resp *pbuser.GetPaginationUsersResp, err error) {\n\tif req.UserID == \"\" && req.NickName == \"\" {\n\t\ttotal, users, err := s.db.PageFindUser(ctx, constant.IMOrdinaryUser, constant.AppOrdinaryUsers, req.Pagination)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &pbuser.GetPaginationUsersResp{Total: int32(total), Users: convert.UsersDB2Pb(users)}, err\n\t} else {\n\t\ttotal, users, err := s.db.PageFindUserWithKeyword(ctx, constant.IMOrdinaryUser, constant.AppOrdinaryUsers, req.UserID, req.NickName, req.Pagination)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &pbuser.GetPaginationUsersResp{Total: int32(total), Users: convert.UsersDB2Pb(users)}, err\n\n\t}\n\n}\n\nfunc (s *userServer) UserRegister(ctx context.Context, req *pbuser.UserRegisterReq) (resp *pbuser.UserRegisterResp, err error) {\n\tresp = &pbuser.UserRegisterResp{}\n\tif len(req.Users) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"users is empty\")\n\t}\n\t// check if secret is changed\n\t//if s.config.Share.Secret == defaultSecret {\n\t//\treturn nil, servererrs.ErrSecretNotChanged.Wrap()\n\t//}\n\tif err = authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tif datautil.DuplicateAny(req.Users, func(e *sdkws.UserInfo) string { return e.UserID }) {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userID repeated\")\n\t}\n\tuserIDs := make([]string, 0)\n\tfor _, user := range req.Users {\n\t\tif user.UserID == \"\" {\n\t\t\treturn nil, errs.ErrArgs.WrapMsg(\"userID is empty\")\n\t\t}\n\t\tif strings.Contains(user.UserID, \":\") {\n\t\t\treturn nil, errs.ErrArgs.WrapMsg(\"userID contains ':' is invalid userID\")\n\t\t}\n\t\tuserIDs = append(userIDs, user.UserID)\n\t}\n\texist, err := s.db.IsExist(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exist {\n\t\treturn nil, servererrs.ErrRegisteredAlready.WrapMsg(\"userID registered already\")\n\t}\n\tif err := s.webhookBeforeUserRegister(ctx, &s.config.WebhooksConfig.BeforeUserRegister, req); err != nil {\n\t\treturn nil, err\n\t}\n\tnow := time.Now()\n\tusers := make([]*tablerelation.User, 0, len(req.Users))\n\tfor _, user := range req.Users {\n\t\tusers = append(users, &tablerelation.User{\n\t\t\tUserID:           user.UserID,\n\t\t\tNickname:         user.Nickname,\n\t\t\tFaceURL:          user.FaceURL,\n\t\t\tEx:               user.Ex,\n\t\t\tCreateTime:       now,\n\t\t\tAppMangerLevel:   user.AppMangerLevel,\n\t\t\tGlobalRecvMsgOpt: user.GlobalRecvMsgOpt,\n\t\t})\n\t}\n\tif err := s.db.Create(ctx, users); err != nil {\n\t\treturn nil, err\n\t}\n\n\tprommetrics.UserRegisterCounter.Add(float64(len(users)))\n\n\ts.webhookAfterUserRegister(ctx, &s.config.WebhooksConfig.AfterUserRegister, req)\n\treturn resp, nil\n}\n\nfunc (s *userServer) GetGlobalRecvMessageOpt(ctx context.Context, req *pbuser.GetGlobalRecvMessageOptReq) (resp *pbuser.GetGlobalRecvMessageOptResp, err error) {\n\tuser, err := s.db.FindWithError(ctx, []string{req.UserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.GetGlobalRecvMessageOptResp{GlobalRecvMsgOpt: user[0].GlobalRecvMsgOpt}, nil\n}\n\n// GetAllUserID Get user account by page.\nfunc (s *userServer) GetAllUserID(ctx context.Context, req *pbuser.GetAllUserIDReq) (resp *pbuser.GetAllUserIDResp, err error) {\n\ttotal, userIDs, err := s.db.GetAllUserID(ctx, req.Pagination)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.GetAllUserIDResp{Total: int32(total), UserIDs: userIDs}, nil\n}\n\n// ProcessUserCommandAdd user general function add.\nfunc (s *userServer) ProcessUserCommandAdd(ctx context.Context, req *pbuser.ProcessUserCommandAddReq) (*pbuser.ProcessUserCommandAddResp, error) {\n\terr := authverify.CheckAccess(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar value string\n\tif req.Value != nil {\n\t\tvalue = req.Value.Value\n\t}\n\tvar ex string\n\tif req.Ex != nil {\n\t\tvalue = req.Ex.Value\n\t}\n\t// Assuming you have a method in s.storage to add a user command\n\terr = s.db.AddUserCommand(ctx, req.UserID, req.Type, req.Uuid, value, ex)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttips := &sdkws.UserCommandAddTips{\n\t\tFromUserID: req.UserID,\n\t\tToUserID:   req.UserID,\n\t}\n\ts.userNotificationSender.UserCommandAddNotification(ctx, tips)\n\treturn &pbuser.ProcessUserCommandAddResp{}, nil\n}\n\n// ProcessUserCommandDelete user general function delete.\nfunc (s *userServer) ProcessUserCommandDelete(ctx context.Context, req *pbuser.ProcessUserCommandDeleteReq) (*pbuser.ProcessUserCommandDeleteResp, error) {\n\terr := authverify.CheckAccess(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = s.db.DeleteUserCommand(ctx, req.UserID, req.Type, req.Uuid)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttips := &sdkws.UserCommandDeleteTips{\n\t\tFromUserID: req.UserID,\n\t\tToUserID:   req.UserID,\n\t}\n\ts.userNotificationSender.UserCommandDeleteNotification(ctx, tips)\n\treturn &pbuser.ProcessUserCommandDeleteResp{}, nil\n}\n\n// ProcessUserCommandUpdate user general function update.\nfunc (s *userServer) ProcessUserCommandUpdate(ctx context.Context, req *pbuser.ProcessUserCommandUpdateReq) (*pbuser.ProcessUserCommandUpdateResp, error) {\n\terr := authverify.CheckAccess(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tval := make(map[string]any)\n\n\t// Map fields from eax to val\n\tif req.Value != nil {\n\t\tval[\"value\"] = req.Value.Value\n\t}\n\tif req.Ex != nil {\n\t\tval[\"ex\"] = req.Ex.Value\n\t}\n\n\t// Assuming you have a method in s.storage to update a user command\n\terr = s.db.UpdateUserCommand(ctx, req.UserID, req.Type, req.Uuid, val)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttips := &sdkws.UserCommandUpdateTips{\n\t\tFromUserID: req.UserID,\n\t\tToUserID:   req.UserID,\n\t}\n\ts.userNotificationSender.UserCommandUpdateNotification(ctx, tips)\n\treturn &pbuser.ProcessUserCommandUpdateResp{}, nil\n}\n\nfunc (s *userServer) ProcessUserCommandGet(ctx context.Context, req *pbuser.ProcessUserCommandGetReq) (*pbuser.ProcessUserCommandGetResp, error) {\n\n\terr := authverify.CheckAccess(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Fetch user commands from the database\n\tcommands, err := s.db.GetUserCommands(ctx, req.UserID, req.Type)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Initialize commandInfoSlice as an empty slice\n\tcommandInfoSlice := make([]*pbuser.CommandInfoResp, 0, len(commands))\n\n\tfor _, command := range commands {\n\t\t// No need to use index since command is already a pointer\n\t\tcommandInfoSlice = append(commandInfoSlice, &pbuser.CommandInfoResp{\n\t\t\tType:       command.Type,\n\t\t\tUuid:       command.Uuid,\n\t\t\tValue:      command.Value,\n\t\t\tCreateTime: command.CreateTime,\n\t\t\tEx:         command.Ex,\n\t\t})\n\t}\n\n\t// Return the response with the slice\n\treturn &pbuser.ProcessUserCommandGetResp{CommandResp: commandInfoSlice}, nil\n}\n\nfunc (s *userServer) ProcessUserCommandGetAll(ctx context.Context, req *pbuser.ProcessUserCommandGetAllReq) (*pbuser.ProcessUserCommandGetAllResp, error) {\n\terr := authverify.CheckAccess(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Fetch user commands from the database\n\tcommands, err := s.db.GetAllUserCommands(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Initialize commandInfoSlice as an empty slice\n\tcommandInfoSlice := make([]*pbuser.AllCommandInfoResp, 0, len(commands))\n\n\tfor _, command := range commands {\n\t\t// No need to use index since command is already a pointer\n\t\tcommandInfoSlice = append(commandInfoSlice, &pbuser.AllCommandInfoResp{\n\t\t\tType:       command.Type,\n\t\t\tUuid:       command.Uuid,\n\t\t\tValue:      command.Value,\n\t\t\tCreateTime: command.CreateTime,\n\t\t\tEx:         command.Ex,\n\t\t})\n\t}\n\n\t// Return the response with the slice\n\treturn &pbuser.ProcessUserCommandGetAllResp{CommandResp: commandInfoSlice}, nil\n}\n\nfunc (s *userServer) AddNotificationAccount(ctx context.Context, req *pbuser.AddNotificationAccountReq) (*pbuser.AddNotificationAccountResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\tif req.AppMangerLevel < constant.AppNotificationAdmin {\n\t\treturn nil, errs.ErrArgs.WithDetail(\"app level not supported\")\n\t}\n\tif req.UserID == \"\" {\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tuserId := s.genUserID()\n\t\t\t_, err := s.db.FindWithError(ctx, []string{userId})\n\t\t\tif err == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treq.UserID = userId\n\t\t\tbreak\n\t\t}\n\t\tif req.UserID == \"\" {\n\t\t\treturn nil, errs.ErrInternalServer.WrapMsg(\"gen user id failed\")\n\t\t}\n\t} else {\n\t\t_, err := s.db.FindWithError(ctx, []string{req.UserID})\n\t\tif err == nil {\n\t\t\treturn nil, errs.ErrArgs.WrapMsg(\"userID is used\")\n\t\t}\n\t}\n\n\tuser := &tablerelation.User{\n\t\tUserID:         req.UserID,\n\t\tNickname:       req.NickName,\n\t\tFaceURL:        req.FaceURL,\n\t\tCreateTime:     time.Now(),\n\t\tAppMangerLevel: req.AppMangerLevel,\n\t}\n\tif err := s.db.Create(ctx, []*tablerelation.User{user}); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &pbuser.AddNotificationAccountResp{\n\t\tUserID:         req.UserID,\n\t\tNickName:       req.NickName,\n\t\tFaceURL:        req.FaceURL,\n\t\tAppMangerLevel: req.AppMangerLevel,\n\t}, nil\n}\n\nfunc (s *userServer) UpdateNotificationAccountInfo(ctx context.Context, req *pbuser.UpdateNotificationAccountInfoReq) (*pbuser.UpdateNotificationAccountInfoResp, error) {\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil {\n\t\treturn nil, errs.ErrArgs.Wrap()\n\t}\n\n\tuser := map[string]interface{}{}\n\n\tif req.NickName != \"\" {\n\t\tuser[\"nickname\"] = req.NickName\n\t}\n\n\tif req.FaceURL != \"\" {\n\t\tuser[\"face_url\"] = req.FaceURL\n\t}\n\n\tif err := s.db.UpdateByMap(ctx, req.UserID, user); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &pbuser.UpdateNotificationAccountInfoResp{}, nil\n}\n\nfunc (s *userServer) SearchNotificationAccount(ctx context.Context, req *pbuser.SearchNotificationAccountReq) (*pbuser.SearchNotificationAccountResp, error) {\n\t// Check if user is an admin\n\tif err := authverify.CheckAdmin(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar users []*tablerelation.User\n\tvar err error\n\n\t// If a keyword is provided in the request\n\tif req.Keyword != \"\" {\n\t\t// Find users by keyword\n\t\tusers, err = s.db.Find(ctx, []string{req.Keyword})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Convert users to response format\n\t\tresp := s.userModelToResp(users, req.Pagination, req.AppManagerLevel)\n\t\tif resp.Total != 0 {\n\t\t\treturn resp, nil\n\t\t}\n\n\t\t// Find users by nickname if no users found by keyword\n\t\tusers, err = s.db.FindByNickname(ctx, req.Keyword)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp = s.userModelToResp(users, req.Pagination, req.AppManagerLevel)\n\t\treturn resp, nil\n\t}\n\n\t// If no keyword, find users with notification settings\n\tif req.AppManagerLevel != nil {\n\t\tusers, err = s.db.FindNotification(ctx, int64(*req.AppManagerLevel))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tusers, err = s.db.FindSystemAccount(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tresp := s.userModelToResp(users, req.Pagination, req.AppManagerLevel)\n\treturn resp, nil\n}\n\nfunc (s *userServer) GetNotificationAccount(ctx context.Context, req *pbuser.GetNotificationAccountReq) (*pbuser.GetNotificationAccountResp, error) {\n\tif req.UserID == \"\" {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"userID is empty\")\n\t}\n\tuser, err := s.db.GetUserByID(ctx, req.UserID)\n\tif err != nil {\n\t\treturn nil, servererrs.ErrUserIDNotFound.Wrap()\n\t}\n\tif user.AppMangerLevel >= constant.AppAdmin {\n\t\treturn &pbuser.GetNotificationAccountResp{Account: &pbuser.NotificationAccountInfo{\n\t\t\tUserID:         user.UserID,\n\t\t\tFaceURL:        user.FaceURL,\n\t\t\tNickName:       user.Nickname,\n\t\t\tAppMangerLevel: user.AppMangerLevel,\n\t\t}}, nil\n\t}\n\n\treturn nil, errs.ErrNoPermission.WrapMsg(\"notification messages cannot be sent for this ID\")\n}\n\nfunc (s *userServer) genUserID() string {\n\tconst l = 10\n\tdata := make([]byte, l)\n\trand.Read(data)\n\tchars := []byte(\"0123456789\")\n\tfor i := 0; i < len(data); i++ {\n\t\tif i == 0 {\n\t\t\tdata[i] = chars[1:][data[i]%9]\n\t\t} else {\n\t\t\tdata[i] = chars[data[i]%10]\n\t\t}\n\t}\n\treturn string(data)\n}\n\nfunc (s *userServer) userModelToResp(users []*tablerelation.User, pagination pagination.Pagination, appManagerLevel *int32) *pbuser.SearchNotificationAccountResp {\n\taccounts := make([]*pbuser.NotificationAccountInfo, 0)\n\tvar total int64\n\tfor _, v := range users {\n\t\tif v.AppMangerLevel >= constant.AppNotificationAdmin && !datautil.Contain(v.UserID, s.adminUserIDs...) {\n\t\t\tif appManagerLevel != nil {\n\t\t\t\tif v.AppMangerLevel != *appManagerLevel {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\ttemp := &pbuser.NotificationAccountInfo{\n\t\t\t\tUserID:         v.UserID,\n\t\t\t\tFaceURL:        v.FaceURL,\n\t\t\t\tNickName:       v.Nickname,\n\t\t\t\tAppMangerLevel: v.AppMangerLevel,\n\t\t\t}\n\t\t\taccounts = append(accounts, temp)\n\t\t\ttotal += 1\n\t\t}\n\t}\n\n\tnotificationAccounts := datautil.Paginate(accounts, int(pagination.GetPageNumber()), int(pagination.GetShowNumber()))\n\n\treturn &pbuser.SearchNotificationAccountResp{Total: total, NotificationAccounts: notificationAccounts}\n}\n\nfunc (s *userServer) NotificationUserInfoUpdate(ctx context.Context, userID string, oldUser *tablerelation.User) error {\n\tuser, err := s.db.GetUserByID(ctx, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif user.Nickname == oldUser.Nickname && user.FaceURL == oldUser.FaceURL {\n\t\treturn nil\n\t}\n\toldUserInfo := convert.UserDB2Pb(oldUser)\n\tnewUserInfo := convert.UserDB2Pb(user)\n\tvar wg sync.WaitGroup\n\tvar es [2]error\n\twg.Add(len(es))\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t_, es[0] = s.groupClient.NotificationUserInfoUpdate(ctx, &group.NotificationUserInfoUpdateReq{\n\t\t\tUserID:      userID,\n\t\t\tOldUserInfo: oldUserInfo,\n\t\t\tNewUserInfo: newUserInfo,\n\t\t})\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t_, es[1] = s.relationClient.NotificationUserInfoUpdate(ctx, &friendpb.NotificationUserInfoUpdateReq{\n\t\t\tUserID:      userID,\n\t\t\tOldUserInfo: oldUserInfo,\n\t\t\tNewUserInfo: newUserInfo,\n\t\t})\n\t}()\n\twg.Wait()\n\treturn errors.Join(es[:]...)\n}\n\nfunc (s *userServer) SortQuery(ctx context.Context, req *pbuser.SortQueryReq) (*pbuser.SortQueryResp, error) {\n\tusers, err := s.db.SortQuery(ctx, req.UserIDName, req.Asc)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pbuser.SortQueryResp{Users: convert.UsersDB2Pb(users)}, nil\n}\n"
  },
  {
    "path": "internal/tools/cron/cron_task.go",
    "content": "package cron\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\tdisetcd \"github.com/openimsdk/open-im-server/v3/pkg/common/discovery/etcd\"\n\tpbconversation \"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/discovery/etcd\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\t\"github.com/robfig/cron/v3\"\n\t\"google.golang.org/grpc\"\n)\n\ntype Config struct {\n\tCronTask  config.CronTask\n\tShare     config.Share\n\tDiscovery config.Discovery\n}\n\nfunc Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegistry, service grpc.ServiceRegistrar) error {\n\tlog.CInfo(ctx, \"CRON-TASK server is initializing\", \"runTimeEnv\", runtimeenv.RuntimeEnvironment(), \"chatRecordsClearTime\", conf.CronTask.CronExecuteTime, \"msgDestructTime\", conf.CronTask.RetainChatRecords)\n\tif conf.CronTask.RetainChatRecords < 1 {\n\t\tlog.ZInfo(ctx, \"disable cron\")\n\t\t<-ctx.Done()\n\t\treturn nil\n\t}\n\tctx = mcontext.SetOpUserID(ctx, conf.Share.IMAdminUser.UserIDs[0])\n\n\tmsgConn, err := client.GetConn(ctx, conf.Discovery.RpcService.Msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tthirdConn, err := client.GetConn(ctx, conf.Discovery.RpcService.Third)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconversationConn, err := client.GetConn(ctx, conf.Discovery.RpcService.Conversation)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar locker Locker\n\tif conf.Discovery.Enable == config.ETCD {\n\t\tcm := disetcd.NewConfigManager(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient(), []string{\n\t\t\tconf.CronTask.GetConfigFileName(),\n\t\t\tconf.Share.GetConfigFileName(),\n\t\t\tconf.Discovery.GetConfigFileName(),\n\t\t})\n\t\tcm.Watch(ctx)\n\t\tlocker, err = NewEtcdLocker(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif locker == nil {\n\t\tlocker = emptyLocker{}\n\t}\n\n\tsrv := &cronServer{\n\t\tctx:                ctx,\n\t\tconfig:             conf,\n\t\tcron:               cron.New(),\n\t\tmsgClient:          msg.NewMsgClient(msgConn),\n\t\tconversationClient: pbconversation.NewConversationClient(conversationConn),\n\t\tthirdClient:        third.NewThirdClient(thirdConn),\n\t\tlocker:             locker,\n\t}\n\n\tif err := srv.registerClearS3(); err != nil {\n\t\treturn err\n\t}\n\tif err := srv.registerDeleteMsg(); err != nil {\n\t\treturn err\n\t}\n\tif err := srv.registerClearUserMsg(); err != nil {\n\t\treturn err\n\t}\n\tlog.ZDebug(ctx, \"start cron task\", \"CronExecuteTime\", conf.CronTask.CronExecuteTime)\n\tsrv.cron.Start()\n\tlog.ZDebug(ctx, \"cron task server is running\")\n\t<-ctx.Done()\n\tlog.ZDebug(ctx, \"cron task server is shutting down\")\n\tsrv.cron.Stop()\n\n\treturn nil\n}\n\ntype Locker interface {\n\tExecuteWithLock(ctx context.Context, taskName string, task func())\n}\n\ntype emptyLocker struct{}\n\nfunc (emptyLocker) ExecuteWithLock(ctx context.Context, taskName string, task func()) {\n\ttask()\n}\n\ntype cronServer struct {\n\tctx                context.Context\n\tconfig             *Config\n\tcron               *cron.Cron\n\tmsgClient          msg.MsgClient\n\tconversationClient pbconversation.ConversationClient\n\tthirdClient        third.ThirdClient\n\tlocker             Locker\n}\n\nfunc (c *cronServer) registerClearS3() error {\n\tif c.config.CronTask.FileExpireTime <= 0 || len(c.config.CronTask.DeleteObjectType) == 0 {\n\t\tlog.ZInfo(c.ctx, \"disable scheduled cleanup of s3\", \"fileExpireTime\", c.config.CronTask.FileExpireTime, \"deleteObjectType\", c.config.CronTask.DeleteObjectType)\n\t\treturn nil\n\t}\n\t_, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() {\n\t\tc.locker.ExecuteWithLock(c.ctx, \"clearS3\", c.clearS3)\n\t})\n\treturn errs.WrapMsg(err, \"failed to register clear s3 cron task\")\n}\n\nfunc (c *cronServer) registerDeleteMsg() error {\n\tif c.config.CronTask.RetainChatRecords <= 0 {\n\t\tlog.ZInfo(c.ctx, \"disable scheduled cleanup of chat records\", \"retainChatRecords\", c.config.CronTask.RetainChatRecords)\n\t\treturn nil\n\t}\n\t_, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() {\n\t\tc.locker.ExecuteWithLock(c.ctx, \"deleteMsg\", c.deleteMsg)\n\t})\n\treturn errs.WrapMsg(err, \"failed to register delete msg cron task\")\n}\n\nfunc (c *cronServer) registerClearUserMsg() error {\n\t_, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() {\n\t\tc.locker.ExecuteWithLock(c.ctx, \"clearUserMsg\", c.clearUserMsg)\n\t})\n\treturn errs.WrapMsg(err, \"failed to register clear user msg cron task\")\n}\n"
  },
  {
    "path": "internal/tools/cron/cron_test.go",
    "content": "package cron\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\tkdisc \"github.com/openimsdk/open-im-server/v3/pkg/common/discovery\"\n\tpbconversation \"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/mw\"\n\t\"github.com/robfig/cron/v3\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\nfunc TestName(t *testing.T) {\n\tconf := &config.Discovery{\n\t\tEnable: config.ETCD,\n\t\tEtcd: config.Etcd{\n\t\t\tRootDirectory: \"openim\",\n\t\t\tAddress:       []string{\"localhost:12379\"},\n\t\t},\n\t}\n\tclient, err := kdisc.NewDiscoveryRegister(conf, nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tclient.AddOption(mw.GrpcClient(), grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tctx := mcontext.SetOpUserID(context.Background(), \"imAdmin\")\n\tmsgConn, err := client.GetConn(ctx, \"msg-rpc-service\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tthirdConn, err := client.GetConn(ctx, \"third-rpc-service\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tconversationConn, err := client.GetConn(ctx, \"conversation-rpc-service\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsrv := &cronServer{\n\t\tctx: ctx,\n\t\tconfig: &Config{\n\t\t\tCronTask: config.CronTask{\n\t\t\t\tRetainChatRecords: 1,\n\t\t\t\tFileExpireTime:    1,\n\t\t\t\tDeleteObjectType:  []string{\"msg-picture\", \"msg-file\", \"msg-voice\", \"msg-video\", \"msg-video-snapshot\", \"sdklog\", \"\"},\n\t\t\t},\n\t\t},\n\t\tcron:               cron.New(),\n\t\tmsgClient:          msg.NewMsgClient(msgConn),\n\t\tconversationClient: pbconversation.NewConversationClient(conversationConn),\n\t\tthirdClient:        third.NewThirdClient(thirdConn),\n\t}\n\tsrv.deleteMsg()\n\t//srv.clearS3()\n\t//srv.clearUserMsg()\n}\n"
  },
  {
    "path": "internal/tools/cron/dist_look.go",
    "content": "package cron\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/openimsdk/tools/log\"\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n\t\"go.etcd.io/etcd/client/v3/concurrency\"\n)\n\nconst (\n\tlockLeaseTTL = 300\n)\n\ntype EtcdLocker struct {\n\tclient     *clientv3.Client\n\tinstanceID string\n}\n\n// NewEtcdLocker creates a new etcd distributed lock\nfunc NewEtcdLocker(client *clientv3.Client) (*EtcdLocker, error) {\n\thostname, _ := os.Hostname()\n\tpid := os.Getpid()\n\tinstanceID := fmt.Sprintf(\"%s-pid-%d-%d\", hostname, pid, time.Now().UnixNano())\n\n\tlocker := &EtcdLocker{\n\t\tclient:     client,\n\t\tinstanceID: instanceID,\n\t}\n\n\treturn locker, nil\n}\n\nfunc (e *EtcdLocker) ExecuteWithLock(ctx context.Context, taskName string, task func()) {\n\tsession, err := concurrency.NewSession(e.client, concurrency.WithTTL(lockLeaseTTL))\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"Failed to create etcd session\", err,\n\t\t\t\"taskName\", taskName,\n\t\t\t\"instanceID\", e.instanceID)\n\t\treturn\n\t}\n\tdefer session.Close()\n\n\tlockKey := fmt.Sprintf(\"openim/crontask/%s\", taskName)\n\tmutex := concurrency.NewMutex(session, lockKey)\n\n\tctxWithTimeout, cancel := context.WithTimeout(ctx, 100*time.Millisecond)\n\tdefer cancel()\n\n\terr = mutex.TryLock(ctxWithTimeout)\n\tif err != nil {\n\t\t// errors.Is(err, concurrency.ErrLocked)\n\t\tlog.ZDebug(ctx, \"Task is being executed by another instance, skipping\",\n\t\t\t\"taskName\", taskName,\n\t\t\t\"instanceID\", e.instanceID,\n\t\t\t\"error\", err.Error())\n\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif err := mutex.Unlock(ctx); err != nil {\n\t\t\tlog.ZWarn(ctx, \"Failed to release task lock\", err,\n\t\t\t\t\"taskName\", taskName,\n\t\t\t\t\"instanceID\", e.instanceID)\n\t\t} else {\n\t\t\tlog.ZInfo(ctx, \"Successfully released task lock\",\n\t\t\t\t\"taskName\", taskName,\n\t\t\t\t\"instanceID\", e.instanceID)\n\t\t}\n\t}()\n\n\tlog.ZInfo(ctx, \"Successfully acquired task lock, starting execution\",\n\t\t\"taskName\", taskName,\n\t\t\"instanceID\", e.instanceID,\n\t\t\"sessionID\", session.Lease())\n\n\ttask()\n\n\tlog.ZInfo(ctx, \"Task execution completed\",\n\t\t\"taskName\", taskName,\n\t\t\"instanceID\", e.instanceID)\n}\n"
  },
  {
    "path": "internal/tools/cron/msg.go",
    "content": "package cron\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nfunc (c *cronServer) deleteMsg() {\n\tnow := time.Now()\n\tdeltime := now.Add(-time.Hour * 24 * time.Duration(c.config.CronTask.RetainChatRecords))\n\toperationID := fmt.Sprintf(\"cron_msg_%d_%d\", os.Getpid(), deltime.UnixMilli())\n\tctx := mcontext.SetOperationID(c.ctx, operationID)\n\tlog.ZDebug(ctx, \"Destruct chat records\", \"deltime\", deltime, \"timestamp\", deltime.UnixMilli())\n\tconst (\n\t\tdeleteCount = 10000\n\t\tdeleteLimit = 50\n\t)\n\tvar count int\n\tfor i := 1; i <= deleteCount; i++ {\n\t\tctx := mcontext.SetOperationID(c.ctx, fmt.Sprintf(\"%s_%d\", operationID, i))\n\t\tresp, err := c.msgClient.DestructMsgs(ctx, &msg.DestructMsgsReq{Timestamp: deltime.UnixMilli(), Limit: deleteLimit})\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"cron destruct chat records failed\", err)\n\t\t\tbreak\n\t\t}\n\t\tcount += int(resp.Count)\n\t\tif resp.Count < deleteLimit {\n\t\t\tbreak\n\t\t}\n\t}\n\tlog.ZDebug(ctx, \"cron destruct chat records end\", \"deltime\", deltime, \"cont\", time.Since(now), \"count\", count)\n}\n"
  },
  {
    "path": "internal/tools/cron/s3.go",
    "content": "package cron\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nfunc (c *cronServer) clearS3() {\n\tstart := time.Now()\n\tdeleteTime := start.Add(-time.Hour * 24 * time.Duration(c.config.CronTask.FileExpireTime))\n\toperationID := fmt.Sprintf(\"cron_s3_%d_%d\", os.Getpid(), deleteTime.UnixMilli())\n\tctx := mcontext.SetOperationID(c.ctx, operationID)\n\tlog.ZDebug(ctx, \"deleteoutDatedData\", \"deletetime\", deleteTime, \"timestamp\", deleteTime.UnixMilli())\n\tconst (\n\t\tdeleteCount = 10000\n\t\tdeleteLimit = 100\n\t)\n\n\tvar count int\n\tfor i := 1; i <= deleteCount; i++ {\n\t\tresp, err := c.thirdClient.DeleteOutdatedData(ctx, &third.DeleteOutdatedDataReq{ExpireTime: deleteTime.UnixMilli(), ObjectGroup: c.config.CronTask.DeleteObjectType, Limit: deleteLimit})\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"cron deleteoutDatedData failed\", err)\n\t\t\treturn\n\t\t}\n\t\tcount += int(resp.Count)\n\t\tif resp.Count < deleteLimit {\n\t\t\tbreak\n\t\t}\n\t}\n\tlog.ZDebug(ctx, \"cron deleteoutDatedData success\", \"deltime\", deleteTime, \"cont\", time.Since(start), \"count\", count)\n}\n\n//\tvar req *third.DeleteOutdatedDataReq\n//\tcount1, err := ExtractField(ctx, c.thirdClient.DeleteOutdatedData, req, (*third.DeleteOutdatedDataResp).GetCount)\n//\n//\tc.thirdClient.DeleteOutdatedData(ctx, &third.DeleteOutdatedDataReq{})\n//\tmsggateway.GetUsersOnlineStatusCaller.Invoke(ctx, &msggateway.GetUsersOnlineStatusReq{})\n//\n//\tvar cli ThirdClient\n//\n//\tc111, err := cli.DeleteOutdatedData(ctx, 100)\n//\n//\tcli.ThirdClient.DeleteOutdatedData(ctx, &third.DeleteOutdatedDataReq{})\n//\n//\tcli.AuthSign(ctx, &third.AuthSignReq{})\n//\n//\tcli.SetAppBadge()\n//\n//}\n//\n//func extractField[A, B, C any](ctx context.Context, fn func(ctx context.Context, req *A, opts ...grpc.CallOption) (*B, error), req *A, get func(*B) C) (C, error) {\n//\tresp, err := fn(ctx, req)\n//\tif err != nil {\n//\t\tvar c C\n//\t\treturn c, err\n//\t}\n//\treturn get(resp), nil\n//}\n//\n//func ignore(_ any, err error) error {\n//\treturn err\n//}\n//\n//type ThirdClient struct {\n//\tthird.ThirdClient\n//}\n//\n//func (c *ThirdClient) DeleteOutdatedData(ctx context.Context, expireTime int64) (int32, error) {\n//\treturn extractField(ctx, c.ThirdClient.DeleteOutdatedData, &third.DeleteOutdatedDataReq{ExpireTime: expireTime}, (*third.DeleteOutdatedDataResp).GetCount)\n//}\n//\n//func (c *ThirdClient) DeleteOutdatedData1(ctx context.Context, expireTime int64) error {\n//\treturn ignore(c.ThirdClient.DeleteOutdatedData(ctx, &third.DeleteOutdatedDataReq{ExpireTime: expireTime}))\n//}\n"
  },
  {
    "path": "internal/tools/cron/user_msg.go",
    "content": "package cron\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\tpbconversation \"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nfunc (c *cronServer) clearUserMsg() {\n\tnow := time.Now()\n\toperationID := fmt.Sprintf(\"cron_user_msg_%d_%d\", os.Getpid(), now.UnixMilli())\n\tctx := mcontext.SetOperationID(c.ctx, operationID)\n\tlog.ZDebug(ctx, \"clear user msg cron start\")\n\tconst (\n\t\tdeleteCount = 10000\n\t\tdeleteLimit = 100\n\t)\n\tvar count int\n\tfor i := 1; i <= deleteCount; i++ {\n\t\tresp, err := c.conversationClient.ClearUserConversationMsg(ctx, &pbconversation.ClearUserConversationMsgReq{Timestamp: now.UnixMilli(), Limit: deleteLimit})\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"ClearUserConversationMsg failed.\", err)\n\t\t\treturn\n\t\t}\n\t\tcount += int(resp.Count)\n\t\tif resp.Count < deleteLimit {\n\t\t\tbreak\n\t\t}\n\t}\n\tlog.ZDebug(ctx, \"clear user msg cron task completed\", \"cont\", time.Since(now), \"count\", count)\n}\n"
  },
  {
    "path": "magefile.go",
    "content": "//go:build mage\n// +build mage\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/openimsdk/gomake/mageutil\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nvar Default = Build\n\nvar Aliases = map[string]any{\n\t\"buildcc\": BuildWithCustomConfig,\n\t\"startcc\": StartWithCustomConfig,\n}\n\nvar (\n\tcustomRootDir   = \".\"       // workDir in mage, default is \"./\"(project root directory)\n\tcustomSrcDir    = \"cmd\"     // source code directory, default is \"cmd\"\n\tcustomOutputDir = \"_output\" // output directory, default is \"_output\"\n\tcustomConfigDir = \"config\"  // configuration directory, default is \"config\"\n\tcustomToolsDir  = \"tools\"   // tools source code directory, default is \"tools\"\n)\n\n// Build support specifical binary build.\n//\n// Example: `mage build openim-api openim-rpc-user seq`\nfunc Build() {\n\tflag.Parse()\n\tbin := flag.Args()\n\tif len(bin) != 0 {\n\t\tbin = bin[1:]\n\t}\n\tmageutil.WithSpinner(\"Building binaries...\", func() { mageutil.Build(bin, nil, nil) })\n}\n\nfunc BuildWithCustomConfig() {\n\tflag.Parse()\n\tbin := flag.Args()\n\tif len(bin) != 0 {\n\t\tbin = bin[1:]\n\t}\n\n\tconfig := &mageutil.PathOptions{\n\t\tRootDir:   &customRootDir,\n\t\tOutputDir: &customOutputDir,\n\t\tSrcDir:    &customSrcDir,\n\t\tToolsDir:  &customToolsDir,\n\t}\n\n\tmageutil.WithSpinner(\"Building binaries with custom config...\", func() {\n\t\tmageutil.Build(bin, config, nil)\n\t})\n}\n\nfunc Start() {\n\tmageutil.InitForSSC()\n\terr := setMaxOpenFiles()\n\tif err != nil {\n\t\tmageutil.PrintRed(\"setMaxOpenFiles failed \" + err.Error())\n\t\tos.Exit(1)\n\t}\n\n\tflag.Parse()\n\tbin := flag.Args()\n\tif len(bin) != 0 {\n\t\tbin = bin[1:]\n\t}\n\n\tmageutil.WithSpinner(\"Starting...\", func() {\n\t\tmageutil.StartToolsAndServices(bin, nil)\n\t})\n}\n\nfunc StartWithCustomConfig() {\n\tmageutil.InitForSSC()\n\terr := setMaxOpenFiles()\n\tif err != nil {\n\t\tmageutil.PrintRed(\"setMaxOpenFiles failed \" + err.Error())\n\t\tos.Exit(1)\n\t}\n\n\tflag.Parse()\n\tbin := flag.Args()\n\tif len(bin) != 0 {\n\t\tbin = bin[1:]\n\t}\n\n\tconfig := &mageutil.PathOptions{\n\t\tRootDir:   &customRootDir,\n\t\tOutputDir: &customOutputDir,\n\t\tConfigDir: &customConfigDir,\n\t}\n\n\tmageutil.WithSpinner(\"Starting with custom config...\", func() {\n\t\tmageutil.StartToolsAndServices(bin, config)\n\t})\n}\n\nfunc Stop() {\n\tmageutil.WithSpinner(\"Stopping...\", mageutil.StopAndCheckBinaries)\n}\n\nfunc Check() {\n\tmageutil.WithSpinner(\"Checking binaries...\", mageutil.CheckAndReportBinariesStatus)\n}\n\nfunc Export() {\n\tmappingPaths, err := mageutil.GetDefaultExportMappingPaths([]string{\n\t\t\"cmd\",\n\t\t\"internal\",\n\t\t\"pkg\",\n\t\t\"test\",\n\t\t\"tools\",\n\t\t\"**/*.go\",\n\t\t\"go.mod\",\n\t\t\"go.work\",\n\t})\n\tif err != nil {\n\t\tmageutil.PrintRed(\"GetDefaultExportMappingPaths failed \" + err.Error())\n\t\tos.Exit(1)\n\t}\n\n\tmageutil.WithSpinner(\"Exporting...\", func() {\n\t\tmageutil.ExportMageLauncherArchived(mappingPaths, &mageutil.ExportOptions{\n\t\t\tProjectName: datautil.ToPtr(fmt.Sprintf(\"open-im-server_%s\", version.Version)),\n\t\t\tBuildOpt: &mageutil.BuildOptions{\n\t\t\t\tRelease:  datautil.ToPtr(true),\n\t\t\t\tCompress: datautil.ToPtr(true),\n\t\t\t},\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "magefile_unix.go",
    "content": "//go:build mage && !windows\n// +build mage,!windows\n\npackage main\n\nimport (\n\t\"syscall\"\n\n\t\"github.com/openimsdk/gomake/mageutil\"\n)\n\nfunc setMaxOpenFiles() error {\n\tvar rLimit syscall.Rlimit\n\terr := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)\n\tif err != nil {\n\t\treturn err\n\t}\n\trLimit.Max = uint64(mageutil.MaxFileDescriptors)\n\trLimit.Cur = uint64(mageutil.MaxFileDescriptors)\n\treturn syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)\n}\n"
  },
  {
    "path": "magefile_windows.go",
    "content": "//go:build mage\n// +build mage\n\npackage main\n\nfunc setMaxOpenFiles() error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/apistruct/config_manager.go",
    "content": "package apistruct\n\ntype GetConfigReq struct {\n\tConfigName string `json:\"configName\"`\n}\n\ntype GetConfigListResp struct {\n\tEnvironment string   `json:\"environment\"`\n\tVersion     string   `json:\"version\"`\n\tConfigNames []string `json:\"configNames\"`\n}\n\ntype SetConfigReq struct {\n\tConfigName string `json:\"configName\"`\n\tData       string `json:\"data\"`\n}\n\ntype SetConfigsReq struct {\n\tConfigs []SetConfigReq `json:\"configs\"`\n}\n\ntype SetEnableConfigManagerReq struct {\n\tEnable bool `json:\"enable\"`\n}\n\ntype GetEnableConfigManagerResp struct {\n\tEnable bool `json:\"enable\"`\n}\n"
  },
  {
    "path": "pkg/apistruct/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage apistruct // import \"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n"
  },
  {
    "path": "pkg/apistruct/manage.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage apistruct\n\nimport (\n\tpbmsg \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\n// SendMsg defines the structure for sending messages with various metadata.\ntype SendMsg struct {\n\t// SendID uniquely identifies the sender.\n\tSendID string `json:\"sendID\" binding:\"required\"`\n\n\t// GroupID is the identifier for the group, required if SessionType is 2 or 3.\n\tGroupID string `json:\"groupID\" binding:\"required_if=SessionType 2|required_if=SessionType 3\"`\n\n\t// SenderNickname is the nickname of the sender.\n\tSenderNickname string `json:\"senderNickname\"`\n\n\t// SenderFaceURL is the URL to the sender's avatar.\n\tSenderFaceURL string `json:\"senderFaceURL\"`\n\n\t// SenderPlatformID is an integer identifier for the sender's platform.\n\tSenderPlatformID int32 `json:\"senderPlatformID\"`\n\n\t// Content is the actual content of the message, required and excluded from Swagger documentation.\n\tContent map[string]any `json:\"content\" binding:\"required\" swaggerignore:\"true\"`\n\n\t// ContentType is an integer that represents the type of the content.\n\tContentType int32 `json:\"contentType\" binding:\"required\"`\n\n\t// SessionType is an integer that represents the type of session for the message.\n\tSessionType int32 `json:\"sessionType\" binding:\"required\"`\n\n\t// IsOnlineOnly specifies if the message is only sent when the receiver is online.\n\tIsOnlineOnly bool `json:\"isOnlineOnly\"`\n\n\t// NotOfflinePush specifies if the message should not trigger offline push notifications.\n\tNotOfflinePush bool `json:\"notOfflinePush\"`\n\n\t// SendTime is a timestamp indicating when the message was sent.\n\tSendTime int64 `json:\"sendTime\"`\n\n\t// OfflinePushInfo contains information for offline push notifications.\n\tOfflinePushInfo *sdkws.OfflinePushInfo `json:\"offlinePushInfo\"`\n\n\t// Ex stores extended fields\n\tEx string `json:\"ex\"`\n}\n\n// SendMsgReq extends SendMsg with the requirement of RecvID when SessionType indicates a one-on-one or notification chat.\ntype SendMsgReq struct {\n\t// RecvID uniquely identifies the receiver and is required for one-on-one or notification chat types.\n\tRecvID string `json:\"recvID\" binding:\"required_if\" message:\"recvID is required if sessionType is SingleChatType or NotificationChatType\"`\n\tSendMsg\n}\n\ntype GetConversationListReq struct {\n\t// userID uniquely identifies the user.\n\tUserID string `protobuf:\"bytes,1,opt,name=userID,proto3\" json:\"userID,omitempty\" binding:\"required\"`\n\n\t// ConversationIDs contains a list of unique identifiers for conversations.\n\tConversationIDs []string `protobuf:\"bytes,2,rep,name=conversationIDs,proto3\" json:\"conversationIDs,omitempty\"`\n}\n\ntype GetConversationListResp struct {\n\t// ConversationElems is a map that associates conversation IDs with their respective details.\n\tConversationElems map[string]*ConversationElem `protobuf:\"bytes,1,rep,name=conversationElems,proto3\" json:\"conversationElems,omitempty\" protobuf_key:\"bytes,1,opt,name=key,proto3\" protobuf_val:\"bytes,2,opt,name=value,proto3\"`\n}\n\ntype ConversationElem struct {\n\t// MaxSeq represents the maximum sequence number within the conversation.\n\tMaxSeq int64 `protobuf:\"varint,1,opt,name=maxSeq,proto3\" json:\"maxSeq,omitempty\"`\n\n\t// UnreadSeq represents the number of unread messages in the conversation.\n\tUnreadSeq int64 `protobuf:\"varint,2,opt,name=unreadSeq,proto3\" json:\"unreadSeq,omitempty\"`\n\n\t// LastSeqTime represents the timestamp of the last sequence in the conversation.\n\tLastSeqTime int64 `protobuf:\"varint,3,opt,name=LastSeqTime,proto3\" json:\"LastSeqTime,omitempty\"`\n}\n\n// BatchSendMsgReq defines the structure for sending a message to multiple recipients.\ntype BatchSendMsgReq struct {\n\tSendMsg\n\n\t// IsSendAll indicates whether the message should be sent to all users.\n\tIsSendAll bool `json:\"isSendAll\"`\n\n\t// RecvIDs is a slice of receiver identifiers to whom the message will be sent, required field.\n\tRecvIDs []string `json:\"recvIDs\" binding:\"required\"`\n}\n\n// BatchSendMsgResp contains the results of a batch message send operation.\ntype BatchSendMsgResp struct {\n\t// Results is a slice of SingleReturnResult, representing the outcome of each message sent.\n\tResults []*SingleReturnResult `json:\"results\"`\n\n\t// FailedIDs is a slice of user IDs for whom the message send failed.\n\tFailedIDs []string `json:\"failedUserIDs\"`\n}\n\n// SendSingleMsgReq defines the structure for sending a message to multiple recipients.\ntype SendSingleMsgReq struct {\n\t// groupMsg should appoint sendID\n\tSendID          string                 `json:\"sendID\"`\n\tContent         string                 `json:\"content\" binding:\"required\"`\n\tOfflinePushInfo *sdkws.OfflinePushInfo `json:\"offlinePushInfo\"`\n\tEx              string                 `json:\"ex\"`\n}\n\ntype KeyMsgData struct {\n\tSendID  string `json:\"sendID\"`\n\tRecvID  string `json:\"recvID\"`\n\tGroupID string `json:\"groupID\"`\n}\n\n// SingleReturnResult encapsulates the result of a single message send attempt.\ntype SingleReturnResult struct {\n\t// ServerMsgID is the message identifier on the server-side.\n\tServerMsgID string `json:\"serverMsgID\"`\n\n\t// ClientMsgID is the message identifier on the client-side.\n\tClientMsgID string `json:\"clientMsgID\"`\n\n\t// SendTime is the timestamp of when the message was sent.\n\tSendTime int64 `json:\"sendTime\"`\n\n\t// RecvID uniquely identifies the receiver of the message.\n\tRecvID string `json:\"recvID\"`\n\n\t// Modify fields modified via webhook.\n\tModify map[string]any `json:\"modify,omitempty\"`\n}\n\ntype SendMsgResp struct {\n\t// SendMsgResp original response.\n\t*pbmsg.SendMsgResp\n\n\t// Modify fields modified via webhook.\n\tModify map[string]any `json:\"modify,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/apistruct/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage apistruct\n\nimport \"github.com/openimsdk/protocol/sdkws\"\n\ntype PictureBaseInfo struct {\n\tUUID   string `mapstructure:\"uuid\"`\n\tType   string `mapstructure:\"type\"   validate:\"required\"`\n\tSize   int64  `mapstructure:\"size\"`\n\tWidth  int32  `mapstructure:\"width\"  validate:\"required\"`\n\tHeight int32  `mapstructure:\"height\" validate:\"required\"`\n\tUrl    string `mapstructure:\"url\"    validate:\"required\"`\n}\n\ntype PictureElem struct {\n\tSourcePath      string          `mapstructure:\"sourcePath\"`\n\tSourcePicture   PictureBaseInfo `mapstructure:\"sourcePicture\"   validate:\"required\"`\n\tBigPicture      PictureBaseInfo `mapstructure:\"bigPicture\"      validate:\"required\"`\n\tSnapshotPicture PictureBaseInfo `mapstructure:\"snapshotPicture\" validate:\"required\"`\n}\n\ntype SoundElem struct {\n\tUUID      string `mapstructure:\"uuid\"`\n\tSoundPath string `mapstructure:\"soundPath\"`\n\tSourceURL string `mapstructure:\"sourceUrl\" validate:\"required\"`\n\tDataSize  int64  `mapstructure:\"dataSize\"`\n\tDuration  int64  `mapstructure:\"duration\"  validate:\"required,min=1\"`\n}\n\ntype VideoElem struct {\n\tVideoPath      string `mapstructure:\"videoPath\"`\n\tVideoUUID      string `mapstructure:\"videoUUID\"`\n\tVideoURL       string `mapstructure:\"videoUrl\"       validate:\"required\"`\n\tVideoType      string `mapstructure:\"videoType\"      validate:\"required\"`\n\tVideoSize      int64  `mapstructure:\"videoSize\"      validate:\"required\"`\n\tDuration       int64  `mapstructure:\"duration\"       validate:\"required\"`\n\tSnapshotPath   string `mapstructure:\"snapshotPath\"`\n\tSnapshotUUID   string `mapstructure:\"snapshotUUID\"`\n\tSnapshotSize   int64  `mapstructure:\"snapshotSize\"`\n\tSnapshotURL    string `mapstructure:\"snapshotUrl\"    validate:\"required\"`\n\tSnapshotWidth  int32  `mapstructure:\"snapshotWidth\"  validate:\"required\"`\n\tSnapshotHeight int32  `mapstructure:\"snapshotHeight\" validate:\"required\"`\n}\n\ntype FileElem struct {\n\tFilePath  string `mapstructure:\"filePath\"`\n\tUUID      string `mapstructure:\"uuid\"`\n\tSourceURL string `mapstructure:\"sourceUrl\" validate:\"required\"`\n\tFileName  string `mapstructure:\"fileName\"  validate:\"required\"`\n\tFileSize  int64  `mapstructure:\"fileSize\"  validate:\"required\"`\n}\ntype AtElem struct {\n\tText         string     `mapstructure:\"text\"`\n\tAtUserList   []string   `mapstructure:\"atUserList\" validate:\"required,max=1000\"`\n\tAtUsersInfo  []*AtInfo  `json:\"atUsersInfo\"`\n\tQuoteMessage *MsgStruct `json:\"quoteMessage\"`\n\tIsAtSelf     bool       `mapstructure:\"isAtSelf\"`\n}\ntype LocationElem struct {\n\tDescription string  `mapstructure:\"description\"`\n\tLongitude   float64 `mapstructure:\"longitude\"   validate:\"required\"`\n\tLatitude    float64 `mapstructure:\"latitude\"    validate:\"required\"`\n}\n\ntype CustomElem struct {\n\tData        string `mapstructure:\"data\"        validate:\"required\"`\n\tDescription string `mapstructure:\"description\"`\n\tExtension   string `mapstructure:\"extension\"`\n}\n\ntype TextElem struct {\n\tContent string `json:\"content\" validate:\"required\"`\n}\n\ntype MarkdownTextElem struct {\n\tContent string `mapstructure:\"content\" validate:\"required\"`\n}\n\ntype StreamMsgElem struct {\n\tType    string `mapstructure:\"type\" validate:\"required\"`\n\tContent string `mapstructure:\"content\" validate:\"required\"`\n}\n\ntype RevokeElem struct {\n\tRevokeMsgClientID string `mapstructure:\"revokeMsgClientID\" validate:\"required\"`\n}\n\ntype QuoteElem struct {\n\tText         string     `json:\"text,omitempty\"`\n\tQuoteMessage *MsgStruct `json:\"quoteMessage,omitempty\"`\n}\n\ntype OANotificationElem struct {\n\tNotificationName    string       `mapstructure:\"notificationName\"    json:\"notificationName\"    validate:\"required\"`\n\tNotificationFaceURL string       `mapstructure:\"notificationFaceURL\" json:\"notificationFaceURL\"`\n\tNotificationType    int32        `mapstructure:\"notificationType\"    json:\"notificationType\"    validate:\"required\"`\n\tText                string       `mapstructure:\"text\"                json:\"text\"                validate:\"required\"`\n\tUrl                 string       `mapstructure:\"url\"                 json:\"url\"`\n\tMixType             int32        `mapstructure:\"mixType\"             json:\"mixType\"             validate:\"gte=0,lte=5\"`\n\tPictureElem         *PictureElem `mapstructure:\"pictureElem\"         json:\"pictureElem\"`\n\tSoundElem           *SoundElem   `mapstructure:\"soundElem\"           json:\"soundElem\"`\n\tVideoElem           *VideoElem   `mapstructure:\"videoElem\"           json:\"videoElem\"`\n\tFileElem            *FileElem    `mapstructure:\"fileElem\"            json:\"fileElem\"`\n\tEx                  string       `mapstructure:\"ex\"                  json:\"ex\"`\n}\n\ntype MessageRevoked struct {\n\tRevokerID       string `mapstructure:\"revokerID\"       json:\"revokerID\"       validate:\"required\"`\n\tRevokerRole     int32  `mapstructure:\"revokerRole\"     json:\"revokerRole\"     validate:\"required\"`\n\tClientMsgID     string `mapstructure:\"clientMsgID\"     json:\"clientMsgID\"     validate:\"required\"`\n\tRevokerNickname string `mapstructure:\"revokerNickname\" json:\"revokerNickname\"`\n\tSessionType     int32  `mapstructure:\"sessionType\"     json:\"sessionType\"     validate:\"required\"`\n\tSeq             uint32 `mapstructure:\"seq\"             json:\"seq\"             validate:\"required\"`\n}\n\ntype MsgStruct struct {\n\tClientMsgID          string                 `json:\"clientMsgID,omitempty\"`\n\tServerMsgID          string                 `json:\"serverMsgID,omitempty\"`\n\tCreateTime           int64                  `json:\"createTime\"`\n\tSendTime             int64                  `json:\"sendTime\"`\n\tSessionType          int32                  `json:\"sessionType\"`\n\tSendID               string                 `json:\"sendID,omitempty\"`\n\tRecvID               string                 `json:\"recvID,omitempty\"`\n\tMsgFrom              int32                  `json:\"msgFrom\"`\n\tContentType          int32                  `json:\"contentType\"`\n\tSenderPlatformID     int32                  `json:\"senderPlatformID\"`\n\tSenderNickname       string                 `json:\"senderNickname,omitempty\"`\n\tSenderFaceURL        string                 `json:\"senderFaceUrl,omitempty\"`\n\tGroupID              string                 `json:\"groupID,omitempty\"`\n\tContent              string                 `json:\"content,omitempty\"`\n\tSeq                  int64                  `json:\"seq\"`\n\tIsRead               bool                   `json:\"isRead\"`\n\tStatus               int32                  `json:\"status\"`\n\tIsReact              bool                   `json:\"isReact,omitempty\"`\n\tIsExternalExtensions bool                   `json:\"isExternalExtensions,omitempty\"`\n\tOfflinePush          *sdkws.OfflinePushInfo `json:\"offlinePush,omitempty\"`\n\tAttachedInfo         string                 `json:\"attachedInfo,omitempty\"`\n\tEx                   string                 `json:\"ex,omitempty\"`\n\tLocalEx              string                 `json:\"localEx,omitempty\"`\n\tTextElem             *TextElem              `json:\"textElem,omitempty\"`\n\tPictureElem          *PictureElem           `json:\"pictureElem,omitempty\"`\n\tSoundElem            *SoundElem             `json:\"soundElem,omitempty\"`\n\tVideoElem            *VideoElem             `json:\"videoElem,omitempty\"`\n\tFileElem             *FileElem              `json:\"fileElem,omitempty\"`\n\tAtTextElem           *AtElem                `json:\"atTextElem,omitempty\"`\n\tLocationElem         *LocationElem          `json:\"locationElem,omitempty\"`\n\tCustomElem           *CustomElem            `json:\"customElem,omitempty\"`\n\tQuoteElem            *QuoteElem             `json:\"quoteElem,omitempty\"`\n}\n\ntype AtInfo struct {\n\tAtUserID      string `json:\"atUserID,omitempty\"`\n\tGroupNickname string `json:\"groupNickname,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/apistruct/public.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage apistruct\n\ntype GroupAddMemberInfo struct {\n\tUserID    string `json:\"userID\"    binding:\"required\"`\n\tRoleLevel int32  `json:\"roleLevel\" binding:\"required,oneof= 1 3\"`\n}\n"
  },
  {
    "path": "pkg/authverify/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage authverify // import \"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n"
  },
  {
    "path": "pkg/authverify/token.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage authverify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/golang-jwt/jwt/v4\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc Secret(secret string) jwt.Keyfunc {\n\treturn func(token *jwt.Token) (any, error) {\n\t\treturn []byte(secret), nil\n\t}\n}\n\nfunc CheckAdmin(ctx context.Context) error {\n\tif IsAdmin(ctx) {\n\t\treturn nil\n\t}\n\treturn servererrs.ErrNoPermission.WrapMsg(fmt.Sprintf(\"user %s is not admin userID\", mcontext.GetOpUserID(ctx)))\n}\n\n//func IsManagerUserID(opUserID string, imAdminUserID []string) bool {\n//\treturn datautil.Contain(opUserID, imAdminUserID...)\n//}\n\nfunc CheckUserIsAdmin(ctx context.Context, userID string) bool {\n\treturn datautil.Contain(userID, GetIMAdminUserIDs(ctx)...)\n}\n\nfunc CheckSystemAccount(ctx context.Context, level int32) bool {\n\treturn level >= constant.AppAdmin\n}\n\nconst (\n\tCtxAdminUserIDsKey = \"CtxAdminUserIDsKey\"\n)\n\nfunc WithIMAdminUserIDs(ctx context.Context, imAdminUserID []string) context.Context {\n\treturn context.WithValue(ctx, CtxAdminUserIDsKey, imAdminUserID)\n}\n\nfunc GetIMAdminUserIDs(ctx context.Context) []string {\n\timAdminUserID, _ := ctx.Value(CtxAdminUserIDsKey).([]string)\n\treturn imAdminUserID\n}\n\nfunc IsAdmin(ctx context.Context) bool {\n\treturn IsTempAdmin(ctx) || IsSystemAdmin(ctx)\n}\n\nfunc CheckAccess(ctx context.Context, ownerUserID string) error {\n\tif mcontext.GetOpUserID(ctx) == ownerUserID {\n\t\treturn nil\n\t}\n\tif IsAdmin(ctx) {\n\t\treturn nil\n\t}\n\treturn servererrs.ErrNoPermission.WrapMsg(\"ownerUserID\", ownerUserID)\n}\n\nfunc CheckAccessIn(ctx context.Context, ownerUserIDs ...string) error {\n\topUserID := mcontext.GetOpUserID(ctx)\n\tfor _, userID := range ownerUserIDs {\n\t\tif opUserID == userID {\n\t\t\treturn nil\n\t\t}\n\t}\n\tif IsAdmin(ctx) {\n\t\treturn nil\n\t}\n\treturn servererrs.ErrNoPermission.WrapMsg(\"opUser in ownerUserIDs\")\n}\n\nvar tempAdminValue = []string{\"1\"}\n\nconst ctxTempAdminKey = \"ctxImTempAdminKey\"\n\nfunc WithTempAdmin(ctx context.Context) context.Context {\n\tkeys, _ := ctx.Value(constant.RpcCustomHeader).([]string)\n\tif datautil.Contain(ctxTempAdminKey, keys...) {\n\t\treturn ctx\n\t}\n\tif len(keys) > 0 {\n\t\ttemp := make([]string, 0, len(keys)+1)\n\t\ttemp = append(temp, keys...)\n\t\tkeys = append(temp, ctxTempAdminKey)\n\t} else {\n\t\tkeys = []string{ctxTempAdminKey}\n\t}\n\tctx = context.WithValue(ctx, constant.RpcCustomHeader, keys)\n\treturn context.WithValue(ctx, ctxTempAdminKey, tempAdminValue)\n}\n\nfunc IsTempAdmin(ctx context.Context) bool {\n\tvalues, _ := ctx.Value(ctxTempAdminKey).([]string)\n\treturn datautil.Equal(tempAdminValue, values)\n}\n\nfunc IsSystemAdmin(ctx context.Context) bool {\n\treturn datautil.Contain(mcontext.GetOpUserID(ctx), GetIMAdminUserIDs(ctx)...)\n}\n"
  },
  {
    "path": "pkg/callbackstruct/common.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nconst (\n\tNext = 1\n)\n\ntype CommonCallbackReq struct {\n\tSendID           string   `json:\"sendID\"`\n\tCallbackCommand  string   `json:\"callbackCommand\"`\n\tServerMsgID      string   `json:\"serverMsgID\"`\n\tClientMsgID      string   `json:\"clientMsgID\"`\n\tOperationID      string   `json:\"operationID\"`\n\tSenderPlatformID int32    `json:\"senderPlatformID\"`\n\tSenderNickname   string   `json:\"senderNickname\"`\n\tSessionType      int32    `json:\"sessionType\"`\n\tMsgFrom          int32    `json:\"msgFrom\"`\n\tContentType      int32    `json:\"contentType\"`\n\tStatus           int32    `json:\"status\"`\n\tSendTime         int64    `json:\"sendTime\"`\n\tCreateTime       int64    `json:\"createTime\"`\n\tContent          string   `json:\"content\"`\n\tSeq              uint32   `json:\"seq\"`\n\tAtUserIDList     []string `json:\"atUserList\"`\n\tSenderFaceURL    string   `json:\"faceURL\"`\n\tEx               string   `json:\"ex\"`\n}\n\nfunc (c *CommonCallbackReq) GetCallbackCommand() string {\n\treturn c.CallbackCommand\n}\n\ntype CallbackReq interface {\n\tGetCallbackCommand() string\n}\n\ntype CallbackResp interface {\n\tParse() (err error)\n}\n\ntype CommonCallbackResp struct {\n\tActionCode int32  `json:\"actionCode\"`\n\tErrCode    int32  `json:\"errCode\"`\n\tErrMsg     string `json:\"errMsg\"`\n\tErrDlt     string `json:\"errDlt\"`\n\tNextCode   int32  `json:\"nextCode\"`\n}\n\nfunc (c CommonCallbackResp) Parse() error {\n\tif c.ActionCode == servererrs.NoError && c.NextCode == Next {\n\t\treturn errs.NewCodeError(int(c.ErrCode), c.ErrMsg).WithDetail(c.ErrDlt)\n\t}\n\treturn nil\n}\n\ntype UserStatusBaseCallback struct {\n\tCallbackCommand string `json:\"callbackCommand\"`\n\tOperationID     string `json:\"operationID\"`\n\tPlatformID      int    `json:\"platformID\"`\n\tPlatform        string `json:\"platform\"`\n}\n\nfunc (c UserStatusBaseCallback) GetCallbackCommand() string {\n\treturn c.CallbackCommand\n}\n\ntype UserStatusCallbackReq struct {\n\tUserStatusBaseCallback\n\tUserID string `json:\"userID\"`\n}\n\ntype UserStatusBatchCallbackReq struct {\n\tUserStatusBaseCallback\n\tUserIDList []string `json:\"userIDList\"`\n}\n"
  },
  {
    "path": "pkg/callbackstruct/constant.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\nconst (\n\tCallbackBeforeInviteJoinGroupCommand               = \"callbackBeforeInviteJoinGroupCommand\"\n\tCallbackAfterJoinGroupCommand                      = \"callbackAfterJoinGroupCommand\"\n\tCallbackAfterSetGroupInfoCommand                   = \"callbackAfterSetGroupInfoCommand\"\n\tCallbackAfterSetGroupInfoExCommand                 = \"callbackAfterSetGroupInfoExCommand\"\n\tCallbackBeforeSetGroupInfoCommand                  = \"callbackBeforeSetGroupInfoCommand\"\n\tCallbackBeforeSetGroupInfoExCommand                = \"callbackBeforeSetGroupInfoExCommand\"\n\tCallbackAfterRevokeMsgCommand                      = \"callbackBeforeAfterMsgCommand\"\n\tCallbackBeforeAddBlackCommand                      = \"callbackBeforeAddBlackCommand\"\n\tCallbackAfterAddFriendCommand                      = \"callbackAfterAddFriendCommand\"\n\tCallbackBeforeAddFriendAgreeCommand                = \"callbackBeforeAddFriendAgreeCommand\"\n\tCallbackAfterAddFriendAgreeCommand                 = \"callbackAfterAddFriendAgreeCommand\"\n\tCallbackAfterDeleteFriendCommand                   = \"callbackAfterDeleteFriendCommand\"\n\tCallbackBeforeImportFriendsCommand                 = \"callbackBeforeImportFriendsCommand\"\n\tCallbackAfterImportFriendsCommand                  = \"callbackAfterImportFriendsCommand\"\n\tCallbackAfterRemoveBlackCommand                    = \"callbackAfterRemoveBlackCommand\"\n\tCallbackAfterQuitGroupCommand                      = \"callbackAfterQuitGroupCommand\"\n\tCallbackAfterKickGroupCommand                      = \"callbackAfterKickGroupCommand\"\n\tCallbackAfterDisMissGroupCommand                   = \"callbackAfterDisMissGroupCommand\"\n\tCallbackBeforeJoinGroupCommand                     = \"callbackBeforeJoinGroupCommand\"\n\tCallbackAfterGroupMsgReadCommand                   = \"callbackAfterGroupMsgReadCommand\"\n\tCallbackBeforeMsgModifyCommand                     = \"callbackBeforeMsgModifyCommand\"\n\tCallbackAfterUpdateUserInfoCommand                 = \"callbackAfterUpdateUserInfoCommand\"\n\tCallbackAfterUpdateUserInfoExCommand               = \"callbackAfterUpdateUserInfoExCommand\"\n\tCallbackBeforeUpdateUserInfoExCommand              = \"callbackBeforeUpdateUserInfoExCommand\"\n\tCallbackBeforeUserRegisterCommand                  = \"callbackBeforeUserRegisterCommand\"\n\tCallbackAfterUserRegisterCommand                   = \"callbackAfterUserRegisterCommand\"\n\tCallbackAfterTransferGroupOwnerCommand             = \"callbackAfterTransferGroupOwnerCommand\"\n\tCallbackBeforeSetFriendRemarkCommand               = \"callbackBeforeSetFriendRemarkCommand\"\n\tCallbackAfterSetFriendRemarkCommand                = \"callbackAfterSetFriendRemarkCommand\"\n\tCallbackAfterSingleMsgReadCommand                  = \"callbackAfterSingleMsgReadCommand\"\n\tCallbackBeforeSendSingleMsgCommand                 = \"callbackBeforeSendSingleMsgCommand\"\n\tCallbackAfterSendSingleMsgCommand                  = \"callbackAfterSendSingleMsgCommand\"\n\tCallbackBeforeSendGroupMsgCommand                  = \"callbackBeforeSendGroupMsgCommand\"\n\tCallbackAfterSendGroupMsgCommand                   = \"callbackAfterSendGroupMsgCommand\"\n\tCallbackAfterUserOnlineCommand                     = \"callbackAfterUserOnlineCommand\"\n\tCallbackAfterUserOfflineCommand                    = \"callbackAfterUserOfflineCommand\"\n\tCallbackAfterUserKickOffCommand                    = \"callbackAfterUserKickOffCommand\"\n\tCallbackBeforeOfflinePushCommand                   = \"callbackBeforeOfflinePushCommand\"\n\tCallbackBeforeOnlinePushCommand                    = \"callbackBeforeOnlinePushCommand\"\n\tCallbackBeforeGroupOnlinePushCommand               = \"callbackBeforeGroupOnlinePushCommand\"\n\tCallbackBeforeAddFriendCommand                     = \"callbackBeforeAddFriendCommand\"\n\tCallbackBeforeUpdateUserInfoCommand                = \"callbackBeforeUpdateUserInfoCommand\"\n\tCallbackBeforeCreateGroupCommand                   = \"callbackBeforeCreateGroupCommand\"\n\tCallbackAfterCreateGroupCommand                    = \"callbackAfterCreateGroupCommand\"\n\tCallbackBeforeMembersJoinGroupCommand              = \"callbackBeforeMembersJoinGroupCommand\"\n\tCallbackBeforeSetGroupMemberInfoCommand            = \"callbackBeforeSetGroupMemberInfoCommand\"\n\tCallbackAfterSetGroupMemberInfoCommand             = \"callbackAfterSetGroupMemberInfoCommand\"\n\tCallbackBeforeCreateSingleChatConversationsCommand = \"callbackBeforeCreateSingleChatConversationsCommand\"\n\tCallbackAfterCreateSingleChatConversationsCommand  = \"callbackAfterCreateSingleChatConversationsCommand\"\n\tCallbackBeforeCreateGroupChatConversationsCommand  = \"callbackBeforeCreateGroupChatConversationsCommand\"\n\tCallbackAfterCreateGroupChatConversationsCommand   = \"callbackAfterCreateGroupChatConversationsCommand\"\n\tCallbackAfterMsgSaveDBCommand                      = \"callbackAfterMsgSaveDBCommand\"\n)\n"
  },
  {
    "path": "pkg/callbackstruct/conversation.go",
    "content": "package callbackstruct\n\ntype CallbackBeforeCreateSingleChatConversationsReq struct {\n\tCallbackCommand  `json:\"callbackCommand\"`\n\tOwnerUserID      string `json:\"ownerUserId\"`\n\tConversationID   string `json:\"conversationId\"`\n\tConversationType int32  `json:\"conversationType\"`\n\tUserID           string `json:\"userId\"`\n\tRecvMsgOpt       int32  `json:\"recvMsgOpt\"`\n\tIsPinned         bool   `json:\"isPinned\"`\n\tIsPrivateChat    bool   `json:\"isPrivateChat\"`\n\tBurnDuration     int32  `json:\"burnDuration\"`\n\tGroupAtType      int32  `json:\"groupAtType\"`\n\tAttachedInfo     string `json:\"attachedInfo\"`\n\tEx               string `json:\"ex\"`\n}\n\ntype CallbackBeforeCreateSingleChatConversationsResp struct {\n\tCommonCallbackResp\n\tRecvMsgOpt    *int32  `json:\"recvMsgOpt\"`\n\tIsPinned      *bool   `json:\"isPinned\"`\n\tIsPrivateChat *bool   `json:\"isPrivateChat\"`\n\tBurnDuration  *int32  `json:\"burnDuration\"`\n\tGroupAtType   *int32  `json:\"groupAtType\"`\n\tAttachedInfo  *string `json:\"attachedInfo\"`\n\tEx            *string `json:\"ex\"`\n}\n\ntype CallbackAfterCreateSingleChatConversationsReq struct {\n\tCallbackCommand  `json:\"callbackCommand\"`\n\tOwnerUserID      string `json:\"ownerUserId\"`\n\tConversationID   string `json:\"conversationId\"`\n\tConversationType int32  `json:\"conversationType\"`\n\tUserID           string `json:\"userId\"`\n\tRecvMsgOpt       int32  `json:\"recvMsgOpt\"`\n\tIsPinned         bool   `json:\"isPinned\"`\n\tIsPrivateChat    bool   `json:\"isPrivateChat\"`\n\tBurnDuration     int32  `json:\"burnDuration\"`\n\tGroupAtType      int32  `json:\"groupAtType\"`\n\tAttachedInfo     string `json:\"attachedInfo\"`\n\tEx               string `json:\"ex\"`\n}\n\ntype CallbackAfterCreateSingleChatConversationsResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeCreateGroupChatConversationsReq struct {\n\tCallbackCommand  `json:\"callbackCommand\"`\n\tOwnerUserID      string `json:\"ownerUserId\"`\n\tConversationID   string `json:\"conversationId\"`\n\tConversationType int32  `json:\"conversationType\"`\n\tGroupID          string `json:\"groupId\"`\n\tRecvMsgOpt       int32  `json:\"recvMsgOpt\"`\n\tIsPinned         bool   `json:\"isPinned\"`\n\tIsPrivateChat    bool   `json:\"isPrivateChat\"`\n\tBurnDuration     int32  `json:\"burnDuration\"`\n\tGroupAtType      int32  `json:\"groupAtType\"`\n\tAttachedInfo     string `json:\"attachedInfo\"`\n\tEx               string `json:\"ex\"`\n}\n\ntype CallbackBeforeCreateGroupChatConversationsResp struct {\n\tCommonCallbackResp\n\tRecvMsgOpt    *int32  `json:\"recvMsgOpt\"`\n\tIsPinned      *bool   `json:\"isPinned\"`\n\tIsPrivateChat *bool   `json:\"isPrivateChat\"`\n\tBurnDuration  *int32  `json:\"burnDuration\"`\n\tGroupAtType   *int32  `json:\"groupAtType\"`\n\tAttachedInfo  *string `json:\"attachedInfo\"`\n\tEx            *string `json:\"ex\"`\n}\n\ntype CallbackAfterCreateGroupChatConversationsReq struct {\n\tCallbackCommand  `json:\"callbackCommand\"`\n\tOwnerUserID      string `json:\"ownerUserId\"`\n\tConversationID   string `json:\"conversationId\"`\n\tConversationType int32  `json:\"conversationType\"`\n\tGroupID          string `json:\"groupId\"`\n\tRecvMsgOpt       int32  `json:\"recvMsgOpt\"`\n\tIsPinned         bool   `json:\"isPinned\"`\n\tIsPrivateChat    bool   `json:\"isPrivateChat\"`\n\tBurnDuration     int32  `json:\"burnDuration\"`\n\tGroupAtType      int32  `json:\"groupAtType\"`\n\tAttachedInfo     string `json:\"attachedInfo\"`\n\tEx               string `json:\"ex\"`\n}\n\ntype CallbackAfterCreateGroupChatConversationsResp struct {\n\tCommonCallbackResp\n}\n"
  },
  {
    "path": "pkg/callbackstruct/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage callbackstruct // import \"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n"
  },
  {
    "path": "pkg/callbackstruct/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\ntype CallbackBeforeAddFriendReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tFromUserID      string `json:\"fromUserID\" `\n\tToUserID        string `json:\"toUserID\"`\n\tReqMsg          string `json:\"reqMsg\"`\n\tEx              string `json:\"ex\"`\n}\n\ntype CallbackBeforeAddFriendResp struct {\n\tCommonCallbackResp\n}\n\ntype CallBackAddFriendReplyBeforeReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tFromUserID      string `json:\"fromUserID\" `\n\tToUserID        string `json:\"toUserID\"`\n}\n\ntype CallBackAddFriendReplyBeforeResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeSetFriendRemarkReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOwnerUserID     string `json:\"ownerUserID\"`\n\tFriendUserID    string `json:\"friendUserID\"`\n\tRemark          string `json:\"remark\"`\n}\n\ntype CallbackBeforeSetFriendRemarkResp struct {\n\tCommonCallbackResp\n\tRemark string `json:\"remark\"`\n}\n\ntype CallbackAfterSetFriendRemarkReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOwnerUserID     string `json:\"ownerUserID\"`\n\tFriendUserID    string `json:\"friendUserID\"`\n\tRemark          string `json:\"remark\"`\n}\n\ntype CallbackAfterSetFriendRemarkResp struct {\n\tCommonCallbackResp\n}\ntype CallbackAfterAddFriendReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tFromUserID      string `json:\"fromUserID\" `\n\tToUserID        string `json:\"toUserID\"`\n\tReqMsg          string `json:\"reqMsg\"`\n}\n\ntype CallbackAfterAddFriendResp struct {\n\tCommonCallbackResp\n}\ntype CallbackBeforeAddBlackReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOwnerUserID     string `json:\"ownerUserID\" `\n\tBlackUserID     string `json:\"blackUserID\"`\n}\n\ntype CallbackBeforeAddBlackResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeAddFriendAgreeReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tFromUserID      string `json:\"fromUserID\" `\n\tToUserID        string `json:\"blackUserID\"`\n\tHandleResult    int32  `json:\"HandleResult\"`\n\tHandleMsg       string `json:\"HandleMsg\"`\n}\n\ntype CallbackBeforeAddFriendAgreeResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackAfterAddFriendAgreeReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tFromUserID      string `json:\"fromUserID\" `\n\tToUserID        string `json:\"blackUserID\"`\n\tHandleResult    int32  `json:\"HandleResult\"`\n\tHandleMsg       string `json:\"HandleMsg\"`\n}\n\ntype CallbackAfterAddFriendAgreeResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackAfterDeleteFriendReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOwnerUserID     string `json:\"ownerUserID\" `\n\tFriendUserID    string `json:\"friendUserID\"`\n}\ntype CallbackAfterDeleteFriendResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeImportFriendsReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOwnerUserID     string   `json:\"ownerUserID\" `\n\tFriendUserIDs   []string `json:\"friendUserIDs\"`\n}\ntype CallbackBeforeImportFriendsResp struct {\n\tCommonCallbackResp\n\tFriendUserIDs []string `json:\"friendUserIDs\"`\n}\ntype CallbackAfterImportFriendsReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOwnerUserID     string   `json:\"ownerUserID\" `\n\tFriendUserIDs   []string `json:\"friendUserIDs\"`\n}\ntype CallbackAfterImportFriendsResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackAfterRemoveBlackReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOwnerUserID     string `json:\"ownerUserID\"`\n\tBlackUserID     string `json:\"blackUserID\"`\n}\ntype CallbackAfterRemoveBlackResp struct {\n\tCommonCallbackResp\n}\n"
  },
  {
    "path": "pkg/callbackstruct/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\tcommon \"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/protocol/wrapperspb\"\n)\n\ntype CallbackCommand string\n\nfunc (c CallbackCommand) GetCallbackCommand() string {\n\treturn string(c)\n}\n\ntype CallbackBeforeCreateGroupReq struct {\n\tOperationID     string `json:\"operationID\"`\n\tCallbackCommand `json:\"callbackCommand\"`\n\t*common.GroupInfo\n\tInitMemberList []*apistruct.GroupAddMemberInfo `json:\"initMemberList\"`\n}\n\ntype CallbackBeforeCreateGroupResp struct {\n\tCommonCallbackResp\n\tGroupID           *string `json:\"groupID\"`\n\tGroupName         *string `json:\"groupName\"`\n\tNotification      *string `json:\"notification\"`\n\tIntroduction      *string `json:\"introduction\"`\n\tFaceURL           *string `json:\"faceURL\"`\n\tOwnerUserID       *string `json:\"ownerUserID\"`\n\tEx                *string `json:\"ex\"`\n\tStatus            *int32  `json:\"status\"`\n\tCreatorUserID     *string `json:\"creatorUserID\"`\n\tGroupType         *int32  `json:\"groupType\"`\n\tNeedVerification  *int32  `json:\"needVerification\"`\n\tLookMemberInfo    *int32  `json:\"lookMemberInfo\"`\n\tApplyMemberFriend *int32  `json:\"applyMemberFriend\"`\n}\n\ntype CallbackAfterCreateGroupReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\t*common.GroupInfo\n\tInitMemberList []*apistruct.GroupAddMemberInfo `json:\"initMemberList\"`\n}\n\ntype CallbackAfterCreateGroupResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackGroupMember struct {\n\tUserID string `json:\"userID\"`\n\tEx     string `json:\"ex\"`\n}\n\ntype CallbackBeforeMembersJoinGroupReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tGroupID         string                 `json:\"groupID\"`\n\tMembersList     []*CallbackGroupMember `json:\"memberList\"`\n\tGroupEx         string                 `json:\"groupEx\"`\n}\n\ntype MemberJoinGroupCallBack struct {\n\tUserID      *string `json:\"userID\"`\n\tNickname    *string `json:\"nickname\"`\n\tFaceURL     *string `json:\"faceURL\"`\n\tRoleLevel   *int32  `json:\"roleLevel\"`\n\tMuteEndTime *int64  `json:\"muteEndTime\"`\n\tEx          *string `json:\"ex\"`\n}\n\ntype CallbackBeforeMembersJoinGroupResp struct {\n\tCommonCallbackResp\n\tMemberCallbackList []*MemberJoinGroupCallBack `json:\"memberCallbackList\"`\n}\n\ntype CallbackBeforeSetGroupMemberInfoReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tGroupID         string  `json:\"groupID\"`\n\tUserID          string  `json:\"userID\"`\n\tNickname        *string `json:\"nickName\"`\n\tFaceURL         *string `json:\"faceURL\"`\n\tRoleLevel       *int32  `json:\"roleLevel\"`\n\tEx              *string `json:\"ex\"`\n}\n\ntype CallbackBeforeSetGroupMemberInfoResp struct {\n\tCommonCallbackResp\n\tEx        *string `json:\"ex\"`\n\tNickname  *string `json:\"nickName\"`\n\tFaceURL   *string `json:\"faceURL\"`\n\tRoleLevel *int32  `json:\"roleLevel\"`\n}\n\ntype CallbackAfterSetGroupMemberInfoReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tGroupID         string  `json:\"groupID\"`\n\tUserID          string  `json:\"userID\"`\n\tNickname        *string `json:\"nickName\"`\n\tFaceURL         *string `json:\"faceURL\"`\n\tRoleLevel       *int32  `json:\"roleLevel\"`\n\tEx              *string `json:\"ex\"`\n}\n\ntype CallbackAfterSetGroupMemberInfoResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackQuitGroupReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tGroupID         string `json:\"groupID\"`\n\tUserID          string `json:\"userID\"`\n}\n\ntype CallbackQuitGroupResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackKillGroupMemberReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tGroupID         string   `json:\"groupID\"`\n\tKickedUserIDs   []string `json:\"kickedUserIDs\"`\n\tReason          string   `json:\"reason\"`\n}\n\ntype CallbackKillGroupMemberResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackDisMissGroupReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tGroupID         string   `json:\"groupID\"`\n\tOwnerID         string   `json:\"ownerID\"`\n\tGroupType       string   `json:\"groupType\"`\n\tMembersID       []string `json:\"membersID\"`\n}\n\ntype CallbackDisMissGroupResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackJoinGroupReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tGroupID         string `json:\"groupID\"`\n\tGroupType       string `json:\"groupType\"`\n\tApplyID         string `json:\"applyID\"`\n\tReqMessage      string `json:\"reqMessage\"`\n\tEx              string `json:\"ex\"`\n}\n\ntype CallbackJoinGroupResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackTransferGroupOwnerReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tGroupID         string `json:\"groupID\"`\n\tOldOwnerUserID  string `json:\"oldOwnerUserID\"`\n\tNewOwnerUserID  string `json:\"newOwnerUserID\"`\n}\n\ntype CallbackTransferGroupOwnerResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeInviteUserToGroupReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOperationID     string   `json:\"operationID\"`\n\tGroupID         string   `json:\"groupID\"`\n\tReason          string   `json:\"reason\"`\n\tInvitedUserIDs  []string `json:\"invitedUserIDs\"`\n}\ntype CallbackBeforeInviteUserToGroupResp struct {\n\tCommonCallbackResp\n\tRefusedMembersAccount []string `json:\"refusedMembersAccount,omitempty\"` // Optional field to list members whose invitation is refused.\n}\n\ntype CallbackAfterJoinGroupReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tOperationID     string `json:\"operationID\"`\n\tGroupID         string `json:\"groupID\"`\n\tReqMessage      string `json:\"reqMessage\"`\n\tJoinSource      int32  `json:\"joinSource\"`\n\tInviterUserID   string `json:\"inviterUserID\"`\n}\ntype CallbackAfterJoinGroupResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeSetGroupInfoReq struct {\n\tCallbackCommand   `json:\"callbackCommand\"`\n\tOperationID       string `json:\"operationID\"`\n\tGroupID           string `json:\"groupID\"`\n\tGroupName         string `json:\"groupName\"`\n\tNotification      string `json:\"notification\"`\n\tIntroduction      string `json:\"introduction\"`\n\tFaceURL           string `json:\"faceURL\"`\n\tEx                string `json:\"ex\"`\n\tNeedVerification  int32  `json:\"needVerification\"`\n\tLookMemberInfo    int32  `json:\"lookMemberInfo\"`\n\tApplyMemberFriend int32  `json:\"applyMemberFriend\"`\n}\n\ntype CallbackBeforeSetGroupInfoResp struct {\n\tCommonCallbackResp\n\tGroupID           string  ` json:\"groupID\"`\n\tGroupName         string  `json:\"groupName\"`\n\tNotification      string  `json:\"notification\"`\n\tIntroduction      string  `json:\"introduction\"`\n\tFaceURL           string  `json:\"faceURL\"`\n\tEx                *string `json:\"ex\"`\n\tNeedVerification  *int32  `json:\"needVerification\"`\n\tLookMemberInfo    *int32  `json:\"lookMemberInfo\"`\n\tApplyMemberFriend *int32  `json:\"applyMemberFriend\"`\n}\n\ntype CallbackAfterSetGroupInfoReq struct {\n\tCallbackCommand   `json:\"callbackCommand\"`\n\tOperationID       string  `json:\"operationID\"`\n\tGroupID           string  `json:\"groupID\"`\n\tGroupName         string  `json:\"groupName\"`\n\tNotification      string  `json:\"notification\"`\n\tIntroduction      string  `json:\"introduction\"`\n\tFaceURL           string  `json:\"faceURL\"`\n\tEx                *string `json:\"ex\"`\n\tNeedVerification  *int32  `json:\"needVerification\"`\n\tLookMemberInfo    *int32  `json:\"lookMemberInfo\"`\n\tApplyMemberFriend *int32  `json:\"applyMemberFriend\"`\n}\n\ntype CallbackAfterSetGroupInfoResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeSetGroupInfoExReq struct {\n\tCallbackCommand   `json:\"callbackCommand\"`\n\tOperationID       string                  `json:\"operationID\"`\n\tGroupID           string                  `json:\"groupID\"`\n\tGroupName         *wrapperspb.StringValue `json:\"groupName\"`\n\tNotification      *wrapperspb.StringValue `json:\"notification\"`\n\tIntroduction      *wrapperspb.StringValue `json:\"introduction\"`\n\tFaceURL           *wrapperspb.StringValue `json:\"faceURL\"`\n\tEx                *wrapperspb.StringValue `json:\"ex\"`\n\tNeedVerification  *wrapperspb.Int32Value  `json:\"needVerification\"`\n\tLookMemberInfo    *wrapperspb.Int32Value  `json:\"lookMemberInfo\"`\n\tApplyMemberFriend *wrapperspb.Int32Value  `json:\"applyMemberFriend\"`\n}\n\ntype CallbackBeforeSetGroupInfoExResp struct {\n\tCommonCallbackResp\n\tGroupID           string                  `json:\"groupID\"`\n\tGroupName         *wrapperspb.StringValue `json:\"groupName\"`\n\tNotification      *wrapperspb.StringValue `json:\"notification\"`\n\tIntroduction      *wrapperspb.StringValue `json:\"introduction\"`\n\tFaceURL           *wrapperspb.StringValue `json:\"faceURL\"`\n\tEx                *wrapperspb.StringValue `json:\"ex\"`\n\tNeedVerification  *wrapperspb.Int32Value  `json:\"needVerification\"`\n\tLookMemberInfo    *wrapperspb.Int32Value  `json:\"lookMemberInfo\"`\n\tApplyMemberFriend *wrapperspb.Int32Value  `json:\"applyMemberFriend\"`\n}\n\ntype CallbackAfterSetGroupInfoExReq struct {\n\tCallbackCommand   `json:\"callbackCommand\"`\n\tOperationID       string                  `json:\"operationID\"`\n\tGroupID           string                  `json:\"groupID\"`\n\tGroupName         *wrapperspb.StringValue `json:\"groupName\"`\n\tNotification      *wrapperspb.StringValue `json:\"notification\"`\n\tIntroduction      *wrapperspb.StringValue `json:\"introduction\"`\n\tFaceURL           *wrapperspb.StringValue `json:\"faceURL\"`\n\tEx                *wrapperspb.StringValue `json:\"ex\"`\n\tNeedVerification  *wrapperspb.Int32Value  `json:\"needVerification\"`\n\tLookMemberInfo    *wrapperspb.Int32Value  `json:\"lookMemberInfo\"`\n\tApplyMemberFriend *wrapperspb.Int32Value  `json:\"applyMemberFriend\"`\n}\n\ntype CallbackAfterSetGroupInfoExResp struct {\n\tCommonCallbackResp\n}\n"
  },
  {
    "path": "pkg/callbackstruct/message.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\nimport (\n\tsdkws \"github.com/openimsdk/protocol/sdkws\"\n)\n\ntype CallbackBeforeSendSingleMsgReq struct {\n\tCommonCallbackReq\n\tRecvID string `json:\"recvID\"`\n}\n\ntype CallbackBeforeSendSingleMsgResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackAfterSendSingleMsgReq struct {\n\tCommonCallbackReq\n\tRecvID string `json:\"recvID\"`\n}\n\ntype CallbackAfterSendSingleMsgResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeSendGroupMsgReq struct {\n\tCommonCallbackReq\n\tGroupID string `json:\"groupID\"`\n}\n\ntype CallbackBeforeSendGroupMsgResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackAfterSendGroupMsgReq struct {\n\tCommonCallbackReq\n\tGroupID string `json:\"groupID\"`\n}\n\ntype CallbackAfterSendGroupMsgResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackMsgModifyCommandReq struct {\n\tCommonCallbackReq\n}\n\ntype CallbackMsgModifyCommandResp struct {\n\tCommonCallbackResp\n\tContent          *string                `json:\"content\"`\n\tRecvID           *string                `json:\"recvID\"`\n\tGroupID          *string                `json:\"groupID\"`\n\tClientMsgID      *string                `json:\"clientMsgID\"`\n\tServerMsgID      *string                `json:\"serverMsgID\"`\n\tSenderPlatformID *int32                 `json:\"senderPlatformID\"`\n\tSenderNickname   *string                `json:\"senderNickname\"`\n\tSenderFaceURL    *string                `json:\"senderFaceURL\"`\n\tSessionType      *int32                 `json:\"sessionType\"`\n\tMsgFrom          *int32                 `json:\"msgFrom\"`\n\tContentType      *int32                 `json:\"contentType\"`\n\tStatus           *int32                 `json:\"status\"`\n\tOptions          *map[string]bool       `json:\"options\"`\n\tOfflinePushInfo  *sdkws.OfflinePushInfo `json:\"offlinePushInfo\"`\n\tAtUserIDList     *[]string              `json:\"atUserIDList\"`\n\tMsgDataList      *[]byte                `json:\"msgDataList\"`\n\tAttachedInfo     *string                `json:\"attachedInfo\"`\n\tEx               *string                `json:\"ex\"`\n}\n\ntype CallbackGroupMsgReadReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tSendID          string `json:\"sendID\"`\n\tReceiveID       string `json:\"receiveID\"`\n\tUnreadMsgNum    int64  `json:\"unreadMsgNum\"`\n\tContentType     int64  `json:\"contentType\"`\n}\n\ntype CallbackGroupMsgReadResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackSingleMsgReadReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tConversationID  string  `json:\"conversationID\"`\n\tUserID          string  `json:\"userID\"`\n\tSeqs            []int64 `json:\"Seqs\"`\n\tContentType     int32   `json:\"contentType\"`\n}\n\ntype CallbackSingleMsgReadResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackAfterMsgSaveDBReq struct {\n\tCommonCallbackReq\n\tRecvID  string `json:\"recvID\"`\n\tGroupID string `json:\"groupID\"`\n}\n\ntype CallbackAfterMsgSaveDBResp struct {\n\tCommonCallbackResp\n}\n"
  },
  {
    "path": "pkg/callbackstruct/msg_gateway.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\ntype CallbackUserOnlineReq struct {\n\tUserStatusCallbackReq\n\t// Token           string `json:\"token\"`\n\tSeq             int64  `json:\"seq\"`\n\tIsAppBackground bool   `json:\"isAppBackground\"`\n\tConnID          string `json:\"connID\"`\n}\n\ntype CallbackUserOnlineResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackUserOfflineReq struct {\n\tUserStatusCallbackReq\n\tSeq    int64  `json:\"seq\"`\n\tConnID string `json:\"connID\"`\n}\n\ntype CallbackUserOfflineResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackUserKickOffReq struct {\n\tUserStatusCallbackReq\n\tSeq int64 `json:\"seq\"`\n}\n\ntype CallbackUserKickOffResp struct {\n\tCommonCallbackResp\n}\n"
  },
  {
    "path": "pkg/callbackstruct/push.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\nimport common \"github.com/openimsdk/protocol/sdkws\"\n\ntype CallbackBeforePushReq struct {\n\tUserStatusBatchCallbackReq\n\t*common.OfflinePushInfo\n\tClientMsgID string   `json:\"clientMsgID\"`\n\tSendID      string   `json:\"sendID\"`\n\tGroupID     string   `json:\"groupID\"`\n\tContentType int32    `json:\"contentType\"`\n\tSessionType int32    `json:\"sessionType\"`\n\tAtUserIDs   []string `json:\"atUserIDList\"`\n\tContent     string   `json:\"content\"`\n}\n\ntype CallbackBeforePushResp struct {\n\tCommonCallbackResp\n\tUserIDs         []string                `json:\"userIDList\"`\n\tOfflinePushInfo *common.OfflinePushInfo `json:\"offlinePushInfo\"`\n}\n\ntype CallbackBeforeSuperGroupOnlinePushReq struct {\n\tUserStatusBaseCallback\n\tClientMsgID string   `json:\"clientMsgID\"`\n\tSendID      string   `json:\"sendID\"`\n\tGroupID     string   `json:\"groupID\"`\n\tContentType int32    `json:\"contentType\"`\n\tSessionType int32    `json:\"sessionType\"`\n\tAtUserIDs   []string `json:\"atUserIDList\"`\n\tContent     string   `json:\"content\"`\n\tSeq         int64    `json:\"seq\"`\n}\n\ntype CallbackBeforeSuperGroupOnlinePushResp struct {\n\tCommonCallbackResp\n\tUserIDs         []string                `json:\"userIDList\"`\n\tOfflinePushInfo *common.OfflinePushInfo `json:\"offlinePushInfo\"`\n}\n"
  },
  {
    "path": "pkg/callbackstruct/revoke.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\ntype CallbackAfterRevokeMsgReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tConversationID  string `json:\"conversationID\"`\n\tSeq             int64  `json:\"seq\"`\n\tUserID          string `json:\"userID\"`\n}\n\ntype CallbackAfterRevokeMsgResp struct {\n\tCommonCallbackResp\n}\n"
  },
  {
    "path": "pkg/callbackstruct/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage callbackstruct\n\nimport (\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/protocol/wrapperspb\"\n)\n\ntype CallbackBeforeUpdateUserInfoReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tUserID          string  `json:\"userID\"`\n\tNickname        *string `json:\"nickName\"`\n\tFaceURL         *string `json:\"faceURL\"`\n\tEx              *string `json:\"ex\"`\n}\n\ntype CallbackBeforeUpdateUserInfoResp struct {\n\tCommonCallbackResp\n\tNickname *string `json:\"nickName\"`\n\tFaceURL  *string `json:\"faceURL\"`\n\tEx       *string `json:\"ex\"`\n}\n\ntype CallbackAfterUpdateUserInfoReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tUserID          string `json:\"userID\"`\n\tNickname        string `json:\"nickName\"`\n\tFaceURL         string `json:\"faceURL\"`\n\tEx              string `json:\"ex\"`\n}\ntype CallbackAfterUpdateUserInfoResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeUpdateUserInfoExReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tUserID          string                  `json:\"userID\"`\n\tNickname        *wrapperspb.StringValue `json:\"nickName\"`\n\tFaceURL         *wrapperspb.StringValue `json:\"faceURL\"`\n\tEx              *wrapperspb.StringValue `json:\"ex\"`\n}\ntype CallbackBeforeUpdateUserInfoExResp struct {\n\tCommonCallbackResp\n\tNickname *wrapperspb.StringValue `json:\"nickName\"`\n\tFaceURL  *wrapperspb.StringValue `json:\"faceURL\"`\n\tEx       *wrapperspb.StringValue `json:\"ex\"`\n}\n\ntype CallbackAfterUpdateUserInfoExReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tUserID          string                  `json:\"userID\"`\n\tNickname        *wrapperspb.StringValue `json:\"nickName\"`\n\tFaceURL         *wrapperspb.StringValue `json:\"faceURL\"`\n\tEx              *wrapperspb.StringValue `json:\"ex\"`\n}\ntype CallbackAfterUpdateUserInfoExResp struct {\n\tCommonCallbackResp\n}\n\ntype CallbackBeforeUserRegisterReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tUsers           []*sdkws.UserInfo `json:\"users\"`\n}\n\ntype CallbackBeforeUserRegisterResp struct {\n\tCommonCallbackResp\n\tUsers []*sdkws.UserInfo `json:\"users\"`\n}\n\ntype CallbackAfterUserRegisterReq struct {\n\tCallbackCommand `json:\"callbackCommand\"`\n\tUsers           []*sdkws.UserInfo `json:\"users\"`\n}\n\ntype CallbackAfterUserRegisterResp struct {\n\tCommonCallbackResp\n}\n"
  },
  {
    "path": "pkg/common/cmd/api.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/api\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype ApiCmd struct {\n\t*RootCmd\n\tctx       context.Context\n\tconfigMap map[string]any\n\tapiConfig *api.Config\n}\n\nfunc NewApiCmd() *ApiCmd {\n\tvar apiConfig api.Config\n\tret := &ApiCmd{apiConfig: &apiConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.DiscoveryConfigFilename:          &apiConfig.Discovery,\n\t\tconfig.KafkaConfigFileName:              &apiConfig.Kafka,\n\t\tconfig.LocalCacheConfigFileName:         &apiConfig.LocalCache,\n\t\tconfig.LogConfigFileName:                &apiConfig.Log,\n\t\tconfig.MinioConfigFileName:              &apiConfig.Minio,\n\t\tconfig.MongodbConfigFileName:            &apiConfig.Mongo,\n\t\tconfig.NotificationFileName:             &apiConfig.Notification,\n\t\tconfig.OpenIMAPICfgFileName:             &apiConfig.API,\n\t\tconfig.OpenIMCronTaskCfgFileName:        &apiConfig.CronTask,\n\t\tconfig.OpenIMMsgGatewayCfgFileName:      &apiConfig.MsgGateway,\n\t\tconfig.OpenIMMsgTransferCfgFileName:     &apiConfig.MsgTransfer,\n\t\tconfig.OpenIMPushCfgFileName:            &apiConfig.Push,\n\t\tconfig.OpenIMRPCAuthCfgFileName:         &apiConfig.Auth,\n\t\tconfig.OpenIMRPCConversationCfgFileName: &apiConfig.Conversation,\n\t\tconfig.OpenIMRPCFriendCfgFileName:       &apiConfig.Friend,\n\t\tconfig.OpenIMRPCGroupCfgFileName:        &apiConfig.Group,\n\t\tconfig.OpenIMRPCMsgCfgFileName:          &apiConfig.Msg,\n\t\tconfig.OpenIMRPCThirdCfgFileName:        &apiConfig.Third,\n\t\tconfig.OpenIMRPCUserCfgFileName:         &apiConfig.User,\n\t\tconfig.RedisConfigFileName:              &apiConfig.Redis,\n\t\tconfig.ShareFileName:                    &apiConfig.Share,\n\t\tconfig.WebhooksConfigFileName:           &apiConfig.Webhooks,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tapiConfig.ConfigPath = config.Path(ret.configPath)\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *ApiCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *ApiCmd) runE() error {\n\ta.apiConfig.Index = config.Index(a.Index())\n\tprometheus := config.Prometheus{\n\t\tEnable: a.apiConfig.API.Prometheus.Enable,\n\t\tPorts:  a.apiConfig.API.Prometheus.Ports,\n\t}\n\treturn startrpc.Start(\n\t\ta.ctx, &a.apiConfig.Discovery,\n\t\tnil,\n\t\tnil,\n\t\t// &a.apiConfig.API.RateLimiter,\n\t\t&prometheus,\n\t\ta.apiConfig.API.Api.ListenIP, \"\",\n\t\ta.apiConfig.API.Prometheus.AutoSetPorts,\n\t\tnil, int(a.apiConfig.Index),\n\t\tprommetrics.APIKeyName,\n\t\t&a.apiConfig.Notification,\n\t\ta.apiConfig,\n\t\t[]string{},\n\t\t[]string{},\n\t\tapi.Start,\n\t)\n}\n"
  },
  {
    "path": "pkg/common/cmd/auth.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/auth\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype AuthRpcCmd struct {\n\t*RootCmd\n\tctx        context.Context\n\tconfigMap  map[string]any\n\tauthConfig *auth.Config\n}\n\nfunc NewAuthRpcCmd() *AuthRpcCmd {\n\tvar authConfig auth.Config\n\tret := &AuthRpcCmd{authConfig: &authConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMRPCAuthCfgFileName: &authConfig.RpcConfig,\n\t\tconfig.RedisConfigFileName:      &authConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:    &authConfig.MongoConfig,\n\t\tconfig.ShareFileName:            &authConfig.Share,\n\t\tconfig.LocalCacheConfigFileName: &authConfig.LocalCacheConfig,\n\t\tconfig.DiscoveryConfigFilename:  &authConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\n\treturn ret\n}\n\nfunc (a *AuthRpcCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *AuthRpcCmd) runE() error {\n\treturn startrpc.Start(a.ctx, &a.authConfig.Discovery, &a.authConfig.RpcConfig.CircuitBreaker, &a.authConfig.RpcConfig.RateLimiter, &a.authConfig.RpcConfig.Prometheus, a.authConfig.RpcConfig.RPC.ListenIP,\n\t\ta.authConfig.RpcConfig.RPC.RegisterIP, a.authConfig.RpcConfig.RPC.AutoSetPorts, a.authConfig.RpcConfig.RPC.Ports,\n\t\ta.Index(), a.authConfig.Discovery.RpcService.Auth, nil, a.authConfig,\n\t\t[]string{\n\t\t\ta.authConfig.RpcConfig.GetConfigFileName(),\n\t\t\ta.authConfig.Share.GetConfigFileName(),\n\t\t\ta.authConfig.RedisConfig.GetConfigFileName(),\n\t\t\ta.authConfig.Discovery.GetConfigFileName(),\n\t\t},\n\t\t[]string{\n\t\t\ta.authConfig.Discovery.RpcService.MessageGateway,\n\t\t},\n\t\tauth.Start)\n}\n"
  },
  {
    "path": "pkg/common/cmd/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/conversation\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype ConversationRpcCmd struct {\n\t*RootCmd\n\tctx                context.Context\n\tconfigMap          map[string]any\n\tconversationConfig *conversation.Config\n}\n\nfunc NewConversationRpcCmd() *ConversationRpcCmd {\n\tvar conversationConfig conversation.Config\n\tret := &ConversationRpcCmd{conversationConfig: &conversationConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMRPCConversationCfgFileName: &conversationConfig.RpcConfig,\n\t\tconfig.RedisConfigFileName:              &conversationConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:            &conversationConfig.MongodbConfig,\n\t\tconfig.ShareFileName:                    &conversationConfig.Share,\n\t\tconfig.NotificationFileName:             &conversationConfig.NotificationConfig,\n\t\tconfig.WebhooksConfigFileName:           &conversationConfig.WebhooksConfig,\n\t\tconfig.LocalCacheConfigFileName:         &conversationConfig.LocalCacheConfig,\n\t\tconfig.DiscoveryConfigFilename:          &conversationConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *ConversationRpcCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *ConversationRpcCmd) runE() error {\n\treturn startrpc.Start(a.ctx, &a.conversationConfig.Discovery, &a.conversationConfig.RpcConfig.CircuitBreaker, &a.conversationConfig.RpcConfig.RateLimiter, &a.conversationConfig.RpcConfig.Prometheus, a.conversationConfig.RpcConfig.RPC.ListenIP,\n\t\ta.conversationConfig.RpcConfig.RPC.RegisterIP, a.conversationConfig.RpcConfig.RPC.AutoSetPorts, a.conversationConfig.RpcConfig.RPC.Ports,\n\t\ta.Index(), a.conversationConfig.Discovery.RpcService.Conversation, &a.conversationConfig.NotificationConfig, a.conversationConfig,\n\t\t[]string{\n\t\t\ta.conversationConfig.RpcConfig.GetConfigFileName(),\n\t\t\ta.conversationConfig.RedisConfig.GetConfigFileName(),\n\t\t\ta.conversationConfig.MongodbConfig.GetConfigFileName(),\n\t\t\ta.conversationConfig.NotificationConfig.GetConfigFileName(),\n\t\t\ta.conversationConfig.Share.GetConfigFileName(),\n\t\t\ta.conversationConfig.LocalCacheConfig.GetConfigFileName(),\n\t\t\ta.conversationConfig.WebhooksConfig.GetConfigFileName(),\n\t\t\ta.conversationConfig.Discovery.GetConfigFileName(),\n\t\t}, nil,\n\t\tconversation.Start)\n}\n"
  },
  {
    "path": "pkg/common/cmd/cron_task.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/tools/cron\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype CronTaskCmd struct {\n\t*RootCmd\n\tctx            context.Context\n\tconfigMap      map[string]any\n\tcronTaskConfig *cron.Config\n}\n\nfunc NewCronTaskCmd() *CronTaskCmd {\n\tvar cronTaskConfig cron.Config\n\tret := &CronTaskCmd{cronTaskConfig: &cronTaskConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMCronTaskCfgFileName: &cronTaskConfig.CronTask,\n\t\tconfig.ShareFileName:             &cronTaskConfig.Share,\n\t\tconfig.DiscoveryConfigFilename:   &cronTaskConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *CronTaskCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *CronTaskCmd) runE() error {\n\tvar prometheus config.Prometheus\n\treturn startrpc.Start(\n\t\ta.ctx, &a.cronTaskConfig.Discovery,\n\t\tnil,\n\t\tnil,\n\t\t&prometheus,\n\t\t\"\", \"\",\n\t\ttrue,\n\t\tnil, 0,\n\t\t\"\",\n\t\tnil,\n\t\ta.cronTaskConfig,\n\t\t[]string{},\n\t\t[]string{},\n\t\tcron.Start,\n\t)\n}\n"
  },
  {
    "path": "pkg/common/cmd/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cmd // import \"github.com/openimsdk/open-im-server/v3/pkg/common/cmd\"\n"
  },
  {
    "path": "pkg/common/cmd/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/relation\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype FriendRpcCmd struct {\n\t*RootCmd\n\tctx            context.Context\n\tconfigMap      map[string]any\n\trelationConfig *relation.Config\n}\n\nfunc NewFriendRpcCmd() *FriendRpcCmd {\n\tvar relationConfig relation.Config\n\tret := &FriendRpcCmd{relationConfig: &relationConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMRPCFriendCfgFileName: &relationConfig.RpcConfig,\n\t\tconfig.RedisConfigFileName:        &relationConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:      &relationConfig.MongodbConfig,\n\t\tconfig.ShareFileName:              &relationConfig.Share,\n\t\tconfig.NotificationFileName:       &relationConfig.NotificationConfig,\n\t\tconfig.WebhooksConfigFileName:     &relationConfig.WebhooksConfig,\n\t\tconfig.LocalCacheConfigFileName:   &relationConfig.LocalCacheConfig,\n\t\tconfig.DiscoveryConfigFilename:    &relationConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *FriendRpcCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *FriendRpcCmd) runE() error {\n\treturn startrpc.Start(a.ctx, &a.relationConfig.Discovery, &a.relationConfig.RpcConfig.CircuitBreaker, &a.relationConfig.RpcConfig.RateLimiter, &a.relationConfig.RpcConfig.Prometheus, a.relationConfig.RpcConfig.RPC.ListenIP,\n\t\ta.relationConfig.RpcConfig.RPC.RegisterIP, a.relationConfig.RpcConfig.RPC.AutoSetPorts, a.relationConfig.RpcConfig.RPC.Ports,\n\t\ta.Index(), a.relationConfig.Discovery.RpcService.Friend, &a.relationConfig.NotificationConfig, a.relationConfig,\n\t\t[]string{\n\t\t\ta.relationConfig.RpcConfig.GetConfigFileName(),\n\t\t\ta.relationConfig.RedisConfig.GetConfigFileName(),\n\t\t\ta.relationConfig.MongodbConfig.GetConfigFileName(),\n\t\t\ta.relationConfig.NotificationConfig.GetConfigFileName(),\n\t\t\ta.relationConfig.Share.GetConfigFileName(),\n\t\t\ta.relationConfig.WebhooksConfig.GetConfigFileName(),\n\t\t\ta.relationConfig.LocalCacheConfig.GetConfigFileName(),\n\t\t\ta.relationConfig.Discovery.GetConfigFileName(),\n\t\t}, nil,\n\t\trelation.Start)\n}\n"
  },
  {
    "path": "pkg/common/cmd/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/group\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/versionctx\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype GroupRpcCmd struct {\n\t*RootCmd\n\tctx         context.Context\n\tconfigMap   map[string]any\n\tgroupConfig *group.Config\n}\n\nfunc NewGroupRpcCmd() *GroupRpcCmd {\n\tvar groupConfig group.Config\n\tret := &GroupRpcCmd{groupConfig: &groupConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMRPCGroupCfgFileName: &groupConfig.RpcConfig,\n\t\tconfig.RedisConfigFileName:       &groupConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:     &groupConfig.MongodbConfig,\n\t\tconfig.ShareFileName:             &groupConfig.Share,\n\t\tconfig.NotificationFileName:      &groupConfig.NotificationConfig,\n\t\tconfig.WebhooksConfigFileName:    &groupConfig.WebhooksConfig,\n\t\tconfig.LocalCacheConfigFileName:  &groupConfig.LocalCacheConfig,\n\t\tconfig.DiscoveryConfigFilename:   &groupConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *GroupRpcCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *GroupRpcCmd) runE() error {\n\treturn startrpc.Start(a.ctx, &a.groupConfig.Discovery, &a.groupConfig.RpcConfig.CircuitBreaker, &a.groupConfig.RpcConfig.RateLimiter, &a.groupConfig.RpcConfig.Prometheus, a.groupConfig.RpcConfig.RPC.ListenIP,\n\t\ta.groupConfig.RpcConfig.RPC.RegisterIP, a.groupConfig.RpcConfig.RPC.AutoSetPorts, a.groupConfig.RpcConfig.RPC.Ports,\n\t\ta.Index(), a.groupConfig.Discovery.RpcService.Group, &a.groupConfig.NotificationConfig, a.groupConfig,\n\t\t[]string{\n\t\t\ta.groupConfig.RpcConfig.GetConfigFileName(),\n\t\t\ta.groupConfig.RedisConfig.GetConfigFileName(),\n\t\t\ta.groupConfig.MongodbConfig.GetConfigFileName(),\n\t\t\ta.groupConfig.NotificationConfig.GetConfigFileName(),\n\t\t\ta.groupConfig.Share.GetConfigFileName(),\n\t\t\ta.groupConfig.WebhooksConfig.GetConfigFileName(),\n\t\t\ta.groupConfig.LocalCacheConfig.GetConfigFileName(),\n\t\t\ta.groupConfig.Discovery.GetConfigFileName(),\n\t\t}, nil,\n\t\tgroup.Start, versionctx.EnableVersionCtx())\n}\n"
  },
  {
    "path": "pkg/common/cmd/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/msg\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype MsgRpcCmd struct {\n\t*RootCmd\n\tctx       context.Context\n\tconfigMap map[string]any\n\tmsgConfig *msg.Config\n}\n\nfunc NewMsgRpcCmd() *MsgRpcCmd {\n\tvar msgConfig msg.Config\n\tret := &MsgRpcCmd{msgConfig: &msgConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMRPCMsgCfgFileName:  &msgConfig.RpcConfig,\n\t\tconfig.RedisConfigFileName:      &msgConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:    &msgConfig.MongodbConfig,\n\t\tconfig.KafkaConfigFileName:      &msgConfig.KafkaConfig,\n\t\tconfig.ShareFileName:            &msgConfig.Share,\n\t\tconfig.NotificationFileName:     &msgConfig.NotificationConfig,\n\t\tconfig.WebhooksConfigFileName:   &msgConfig.WebhooksConfig,\n\t\tconfig.LocalCacheConfigFileName: &msgConfig.LocalCacheConfig,\n\t\tconfig.DiscoveryConfigFilename:  &msgConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *MsgRpcCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *MsgRpcCmd) runE() error {\n\treturn startrpc.Start(a.ctx, &a.msgConfig.Discovery, &a.msgConfig.RpcConfig.CircuitBreaker, &a.msgConfig.RpcConfig.RateLimiter, &a.msgConfig.RpcConfig.Prometheus, a.msgConfig.RpcConfig.RPC.ListenIP,\n\t\ta.msgConfig.RpcConfig.RPC.RegisterIP, a.msgConfig.RpcConfig.RPC.AutoSetPorts, a.msgConfig.RpcConfig.RPC.Ports,\n\t\ta.Index(), a.msgConfig.Discovery.RpcService.Msg, &a.msgConfig.NotificationConfig, a.msgConfig,\n\t\t[]string{\n\t\t\ta.msgConfig.RpcConfig.GetConfigFileName(),\n\t\t\ta.msgConfig.RedisConfig.GetConfigFileName(),\n\t\t\ta.msgConfig.MongodbConfig.GetConfigFileName(),\n\t\t\ta.msgConfig.KafkaConfig.GetConfigFileName(),\n\t\t\ta.msgConfig.NotificationConfig.GetConfigFileName(),\n\t\t\ta.msgConfig.Share.GetConfigFileName(),\n\t\t\ta.msgConfig.WebhooksConfig.GetConfigFileName(),\n\t\t\ta.msgConfig.LocalCacheConfig.GetConfigFileName(),\n\t\t\ta.msgConfig.Discovery.GetConfigFileName(),\n\t\t}, nil,\n\t\tmsg.Start)\n}\n"
  },
  {
    "path": "pkg/common/cmd/msg_gateway.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/msggateway\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype MsgGatewayCmd struct {\n\t*RootCmd\n\tctx              context.Context\n\tconfigMap        map[string]any\n\tmsgGatewayConfig *msggateway.Config\n}\n\nfunc NewMsgGatewayCmd() *MsgGatewayCmd {\n\tvar msgGatewayConfig msggateway.Config\n\tret := &MsgGatewayCmd{msgGatewayConfig: &msgGatewayConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMMsgGatewayCfgFileName: &msgGatewayConfig.MsgGateway,\n\t\tconfig.ShareFileName:               &msgGatewayConfig.Share,\n\t\tconfig.RedisConfigFileName:         &msgGatewayConfig.RedisConfig,\n\t\tconfig.WebhooksConfigFileName:      &msgGatewayConfig.WebhooksConfig,\n\t\tconfig.DiscoveryConfigFilename:     &msgGatewayConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (m *MsgGatewayCmd) Exec() error {\n\treturn m.Execute()\n}\n\nfunc (m *MsgGatewayCmd) runE() error {\n\tm.msgGatewayConfig.Index = config.Index(m.Index())\n\trpc := m.msgGatewayConfig.MsgGateway.RPC\n\tvar prometheus config.Prometheus\n\treturn startrpc.Start(\n\t\tm.ctx, &m.msgGatewayConfig.Discovery,\n\t\t&m.msgGatewayConfig.MsgGateway.CircuitBreaker,\n\t\t&m.msgGatewayConfig.MsgGateway.RateLimiter,\n\t\t&prometheus,\n\t\trpc.ListenIP, rpc.RegisterIP,\n\t\trpc.AutoSetPorts,\n\t\trpc.Ports, int(m.msgGatewayConfig.Index),\n\t\tm.msgGatewayConfig.Discovery.RpcService.MessageGateway,\n\t\tnil,\n\t\tm.msgGatewayConfig,\n\t\t[]string{},\n\t\t[]string{},\n\t\tmsggateway.Start,\n\t)\n}\n"
  },
  {
    "path": "pkg/common/cmd/msg_gateway_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"math\"\n\t\"testing\"\n\n\t\"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/tools/apiresp\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n)\n\n// MockRootCmd is a mock type for the RootCmd type\ntype MockRootCmd struct {\n\tmock.Mock\n}\n\nfunc (m *MockRootCmd) Execute() error {\n\targs := m.Called()\n\treturn args.Error(0)\n}\n\nfunc TestName(t *testing.T) {\n\tresp := &apiresp.ApiResponse{\n\t\tErrCode: 1234,\n\t\tErrMsg:  \"test\",\n\t\tErrDlt:  \"4567\",\n\t\tData: &auth.GetUserTokenResp{\n\t\t\tToken:             \"1234567\",\n\t\t\tExpireTimeSeconds: math.MaxInt64,\n\t\t},\n\t}\n\tdata, err := resp.MarshalJSON()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(string(data))\n\n\tvar rReso apiresp.ApiResponse\n\trReso.Data = &auth.GetUserTokenResp{}\n\n\tif err := jsonutil.JsonUnmarshal(data, &rReso); err != nil {\n\t\tpanic(err)\n\t}\n\n\tt.Logf(\"%+v\\n\", rReso)\n\n}\n\nfunc TestName1(t *testing.T) {\n\tt.Log(primitive.NewObjectID().String())\n\tt.Log(primitive.NewObjectID().Hex())\n\n}\n"
  },
  {
    "path": "pkg/common/cmd/msg_transfer.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/msgtransfer\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype MsgTransferCmd struct {\n\t*RootCmd\n\tctx               context.Context\n\tconfigMap         map[string]any\n\tmsgTransferConfig *msgtransfer.Config\n}\n\nfunc NewMsgTransferCmd() *MsgTransferCmd {\n\tvar msgTransferConfig msgtransfer.Config\n\tret := &MsgTransferCmd{msgTransferConfig: &msgTransferConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMMsgTransferCfgFileName: &msgTransferConfig.MsgTransfer,\n\t\tconfig.RedisConfigFileName:          &msgTransferConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:        &msgTransferConfig.MongodbConfig,\n\t\tconfig.KafkaConfigFileName:          &msgTransferConfig.KafkaConfig,\n\t\tconfig.ShareFileName:                &msgTransferConfig.Share,\n\t\tconfig.WebhooksConfigFileName:       &msgTransferConfig.WebhooksConfig,\n\t\tconfig.DiscoveryConfigFilename:      &msgTransferConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (m *MsgTransferCmd) Exec() error {\n\treturn m.Execute()\n}\n\nfunc (m *MsgTransferCmd) runE() error {\n\tm.msgTransferConfig.Index = config.Index(m.Index())\n\tvar prometheus config.Prometheus\n\treturn startrpc.Start(\n\t\tm.ctx, &m.msgTransferConfig.Discovery,\n\t\t&m.msgTransferConfig.MsgTransfer.CircuitBreaker,\n\t\t&m.msgTransferConfig.MsgTransfer.RateLimiter,\n\t\t&prometheus,\n\t\t\"\", \"\",\n\t\ttrue,\n\t\tnil, int(m.msgTransferConfig.Index),\n\t\tprommetrics.MessageTransferKeyName,\n\t\tnil,\n\t\tm.msgTransferConfig,\n\t\t[]string{},\n\t\t[]string{},\n\t\tmsgtransfer.Start,\n\t)\n}\n"
  },
  {
    "path": "pkg/common/cmd/msg_utils.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype MsgUtilsCmd struct {\n\tcobra.Command\n}\n\nfunc (m *MsgUtilsCmd) AddUserIDFlag() {\n\tm.Command.PersistentFlags().StringP(\"userID\", \"u\", \"\", \"openIM userID\")\n}\nfunc (m *MsgUtilsCmd) AddIndexFlag() {\n\tm.Command.PersistentFlags().IntP(config.FlagTransferIndex, \"i\", 0, \"process startup sequence number\")\n}\n\nfunc (m *MsgUtilsCmd) AddConfigDirFlag() {\n\tm.Command.PersistentFlags().StringP(config.FlagConf, \"c\", \"\", \"path of config directory\")\n\n}\n\nfunc (m *MsgUtilsCmd) getUserIDFlag(cmdLines *cobra.Command) string {\n\tuserID, _ := cmdLines.Flags().GetString(\"userID\")\n\treturn userID\n}\n\nfunc (m *MsgUtilsCmd) AddFixAllFlag() {\n\tm.Command.PersistentFlags().BoolP(\"fixAll\", \"f\", false, \"openIM fix all seqs\")\n}\n\n/* func (m *MsgUtilsCmd) getFixAllFlag(cmdLines *cobra.Command) bool {\n\tfixAll, _ := cmdLines.Flags().GetBool(\"fixAll\")\n\treturn fixAll\n} */\n\nfunc (m *MsgUtilsCmd) AddClearAllFlag() {\n\tm.Command.PersistentFlags().BoolP(\"clearAll\", \"\", false, \"openIM clear all seqs\")\n}\n\n/* func (m *MsgUtilsCmd) getClearAllFlag(cmdLines *cobra.Command) bool {\n\tclearAll, _ := cmdLines.Flags().GetBool(\"clearAll\")\n\treturn clearAll\n} */\n\nfunc (m *MsgUtilsCmd) AddSuperGroupIDFlag() {\n\tm.Command.PersistentFlags().StringP(\"superGroupID\", \"g\", \"\", \"openIM superGroupID\")\n}\n\nfunc (m *MsgUtilsCmd) getSuperGroupIDFlag(cmdLines *cobra.Command) string {\n\tsuperGroupID, _ := cmdLines.Flags().GetString(\"superGroupID\")\n\treturn superGroupID\n}\n\nfunc (m *MsgUtilsCmd) AddBeginSeqFlag() {\n\tm.Command.PersistentFlags().Int64P(\"beginSeq\", \"b\", 0, \"openIM beginSeq\")\n}\n\n/* func (m *MsgUtilsCmd) getBeginSeqFlag(cmdLines *cobra.Command) int64 {\n\tbeginSeq, _ := cmdLines.Flags().GetInt64(\"beginSeq\")\n\treturn beginSeq\n} */\n\nfunc (m *MsgUtilsCmd) AddLimitFlag() {\n\tm.Command.PersistentFlags().Int64P(\"limit\", \"l\", 0, \"openIM limit\")\n}\n\n/* func (m *MsgUtilsCmd) getLimitFlag(cmdLines *cobra.Command) int64 {\n\tlimit, _ := cmdLines.Flags().GetInt64(\"limit\")\n\treturn limit\n} */\n\nfunc (m *MsgUtilsCmd) Execute() error {\n\treturn m.Command.Execute()\n}\n\nfunc NewMsgUtilsCmd(use, short string, args cobra.PositionalArgs) *MsgUtilsCmd {\n\treturn &MsgUtilsCmd{\n\t\tCommand: cobra.Command{\n\t\t\tUse:   use,\n\t\t\tShort: short,\n\t\t\tArgs:  args,\n\t\t},\n\t}\n}\n\ntype GetCmd struct {\n\t*MsgUtilsCmd\n}\n\nfunc NewGetCmd() *GetCmd {\n\treturn &GetCmd{\n\t\tNewMsgUtilsCmd(\"get [resource]\", \"get action\", cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)),\n\t}\n}\n\ntype FixCmd struct {\n\t*MsgUtilsCmd\n}\n\nfunc NewFixCmd() *FixCmd {\n\treturn &FixCmd{\n\t\tNewMsgUtilsCmd(\"fix [resource]\", \"fix action\", cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)),\n\t}\n}\n\ntype ClearCmd struct {\n\t*MsgUtilsCmd\n}\n\nfunc NewClearCmd() *ClearCmd {\n\treturn &ClearCmd{\n\t\tNewMsgUtilsCmd(\"clear [resource]\", \"clear action\", cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)),\n\t}\n}\n\ntype SeqCmd struct {\n\t*MsgUtilsCmd\n}\n\nfunc NewSeqCmd() *SeqCmd {\n\tseqCmd := &SeqCmd{\n\t\tNewMsgUtilsCmd(\"seq\", \"seq\", nil),\n\t}\n\treturn seqCmd\n}\n\nfunc (s *SeqCmd) GetSeqCmd() *cobra.Command {\n\ts.Command.Run = func(cmdLines *cobra.Command, args []string) {\n\n\t}\n\treturn &s.Command\n}\n\nfunc (s *SeqCmd) FixSeqCmd() *cobra.Command {\n\treturn &s.Command\n}\n\ntype MsgCmd struct {\n\t*MsgUtilsCmd\n}\n\nfunc NewMsgCmd() *MsgCmd {\n\tmsgCmd := &MsgCmd{\n\t\tNewMsgUtilsCmd(\"msg\", \"msg\", nil),\n\t}\n\treturn msgCmd\n}\n\nfunc (m *MsgCmd) GetMsgCmd() *cobra.Command {\n\treturn &m.Command\n}\n\nfunc (m *MsgCmd) ClearMsgCmd() *cobra.Command {\n\treturn &m.Command\n}\n"
  },
  {
    "path": "pkg/common/cmd/push.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/push\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype PushRpcCmd struct {\n\t*RootCmd\n\tctx        context.Context\n\tconfigMap  map[string]any\n\tpushConfig *push.Config\n}\n\nfunc NewPushRpcCmd() *PushRpcCmd {\n\tvar pushConfig push.Config\n\tret := &PushRpcCmd{pushConfig: &pushConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMPushCfgFileName:    &pushConfig.RpcConfig,\n\t\tconfig.RedisConfigFileName:      &pushConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:    &pushConfig.MongoConfig,\n\t\tconfig.KafkaConfigFileName:      &pushConfig.KafkaConfig,\n\t\tconfig.ShareFileName:            &pushConfig.Share,\n\t\tconfig.NotificationFileName:     &pushConfig.NotificationConfig,\n\t\tconfig.WebhooksConfigFileName:   &pushConfig.WebhooksConfig,\n\t\tconfig.LocalCacheConfigFileName: &pushConfig.LocalCacheConfig,\n\t\tconfig.DiscoveryConfigFilename:  &pushConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\tret.pushConfig.FcmConfigPath = config.Path(ret.ConfigPath())\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *PushRpcCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *PushRpcCmd) runE() error {\n\treturn startrpc.Start(a.ctx, &a.pushConfig.Discovery, &a.pushConfig.RpcConfig.CircuitBreaker, &a.pushConfig.RpcConfig.RateLimiter, &a.pushConfig.RpcConfig.Prometheus, a.pushConfig.RpcConfig.RPC.ListenIP,\n\t\ta.pushConfig.RpcConfig.RPC.RegisterIP, a.pushConfig.RpcConfig.RPC.AutoSetPorts, a.pushConfig.RpcConfig.RPC.Ports,\n\t\ta.Index(), a.pushConfig.Discovery.RpcService.Push, &a.pushConfig.NotificationConfig, a.pushConfig,\n\t\t[]string{\n\t\t\ta.pushConfig.RpcConfig.GetConfigFileName(),\n\t\t\ta.pushConfig.RedisConfig.GetConfigFileName(),\n\t\t\ta.pushConfig.KafkaConfig.GetConfigFileName(),\n\t\t\ta.pushConfig.NotificationConfig.GetConfigFileName(),\n\t\t\ta.pushConfig.Share.GetConfigFileName(),\n\t\t\ta.pushConfig.WebhooksConfig.GetConfigFileName(),\n\t\t\ta.pushConfig.LocalCacheConfig.GetConfigFileName(),\n\t\t\ta.pushConfig.Discovery.GetConfigFileName(),\n\t\t},\n\t\t[]string{\n\t\t\ta.pushConfig.Discovery.RpcService.MessageGateway,\n\t\t},\n\t\tpush.Start)\n}\n"
  },
  {
    "path": "pkg/common/cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\tkdisc \"github.com/openimsdk/open-im-server/v3/pkg/common/discovery\"\n\tdisetcd \"github.com/openimsdk/open-im-server/v3/pkg/common/discovery/etcd\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/discovery/etcd\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/spf13/cobra\"\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n)\n\ntype RootCmd struct {\n\tCommand        cobra.Command\n\tprocessName    string\n\tport           int\n\tprometheusPort int\n\tlog            config.Log\n\tindex          int\n\tconfigPath     string\n\tetcdClient     *clientv3.Client\n}\n\nfunc (r *RootCmd) ConfigPath() string {\n\treturn r.configPath\n}\n\nfunc (r *RootCmd) Index() int {\n\treturn r.index\n}\n\nfunc (r *RootCmd) Port() int {\n\treturn r.port\n}\n\ntype CmdOpts struct {\n\tloggerPrefixName string\n\tconfigMap        map[string]any\n}\n\nfunc WithCronTaskLogName() func(*CmdOpts) {\n\treturn func(opts *CmdOpts) {\n\t\topts.loggerPrefixName = \"openim-crontask\"\n\t}\n}\n\nfunc WithLogName(logName string) func(*CmdOpts) {\n\treturn func(opts *CmdOpts) {\n\t\topts.loggerPrefixName = logName\n\t}\n}\nfunc WithConfigMap(configMap map[string]any) func(*CmdOpts) {\n\treturn func(opts *CmdOpts) {\n\t\topts.configMap = configMap\n\t}\n}\n\nfunc NewRootCmd(processName string, opts ...func(*CmdOpts)) *RootCmd {\n\trootCmd := &RootCmd{processName: processName}\n\tcmd := cobra.Command{\n\t\tUse:  \"Start openIM application\",\n\t\tLong: fmt.Sprintf(`Start %s `, processName),\n\t\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treturn rootCmd.persistentPreRun(cmd, opts...)\n\t\t},\n\t\tSilenceUsage:  true,\n\t\tSilenceErrors: false,\n\t}\n\tcmd.Flags().StringP(config.FlagConf, \"c\", \"\", \"path of config directory\")\n\tcmd.Flags().IntP(config.FlagTransferIndex, \"i\", 0, \"process startup sequence number\")\n\n\trootCmd.Command = cmd\n\treturn rootCmd\n}\n\nfunc (r *RootCmd) initEtcd() error {\n\tconfigDirectory, _, err := r.getFlag(&r.Command)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdisConfig := config.Discovery{}\n\terr = config.Load(configDirectory, config.DiscoveryConfigFilename, config.EnvPrefixMap[config.DiscoveryConfigFilename], &disConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif disConfig.Enable == config.ETCD {\n\t\tdiscov, _ := kdisc.NewDiscoveryRegister(&disConfig, nil)\n\t\tr.etcdClient = discov.(*etcd.SvcDiscoveryRegistryImpl).GetClient()\n\t}\n\treturn nil\n}\n\nfunc (r *RootCmd) persistentPreRun(cmd *cobra.Command, opts ...func(*CmdOpts)) error {\n\tif err := r.initEtcd(); err != nil {\n\t\treturn err\n\t}\n\tcmdOpts := r.applyOptions(opts...)\n\tif err := r.initializeConfiguration(cmd, cmdOpts); err != nil {\n\t\treturn err\n\t}\n\tif err := r.updateConfigFromEtcd(cmdOpts); err != nil {\n\t\treturn err\n\t}\n\tif err := r.initializeLogger(cmdOpts); err != nil {\n\t\treturn errs.WrapMsg(err, \"failed to initialize logger\")\n\t}\n\tif err := r.etcdClient.Close(); err != nil {\n\t\treturn errs.WrapMsg(err, \"failed to close etcd client\")\n\t}\n\treturn nil\n}\n\nfunc (r *RootCmd) initializeConfiguration(cmd *cobra.Command, opts *CmdOpts) error {\n\tconfigDirectory, _, err := r.getFlag(cmd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Load common configuration file\n\t//opts.configMap[ShareFileName] = StructEnvPrefix{EnvPrefix: shareEnvPrefix, ConfigStruct: &r.share}\n\tfor configFileName, configStruct := range opts.configMap {\n\t\terr := config.Load(configDirectory, configFileName, config.EnvPrefixMap[configFileName], configStruct)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// Load common log configuration file\n\treturn config.Load(configDirectory, config.LogConfigFileName, config.EnvPrefixMap[config.LogConfigFileName], &r.log)\n}\n\nfunc (r *RootCmd) updateConfigFromEtcd(opts *CmdOpts) error {\n\tif r.etcdClient == nil {\n\t\treturn nil\n\t}\n\tctx := context.TODO()\n\n\tres, err := r.etcdClient.Get(ctx, disetcd.BuildKey(disetcd.EnableConfigCenterKey))\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"root cmd updateConfigFromEtcd, etcd Get EnableConfigCenterKey err: %v\", errs.Wrap(err))\n\t\treturn nil\n\t}\n\tif res.Count == 0 {\n\t\treturn nil\n\t} else {\n\t\tif string(res.Kvs[0].Value) == disetcd.Disable {\n\t\t\treturn nil\n\t\t} else if string(res.Kvs[0].Value) != disetcd.Enable {\n\t\t\treturn errs.New(\"unknown EnableConfigCenter value\").Wrap()\n\t\t}\n\t}\n\n\tupdate := func(configFileName string, configStruct any) error {\n\t\tkey := disetcd.BuildKey(configFileName)\n\t\tetcdRes, err := r.etcdClient.Get(ctx, key)\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"root cmd updateConfigFromEtcd, etcd Get err: %v\", errs.Wrap(err))\n\t\t\treturn nil\n\t\t}\n\t\tif etcdRes.Count == 0 {\n\t\t\tdata, err := json.Marshal(configStruct)\n\t\t\tif err != nil {\n\t\t\t\treturn errs.ErrArgs.WithDetail(err.Error()).Wrap()\n\t\t\t}\n\t\t\t_, err = r.etcdClient.Put(ctx, disetcd.BuildKey(configFileName), string(data))\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"root cmd updateConfigFromEtcd, etcd Put err: %v\", errs.Wrap(err))\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\terr = json.Unmarshal(etcdRes.Kvs[0].Value, configStruct)\n\t\tif err != nil {\n\t\t\treturn errs.WrapMsg(err, \"failed to unmarshal config from etcd\")\n\t\t}\n\t\treturn nil\n\t}\n\tfor configFileName, configStruct := range opts.configMap {\n\t\tif err := update(configFileName, configStruct); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := update(config.LogConfigFileName, &r.log); err != nil {\n\t\treturn err\n\t}\n\t// Load common log configuration file\n\treturn nil\n\n}\n\nfunc (r *RootCmd) applyOptions(opts ...func(*CmdOpts)) *CmdOpts {\n\tcmdOpts := defaultCmdOpts()\n\tfor _, opt := range opts {\n\t\topt(cmdOpts)\n\t}\n\n\treturn cmdOpts\n}\n\nfunc (r *RootCmd) initializeLogger(cmdOpts *CmdOpts) error {\n\terr := log.InitLoggerFromConfig(\n\t\tcmdOpts.loggerPrefixName,\n\t\tr.processName,\n\t\t\"\", \"\",\n\t\tr.log.RemainLogLevel,\n\t\tr.log.IsStdout,\n\t\tr.log.IsJson,\n\t\tr.log.StorageLocation,\n\t\tr.log.RemainRotationCount,\n\t\tr.log.RotationTime,\n\t\tversion.Version,\n\t\tr.log.IsSimplify,\n\t)\n\tif err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\treturn errs.Wrap(log.InitConsoleLogger(r.processName, r.log.RemainLogLevel, r.log.IsJson, version.Version))\n\n}\n\nfunc defaultCmdOpts() *CmdOpts {\n\treturn &CmdOpts{\n\t\tloggerPrefixName: \"openim-service-log\",\n\t}\n}\n\nfunc (r *RootCmd) getFlag(cmd *cobra.Command) (string, int, error) {\n\tconfigDirectory, err := cmd.Flags().GetString(config.FlagConf)\n\tif err != nil {\n\t\treturn \"\", 0, errs.Wrap(err)\n\t}\n\tr.configPath = configDirectory\n\tindex, err := cmd.Flags().GetInt(config.FlagTransferIndex)\n\tif err != nil {\n\t\treturn \"\", 0, errs.Wrap(err)\n\t}\n\tr.index = index\n\treturn configDirectory, index, nil\n}\n\nfunc (r *RootCmd) Execute() error {\n\treturn r.Command.Execute()\n}\n"
  },
  {
    "path": "pkg/common/cmd/third.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/third\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype ThirdRpcCmd struct {\n\t*RootCmd\n\tctx         context.Context\n\tconfigMap   map[string]any\n\tthirdConfig *third.Config\n}\n\nfunc NewThirdRpcCmd() *ThirdRpcCmd {\n\tvar thirdConfig third.Config\n\tret := &ThirdRpcCmd{thirdConfig: &thirdConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMRPCThirdCfgFileName: &thirdConfig.RpcConfig,\n\t\tconfig.RedisConfigFileName:       &thirdConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:     &thirdConfig.MongodbConfig,\n\t\tconfig.ShareFileName:             &thirdConfig.Share,\n\t\tconfig.NotificationFileName:      &thirdConfig.NotificationConfig,\n\t\tconfig.MinioConfigFileName:       &thirdConfig.MinioConfig,\n\t\tconfig.LocalCacheConfigFileName:  &thirdConfig.LocalCacheConfig,\n\t\tconfig.DiscoveryConfigFilename:   &thirdConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *ThirdRpcCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *ThirdRpcCmd) runE() error {\n\treturn startrpc.Start(a.ctx, &a.thirdConfig.Discovery, &a.thirdConfig.RpcConfig.CircuitBreaker, &a.thirdConfig.RpcConfig.RateLimiter, &a.thirdConfig.RpcConfig.Prometheus, a.thirdConfig.RpcConfig.RPC.ListenIP,\n\t\ta.thirdConfig.RpcConfig.RPC.RegisterIP, a.thirdConfig.RpcConfig.RPC.AutoSetPorts, a.thirdConfig.RpcConfig.RPC.Ports,\n\t\ta.Index(), a.thirdConfig.Discovery.RpcService.Third, &a.thirdConfig.NotificationConfig, a.thirdConfig,\n\t\t[]string{\n\t\t\ta.thirdConfig.RpcConfig.GetConfigFileName(),\n\t\t\ta.thirdConfig.RedisConfig.GetConfigFileName(),\n\t\t\ta.thirdConfig.MongodbConfig.GetConfigFileName(),\n\t\t\ta.thirdConfig.NotificationConfig.GetConfigFileName(),\n\t\t\ta.thirdConfig.Share.GetConfigFileName(),\n\t\t\ta.thirdConfig.MinioConfig.GetConfigFileName(),\n\t\t\ta.thirdConfig.LocalCacheConfig.GetConfigFileName(),\n\t\t\ta.thirdConfig.Discovery.GetConfigFileName(),\n\t\t}, nil,\n\t\tthird.Start)\n}\n"
  },
  {
    "path": "pkg/common/cmd/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cmd\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/internal/rpc/user\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc\"\n\t\"github.com/openimsdk/open-im-server/v3/version\"\n\t\"github.com/openimsdk/tools/system/program\"\n\t\"github.com/spf13/cobra\"\n)\n\ntype UserRpcCmd struct {\n\t*RootCmd\n\tctx        context.Context\n\tconfigMap  map[string]any\n\tuserConfig *user.Config\n}\n\nfunc NewUserRpcCmd() *UserRpcCmd {\n\tvar userConfig user.Config\n\tret := &UserRpcCmd{userConfig: &userConfig}\n\tret.configMap = map[string]any{\n\t\tconfig.OpenIMRPCUserCfgFileName: &userConfig.RpcConfig,\n\t\tconfig.RedisConfigFileName:      &userConfig.RedisConfig,\n\t\tconfig.MongodbConfigFileName:    &userConfig.MongodbConfig,\n\t\tconfig.KafkaConfigFileName:      &userConfig.KafkaConfig,\n\t\tconfig.ShareFileName:            &userConfig.Share,\n\t\tconfig.NotificationFileName:     &userConfig.NotificationConfig,\n\t\tconfig.WebhooksConfigFileName:   &userConfig.WebhooksConfig,\n\t\tconfig.LocalCacheConfigFileName: &userConfig.LocalCacheConfig,\n\t\tconfig.DiscoveryConfigFilename:  &userConfig.Discovery,\n\t}\n\tret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))\n\tret.ctx = context.WithValue(context.Background(), \"version\", version.Version)\n\tret.Command.RunE = func(cmd *cobra.Command, args []string) error {\n\t\treturn ret.runE()\n\t}\n\treturn ret\n}\n\nfunc (a *UserRpcCmd) Exec() error {\n\treturn a.Execute()\n}\n\nfunc (a *UserRpcCmd) runE() error {\n\treturn startrpc.Start(a.ctx, &a.userConfig.Discovery, &a.userConfig.RpcConfig.CircuitBreaker, &a.userConfig.RpcConfig.RateLimiter, &a.userConfig.RpcConfig.Prometheus, a.userConfig.RpcConfig.RPC.ListenIP,\n\t\ta.userConfig.RpcConfig.RPC.RegisterIP, a.userConfig.RpcConfig.RPC.AutoSetPorts, a.userConfig.RpcConfig.RPC.Ports,\n\t\ta.Index(), a.userConfig.Discovery.RpcService.User, &a.userConfig.NotificationConfig, a.userConfig,\n\t\t[]string{\n\t\t\ta.userConfig.RpcConfig.GetConfigFileName(),\n\t\t\ta.userConfig.RedisConfig.GetConfigFileName(),\n\t\t\ta.userConfig.MongodbConfig.GetConfigFileName(),\n\t\t\ta.userConfig.KafkaConfig.GetConfigFileName(),\n\t\t\ta.userConfig.NotificationConfig.GetConfigFileName(),\n\t\t\ta.userConfig.Share.GetConfigFileName(),\n\t\t\ta.userConfig.WebhooksConfig.GetConfigFileName(),\n\t\t\ta.userConfig.LocalCacheConfig.GetConfigFileName(),\n\t\t\ta.userConfig.Discovery.GetConfigFileName(),\n\t\t}, nil,\n\t\tuser.Start)\n}\n"
  },
  {
    "path": "pkg/common/config/config.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage config\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/redisutil\"\n\t\"github.com/openimsdk/tools/mq/kafka\"\n\t\"github.com/openimsdk/tools/s3/aws\"\n\t\"github.com/openimsdk/tools/s3/cos\"\n\t\"github.com/openimsdk/tools/s3/kodo\"\n\t\"github.com/openimsdk/tools/s3/minio\"\n\t\"github.com/openimsdk/tools/s3/oss\"\n)\n\nconst StructTagName = \"yaml\"\n\ntype Path string\n\ntype Index int\n\ntype CacheConfig struct {\n\tTopic         string `yaml:\"topic\"`\n\tSlotNum       int    `yaml:\"slotNum\"`\n\tSlotSize      int    `yaml:\"slotSize\"`\n\tSuccessExpire int    `yaml:\"successExpire\"`\n\tFailedExpire  int    `yaml:\"failedExpire\"`\n}\n\ntype LocalCache struct {\n\tAuth         CacheConfig `yaml:\"auth\"`\n\tUser         CacheConfig `yaml:\"user\"`\n\tGroup        CacheConfig `yaml:\"group\"`\n\tFriend       CacheConfig `yaml:\"friend\"`\n\tConversation CacheConfig `yaml:\"conversation\"`\n}\n\ntype Log struct {\n\tStorageLocation     string `yaml:\"storageLocation\"`\n\tRotationTime        uint   `yaml:\"rotationTime\"`\n\tRemainRotationCount uint   `yaml:\"remainRotationCount\"`\n\tRemainLogLevel      int    `yaml:\"remainLogLevel\"`\n\tIsStdout            bool   `yaml:\"isStdout\"`\n\tIsJson              bool   `yaml:\"isJson\"`\n\tIsSimplify          bool   `yaml:\"isSimplify\"`\n\tWithStack           bool   `yaml:\"withStack\"`\n}\n\ntype Minio struct {\n\tBucket          string `yaml:\"bucket\"`\n\tAccessKeyID     string `yaml:\"accessKeyID\"`\n\tSecretAccessKey string `yaml:\"secretAccessKey\"`\n\tSessionToken    string `yaml:\"sessionToken\"`\n\tInternalAddress string `yaml:\"internalAddress\"`\n\tExternalAddress string `yaml:\"externalAddress\"`\n\tPublicRead      bool   `yaml:\"publicRead\"`\n}\n\ntype Mongo struct {\n\tURI            string   `yaml:\"uri\"`\n\tAddress        []string `yaml:\"address\"`\n\tDatabase       string   `yaml:\"database\"`\n\tUsername       string   `yaml:\"username\"`\n\tPassword       string   `yaml:\"password\"`\n\tAuthSource     string   `yaml:\"authSource\"`\n\tMaxPoolSize    int      `yaml:\"maxPoolSize\"`\n\tMaxRetry       int      `yaml:\"maxRetry\"`\n\tMongoMode      string   `yaml:\"mongoMode\"`\n\tReplicaSet     ReplicaSetConfig\n\tReadPreference ReadPrefConfig\n\tWriteConcern   WriteConcernConfig\n}\n\ntype ReplicaSetConfig struct {\n\tName         string        `yaml:\"name\"`\n\tHosts        []string      `yaml:\"hosts\"`\n\tReadConcern  string        `yaml:\"readConcern\"`\n\tMaxStaleness time.Duration `yaml:\"maxStaleness\"`\n}\n\ntype ReadPrefConfig struct {\n\tMode         string              `yaml:\"mode\"`\n\tTagSets      []map[string]string `yaml:\"tagSets\"`\n\tMaxStaleness time.Duration       `yaml:\"maxStaleness\"`\n}\n\ntype WriteConcernConfig struct {\n\tW        any           `yaml:\"w\"`\n\tJ        bool          `yaml:\"j\"`\n\tWTimeout time.Duration `yaml:\"wtimeout\"`\n}\n\ntype Kafka struct {\n\tUsername           string   `yaml:\"username\"`\n\tPassword           string   `yaml:\"password\"`\n\tProducerAck        string   `yaml:\"producerAck\"`\n\tCompressType       string   `yaml:\"compressType\"`\n\tAddress            []string `yaml:\"address\"`\n\tToRedisTopic       string   `yaml:\"toRedisTopic\"`\n\tToMongoTopic       string   `yaml:\"toMongoTopic\"`\n\tToPushTopic        string   `yaml:\"toPushTopic\"`\n\tToOfflinePushTopic string   `yaml:\"toOfflinePushTopic\"`\n\tToRedisGroupID     string   `yaml:\"toRedisGroupID\"`\n\tToMongoGroupID     string   `yaml:\"toMongoGroupID\"`\n\tToPushGroupID      string   `yaml:\"toPushGroupID\"`\n\tToOfflineGroupID   string   `yaml:\"toOfflinePushGroupID\"`\n\n\tTls TLSConfig `yaml:\"tls\"`\n}\ntype TLSConfig struct {\n\tEnableTLS          bool   `yaml:\"enableTLS\"`\n\tCACrt              string `yaml:\"caCrt\"`\n\tClientCrt          string `yaml:\"clientCrt\"`\n\tClientKey          string `yaml:\"clientKey\"`\n\tClientKeyPwd       string `yaml:\"clientKeyPwd\"`\n\tInsecureSkipVerify bool   `yaml:\"insecureSkipVerify\"`\n}\n\ntype API struct {\n\tApi struct {\n\t\tListenIP         string `yaml:\"listenIP\"`\n\t\tPorts            []int  `yaml:\"ports\"`\n\t\tCompressionLevel int    `yaml:\"compressionLevel\"`\n\t} `yaml:\"api\"`\n\tPrometheus struct {\n\t\tEnable       bool   `yaml:\"enable\"`\n\t\tAutoSetPorts bool   `yaml:\"autoSetPorts\"`\n\t\tPorts        []int  `yaml:\"ports\"`\n\t\tGrafanaURL   string `yaml:\"grafanaURL\"`\n\t} `yaml:\"prometheus\"`\n\n\tRateLimiter RateLimiter `yaml:\"rateLimiter\"`\n}\n\ntype RateLimiter struct {\n\tEnable       bool          `yaml:\"enable\"`\n\tWindow       time.Duration `yaml:\"window\"`\n\tBucket       int           `yaml:\"bucket\"`\n\tCPUThreshold int64         `yaml:\"cpuThreshold\"`\n}\n\ntype CircuitBreaker struct {\n\tEnable  bool          `yaml:\"enable\"`\n\tWindow  time.Duration `yaml:\"window\"`\n\tBucket  int           `yaml:\"bucket\"`\n\tSuccess float64       `yaml:\"success\"`\n\tRequest int64         `yaml:\"request\"`\n}\n\ntype CronTask struct {\n\tCronExecuteTime   string   `yaml:\"cronExecuteTime\"`\n\tRetainChatRecords int      `yaml:\"retainChatRecords\"`\n\tFileExpireTime    int      `yaml:\"fileExpireTime\"`\n\tDeleteObjectType  []string `yaml:\"deleteObjectType\"`\n}\n\ntype OfflinePushConfig struct {\n\tEnable bool   `yaml:\"enable\"`\n\tTitle  string `yaml:\"title\"`\n\tDesc   string `yaml:\"desc\"`\n\tExt    string `yaml:\"ext\"`\n}\n\ntype NotificationConfig struct {\n\tIsSendMsg        bool              `yaml:\"isSendMsg\"`\n\tReliabilityLevel int               `yaml:\"reliabilityLevel\"`\n\tUnreadCount      bool              `yaml:\"unreadCount\"`\n\tOfflinePush      OfflinePushConfig `yaml:\"offlinePush\"`\n}\n\ntype Notification struct {\n\tGroupCreated              NotificationConfig `yaml:\"groupCreated\"`\n\tGroupInfoSet              NotificationConfig `yaml:\"groupInfoSet\"`\n\tJoinGroupApplication      NotificationConfig `yaml:\"joinGroupApplication\"`\n\tMemberQuit                NotificationConfig `yaml:\"memberQuit\"`\n\tGroupApplicationAccepted  NotificationConfig `yaml:\"groupApplicationAccepted\"`\n\tGroupApplicationRejected  NotificationConfig `yaml:\"groupApplicationRejected\"`\n\tGroupOwnerTransferred     NotificationConfig `yaml:\"groupOwnerTransferred\"`\n\tMemberKicked              NotificationConfig `yaml:\"memberKicked\"`\n\tMemberInvited             NotificationConfig `yaml:\"memberInvited\"`\n\tMemberEnter               NotificationConfig `yaml:\"memberEnter\"`\n\tGroupDismissed            NotificationConfig `yaml:\"groupDismissed\"`\n\tGroupMuted                NotificationConfig `yaml:\"groupMuted\"`\n\tGroupCancelMuted          NotificationConfig `yaml:\"groupCancelMuted\"`\n\tGroupMemberMuted          NotificationConfig `yaml:\"groupMemberMuted\"`\n\tGroupMemberCancelMuted    NotificationConfig `yaml:\"groupMemberCancelMuted\"`\n\tGroupMemberInfoSet        NotificationConfig `yaml:\"groupMemberInfoSet\"`\n\tGroupMemberSetToAdmin     NotificationConfig `yaml:\"groupMemberSetToAdmin\"`\n\tGroupMemberSetToOrdinary  NotificationConfig `yaml:\"groupMemberSetToOrdinaryUser\"`\n\tGroupInfoSetAnnouncement  NotificationConfig `yaml:\"groupInfoSetAnnouncement\"`\n\tGroupInfoSetName          NotificationConfig `yaml:\"groupInfoSetName\"`\n\tFriendApplicationAdded    NotificationConfig `yaml:\"friendApplicationAdded\"`\n\tFriendApplicationApproved NotificationConfig `yaml:\"friendApplicationApproved\"`\n\tFriendApplicationRejected NotificationConfig `yaml:\"friendApplicationRejected\"`\n\tFriendAdded               NotificationConfig `yaml:\"friendAdded\"`\n\tFriendDeleted             NotificationConfig `yaml:\"friendDeleted\"`\n\tFriendRemarkSet           NotificationConfig `yaml:\"friendRemarkSet\"`\n\tBlackAdded                NotificationConfig `yaml:\"blackAdded\"`\n\tBlackDeleted              NotificationConfig `yaml:\"blackDeleted\"`\n\tFriendInfoUpdated         NotificationConfig `yaml:\"friendInfoUpdated\"`\n\tUserInfoUpdated           NotificationConfig `yaml:\"userInfoUpdated\"`\n\tUserStatusChanged         NotificationConfig `yaml:\"userStatusChanged\"`\n\tConversationChanged       NotificationConfig `yaml:\"conversationChanged\"`\n\tConversationSetPrivate    NotificationConfig `yaml:\"conversationSetPrivate\"`\n}\n\ntype Prometheus struct {\n\tEnable bool  `yaml:\"enable\"`\n\tPorts  []int `yaml:\"ports\"`\n}\n\ntype MsgGateway struct {\n\tRPC         RPC        `yaml:\"rpc\"`\n\tPrometheus  Prometheus `yaml:\"prometheus\"`\n\tListenIP    string     `yaml:\"listenIP\"`\n\tLongConnSvr struct {\n\t\tPorts               []int `yaml:\"ports\"`\n\t\tWebsocketMaxConnNum int   `yaml:\"websocketMaxConnNum\"`\n\t\tWebsocketMaxMsgLen  int   `yaml:\"websocketMaxMsgLen\"`\n\t\tWebsocketTimeout    int   `yaml:\"websocketTimeout\"`\n\t} `yaml:\"longConnSvr\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype MsgTransfer struct {\n\tPrometheus struct {\n\t\tEnable       bool  `yaml:\"enable\"`\n\t\tAutoSetPorts bool  `yaml:\"autoSetPorts\"`\n\t\tPorts        []int `yaml:\"ports\"`\n\t} `yaml:\"prometheus\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype Push struct {\n\tRPC                  RPC        `yaml:\"rpc\"`\n\tPrometheus           Prometheus `yaml:\"prometheus\"`\n\tMaxConcurrentWorkers int        `yaml:\"maxConcurrentWorkers\"`\n\tEnable               string     `yaml:\"enable\"`\n\tGeTui                struct {\n\t\tPushUrl      string `yaml:\"pushUrl\"`\n\t\tMasterSecret string `yaml:\"masterSecret\"`\n\t\tAppKey       string `yaml:\"appKey\"`\n\t\tIntent       string `yaml:\"intent\"`\n\t\tChannelID    string `yaml:\"channelID\"`\n\t\tChannelName  string `yaml:\"channelName\"`\n\t} `yaml:\"geTui\"`\n\tFCM struct {\n\t\tFilePath string `yaml:\"filePath\"`\n\t\tAuthURL  string `yaml:\"authURL\"`\n\t} `yaml:\"fcm\"`\n\tJPush struct {\n\t\tAppKey       string `yaml:\"appKey\"`\n\t\tMasterSecret string `yaml:\"masterSecret\"`\n\t\tPushURL      string `yaml:\"pushURL\"`\n\t\tPushIntent   string `yaml:\"pushIntent\"`\n\t} `yaml:\"jpush\"`\n\tIOSPush struct {\n\t\tPushSound  string `yaml:\"pushSound\"`\n\t\tBadgeCount bool   `yaml:\"badgeCount\"`\n\t\tProduction bool   `yaml:\"production\"`\n\t} `yaml:\"iosPush\"`\n\tFullUserCache  bool           `yaml:\"fullUserCache\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype Auth struct {\n\tRPC         RPC        `yaml:\"rpc\"`\n\tPrometheus  Prometheus `yaml:\"prometheus\"`\n\tTokenPolicy struct {\n\t\tExpire int64 `yaml:\"expire\"`\n\t} `yaml:\"tokenPolicy\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype Conversation struct {\n\tRPC            RPC            `yaml:\"rpc\"`\n\tPrometheus     Prometheus     `yaml:\"prometheus\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype Friend struct {\n\tRPC            RPC            `yaml:\"rpc\"`\n\tPrometheus     Prometheus     `yaml:\"prometheus\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype Group struct {\n\tRPC                        RPC            `yaml:\"rpc\"`\n\tPrometheus                 Prometheus     `yaml:\"prometheus\"`\n\tEnableHistoryForNewMembers bool           `yaml:\"enableHistoryForNewMembers\"`\n\tRateLimiter                RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker             CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype Msg struct {\n\tRPC            RPC            `yaml:\"rpc\"`\n\tPrometheus     Prometheus     `yaml:\"prometheus\"`\n\tFriendVerify   bool           `yaml:\"friendVerify\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype Third struct {\n\tRPC        RPC        `yaml:\"rpc\"`\n\tPrometheus Prometheus `yaml:\"prometheus\"`\n\tObject     struct {\n\t\tEnable string `yaml:\"enable\"`\n\t\tCos    Cos    `yaml:\"cos\"`\n\t\tOss    Oss    `yaml:\"oss\"`\n\t\tKodo   Kodo   `yaml:\"kodo\"`\n\t\tAws    Aws    `yaml:\"aws\"`\n\t} `yaml:\"object\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\ntype Cos struct {\n\tBucketURL    string `yaml:\"bucketURL\"`\n\tSecretID     string `yaml:\"secretID\"`\n\tSecretKey    string `yaml:\"secretKey\"`\n\tSessionToken string `yaml:\"sessionToken\"`\n\tPublicRead   bool   `yaml:\"publicRead\"`\n}\ntype Oss struct {\n\tEndpoint        string `yaml:\"endpoint\"`\n\tBucket          string `yaml:\"bucket\"`\n\tBucketURL       string `yaml:\"bucketURL\"`\n\tAccessKeyID     string `yaml:\"accessKeyID\"`\n\tAccessKeySecret string `yaml:\"accessKeySecret\"`\n\tSessionToken    string `yaml:\"sessionToken\"`\n\tPublicRead      bool   `yaml:\"publicRead\"`\n}\n\ntype Kodo struct {\n\tEndpoint        string `yaml:\"endpoint\"`\n\tBucket          string `yaml:\"bucket\"`\n\tBucketURL       string `yaml:\"bucketURL\"`\n\tAccessKeyID     string `yaml:\"accessKeyID\"`\n\tAccessKeySecret string `yaml:\"accessKeySecret\"`\n\tSessionToken    string `yaml:\"sessionToken\"`\n\tPublicRead      bool   `yaml:\"publicRead\"`\n}\n\ntype Aws struct {\n\tRegion          string `yaml:\"region\"`\n\tBucket          string `yaml:\"bucket\"`\n\tAccessKeyID     string `yaml:\"accessKeyID\"`\n\tSecretAccessKey string `yaml:\"secretAccessKey\"`\n\tSessionToken    string `yaml:\"sessionToken\"`\n\tPublicRead      bool   `yaml:\"publicRead\"`\n}\n\ntype User struct {\n\tRPC            RPC            `yaml:\"rpc\"`\n\tPrometheus     Prometheus     `yaml:\"prometheus\"`\n\tRateLimiter    RateLimiter    `yaml:\"rateLimiter\"`\n\tCircuitBreaker CircuitBreaker `yaml:\"circuitBreaker\"`\n}\n\ntype RPC struct {\n\tRegisterIP   string `yaml:\"registerIP\"`\n\tListenIP     string `yaml:\"listenIP\"`\n\tAutoSetPorts bool   `yaml:\"autoSetPorts\"`\n\tPorts        []int  `yaml:\"ports\"`\n}\n\ntype Redis struct {\n\tDisable      bool     `yaml:\"-\"`\n\tAddress      []string `yaml:\"address\"`\n\tUsername     string   `yaml:\"username\"`\n\tPassword     string   `yaml:\"password\"`\n\tRedisMode    string   `yaml:\"redisMode\"`\n\tDB           int      `yaml:\"db\"`\n\tMaxRetry     int      `yaml:\"maxRetry\"`\n\tPoolSize     int      `yaml:\"poolSize\"`\n\tSentinelMode Sentinel `yaml:\"sentinelMode\"`\n}\n\ntype Sentinel struct {\n\tMasterName     string   `yaml:\"masterName\"`\n\tSentinelAddrs  []string `yaml:\"sentinelsAddrs\"`\n\tRouteByLatency bool     `yaml:\"routeByLatency\"`\n\tRouteRandomly  bool     `yaml:\"routeRandomly\"`\n}\n\ntype BeforeConfig struct {\n\tEnable         bool    `yaml:\"enable\"`\n\tTimeout        int     `yaml:\"timeout\"`\n\tFailedContinue bool    `yaml:\"failedContinue\"`\n\tDeniedTypes    []int32 `yaml:\"deniedTypes\"`\n}\n\ntype AfterConfig struct {\n\tEnable       bool     `yaml:\"enable\"`\n\tTimeout      int      `yaml:\"timeout\"`\n\tAttentionIds []string `yaml:\"attentionIds\"`\n\tDeniedTypes  []int32  `yaml:\"deniedTypes\"`\n}\n\ntype Share struct {\n\tSecret      string `yaml:\"secret\"`\n\tIMAdminUser struct {\n\t\tUserIDs   []string `yaml:\"userIDs\"`\n\t\tNicknames []string `yaml:\"nicknames\"`\n\t} `yaml:\"imAdminUser\"`\n\tMultiLogin     MultiLogin     `yaml:\"multiLogin\"`\n\tRPCMaxBodySize MaxRequestBody `yaml:\"rpcMaxBodySize\"`\n}\n\ntype MaxRequestBody struct {\n\tRequestMaxBodySize  int `yaml:\"requestMaxBodySize\"`\n\tResponseMaxBodySize int `yaml:\"responseMaxBodySize\"`\n}\n\ntype MultiLogin struct {\n\tPolicy       int `yaml:\"policy\"`\n\tMaxNumOneEnd int `yaml:\"maxNumOneEnd\"`\n}\n\ntype RpcService struct {\n\tUser           string `yaml:\"user\"`\n\tFriend         string `yaml:\"friend\"`\n\tMsg            string `yaml:\"msg\"`\n\tPush           string `yaml:\"push\"`\n\tMessageGateway string `yaml:\"messageGateway\"`\n\tGroup          string `yaml:\"group\"`\n\tAuth           string `yaml:\"auth\"`\n\tConversation   string `yaml:\"conversation\"`\n\tThird          string `yaml:\"third\"`\n}\n\nfunc (r *RpcService) GetServiceNames() []string {\n\treturn []string{\n\t\tr.User,\n\t\tr.Friend,\n\t\tr.Msg,\n\t\tr.Push,\n\t\tr.MessageGateway,\n\t\tr.Group,\n\t\tr.Auth,\n\t\tr.Conversation,\n\t\tr.Third,\n\t}\n}\n\n// FullConfig stores all configurations for before and after events\ntype Webhooks struct {\n\tURL                                 string       `yaml:\"url\"`\n\tBeforeSendSingleMsg                 BeforeConfig `yaml:\"beforeSendSingleMsg\"`\n\tBeforeUpdateUserInfoEx              BeforeConfig `yaml:\"beforeUpdateUserInfoEx\"`\n\tAfterUpdateUserInfoEx               AfterConfig  `yaml:\"afterUpdateUserInfoEx\"`\n\tAfterSendSingleMsg                  AfterConfig  `yaml:\"afterSendSingleMsg\"`\n\tBeforeSendGroupMsg                  BeforeConfig `yaml:\"beforeSendGroupMsg\"`\n\tBeforeMsgModify                     BeforeConfig `yaml:\"beforeMsgModify\"`\n\tAfterSendGroupMsg                   AfterConfig  `yaml:\"afterSendGroupMsg\"`\n\tAfterMsgSaveDB                      AfterConfig  `yaml:\"afterMsgSaveDB\"`\n\tAfterUserOnline                     AfterConfig  `yaml:\"afterUserOnline\"`\n\tAfterUserOffline                    AfterConfig  `yaml:\"afterUserOffline\"`\n\tAfterUserKickOff                    AfterConfig  `yaml:\"afterUserKickOff\"`\n\tBeforeOfflinePush                   BeforeConfig `yaml:\"beforeOfflinePush\"`\n\tBeforeOnlinePush                    BeforeConfig `yaml:\"beforeOnlinePush\"`\n\tBeforeGroupOnlinePush               BeforeConfig `yaml:\"beforeGroupOnlinePush\"`\n\tBeforeAddFriend                     BeforeConfig `yaml:\"beforeAddFriend\"`\n\tBeforeUpdateUserInfo                BeforeConfig `yaml:\"beforeUpdateUserInfo\"`\n\tAfterUpdateUserInfo                 AfterConfig  `yaml:\"afterUpdateUserInfo\"`\n\tBeforeCreateGroup                   BeforeConfig `yaml:\"beforeCreateGroup\"`\n\tAfterCreateGroup                    AfterConfig  `yaml:\"afterCreateGroup\"`\n\tBeforeMemberJoinGroup               BeforeConfig `yaml:\"beforeMemberJoinGroup\"`\n\tBeforeSetGroupMemberInfo            BeforeConfig `yaml:\"beforeSetGroupMemberInfo\"`\n\tAfterSetGroupMemberInfo             AfterConfig  `yaml:\"afterSetGroupMemberInfo\"`\n\tAfterQuitGroup                      AfterConfig  `yaml:\"afterQuitGroup\"`\n\tAfterKickGroupMember                AfterConfig  `yaml:\"afterKickGroupMember\"`\n\tAfterDismissGroup                   AfterConfig  `yaml:\"afterDismissGroup\"`\n\tBeforeApplyJoinGroup                BeforeConfig `yaml:\"beforeApplyJoinGroup\"`\n\tAfterGroupMsgRead                   AfterConfig  `yaml:\"afterGroupMsgRead\"`\n\tAfterSingleMsgRead                  AfterConfig  `yaml:\"afterSingleMsgRead\"`\n\tBeforeUserRegister                  BeforeConfig `yaml:\"beforeUserRegister\"`\n\tAfterUserRegister                   AfterConfig  `yaml:\"afterUserRegister\"`\n\tAfterTransferGroupOwner             AfterConfig  `yaml:\"afterTransferGroupOwner\"`\n\tBeforeSetFriendRemark               BeforeConfig `yaml:\"beforeSetFriendRemark\"`\n\tAfterSetFriendRemark                AfterConfig  `yaml:\"afterSetFriendRemark\"`\n\tAfterGroupMsgRevoke                 AfterConfig  `yaml:\"afterGroupMsgRevoke\"`\n\tAfterJoinGroup                      AfterConfig  `yaml:\"afterJoinGroup\"`\n\tBeforeInviteUserToGroup             BeforeConfig `yaml:\"beforeInviteUserToGroup\"`\n\tAfterSetGroupInfo                   AfterConfig  `yaml:\"afterSetGroupInfo\"`\n\tBeforeSetGroupInfo                  BeforeConfig `yaml:\"beforeSetGroupInfo\"`\n\tAfterSetGroupInfoEx                 AfterConfig  `yaml:\"afterSetGroupInfoEx\"`\n\tBeforeSetGroupInfoEx                BeforeConfig `yaml:\"beforeSetGroupInfoEx\"`\n\tAfterRevokeMsg                      AfterConfig  `yaml:\"afterRevokeMsg\"`\n\tBeforeAddBlack                      BeforeConfig `yaml:\"beforeAddBlack\"`\n\tAfterAddFriend                      AfterConfig  `yaml:\"afterAddFriend\"`\n\tBeforeAddFriendAgree                BeforeConfig `yaml:\"beforeAddFriendAgree\"`\n\tAfterAddFriendAgree                 AfterConfig  `yaml:\"afterAddFriendAgree\"`\n\tAfterDeleteFriend                   AfterConfig  `yaml:\"afterDeleteFriend\"`\n\tBeforeImportFriends                 BeforeConfig `yaml:\"beforeImportFriends\"`\n\tAfterImportFriends                  AfterConfig  `yaml:\"afterImportFriends\"`\n\tAfterRemoveBlack                    AfterConfig  `yaml:\"afterRemoveBlack\"`\n\tBeforeCreateSingleChatConversations BeforeConfig `yaml:\"beforeCreateSingleChatConversations\"`\n\tAfterCreateSingleChatConversations  AfterConfig  `yaml:\"afterCreateSingleChatConversations\"`\n\tBeforeCreateGroupChatConversations  BeforeConfig `yaml:\"beforeCreateGroupChatConversations\"`\n\tAfterCreateGroupChatConversations   AfterConfig  `yaml:\"afterCreateGroupChatConversations\"`\n}\n\ntype ZooKeeper struct {\n\tSchema   string   `yaml:\"schema\"`\n\tAddress  []string `yaml:\"address\"`\n\tUsername string   `yaml:\"username\"`\n\tPassword string   `yaml:\"password\"`\n}\n\ntype Discovery struct {\n\tEnable     string     `yaml:\"enable\"`\n\tEtcd       Etcd       `yaml:\"etcd\"`\n\tKubernetes Kubernetes `yaml:\"kubernetes\"`\n\tRpcService RpcService `yaml:\"rpcService\"`\n}\n\ntype Kubernetes struct {\n\tNamespace string `yaml:\"namespace\"`\n}\n\ntype Etcd struct {\n\tRootDirectory string   `yaml:\"rootDirectory\"`\n\tAddress       []string `yaml:\"address\"`\n\tUsername      string   `yaml:\"username\"`\n\tPassword      string   `yaml:\"password\"`\n}\n\nfunc (m *Mongo) Build() *mongoutil.Config {\n\treturn &mongoutil.Config{\n\t\tUri:         m.URI,\n\t\tAddress:     m.Address,\n\t\tDatabase:    m.Database,\n\t\tUsername:    m.Username,\n\t\tPassword:    m.Password,\n\t\tAuthSource:  m.AuthSource,\n\t\tMaxPoolSize: m.MaxPoolSize,\n\t\tMaxRetry:    m.MaxRetry,\n\t\tMongoMode:   m.MongoMode,\n\t\tReplicaSet: &mongoutil.ReplicaSetConfig{\n\t\t\tName:         m.ReplicaSet.Name,\n\t\t\tHosts:        m.ReplicaSet.Hosts,\n\t\t\tReadConcern:  m.ReplicaSet.ReadConcern,\n\t\t\tMaxStaleness: m.ReplicaSet.MaxStaleness,\n\t\t},\n\t\tReadPreference: &mongoutil.ReadPrefConfig{\n\t\t\tMode:         m.ReadPreference.Mode,\n\t\t\tTagSets:      m.ReadPreference.TagSets,\n\t\t\tMaxStaleness: m.ReadPreference.MaxStaleness,\n\t\t},\n\t\tWriteConcern: &mongoutil.WriteConcernConfig{\n\t\t\tW:        m.WriteConcern.W,\n\t\t\tJ:        m.WriteConcern.J,\n\t\t\tWTimeout: m.WriteConcern.WTimeout,\n\t\t},\n\t}\n}\n\nfunc (r *Redis) Build() *redisutil.Config {\n\treturn &redisutil.Config{\n\t\tRedisMode: r.RedisMode,\n\t\tAddress:   r.Address,\n\t\tUsername:  r.Username,\n\t\tPassword:  r.Password,\n\t\tDB:        r.DB,\n\t\tMaxRetry:  r.MaxRetry,\n\t\tPoolSize:  r.PoolSize,\n\t\tSentinel: &redisutil.Sentinel{\n\t\t\tMasterName:     r.SentinelMode.MasterName,\n\t\t\tSentinelAddrs:  r.SentinelMode.SentinelAddrs,\n\t\t\tRouteByLatency: r.SentinelMode.RouteByLatency,\n\t\t\tRouteRandomly:  r.SentinelMode.RouteRandomly,\n\t\t},\n\t}\n}\n\nfunc (k *Kafka) Build() *kafka.Config {\n\treturn &kafka.Config{\n\t\tUsername:     k.Username,\n\t\tPassword:     k.Password,\n\t\tProducerAck:  k.ProducerAck,\n\t\tCompressType: k.CompressType,\n\t\tAddr:         k.Address,\n\t\tTLS: kafka.TLSConfig{\n\t\t\tEnableTLS:          k.Tls.EnableTLS,\n\t\t\tCACrt:              k.Tls.CACrt,\n\t\t\tClientCrt:          k.Tls.ClientCrt,\n\t\t\tClientKey:          k.Tls.ClientKey,\n\t\t\tClientKeyPwd:       k.Tls.ClientKeyPwd,\n\t\t\tInsecureSkipVerify: k.Tls.InsecureSkipVerify,\n\t\t},\n\t}\n}\n\nfunc (m *Minio) Build() *minio.Config {\n\tformatEndpoint := func(address string) string {\n\t\tif strings.HasPrefix(address, \"http://\") || strings.HasPrefix(address, \"https://\") {\n\t\t\treturn address\n\t\t}\n\t\treturn \"http://\" + address\n\t}\n\treturn &minio.Config{\n\t\tBucket:          m.Bucket,\n\t\tAccessKeyID:     m.AccessKeyID,\n\t\tSecretAccessKey: m.SecretAccessKey,\n\t\tSessionToken:    m.SessionToken,\n\t\tPublicRead:      m.PublicRead,\n\t\tEndpoint:        formatEndpoint(m.InternalAddress),\n\t\tSignEndpoint:    formatEndpoint(m.ExternalAddress),\n\t}\n}\n\nfunc (c *Cos) Build() *cos.Config {\n\treturn &cos.Config{\n\t\tBucketURL:    c.BucketURL,\n\t\tSecretID:     c.SecretID,\n\t\tSecretKey:    c.SecretKey,\n\t\tSessionToken: c.SessionToken,\n\t\tPublicRead:   c.PublicRead,\n\t}\n}\n\nfunc (o *Oss) Build() *oss.Config {\n\treturn &oss.Config{\n\t\tEndpoint:        o.Endpoint,\n\t\tBucket:          o.Bucket,\n\t\tBucketURL:       o.BucketURL,\n\t\tAccessKeyID:     o.AccessKeyID,\n\t\tAccessKeySecret: o.AccessKeySecret,\n\t\tSessionToken:    o.SessionToken,\n\t\tPublicRead:      o.PublicRead,\n\t}\n}\n\nfunc (o *Kodo) Build() *kodo.Config {\n\treturn &kodo.Config{\n\t\tEndpoint:        o.Endpoint,\n\t\tBucket:          o.Bucket,\n\t\tBucketURL:       o.BucketURL,\n\t\tAccessKeyID:     o.AccessKeyID,\n\t\tAccessKeySecret: o.AccessKeySecret,\n\t\tSessionToken:    o.SessionToken,\n\t\tPublicRead:      o.PublicRead,\n\t}\n}\n\nfunc (o *Aws) Build() *aws.Config {\n\treturn &aws.Config{\n\t\tRegion:          o.Region,\n\t\tBucket:          o.Bucket,\n\t\tAccessKeyID:     o.AccessKeyID,\n\t\tSecretAccessKey: o.SecretAccessKey,\n\t\tSessionToken:    o.SessionToken,\n\t}\n}\n\nfunc (l *CacheConfig) Failed() time.Duration {\n\treturn time.Second * time.Duration(l.FailedExpire)\n}\n\nfunc (l *CacheConfig) Success() time.Duration {\n\treturn time.Second * time.Duration(l.SuccessExpire)\n}\n\nfunc (l *CacheConfig) Enable() bool {\n\treturn l.Topic != \"\" && l.SlotNum > 0 && l.SlotSize > 0\n}\n\nfunc InitNotification(notification *Notification) {\n\tnotification.GroupCreated.UnreadCount = false\n\tnotification.GroupCreated.ReliabilityLevel = 1\n\tnotification.GroupInfoSet.UnreadCount = false\n\tnotification.GroupInfoSet.ReliabilityLevel = 1\n\tnotification.JoinGroupApplication.UnreadCount = false\n\tnotification.JoinGroupApplication.ReliabilityLevel = 1\n\tnotification.MemberQuit.UnreadCount = false\n\tnotification.MemberQuit.ReliabilityLevel = 1\n\tnotification.GroupApplicationAccepted.UnreadCount = false\n\tnotification.GroupApplicationAccepted.ReliabilityLevel = 1\n\tnotification.GroupApplicationRejected.UnreadCount = false\n\tnotification.GroupApplicationRejected.ReliabilityLevel = 1\n\tnotification.GroupOwnerTransferred.UnreadCount = false\n\tnotification.GroupOwnerTransferred.ReliabilityLevel = 1\n\tnotification.MemberKicked.UnreadCount = false\n\tnotification.MemberKicked.ReliabilityLevel = 1\n\tnotification.MemberInvited.UnreadCount = false\n\tnotification.MemberInvited.ReliabilityLevel = 1\n\tnotification.MemberEnter.UnreadCount = false\n\tnotification.MemberEnter.ReliabilityLevel = 1\n\tnotification.GroupDismissed.UnreadCount = false\n\tnotification.GroupDismissed.ReliabilityLevel = 1\n\tnotification.GroupMuted.UnreadCount = false\n\tnotification.GroupMuted.ReliabilityLevel = 1\n\tnotification.GroupCancelMuted.UnreadCount = false\n\tnotification.GroupCancelMuted.ReliabilityLevel = 1\n\tnotification.GroupMemberMuted.UnreadCount = false\n\tnotification.GroupMemberMuted.ReliabilityLevel = 1\n\tnotification.GroupMemberCancelMuted.UnreadCount = false\n\tnotification.GroupMemberCancelMuted.ReliabilityLevel = 1\n\tnotification.GroupMemberInfoSet.UnreadCount = false\n\tnotification.GroupMemberInfoSet.ReliabilityLevel = 1\n\tnotification.GroupMemberSetToAdmin.UnreadCount = false\n\tnotification.GroupMemberSetToAdmin.ReliabilityLevel = 1\n\tnotification.GroupMemberSetToOrdinary.UnreadCount = false\n\tnotification.GroupMemberSetToOrdinary.ReliabilityLevel = 1\n\tnotification.GroupInfoSetAnnouncement.UnreadCount = false\n\tnotification.GroupInfoSetAnnouncement.ReliabilityLevel = 1\n\tnotification.GroupInfoSetName.UnreadCount = false\n\tnotification.GroupInfoSetName.ReliabilityLevel = 1\n\tnotification.FriendApplicationAdded.UnreadCount = false\n\tnotification.FriendApplicationAdded.ReliabilityLevel = 1\n\tnotification.FriendApplicationApproved.UnreadCount = false\n\tnotification.FriendApplicationApproved.ReliabilityLevel = 1\n\tnotification.FriendApplicationRejected.UnreadCount = false\n\tnotification.FriendApplicationRejected.ReliabilityLevel = 1\n\tnotification.FriendAdded.UnreadCount = false\n\tnotification.FriendAdded.ReliabilityLevel = 1\n\tnotification.FriendDeleted.UnreadCount = false\n\tnotification.FriendDeleted.ReliabilityLevel = 1\n\tnotification.FriendRemarkSet.UnreadCount = false\n\tnotification.FriendRemarkSet.ReliabilityLevel = 1\n\tnotification.BlackAdded.UnreadCount = false\n\tnotification.BlackAdded.ReliabilityLevel = 1\n\tnotification.BlackDeleted.UnreadCount = false\n\tnotification.BlackDeleted.ReliabilityLevel = 1\n\tnotification.FriendInfoUpdated.UnreadCount = false\n\tnotification.FriendInfoUpdated.ReliabilityLevel = 1\n\tnotification.UserInfoUpdated.UnreadCount = false\n\tnotification.UserInfoUpdated.ReliabilityLevel = 1\n\tnotification.UserStatusChanged.UnreadCount = false\n\tnotification.UserStatusChanged.ReliabilityLevel = 1\n\tnotification.ConversationChanged.UnreadCount = false\n\tnotification.ConversationChanged.ReliabilityLevel = 1\n\tnotification.ConversationSetPrivate.UnreadCount = false\n\tnotification.ConversationSetPrivate.ReliabilityLevel = 1\n}\n\ntype AllConfig struct {\n\tDiscovery    Discovery\n\tKafka        Kafka\n\tLocalCache   LocalCache\n\tLog          Log\n\tMinio        Minio\n\tMongo        Mongo\n\tNotification Notification\n\tAPI          API\n\tCronTask     CronTask\n\tMsgGateway   MsgGateway\n\tMsgTransfer  MsgTransfer\n\tPush         Push\n\tAuth         Auth\n\tConversation Conversation\n\tFriend       Friend\n\tGroup        Group\n\tMsg          Msg\n\tThird        Third\n\tUser         User\n\tRedis        Redis\n\tShare        Share\n\tWebhooks     Webhooks\n}\n\nfunc (a *AllConfig) Name2Config(name string) any {\n\tswitch name {\n\tcase a.Discovery.GetConfigFileName():\n\t\treturn a.Discovery\n\tcase a.Kafka.GetConfigFileName():\n\t\treturn a.Kafka\n\tcase a.LocalCache.GetConfigFileName():\n\t\treturn a.LocalCache\n\tcase a.Log.GetConfigFileName():\n\t\treturn a.Log\n\tcase a.Minio.GetConfigFileName():\n\t\treturn a.Minio\n\tcase a.Mongo.GetConfigFileName():\n\t\treturn a.Mongo\n\tcase a.Notification.GetConfigFileName():\n\t\treturn a.Notification\n\tcase a.API.GetConfigFileName():\n\t\treturn a.API\n\tcase a.CronTask.GetConfigFileName():\n\t\treturn a.CronTask\n\tcase a.MsgGateway.GetConfigFileName():\n\t\treturn a.MsgGateway\n\tcase a.MsgTransfer.GetConfigFileName():\n\t\treturn a.MsgTransfer\n\tcase a.Push.GetConfigFileName():\n\t\treturn a.Push\n\tcase a.Auth.GetConfigFileName():\n\t\treturn a.Auth\n\tcase a.Conversation.GetConfigFileName():\n\t\treturn a.Conversation\n\tcase a.Friend.GetConfigFileName():\n\t\treturn a.Friend\n\tcase a.Group.GetConfigFileName():\n\t\treturn a.Group\n\tcase a.Msg.GetConfigFileName():\n\t\treturn a.Msg\n\tcase a.Third.GetConfigFileName():\n\t\treturn a.Third\n\tcase a.User.GetConfigFileName():\n\t\treturn a.User\n\tcase a.Redis.GetConfigFileName():\n\t\treturn a.Redis\n\tcase a.Share.GetConfigFileName():\n\t\treturn a.Share\n\tcase a.Webhooks.GetConfigFileName():\n\t\treturn a.Webhooks\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (a *AllConfig) GetConfigNames() []string {\n\treturn []string{\n\t\ta.Discovery.GetConfigFileName(),\n\t\ta.Kafka.GetConfigFileName(),\n\t\ta.LocalCache.GetConfigFileName(),\n\t\ta.Log.GetConfigFileName(),\n\t\ta.Minio.GetConfigFileName(),\n\t\ta.Mongo.GetConfigFileName(),\n\t\ta.Notification.GetConfigFileName(),\n\t\ta.API.GetConfigFileName(),\n\t\ta.CronTask.GetConfigFileName(),\n\t\ta.MsgGateway.GetConfigFileName(),\n\t\ta.MsgTransfer.GetConfigFileName(),\n\t\ta.Push.GetConfigFileName(),\n\t\ta.Auth.GetConfigFileName(),\n\t\ta.Conversation.GetConfigFileName(),\n\t\ta.Friend.GetConfigFileName(),\n\t\ta.Group.GetConfigFileName(),\n\t\ta.Msg.GetConfigFileName(),\n\t\ta.Third.GetConfigFileName(),\n\t\ta.User.GetConfigFileName(),\n\t\ta.Redis.GetConfigFileName(),\n\t\ta.Share.GetConfigFileName(),\n\t\ta.Webhooks.GetConfigFileName(),\n\t}\n}\n\nconst (\n\tFileName                         = \"config.yaml\"\n\tDiscoveryConfigFilename          = \"discovery.yml\"\n\tKafkaConfigFileName              = \"kafka.yml\"\n\tLocalCacheConfigFileName         = \"local-cache.yml\"\n\tLogConfigFileName                = \"log.yml\"\n\tMinioConfigFileName              = \"minio.yml\"\n\tMongodbConfigFileName            = \"mongodb.yml\"\n\tNotificationFileName             = \"notification.yml\"\n\tOpenIMAPICfgFileName             = \"openim-api.yml\"\n\tOpenIMCronTaskCfgFileName        = \"openim-crontask.yml\"\n\tOpenIMMsgGatewayCfgFileName      = \"openim-msggateway.yml\"\n\tOpenIMMsgTransferCfgFileName     = \"openim-msgtransfer.yml\"\n\tOpenIMPushCfgFileName            = \"openim-push.yml\"\n\tOpenIMRPCAuthCfgFileName         = \"openim-rpc-auth.yml\"\n\tOpenIMRPCConversationCfgFileName = \"openim-rpc-conversation.yml\"\n\tOpenIMRPCFriendCfgFileName       = \"openim-rpc-friend.yml\"\n\tOpenIMRPCGroupCfgFileName        = \"openim-rpc-group.yml\"\n\tOpenIMRPCMsgCfgFileName          = \"openim-rpc-msg.yml\"\n\tOpenIMRPCThirdCfgFileName        = \"openim-rpc-third.yml\"\n\tOpenIMRPCUserCfgFileName         = \"openim-rpc-user.yml\"\n\tRedisConfigFileName              = \"redis.yml\"\n\tShareFileName                    = \"share.yml\"\n\tWebhooksConfigFileName           = \"webhooks.yml\"\n)\n\nfunc (d *Discovery) GetConfigFileName() string {\n\treturn DiscoveryConfigFilename\n}\n\nfunc (k *Kafka) GetConfigFileName() string {\n\treturn KafkaConfigFileName\n}\n\nfunc (lc *LocalCache) GetConfigFileName() string {\n\treturn LocalCacheConfigFileName\n}\n\nfunc (l *Log) GetConfigFileName() string {\n\treturn LogConfigFileName\n}\n\nfunc (m *Minio) GetConfigFileName() string {\n\treturn MinioConfigFileName\n}\n\nfunc (m *Mongo) GetConfigFileName() string {\n\treturn MongodbConfigFileName\n}\n\nfunc (n *Notification) GetConfigFileName() string {\n\treturn NotificationFileName\n}\n\nfunc (a *API) GetConfigFileName() string {\n\treturn OpenIMAPICfgFileName\n}\n\nfunc (ct *CronTask) GetConfigFileName() string {\n\treturn OpenIMCronTaskCfgFileName\n}\n\nfunc (mg *MsgGateway) GetConfigFileName() string {\n\treturn OpenIMMsgGatewayCfgFileName\n}\n\nfunc (mt *MsgTransfer) GetConfigFileName() string {\n\treturn OpenIMMsgTransferCfgFileName\n}\n\nfunc (p *Push) GetConfigFileName() string {\n\treturn OpenIMPushCfgFileName\n}\n\nfunc (a *Auth) GetConfigFileName() string {\n\treturn OpenIMRPCAuthCfgFileName\n}\n\nfunc (c *Conversation) GetConfigFileName() string {\n\treturn OpenIMRPCConversationCfgFileName\n}\n\nfunc (f *Friend) GetConfigFileName() string {\n\treturn OpenIMRPCFriendCfgFileName\n}\n\nfunc (g *Group) GetConfigFileName() string {\n\treturn OpenIMRPCGroupCfgFileName\n}\n\nfunc (m *Msg) GetConfigFileName() string {\n\treturn OpenIMRPCMsgCfgFileName\n}\n\nfunc (t *Third) GetConfigFileName() string {\n\treturn OpenIMRPCThirdCfgFileName\n}\n\nfunc (u *User) GetConfigFileName() string {\n\treturn OpenIMRPCUserCfgFileName\n}\n\nfunc (r *Redis) GetConfigFileName() string {\n\treturn RedisConfigFileName\n}\n\nfunc (s *Share) GetConfigFileName() string {\n\treturn ShareFileName\n}\n\nfunc (w *Webhooks) GetConfigFileName() string {\n\treturn WebhooksConfigFileName\n}\n"
  },
  {
    "path": "pkg/common/config/constant.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage config\n\nimport \"github.com/openimsdk/tools/utils/runtimeenv\"\n\nconst ConfKey = \"conf\"\n\nconst (\n\tMountConfigFilePath = \"CONFIG_PATH\"\n\tDeploymentType      = \"DEPLOYMENT_TYPE\"\n\tKUBERNETES          = runtimeenv.Kubernetes\n\tETCD                = \"etcd\"\n\t//Standalone          = \"standalone\"\n)\n\nconst (\n\t// DefaultDirPerm is used for creating general directories, allowing the owner to read, write, and execute,\n\t// while the group and others can only read and execute.\n\tDefaultDirPerm = 0755\n\n\t// PrivateFilePerm is used for sensitive files, allowing only the owner to read and write.\n\tPrivateFilePerm = 0600\n\n\t// ExecFilePerm is used for executable files, allowing the owner to read, write, and execute,\n\t// while the group and others can only read.\n\tExecFilePerm = 0754\n\n\t// SharedDirPerm is used for shared directories, allowing the owner and group to read, write, and execute,\n\t// with no permissions for others.\n\tSharedDirPerm = 0770\n\n\t// ReadOnlyDirPerm is used for read-only directories, allowing the owner, group, and others to only read.\n\tReadOnlyDirPerm = 0555\n)\n"
  },
  {
    "path": "pkg/common/config/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage config // import \"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n"
  },
  {
    "path": "pkg/common/config/env.go",
    "content": "package config\n\nimport \"strings\"\n\nvar EnvPrefixMap map[string]string\n\nfunc init() {\n\tEnvPrefixMap = make(map[string]string)\n\tfileNames := []string{\n\t\tFileName, NotificationFileName, ShareFileName, WebhooksConfigFileName,\n\t\tKafkaConfigFileName, RedisConfigFileName,\n\t\tMongodbConfigFileName, MinioConfigFileName, LogConfigFileName,\n\t\tOpenIMAPICfgFileName, OpenIMCronTaskCfgFileName, OpenIMMsgGatewayCfgFileName,\n\t\tOpenIMMsgTransferCfgFileName, OpenIMPushCfgFileName, OpenIMRPCAuthCfgFileName,\n\t\tOpenIMRPCConversationCfgFileName, OpenIMRPCFriendCfgFileName, OpenIMRPCGroupCfgFileName,\n\t\tOpenIMRPCMsgCfgFileName, OpenIMRPCThirdCfgFileName, OpenIMRPCUserCfgFileName, DiscoveryConfigFilename,\n\t}\n\n\tfor _, fileName := range fileNames {\n\t\tenvKey := strings.TrimSuffix(strings.TrimSuffix(fileName, \".yml\"), \".yaml\")\n\t\tenvKey = \"IMENV_\" + envKey\n\t\tenvKey = strings.ToUpper(strings.ReplaceAll(envKey, \"-\", \"_\"))\n\t\tEnvPrefixMap[fileName] = envKey\n\t}\n}\n\nconst (\n\tFlagConf          = \"config_folder_path\"\n\tFlagTransferIndex = \"index\"\n)\n"
  },
  {
    "path": "pkg/common/config/global.go",
    "content": "package config\n\nvar standalone bool\n\nfunc SetStandalone() {\n\tstandalone = true\n}\n\nfunc Standalone() bool {\n\treturn standalone\n}\n"
  },
  {
    "path": "pkg/common/config/load_config.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/mitchellh/mapstructure\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\t\"github.com/spf13/viper\"\n)\n\nfunc Load(configDirectory string, configFileName string, envPrefix string, config any) error {\n\tif runtimeenv.RuntimeEnvironment() == KUBERNETES {\n\t\tmountPath := os.Getenv(MountConfigFilePath)\n\t\tif mountPath == \"\" {\n\t\t\treturn errs.ErrArgs.WrapMsg(MountConfigFilePath + \" env is empty\")\n\t\t}\n\n\t\treturn loadConfig(filepath.Join(mountPath, configFileName), envPrefix, config)\n\t}\n\n\treturn loadConfig(filepath.Join(configDirectory, configFileName), envPrefix, config)\n}\n\nfunc loadConfig(path string, envPrefix string, config any) error {\n\tv := viper.New()\n\tv.SetConfigFile(path)\n\tv.SetEnvPrefix(envPrefix)\n\tv.AutomaticEnv()\n\tv.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))\n\n\tif err := v.ReadInConfig(); err != nil {\n\t\treturn errs.WrapMsg(err, \"failed to read config file\", \"path\", path, \"envPrefix\", envPrefix)\n\t}\n\n\tif err := v.Unmarshal(config, func(config *mapstructure.DecoderConfig) {\n\t\tconfig.TagName = StructTagName\n\t}); err != nil {\n\t\treturn errs.WrapMsg(err, \"failed to unmarshal config\", \"path\", path, \"envPrefix\", envPrefix)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/config/load_config_test.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLoadLogConfig(t *testing.T) {\n\tvar log Log\n\tos.Setenv(\"IMENV_LOG_REMAINLOGLEVEL\", \"5\")\n\terr := Load(\"../../../config/\", \"log.yml\", \"IMENV_LOG\", &log)\n\tassert.Nil(t, err)\n\tt.Log(log.RemainLogLevel)\n\t// assert.Equal(t, \"../../../../logs/\", log.StorageLocation)\n}\n\nfunc TestLoadMongoConfig(t *testing.T) {\n\tvar mongo Mongo\n\t// os.Setenv(\"DEPLOYMENT_TYPE\", \"kubernetes\")\n\tos.Setenv(\"IMENV_MONGODB_PASSWORD\", \"openIM1231231\")\n\t// os.Setenv(\"IMENV_MONGODB_URI\", \"openIM123\")\n\t// os.Setenv(\"IMENV_MONGODB_USERNAME\", \"openIM123\")\n\terr := Load(\"../../../config/\", \"mongodb.yml\", \"IMENV_MONGODB\", &mongo)\n\t// err := LoadApiConfig(\"../../../config/mongodb.yml\", \"IMENV_MONGODB\", &mongo)\n\n\tassert.Nil(t, err)\n\tt.Log(mongo.Password)\n\t// assert.Equal(t, \"openIM123\", mongo.Password)\n\tt.Log(os.Getenv(\"IMENV_MONGODB_PASSWORD\"))\n\tt.Log(mongo)\n\t// //export IMENV_OPENIM_RPC_USER_RPC_LISTENIP=\"0.0.0.0\"\n\t// assert.Equal(t, \"0.0.0.0\", user.RPC.ListenIP)\n\t// //export IMENV_OPENIM_RPC_USER_RPC_PORTS=\"10110,10111,10112\"\n\t// assert.Equal(t, []int{10110, 10111, 10112}, user.RPC.Ports)\n}\n\nfunc TestLoadMinioConfig(t *testing.T) {\n\tvar storageConfig Minio\n\terr := Load(\"../../../config/minio.yml\", \"IMENV_MINIO\", \"\", &storageConfig)\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"openim\", storageConfig.Bucket)\n}\n\nfunc TestLoadWebhooksConfig(t *testing.T) {\n\tvar webhooks Webhooks\n\terr := Load(\"../../../config/webhooks.yml\", \"IMENV_WEBHOOKS\", \"\", &webhooks)\n\tassert.Nil(t, err)\n\tassert.Equal(t, 5, webhooks.BeforeAddBlack.Timeout)\n\n}\n\nfunc TestLoadOpenIMRpcUserConfig(t *testing.T) {\n\tvar user User\n\terr := Load(\"../../../config/openim-rpc-user.yml\", \"IMENV_OPENIM_RPC_USER\", \"\", &user)\n\tassert.Nil(t, err)\n\t//export IMENV_OPENIM_RPC_USER_RPC_LISTENIP=\"0.0.0.0\"\n\tassert.Equal(t, \"0.0.0.0\", user.RPC.ListenIP)\n\t//export IMENV_OPENIM_RPC_USER_RPC_PORTS=\"10110,10111,10112\"\n\tassert.Equal(t, []int{10110, 10111, 10112}, user.RPC.Ports)\n}\n\nfunc TestLoadNotificationConfig(t *testing.T) {\n\tvar noti Notification\n\terr := Load(\"../../../config/notification.yml\", \"IMENV_NOTIFICATION\", \"\", &noti)\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"Your friend's profile has been changed\", noti.FriendRemarkSet.OfflinePush.Title)\n}\n\nfunc TestLoadOpenIMThirdConfig(t *testing.T) {\n\tvar third Third\n\terr := Load(\"../../../config/openim-rpc-third.yml\", \"IMENV_OPENIM_RPC_THIRD\", \"\", &third)\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"enabled\", third.Object.Enable)\n\tassert.Equal(t, \"https://oss-cn-chengdu.aliyuncs.com\", third.Object.Oss.Endpoint)\n\tassert.Equal(t, \"my_bucket_name\", third.Object.Oss.Bucket)\n\tassert.Equal(t, \"https://my_bucket_name.oss-cn-chengdu.aliyuncs.com\", third.Object.Oss.BucketURL)\n\tassert.Equal(t, \"AKID1234567890\", third.Object.Oss.AccessKeyID)\n\tassert.Equal(t, \"abc123xyz789\", third.Object.Oss.AccessKeySecret)\n\tassert.Equal(t, \"session_token_value\", third.Object.Oss.SessionToken) // Uncomment if session token is needed\n\tassert.Equal(t, true, third.Object.Oss.PublicRead)\n\n\t// Environment: IMENV_OPENIM_RPC_THIRD_OBJECT_ENABLE=enabled;IMENV_OPENIM_RPC_THIRD_OBJECT_OSS_ENDPOINT=https://oss-cn-chengdu.aliyuncs.com;IMENV_OPENIM_RPC_THIRD_OBJECT_OSS_BUCKET=my_bucket_name;IMENV_OPENIM_RPC_THIRD_OBJECT_OSS_BUCKETURL=https://my_bucket_name.oss-cn-chengdu.aliyuncs.com;IMENV_OPENIM_RPC_THIRD_OBJECT_OSS_ACCESSKEYID=AKID1234567890;IMENV_OPENIM_RPC_THIRD_OBJECT_OSS_ACCESSKEYSECRET=abc123xyz789;IMENV_OPENIM_RPC_THIRD_OBJECT_OSS_SESSIONTOKEN=session_token_value;IMENV_OPENIM_RPC_THIRD_OBJECT_OSS_PUBLICREAD=true\n}\n\nfunc TestTransferConfig(t *testing.T) {\n\tvar tran MsgTransfer\n\terr := Load(\"../../../config/openim-msgtransfer.yml\", \"IMENV_OPENIM-MSGTRANSFER\", \"\", &tran)\n\tassert.Nil(t, err)\n\tassert.Equal(t, true, tran.Prometheus.Enable)\n\tassert.Equal(t, true, tran.Prometheus.AutoSetPorts)\n}\n"
  },
  {
    "path": "pkg/common/config/parse.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"gopkg.in/yaml.v3\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/field\"\n)\n\nconst (\n\tDefaultFolderPath = \"../config/\"\n)\n\n// return absolude path join ../config/, this is k8s container config path.\nfunc GetDefaultConfigPath() (string, error) {\n\texecutablePath, err := os.Executable()\n\tif err != nil {\n\t\treturn \"\", errs.WrapMsg(err, \"failed to get executable path\")\n\t}\n\n\tconfigPath, err := field.OutDir(filepath.Join(filepath.Dir(executablePath), \"../config/\"))\n\tif err != nil {\n\t\treturn \"\", errs.WrapMsg(err, \"failed to get output directory\", \"outDir\", filepath.Join(filepath.Dir(executablePath), \"../config/\"))\n\t}\n\treturn configPath, nil\n}\n\n// getProjectRoot returns the absolute path of the project root directory.\nfunc GetProjectRoot() (string, error) {\n\texecutablePath, err := os.Executable()\n\tif err != nil {\n\t\treturn \"\", errs.Wrap(err)\n\t}\n\tprojectRoot, err := field.OutDir(filepath.Join(filepath.Dir(executablePath), \"../../../../..\"))\n\tif err != nil {\n\t\treturn \"\", errs.Wrap(err)\n\t}\n\treturn projectRoot, nil\n}\n\nfunc GetOptionsByNotification(cfg NotificationConfig, sendMessage *bool) msgprocessor.Options {\n\topts := msgprocessor.NewOptions()\n\n\tif sendMessage != nil {\n\t\tcfg.IsSendMsg = *sendMessage\n\t}\n\tif cfg.IsSendMsg {\n\t\topts = msgprocessor.WithOptions(opts, msgprocessor.WithUnreadCount(true))\n\t}\n\tif cfg.OfflinePush.Enable {\n\t\topts = msgprocessor.WithOptions(opts, msgprocessor.WithOfflinePush(true))\n\t}\n\tswitch cfg.ReliabilityLevel {\n\tcase constant.UnreliableNotification:\n\tcase constant.ReliableNotificationNoMsg:\n\t\topts = msgprocessor.WithOptions(opts, msgprocessor.WithHistory(true), msgprocessor.WithPersistent())\n\t}\n\topts = msgprocessor.WithOptions(opts, msgprocessor.WithSendMsg(cfg.IsSendMsg))\n\n\treturn opts\n}\n\n// initConfig loads configuration from a specified path into the provided config structure.\n// If the specified config file does not exist, it attempts to load from the project's default \"config\" directory.\n// It logs informative messages regarding the configuration path being used.\nfunc initConfig(config any, configName, configFolderPath string) error {\n\tconfigFolderPath = filepath.Join(configFolderPath, configName)\n\t_, err := os.Stat(configFolderPath)\n\tif err != nil {\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn errs.WrapMsg(err, \"stat config path error\", \"config Folder Path\", configFolderPath)\n\t\t}\n\t\tpath, err := GetProjectRoot()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfigFolderPath = filepath.Join(path, \"config\", configName)\n\t}\n\tdata, err := os.ReadFile(configFolderPath)\n\tif err != nil {\n\t\treturn errs.WrapMsg(err, \"read file error\", \"config Folder Path\", configFolderPath)\n\t}\n\tif err = yaml.Unmarshal(data, config); err != nil {\n\t\treturn errs.WrapMsg(err, \"unmarshal yaml error\", \"config Folder Path\", configFolderPath)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/convert/auth.go",
    "content": "package convert\n\nfunc TokenMapDB2Pb(tokenMapDB map[string]int) map[string]int32 {\n    if tokenMapDB == nil {\n        return nil\n    }\n    \n    tokenMapPB := make(map[string]int32, len(tokenMapDB))\n    for k, v := range tokenMapDB {\n        tokenMapPB[k] = int32(v)\n    }\n    return tokenMapPB\n}\n\nfunc TokenMapPb2DB(tokenMapPB map[string]int32) map[string]int {\n    if tokenMapPB == nil {\n        return nil\n    }\n    \n    tokenMapDB := make(map[string]int, len(tokenMapPB))\n    for k, v := range tokenMapPB {\n        tokenMapDB[k] = int(v)\n    }\n    return tokenMapDB\n}"
  },
  {
    "path": "pkg/common/convert/black.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage convert\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/protocol/sdkws\"\n\tsdk \"github.com/openimsdk/protocol/sdkws\"\n)\n\nfunc BlackDB2Pb(ctx context.Context, blackDBs []*model.Black, f func(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error)) (blackPbs []*sdk.BlackInfo, err error) {\n\tif len(blackDBs) == 0 {\n\t\treturn nil, nil\n\t}\n\tvar userIDs []string\n\tfor _, blackDB := range blackDBs {\n\t\tuserIDs = append(userIDs, blackDB.BlockUserID)\n\t}\n\tuserInfos, err := f(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, blackDB := range blackDBs {\n\t\tblackPb := &sdk.BlackInfo{\n\t\t\tOwnerUserID:    blackDB.OwnerUserID,\n\t\t\tCreateTime:     blackDB.CreateTime.Unix(),\n\t\t\tAddSource:      blackDB.AddSource,\n\t\t\tEx:             blackDB.Ex,\n\t\t\tOperatorUserID: blackDB.OperatorUserID,\n\t\t\tBlackUserInfo: &sdkws.PublicUserInfo{\n\t\t\t\tUserID:   userInfos[blackDB.BlockUserID].UserID,\n\t\t\t\tNickname: userInfos[blackDB.BlockUserID].Nickname,\n\t\t\t\tFaceURL:  userInfos[blackDB.BlockUserID].FaceURL,\n\t\t\t\tEx:       userInfos[blackDB.BlockUserID].Ex,\n\t\t\t},\n\t\t}\n\t\tblackPbs = append(blackPbs, blackPb)\n\t}\n\treturn blackPbs, nil\n}\n"
  },
  {
    "path": "pkg/common/convert/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage convert\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc ConversationDB2Pb(conversationDB *model.Conversation) *conversation.Conversation {\n\tconversationPB := &conversation.Conversation{}\n\tconversationPB.LatestMsgDestructTime = conversationDB.LatestMsgDestructTime.UnixMilli()\n\tif err := datautil.CopyStructFields(conversationPB, conversationDB); err != nil {\n\t\treturn nil\n\t}\n\treturn conversationPB\n}\n\nfunc ConversationsDB2Pb(conversationsDB []*model.Conversation) (conversationsPB []*conversation.Conversation) {\n\tfor _, conversationDB := range conversationsDB {\n\t\tconversationPB := &conversation.Conversation{}\n\t\tif err := datautil.CopyStructFields(conversationPB, conversationDB); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tconversationPB.LatestMsgDestructTime = conversationDB.LatestMsgDestructTime.UnixMilli()\n\t\tconversationsPB = append(conversationsPB, conversationPB)\n\t}\n\treturn conversationsPB\n}\n\nfunc ConversationPb2DB(conversationPB *conversation.Conversation) *model.Conversation {\n\tconversationDB := &model.Conversation{}\n\tif err := datautil.CopyStructFields(conversationDB, conversationPB); err != nil {\n\t\treturn nil\n\t}\n\treturn conversationDB\n}\n\nfunc ConversationsPb2DB(conversationsPB []*conversation.Conversation) (conversationsDB []*model.Conversation) {\n\tfor _, conversationPB := range conversationsPB {\n\t\tconversationDB := &model.Conversation{}\n\t\tif err := datautil.CopyStructFields(conversationDB, conversationPB); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tconversationsDB = append(conversationsDB, conversationDB)\n\t}\n\treturn conversationsDB\n}\n"
  },
  {
    "path": "pkg/common/convert/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage convert // import \"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n"
  },
  {
    "path": "pkg/common/convert/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage convert\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/notification/common_user\"\n\t\"github.com/openimsdk/protocol/relation\"\n\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n)\n\nfunc FriendPb2DB(friend *sdkws.FriendInfo) *model.Friend {\n\tdbFriend := &model.Friend{}\n\terr := datautil.CopyStructFields(dbFriend, friend)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tdbFriend.FriendUserID = friend.FriendUser.UserID\n\tdbFriend.CreateTime = timeutil.UnixSecondToTime(friend.CreateTime)\n\treturn dbFriend\n}\n\nfunc FriendDB2Pb(ctx context.Context, friendDB *model.Friend, getUsers func(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error)) (*sdkws.FriendInfo, error) {\n\tusers, err := getUsers(ctx, []string{friendDB.FriendUserID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuser, ok := users[friendDB.FriendUserID]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"user not found: %s\", friendDB.FriendUserID)\n\t}\n\n\treturn &sdkws.FriendInfo{\n\t\tFriendUser: user,\n\t\tCreateTime: friendDB.CreateTime.Unix(),\n\t}, nil\n}\n\nfunc FriendsDB2Pb(ctx context.Context, friendsDB []*model.Friend, getUsers func(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error)) (friendsPb []*sdkws.FriendInfo, err error) {\n\tif len(friendsDB) == 0 {\n\t\treturn nil, nil\n\t}\n\tvar userID []string\n\tfor _, friendDB := range friendsDB {\n\t\tuserID = append(userID, friendDB.FriendUserID)\n\t}\n\n\tusers, err := getUsers(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, friend := range friendsDB {\n\t\tfriendPb := &sdkws.FriendInfo{FriendUser: &sdkws.UserInfo{}}\n\t\terr := datautil.CopyStructFields(friendPb, friend)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfriendPb.FriendUser.UserID = users[friend.FriendUserID].UserID\n\t\tfriendPb.FriendUser.Nickname = users[friend.FriendUserID].Nickname\n\t\tfriendPb.FriendUser.FaceURL = users[friend.FriendUserID].FaceURL\n\t\tfriendPb.FriendUser.Ex = users[friend.FriendUserID].Ex\n\t\tfriendPb.CreateTime = friend.CreateTime.Unix()\n\t\tfriendPb.IsPinned = friend.IsPinned\n\t\tfriendsPb = append(friendsPb, friendPb)\n\t}\n\treturn friendsPb, nil\n}\n\nfunc FriendOnlyDB2PbOnly(friendsDB []*model.Friend) []*relation.FriendInfoOnly {\n\treturn datautil.Slice(friendsDB, func(f *model.Friend) *relation.FriendInfoOnly {\n\t\treturn &relation.FriendInfoOnly{\n\t\t\tOwnerUserID:    f.OwnerUserID,\n\t\t\tFriendUserID:   f.FriendUserID,\n\t\t\tRemark:         f.Remark,\n\t\t\tCreateTime:     f.CreateTime.UnixMilli(),\n\t\t\tAddSource:      f.AddSource,\n\t\t\tOperatorUserID: f.OperatorUserID,\n\t\t\tEx:             f.Ex,\n\t\t\tIsPinned:       f.IsPinned,\n\t\t}\n\t})\n}\n\nfunc FriendRequestDB2Pb(ctx context.Context, friendRequests []*model.FriendRequest, getUsers func(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error)) ([]*sdkws.FriendRequest, error) {\n\tif len(friendRequests) == 0 {\n\t\treturn nil, nil\n\t}\n\tuserIDMap := make(map[string]struct{})\n\tfor _, friendRequest := range friendRequests {\n\t\tuserIDMap[friendRequest.ToUserID] = struct{}{}\n\t\tuserIDMap[friendRequest.FromUserID] = struct{}{}\n\t}\n\tusers, err := getUsers(ctx, datautil.Keys(userIDMap))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make([]*sdkws.FriendRequest, 0, len(friendRequests))\n\tfor _, friendRequest := range friendRequests {\n\t\ttoUser := users[friendRequest.ToUserID]\n\t\tfromUser := users[friendRequest.FromUserID]\n\t\tres = append(res, &sdkws.FriendRequest{\n\t\t\tFromUserID:    friendRequest.FromUserID,\n\t\t\tFromNickname:  fromUser.GetNickname(),\n\t\t\tFromFaceURL:   fromUser.GetFaceURL(),\n\t\t\tToUserID:      friendRequest.ToUserID,\n\t\t\tToNickname:    toUser.GetNickname(),\n\t\t\tToFaceURL:     toUser.GetFaceURL(),\n\t\t\tHandleResult:  friendRequest.HandleResult,\n\t\t\tReqMsg:        friendRequest.ReqMsg,\n\t\t\tCreateTime:    friendRequest.CreateTime.UnixMilli(),\n\t\t\tHandlerUserID: friendRequest.HandlerUserID,\n\t\t\tHandleMsg:     friendRequest.HandleMsg,\n\t\t\tHandleTime:    friendRequest.HandleTime.UnixMilli(),\n\t\t\tEx:            friendRequest.Ex,\n\t\t})\n\t}\n\treturn res, nil\n}\n\n// FriendPb2DBMap converts a FriendInfo protobuf object to a map suitable for database operations.\n// It only includes non-zero or non-empty fields in the map.\nfunc FriendPb2DBMap(friend *sdkws.FriendInfo) map[string]any {\n\tif friend == nil {\n\t\treturn nil\n\t}\n\n\tval := make(map[string]any)\n\n\t// Assuming FriendInfo has similar fields to those in Friend.\n\t// Add or remove fields based on your actual FriendInfo and Friend structures.\n\tif friend.FriendUser != nil {\n\t\tif friend.FriendUser.UserID != \"\" {\n\t\t\tval[\"friend_user_id\"] = friend.FriendUser.UserID\n\t\t}\n\t\tif friend.FriendUser.Nickname != \"\" {\n\t\t\tval[\"nickname\"] = friend.FriendUser.Nickname\n\t\t}\n\t\tif friend.FriendUser.FaceURL != \"\" {\n\t\t\tval[\"face_url\"] = friend.FriendUser.FaceURL\n\t\t}\n\t\tif friend.FriendUser.Ex != \"\" {\n\t\t\tval[\"ex\"] = friend.FriendUser.Ex\n\t\t}\n\t}\n\tif friend.CreateTime != 0 {\n\t\tval[\"create_time\"] = friend.CreateTime // You might need to convert this to a proper time format.\n\t}\n\n\t// Include other fields from FriendInfo as needed, similar to the above pattern.\n\n\treturn val\n}\n"
  },
  {
    "path": "pkg/common/convert/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage convert\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"time\"\n\n\tpbgroup \"github.com/openimsdk/protocol/group\"\n\tsdkws \"github.com/openimsdk/protocol/sdkws\"\n)\n\nfunc Db2PbGroupInfo(m *model.Group, ownerUserID string, memberCount uint32) *sdkws.GroupInfo {\n\treturn &sdkws.GroupInfo{\n\t\tGroupID:                m.GroupID,\n\t\tGroupName:              m.GroupName,\n\t\tNotification:           m.Notification,\n\t\tIntroduction:           m.Introduction,\n\t\tFaceURL:                m.FaceURL,\n\t\tOwnerUserID:            ownerUserID,\n\t\tCreateTime:             m.CreateTime.UnixMilli(),\n\t\tMemberCount:            memberCount,\n\t\tEx:                     m.Ex,\n\t\tStatus:                 m.Status,\n\t\tCreatorUserID:          m.CreatorUserID,\n\t\tGroupType:              m.GroupType,\n\t\tNeedVerification:       m.NeedVerification,\n\t\tLookMemberInfo:         m.LookMemberInfo,\n\t\tApplyMemberFriend:      m.ApplyMemberFriend,\n\t\tNotificationUpdateTime: m.NotificationUpdateTime.UnixMilli(),\n\t\tNotificationUserID:     m.NotificationUserID,\n\t}\n}\n\nfunc Pb2DbGroupRequest(req *pbgroup.GroupApplicationResponseReq, handleUserID string) *model.GroupRequest {\n\treturn &model.GroupRequest{\n\t\tUserID:       req.FromUserID,\n\t\tGroupID:      req.GroupID,\n\t\tHandleResult: req.HandleResult,\n\t\tHandledMsg:   req.HandledMsg,\n\t\tHandleUserID: handleUserID,\n\t\tHandledTime:  time.Now(),\n\t}\n}\n\nfunc Db2PbCMSGroup(m *model.Group, ownerUserID string, ownerUserName string, memberCount uint32) *pbgroup.CMSGroup {\n\treturn &pbgroup.CMSGroup{\n\t\tGroupInfo:          Db2PbGroupInfo(m, ownerUserID, memberCount),\n\t\tGroupOwnerUserID:   ownerUserID,\n\t\tGroupOwnerUserName: ownerUserName,\n\t}\n}\n\nfunc Db2PbGroupMember(m *model.GroupMember) *sdkws.GroupMemberFullInfo {\n\treturn &sdkws.GroupMemberFullInfo{\n\t\tGroupID:   m.GroupID,\n\t\tUserID:    m.UserID,\n\t\tRoleLevel: m.RoleLevel,\n\t\tJoinTime:  m.JoinTime.UnixMilli(),\n\t\tNickname:  m.Nickname,\n\t\tFaceURL:   m.FaceURL,\n\t\t// AppMangerLevel: m.AppMangerLevel,\n\t\tJoinSource:     m.JoinSource,\n\t\tOperatorUserID: m.OperatorUserID,\n\t\tEx:             m.Ex,\n\t\tMuteEndTime:    m.MuteEndTime.UnixMilli(),\n\t\tInviterUserID:  m.InviterUserID,\n\t}\n}\n\nfunc Db2PbGroupRequest(m *model.GroupRequest, user *sdkws.UserInfo, group *sdkws.GroupInfo) *sdkws.GroupRequest {\n\tvar pu *sdkws.PublicUserInfo\n\tif user != nil {\n\t\tpu = &sdkws.PublicUserInfo{\n\t\t\tUserID:   user.UserID,\n\t\t\tNickname: user.Nickname,\n\t\t\tFaceURL:  user.FaceURL,\n\t\t\tEx:       user.Ex,\n\t\t}\n\t}\n\treturn &sdkws.GroupRequest{\n\t\tUserInfo:      pu,\n\t\tGroupInfo:     group,\n\t\tHandleResult:  m.HandleResult,\n\t\tReqMsg:        m.ReqMsg,\n\t\tHandleMsg:     m.HandledMsg,\n\t\tReqTime:       m.ReqTime.UnixMilli(),\n\t\tHandleUserID:  m.HandleUserID,\n\t\tHandleTime:    m.HandledTime.UnixMilli(),\n\t\tEx:            m.Ex,\n\t\tJoinSource:    m.JoinSource,\n\t\tInviterUserID: m.InviterUserID,\n\t}\n}\n\nfunc Db2PbGroupAbstractInfo(\n\tgroupID string,\n\tgroupMemberNumber uint32,\n\tgroupMemberListHash uint64,\n) *pbgroup.GroupAbstractInfo {\n\treturn &pbgroup.GroupAbstractInfo{\n\t\tGroupID:             groupID,\n\t\tGroupMemberNumber:   groupMemberNumber,\n\t\tGroupMemberListHash: groupMemberListHash,\n\t}\n}\n\nfunc Pb2DBGroupInfo(m *sdkws.GroupInfo) *model.Group {\n\treturn &model.Group{\n\t\tGroupID:                m.GroupID,\n\t\tGroupName:              m.GroupName,\n\t\tNotification:           m.Notification,\n\t\tIntroduction:           m.Introduction,\n\t\tFaceURL:                m.FaceURL,\n\t\tCreateTime:             time.Now(),\n\t\tEx:                     m.Ex,\n\t\tStatus:                 m.Status,\n\t\tCreatorUserID:          m.CreatorUserID,\n\t\tGroupType:              m.GroupType,\n\t\tNeedVerification:       m.NeedVerification,\n\t\tLookMemberInfo:         m.LookMemberInfo,\n\t\tApplyMemberFriend:      m.ApplyMemberFriend,\n\t\tNotificationUpdateTime: time.UnixMilli(m.NotificationUpdateTime),\n\t\tNotificationUserID:     m.NotificationUserID,\n\t}\n}\n\n// func Pb2DbGroupMember(m *sdkws.UserInfo) *relation.GroupMember {\n//\treturn &relation.GroupMember{\n//\t\tUserID:   m.UserID,\n//\t\tNickname: m.Nickname,\n//\t\tFaceURL:  m.FaceURL,\n//\t\tEx:       m.Ex,\n//\t}\n//}\n"
  },
  {
    "path": "pkg/common/convert/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage convert\n\nimport (\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\nfunc MsgPb2DB(msg *sdkws.MsgData) *model.MsgDataModel {\n\tif msg == nil {\n\t\treturn nil\n\t}\n\tvar msgDataModel model.MsgDataModel\n\tmsgDataModel.SendID = msg.SendID\n\tmsgDataModel.RecvID = msg.RecvID\n\tmsgDataModel.GroupID = msg.GroupID\n\tmsgDataModel.ClientMsgID = msg.ClientMsgID\n\tmsgDataModel.ServerMsgID = msg.ServerMsgID\n\tmsgDataModel.SenderPlatformID = msg.SenderPlatformID\n\tmsgDataModel.SenderNickname = msg.SenderNickname\n\tmsgDataModel.SenderFaceURL = msg.SenderFaceURL\n\tmsgDataModel.SessionType = msg.SessionType\n\tmsgDataModel.MsgFrom = msg.MsgFrom\n\tmsgDataModel.ContentType = msg.ContentType\n\tmsgDataModel.Content = string(msg.Content)\n\tmsgDataModel.Seq = msg.Seq\n\tmsgDataModel.SendTime = msg.SendTime\n\tmsgDataModel.CreateTime = msg.CreateTime\n\tmsgDataModel.Status = msg.Status\n\tmsgDataModel.Options = msg.Options\n\tif msg.OfflinePushInfo != nil {\n\t\tmsgDataModel.OfflinePush = &model.OfflinePushModel{\n\t\t\tTitle:         msg.OfflinePushInfo.Title,\n\t\t\tDesc:          msg.OfflinePushInfo.Desc,\n\t\t\tEx:            msg.OfflinePushInfo.Ex,\n\t\t\tIOSPushSound:  msg.OfflinePushInfo.IOSPushSound,\n\t\t\tIOSBadgeCount: msg.OfflinePushInfo.IOSBadgeCount,\n\t\t}\n\t}\n\tmsgDataModel.AtUserIDList = msg.AtUserIDList\n\tmsgDataModel.AttachedInfo = msg.AttachedInfo\n\tmsgDataModel.Ex = msg.Ex\n\treturn &msgDataModel\n}\n\nfunc MsgDB2Pb(msgModel *model.MsgDataModel) *sdkws.MsgData {\n\tif msgModel == nil {\n\t\treturn nil\n\t}\n\tvar msg sdkws.MsgData\n\tmsg.SendID = msgModel.SendID\n\tmsg.RecvID = msgModel.RecvID\n\tmsg.GroupID = msgModel.GroupID\n\tmsg.ClientMsgID = msgModel.ClientMsgID\n\tmsg.ServerMsgID = msgModel.ServerMsgID\n\tmsg.SenderPlatformID = msgModel.SenderPlatformID\n\tmsg.SenderNickname = msgModel.SenderNickname\n\tmsg.SenderFaceURL = msgModel.SenderFaceURL\n\tmsg.SessionType = msgModel.SessionType\n\tmsg.MsgFrom = msgModel.MsgFrom\n\tmsg.ContentType = msgModel.ContentType\n\tmsg.Content = []byte(msgModel.Content)\n\tmsg.Seq = msgModel.Seq\n\tmsg.SendTime = msgModel.SendTime\n\tmsg.CreateTime = msgModel.CreateTime\n\tmsg.Status = msgModel.Status\n\tif msgModel.SessionType == constant.SingleChatType {\n\t\tmsg.IsRead = msgModel.IsRead\n\t}\n\tmsg.Options = msgModel.Options\n\tif msgModel.OfflinePush != nil {\n\t\tmsg.OfflinePushInfo = &sdkws.OfflinePushInfo{\n\t\t\tTitle:         msgModel.OfflinePush.Title,\n\t\t\tDesc:          msgModel.OfflinePush.Desc,\n\t\t\tEx:            msgModel.OfflinePush.Ex,\n\t\t\tIOSPushSound:  msgModel.OfflinePush.IOSPushSound,\n\t\t\tIOSBadgeCount: msgModel.OfflinePush.IOSBadgeCount,\n\t\t}\n\t}\n\tmsg.AtUserIDList = msgModel.AtUserIDList\n\tmsg.AttachedInfo = msgModel.AttachedInfo\n\tmsg.Ex = msgModel.Ex\n\treturn &msg\n}\n"
  },
  {
    "path": "pkg/common/convert/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage convert\n\nimport (\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"time\"\n\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\nfunc UserDB2Pb(user *relationtb.User) *sdkws.UserInfo {\n\treturn &sdkws.UserInfo{\n\t\tUserID:           user.UserID,\n\t\tNickname:         user.Nickname,\n\t\tFaceURL:          user.FaceURL,\n\t\tEx:               user.Ex,\n\t\tCreateTime:       user.CreateTime.UnixMilli(),\n\t\tAppMangerLevel:   user.AppMangerLevel,\n\t\tGlobalRecvMsgOpt: user.GlobalRecvMsgOpt,\n\t}\n}\n\nfunc UsersDB2Pb(users []*relationtb.User) []*sdkws.UserInfo {\n\treturn datautil.Slice(users, UserDB2Pb)\n}\n\nfunc UserPb2DB(user *sdkws.UserInfo) *relationtb.User {\n\treturn &relationtb.User{\n\t\tUserID:           user.UserID,\n\t\tNickname:         user.Nickname,\n\t\tFaceURL:          user.FaceURL,\n\t\tEx:               user.Ex,\n\t\tCreateTime:       time.UnixMilli(user.CreateTime),\n\t\tAppMangerLevel:   user.AppMangerLevel,\n\t\tGlobalRecvMsgOpt: user.GlobalRecvMsgOpt,\n\t}\n}\n\nfunc UserPb2DBMap(user *sdkws.UserInfo) map[string]any {\n\tif user == nil {\n\t\treturn nil\n\t}\n\tval := make(map[string]any)\n\tfields := map[string]any{\n\t\t\"nickname\":            user.Nickname,\n\t\t\"face_url\":            user.FaceURL,\n\t\t\"ex\":                  user.Ex,\n\t\t\"app_manager_level\":   user.AppMangerLevel,\n\t\t\"global_recv_msg_opt\": user.GlobalRecvMsgOpt,\n\t}\n\tfor key, value := range fields {\n\t\tif v, ok := value.(string); ok && v != \"\" {\n\t\t\tval[key] = v\n\t\t} else if v, ok := value.(int32); ok && v != 0 {\n\t\t\tval[key] = v\n\t\t}\n\t}\n\treturn val\n}\nfunc UserPb2DBMapEx(user *sdkws.UserInfoWithEx) map[string]any {\n\tif user == nil {\n\t\treturn nil\n\t}\n\tval := make(map[string]any)\n\n\t// Map fields from UserInfoWithEx to val\n\tif user.Nickname != nil {\n\t\tval[\"nickname\"] = user.Nickname.Value\n\t}\n\tif user.FaceURL != nil {\n\t\tval[\"face_url\"] = user.FaceURL.Value\n\t}\n\tif user.Ex != nil {\n\t\tval[\"ex\"] = user.Ex.Value\n\t}\n\tif user.GlobalRecvMsgOpt != nil {\n\t\tval[\"global_recv_msg_opt\"] = user.GlobalRecvMsgOpt.Value\n\t}\n\n\treturn val\n}\n"
  },
  {
    "path": "pkg/common/convert/user_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage convert\n\nimport (\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\nfunc TestUsersDB2Pb(t *testing.T) {\n\ttype args struct {\n\t\tusers []*relationtb.User\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\targs       args\n\t\twantResult []*sdkws.UserInfo\n\t}{\n\t\t// TODO: Add test cases.\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif gotResult := UsersDB2Pb(tt.args.users); !reflect.DeepEqual(gotResult, tt.wantResult) {\n\t\t\t\tt.Errorf(\"UsersDB2Pb() = %v, want %v\", gotResult, tt.wantResult)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUserPb2DB(t *testing.T) {\n\ttype args struct {\n\t\tuser *sdkws.UserInfo\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *relationtb.User\n\t}{\n\t\t// TODO: Add test cases.\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := UserPb2DB(tt.args.user); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"UserPb2DB() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUserPb2DBMap(t *testing.T) {\n\tuser := &sdkws.UserInfo{\n\t\tNickname:         \"TestUser\",\n\t\tFaceURL:          \"http://openim.io/logo.jpg\",\n\t\tEx:               \"Extra Data\",\n\t\tAppMangerLevel:   1,\n\t\tGlobalRecvMsgOpt: 2,\n\t}\n\n\texpected := map[string]any{\n\t\t\"nickname\":            \"TestUser\",\n\t\t\"face_url\":            \"http://openim.io/logo.jpg\",\n\t\t\"ex\":                  \"Extra Data\",\n\t\t\"app_manager_level\":   int32(1),\n\t\t\"global_recv_msg_opt\": int32(2),\n\t}\n\n\tresult := UserPb2DBMap(user)\n\tif !reflect.DeepEqual(result, expected) {\n\t\tt.Errorf(\"UserPb2DBMap returned unexpected map. Got %v, want %v\", result, expected)\n\t}\n}\n"
  },
  {
    "path": "pkg/common/discovery/direct/direct_resolver.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage direct\n\nimport (\n\t\"context\"\n\t\"math/rand\"\n\t\"strings\"\n\n\t\"github.com/openimsdk/tools/log\"\n\t\"google.golang.org/grpc/resolver\"\n)\n\nconst (\n\tslashSeparator = \"/\"\n\t// EndpointSepChar is the separator char in endpoints.\n\tEndpointSepChar = ','\n\n\tsubsetSize = 32\n\tscheme     = \"direct\"\n)\n\ntype ResolverDirect struct {\n}\n\nfunc NewResolverDirect() *ResolverDirect {\n\treturn &ResolverDirect{}\n}\n\nfunc (rd *ResolverDirect) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (\n\tresolver.Resolver, error) {\n\tlog.ZDebug(context.Background(), \"Build\", \"target\", target)\n\tendpoints := strings.FieldsFunc(GetEndpoints(target), func(r rune) bool {\n\t\treturn r == EndpointSepChar\n\t})\n\tendpoints = subset(endpoints, subsetSize)\n\taddrs := make([]resolver.Address, 0, len(endpoints))\n\n\tfor _, val := range endpoints {\n\t\taddrs = append(addrs, resolver.Address{\n\t\t\tAddr: val,\n\t\t})\n\t}\n\tif err := cc.UpdateState(resolver.State{\n\t\tAddresses: addrs,\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &nopResolver{cc: cc}, nil\n}\nfunc init() {\n\tresolver.Register(&ResolverDirect{})\n}\nfunc (rd *ResolverDirect) Scheme() string {\n\treturn scheme // return your custom scheme name\n}\n\n// GetEndpoints returns the endpoints from the given target.\nfunc GetEndpoints(target resolver.Target) string {\n\treturn strings.Trim(target.URL.Path, slashSeparator)\n}\nfunc subset(set []string, sub int) []string {\n\trand.Shuffle(len(set), func(i, j int) {\n\t\tset[i], set[j] = set[j], set[i]\n\t})\n\tif len(set) <= sub {\n\t\treturn set\n\t}\n\n\treturn set[:sub]\n}\n\ntype nopResolver struct {\n\tcc resolver.ClientConn\n}\n\nfunc (n nopResolver) ResolveNow(options resolver.ResolveNowOptions) {\n\n}\n\nfunc (n nopResolver) Close() {\n\n}\n"
  },
  {
    "path": "pkg/common/discovery/direct/directconn.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage direct\n\n//import (\n//\t\"context\"\n//\t\"fmt\"\n//\n//\tconfig2 \"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n//\t\"github.com/openimsdk/tools/errs\"\n//\t\"google.golang.org/grpc\"\n//\t\"google.golang.org/grpc/credentials/insecure\"\n//)\n//\n//type ServiceAddresses map[string][]int\n//\n//func getServiceAddresses(rpcRegisterName *config2.RpcRegisterName,\n//\trpcPort *config2.RpcPort, longConnSvrPort []int) ServiceAddresses {\n//\treturn ServiceAddresses{\n//\t\trpcRegisterName.OpenImUserName:           rpcPort.OpenImUserPort,\n//\t\trpcRegisterName.OpenImFriendName:         rpcPort.OpenImFriendPort,\n//\t\trpcRegisterName.OpenImMsgName:            rpcPort.OpenImMessagePort,\n//\t\trpcRegisterName.OpenImMessageGatewayName: longConnSvrPort,\n//\t\trpcRegisterName.OpenImGroupName:          rpcPort.OpenImGroupPort,\n//\t\trpcRegisterName.OpenImAuthName:           rpcPort.OpenImAuthPort,\n//\t\trpcRegisterName.OpenImPushName:           rpcPort.OpenImPushPort,\n//\t\trpcRegisterName.OpenImConversationName:   rpcPort.OpenImConversationPort,\n//\t\trpcRegisterName.OpenImThirdName:          rpcPort.OpenImThirdPort,\n//\t}\n//}\n//\n//type ConnDirect struct {\n//\tadditionalOpts        []grpc.DialOption\n//\tcurrentServiceAddress string\n//\tconns                 map[string][]*grpc.ClientConn\n//\tresolverDirect        *ResolverDirect\n//\tconfig                *config2.GlobalConfig\n//}\n//\n//func (cd *ConnDirect) GetClientLocalConns() map[string][]*grpc.ClientConn {\n//\treturn nil\n//}\n//\n//func (cd *ConnDirect) GetUserIdHashGatewayHost(ctx context.Context, userId string) (string, error) {\n//\treturn \"\", nil\n//}\n//\n//func (cd *ConnDirect) Register(serviceName, host string, port int, opts ...grpc.DialOption) error {\n//\treturn nil\n//}\n//\n//func (cd *ConnDirect) UnRegister() error {\n//\treturn nil\n//}\n//\n//func (cd *ConnDirect) CreateRpcRootNodes(serviceNames []string) error {\n//\treturn nil\n//}\n//\n//func (cd *ConnDirect) RegisterConf2Registry(key string, conf []byte) error {\n//\treturn nil\n//}\n//\n//func (cd *ConnDirect) GetConfFromRegistry(key string) ([]byte, error) {\n//\treturn nil, nil\n//}\n//\n//func (cd *ConnDirect) Close() {\n//\n//}\n//\n//func NewConnDirect(config *config2.GlobalConfig) (*ConnDirect, error) {\n//\treturn &ConnDirect{\n//\t\tconns:          make(map[string][]*grpc.ClientConn),\n//\t\tresolverDirect: NewResolverDirect(),\n//\t\tconfig:         config,\n//\t}, nil\n//}\n//\n//func (cd *ConnDirect) GetConns(ctx context.Context,\n//\tserviceName string, opts ...grpc.DialOption) ([]*grpc.ClientConn, error) {\n//\n//\tif conns, exists := cd.conns[serviceName]; exists {\n//\t\treturn conns, nil\n//\t}\n//\tports := getServiceAddresses(&cd.config.RpcRegisterName,\n//\t\t&cd.config.RpcPort, cd.config.LongConnSvr.OpenImMessageGatewayPort)[serviceName]\n//\tvar connections []*grpc.ClientConn\n//\tfor _, port := range ports {\n//\t\tconn, err := cd.dialServiceWithoutResolver(ctx, fmt.Sprintf(cd.config.Rpc.ListenIP+\":%d\", port), append(cd.additionalOpts, opts...)...)\n//\t\tif err != nil {\n//\t\t\treturn nil, errs.Wrap(fmt.Errorf(\"connect to port %d failed,serviceName %s, IP %s\", port, serviceName, cd.config.Rpc.ListenIP))\n//\t\t}\n//\t\tconnections = append(connections, conn)\n//\t}\n//\n//\tif len(connections) == 0 {\n//\t\treturn nil, errs.New(\"no connections found for service\", \"serviceName\", serviceName).Wrap()\n//\t}\n//\treturn connections, nil\n//}\n//\n//func (cd *ConnDirect) GetConn(ctx context.Context, serviceName string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {\n//\t// Get service addresses\n//\taddresses := getServiceAddresses(&cd.config.RpcRegisterName,\n//\t\t&cd.config.RpcPort, cd.config.LongConnSvr.OpenImMessageGatewayPort)\n//\taddress, ok := addresses[serviceName]\n//\tif !ok {\n//\t\treturn nil, errs.New(\"unknown service name\", \"serviceName\", serviceName).Wrap()\n//\t}\n//\tvar result string\n//\tfor _, addr := range address {\n//\t\tif result != \"\" {\n//\t\t\tresult = result + \",\" + fmt.Sprintf(cd.config.Rpc.ListenIP+\":%d\", addr)\n//\t\t} else {\n//\t\t\tresult = fmt.Sprintf(cd.config.Rpc.ListenIP+\":%d\", addr)\n//\t\t}\n//\t}\n//\t// Try to dial a new connection\n//\tconn, err := cd.dialService(ctx, result, append(cd.additionalOpts, opts...)...)\n//\tif err != nil {\n//\t\treturn nil, errs.WrapMsg(err, \"address\", result)\n//\t}\n//\n//\t// Store the new connection\n//\tcd.conns[serviceName] = append(cd.conns[serviceName], conn)\n//\treturn conn, nil\n//}\n//\n//func (cd *ConnDirect) GetSelfConnTarget() string {\n//\treturn cd.currentServiceAddress\n//}\n//\n//func (cd *ConnDirect) AddOption(opts ...grpc.DialOption) {\n//\tcd.additionalOpts = append(cd.additionalOpts, opts...)\n//}\n//\n//func (cd *ConnDirect) CloseConn(conn *grpc.ClientConn) {\n//\tif conn != nil {\n//\t\tconn.Close()\n//\t}\n//}\n//\n//func (cd *ConnDirect) dialService(ctx context.Context, address string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {\n//\toptions := append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))\n//\tconn, err := grpc.DialContext(ctx, cd.resolverDirect.Scheme()+\":///\"+address, options...)\n//\n//\tif err != nil {\n//\t\treturn nil, errs.WrapMsg(err, \"address\", address)\n//\t}\n//\treturn conn, nil\n//}\n//\n//func (cd *ConnDirect) dialServiceWithoutResolver(ctx context.Context, address string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {\n//\toptions := append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))\n//\tconn, err := grpc.DialContext(ctx, address, options...)\n//\n//\tif err != nil {\n//\t\treturn nil, errs.Wrap(err)\n//\t}\n//\treturn conn, nil\n//}\n"
  },
  {
    "path": "pkg/common/discovery/direct/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage direct // import \"github.com/openimsdk/open-im-server/v3/pkg/common/discovery/direct\"\n"
  },
  {
    "path": "pkg/common/discovery/discoveryregister.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage discovery\n\nimport (\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/discovery/standalone\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/openimsdk/tools/discovery/kubernetes\"\n\n\t\"github.com/openimsdk/tools/discovery/etcd\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\n// NewDiscoveryRegister creates a new service discovery and registry client based on the provided environment type.\nfunc NewDiscoveryRegister(discovery *config.Discovery, watchNames []string) (discovery.SvcDiscoveryRegistry, error) {\n\tif config.Standalone() {\n\t\treturn standalone.GetSvcDiscoveryRegistry(), nil\n\t}\n\tif runtimeenv.RuntimeEnvironment() == config.KUBERNETES {\n\t\treturn kubernetes.NewConnManager(discovery.Kubernetes.Namespace, nil,\n\t\t\tgrpc.WithDefaultCallOptions(\n\t\t\t\tgrpc.MaxCallSendMsgSize(1024*1024*20),\n\t\t\t),\n\t\t)\n\t}\n\n\tswitch discovery.Enable {\n\tcase config.ETCD:\n\t\treturn etcd.NewSvcDiscoveryRegistry(\n\t\t\tdiscovery.Etcd.RootDirectory,\n\t\t\tdiscovery.Etcd.Address,\n\t\t\twatchNames,\n\t\t\tetcd.WithDialTimeout(10*time.Second),\n\t\t\tetcd.WithMaxCallSendMsgSize(20*1024*1024),\n\t\t\tetcd.WithUsernameAndPassword(discovery.Etcd.Username, discovery.Etcd.Password))\n\tdefault:\n\t\treturn nil, errs.New(\"unsupported discovery type\", \"type\", discovery.Enable).Wrap()\n\t}\n}\n"
  },
  {
    "path": "pkg/common/discovery/discoveryregister_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage discovery\n\nimport (\n\t\"os\"\n)\n\nfunc setupTestEnvironment() {\n\tos.Setenv(\"ZOOKEEPER_SCHEMA\", \"openim\")\n\tos.Setenv(\"ZOOKEEPER_ADDRESS\", \"172.28.0.1\")\n\tos.Setenv(\"ZOOKEEPER_PORT\", \"12181\")\n\tos.Setenv(\"ZOOKEEPER_USERNAME\", \"\")\n\tos.Setenv(\"ZOOKEEPER_PASSWORD\", \"\")\n}\n\n//func TestNewDiscoveryRegister(t *testing.T) {\n//\tsetupTestEnvironment()\n//\tconf := config.NewGlobalConfig()\n//\ttests := []struct {\n//\t\tenvType        string\n//\t\tgatewayName    string\n//\t\texpectedError  bool\n//\t\texpectedResult bool\n//\t}{\n//\t\t{\"zookeeper\", \"MessageGateway\", false, true},\n//\t\t{\"k8s\", \"MessageGateway\", false, true},\n//\t\t{\"direct\", \"MessageGateway\", false, true},\n//\t\t{\"invalid\", \"MessageGateway\", true, false},\n//\t}\n//\n//\tfor _, test := range tests {\n//\t\tconf.Envs.Discovery = test.envType\n//\t\tconf.RpcRegisterName.OpenImMessageGatewayName = test.gatewayName\n//\t\tclient, err := NewDiscoveryRegister(conf)\n//\n//\t\tif test.expectedError {\n//\t\t\tassert.Error(t, err)\n//\t\t} else {\n//\t\t\tassert.NoError(t, err)\n//\t\t\tif test.expectedResult {\n//\t\t\t\tassert.Implements(t, (*discovery.SvcDiscoveryRegistry)(nil), client)\n//\t\t\t} else {\n//\t\t\t\tassert.Nil(t, client)\n//\t\t\t}\n//\t\t}\n//\t}\n//}\n"
  },
  {
    "path": "pkg/common/discovery/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage discovery // import \"github.com/openimsdk/open-im-server/v3/pkg/common/discovery\"\n"
  },
  {
    "path": "pkg/common/discovery/etcd/config_manager.go",
    "content": "package etcd\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n)\n\nvar (\n\tShutDowns []func() error\n)\n\nfunc RegisterShutDown(shutDown ...func() error) {\n\tShutDowns = append(ShutDowns, shutDown...)\n}\n\ntype ConfigManager struct {\n\tclient           *clientv3.Client\n\twatchConfigNames []string\n\tlock             sync.Mutex\n}\n\nfunc BuildKey(s string) string {\n\treturn ConfigKeyPrefix + s\n}\n\nfunc NewConfigManager(client *clientv3.Client, configNames []string) *ConfigManager {\n\treturn &ConfigManager{\n\t\tclient:           client,\n\t\twatchConfigNames: datautil.Batch(func(s string) string { return BuildKey(s) }, append(configNames, RestartKey))}\n}\n\nfunc (c *ConfigManager) Watch(ctx context.Context) {\n\tchans := make([]clientv3.WatchChan, 0, len(c.watchConfigNames))\n\tfor _, name := range c.watchConfigNames {\n\t\tchans = append(chans, c.client.Watch(ctx, name, clientv3.WithPrefix()))\n\t}\n\n\tdoWatch := func(watchChan clientv3.WatchChan) {\n\t\tfor watchResp := range watchChan {\n\t\t\tif watchResp.Err() != nil {\n\t\t\t\tlog.ZError(ctx, \"watch err\", errs.Wrap(watchResp.Err()))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, event := range watchResp.Events {\n\t\t\t\tif event.IsModify() {\n\t\t\t\t\tif datautil.Contain(string(event.Kv.Key), c.watchConfigNames...) {\n\t\t\t\t\t\tc.lock.Lock()\n\t\t\t\t\t\terr := restartServer(ctx)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.ZError(ctx, \"restart server err\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tc.lock.Unlock()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor _, ch := range chans {\n\t\tgo doWatch(ch)\n\t}\n}\n\nfunc restartServer(ctx context.Context) error {\n\texePath, err := os.Executable()\n\tif err != nil {\n\t\treturn errs.New(\"get executable path fail\").Wrap()\n\t}\n\n\targs := os.Args\n\tenv := os.Environ()\n\n\tcmd := exec.Command(exePath, args[1:]...)\n\tcmd.Env = env\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdin = os.Stdin\n\n\tif runtime.GOOS != \"windows\" {\n\t\tcmd.SysProcAttr = &syscall.SysProcAttr{}\n\t}\n\tlog.ZInfo(ctx, \"shutdown server\")\n\tfor _, f := range ShutDowns {\n\t\tif err = f(); err != nil {\n\t\t\tlog.ZError(ctx, \"shutdown fail\", err)\n\t\t}\n\t}\n\n\tlog.ZInfo(ctx, \"restart server\")\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn errs.New(\"restart server fail\").Wrap()\n\t}\n\tlog.ZInfo(ctx, \"cmd start over\")\n\n\tos.Exit(0)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/discovery/etcd/const.go",
    "content": "package etcd\n\nconst (\n\tConfigKeyPrefix       = \"/open-im/config/\"\n\tRestartKey            = \"restart\"\n\tEnableConfigCenterKey = \"enable-config-center\"\n\tEnable                = \"enable\"\n\tDisable               = \"disable\"\n)\n"
  },
  {
    "path": "pkg/common/discovery/kubernetes/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage kubernetes // import \"github.com/openimsdk/open-im-server/v3/pkg/common/discovery/kubernetes\"\n"
  },
  {
    "path": "pkg/common/discovery/kubernetes/kubernetes.go",
    "content": "package kubernetes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/client-go/informers\"\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/cache\"\n)\n\ntype KubernetesConnManager struct {\n\tclientset   *kubernetes.Clientset\n\tnamespace   string\n\tdialOptions []grpc.DialOption\n\n\trpcTargets map[string]string\n\tselfTarget string\n\n\tmu      sync.RWMutex\n\tconnMap map[string][]*grpc.ClientConn\n}\n\n// NewKubernetesConnManager creates a new connection manager that uses Kubernetes services for service discovery.\nfunc NewKubernetesConnManager(namespace string, options ...grpc.DialOption) (*KubernetesConnManager, error) {\n\tconfig, err := rest.InClusterConfig()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create in-cluster config: %v\", err)\n\t}\n\n\tclientset, err := kubernetes.NewForConfig(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create clientset: %v\", err)\n\t}\n\n\tk := &KubernetesConnManager{\n\t\tclientset:   clientset,\n\t\tnamespace:   namespace,\n\t\tdialOptions: options,\n\t\tconnMap:     make(map[string][]*grpc.ClientConn),\n\t}\n\n\tgo k.watchEndpoints()\n\n\treturn k, nil\n}\n\nfunc (k *KubernetesConnManager) initializeConns(serviceName string) error {\n\tport, err := k.getServicePort(serviceName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tendpoints, err := k.clientset.CoreV1().Endpoints(k.namespace).Get(context.Background(), serviceName, metav1.GetOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get endpoints for service %s: %v\", serviceName, err)\n\t}\n\n\t// fmt.Println(\"Endpoints:\", endpoints, \"endpoints.Subsets:\", endpoints.Subsets)\n\n\tvar conns []*grpc.ClientConn\n\tfor _, subset := range endpoints.Subsets {\n\t\tfor _, address := range subset.Addresses {\n\t\t\ttarget := fmt.Sprintf(\"%s:%d\", address.IP, port)\n\t\t\t// fmt.Println(\"IP target:\", target)\n\t\t\tconn, err := grpc.Dial(target, append(k.dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials()))...)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to dial endpoint %s: %v\", target, err)\n\t\t\t}\n\t\t\tconns = append(conns, conn)\n\t\t}\n\t}\n\n\tk.mu.Lock()\n\tk.connMap[serviceName] = conns\n\tk.mu.Unlock()\n\n\treturn nil\n}\n\n// GetConns returns gRPC client connections for a given Kubernetes service name.\nfunc (k *KubernetesConnManager) GetConns(ctx context.Context, serviceName string, opts ...grpc.DialOption) ([]*grpc.ClientConn, error) {\n\tk.mu.RLock()\n\n\tconns, exists := k.connMap[serviceName]\n\tk.mu.RUnlock()\n\tif exists {\n\t\treturn conns, nil\n\t}\n\n\tk.mu.Lock()\n\t// Check if another goroutine has already initialized the connections when we released the read lock\n\tconns, exists = k.connMap[serviceName]\n\tif exists {\n\t\treturn conns, nil\n\t}\n\tk.mu.Unlock()\n\n\tif err := k.initializeConns(serviceName); err != nil {\n\t\tfmt.Println(\"Failed to initialize connections:\", err)\n\t\treturn nil, fmt.Errorf(\"failed to initialize connections for service %s: %v\", serviceName, err)\n\t}\n\n\treturn k.connMap[serviceName], nil\n}\n\n// GetConn returns a single gRPC client connection for a given Kubernetes service name.\nfunc (k *KubernetesConnManager) GetConn(ctx context.Context, serviceName string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {\n\tvar target string\n\n\tif k.rpcTargets[serviceName] == \"\" {\n\t\tvar err error\n\n\t\tsvcPort, err := k.getServicePort(serviceName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\ttarget = fmt.Sprintf(\"%s.%s.svc.cluster.local:%d\", serviceName, k.namespace, svcPort)\n\n\t\t// fmt.Println(\"SVC target:\", target)\n\t} else {\n\t\ttarget = k.rpcTargets[serviceName]\n\t}\n\n\treturn grpc.DialContext(\n\t\tctx,\n\t\ttarget,\n\t\tappend([]grpc.DialOption{\n\t\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\t\tgrpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*10), grpc.MaxCallSendMsgSize(1024*1024*20)),\n\t\t}, k.dialOptions...)...,\n\t)\n}\n\n// GetSelfConnTarget returns the connection target for the current service.\nfunc (k *KubernetesConnManager) GetSelfConnTarget() string {\n\tif k.selfTarget == \"\" {\n\t\thostName := os.Getenv(\"HOSTNAME\")\n\n\t\tpod, err := k.clientset.CoreV1().Pods(k.namespace).Get(context.Background(), hostName, metav1.GetOptions{})\n\t\tif err != nil {\n\t\t\tlog.Printf(\"failed to get pod %s: %v \\n\", hostName, err)\n\t\t}\n\n\t\tfor pod.Status.PodIP == \"\" {\n\t\t\tpod, err = k.clientset.CoreV1().Pods(k.namespace).Get(context.TODO(), hostName, metav1.GetOptions{})\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting pod: %v \\n\", err)\n\t\t\t}\n\n\t\t\ttime.Sleep(3 * time.Second)\n\t\t}\n\n\t\tvar selfPort int32\n\n\t\tfor _, port := range pod.Spec.Containers[0].Ports {\n\t\t\tif port.ContainerPort != 10001 {\n\t\t\t\tselfPort = port.ContainerPort\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tk.selfTarget = fmt.Sprintf(\"%s:%d\", pod.Status.PodIP, selfPort)\n\t}\n\n\treturn k.selfTarget\n}\n\n// AddOption appends gRPC dial options to the existing options.\nfunc (k *KubernetesConnManager) AddOption(opts ...grpc.DialOption) {\n\tk.mu.Lock()\n\tdefer k.mu.Unlock()\n\tk.dialOptions = append(k.dialOptions, opts...)\n}\n\n// CloseConn closes a given gRPC client connection.\nfunc (k *KubernetesConnManager) CloseConn(conn *grpc.ClientConn) {\n\tconn.Close()\n}\n\n// Close closes all gRPC connections managed by KubernetesConnManager.\nfunc (k *KubernetesConnManager) Close() {\n\tk.mu.Lock()\n\tdefer k.mu.Unlock()\n\tfor _, conns := range k.connMap {\n\t\tfor _, conn := range conns {\n\t\t\t_ = conn.Close()\n\t\t}\n\t}\n\tk.connMap = make(map[string][]*grpc.ClientConn)\n}\n\nfunc (k *KubernetesConnManager) Register(serviceName, host string, port int, opts ...grpc.DialOption) error {\n\treturn nil\n}\n\nfunc (k *KubernetesConnManager) UnRegister() error {\n\treturn nil\n}\n\nfunc (k *KubernetesConnManager) GetUserIdHashGatewayHost(ctx context.Context, userId string) (string, error) {\n\treturn \"\", nil\n}\n\nfunc (k *KubernetesConnManager) getServicePort(serviceName string) (int32, error) {\n\tvar svcPort int32\n\n\tsvc, err := k.clientset.CoreV1().Services(k.namespace).Get(context.Background(), serviceName, metav1.GetOptions{})\n\tif err != nil {\n\t\tfmt.Print(\"namespace:\", k.namespace)\n\t\treturn 0, fmt.Errorf(\"failed to get service %s: %v\", serviceName, err)\n\t}\n\n\tif len(svc.Spec.Ports) == 0 {\n\t\treturn 0, fmt.Errorf(\"service %s has no ports defined\", serviceName)\n\t}\n\n\tfor _, port := range svc.Spec.Ports {\n\t\t// fmt.Println(serviceName, \" Now Get Port:\", port.Port)\n\t\tif port.Port != 10001 {\n\t\t\tsvcPort = port.Port\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn svcPort, nil\n}\n\n// watchEndpoints listens for changes in Pod resources.\nfunc (k *KubernetesConnManager) watchEndpoints() {\n\tinformerFactory := informers.NewSharedInformerFactory(k.clientset, time.Minute*10)\n\tinformer := informerFactory.Core().V1().Pods().Informer()\n\n\t// Watch for Pod changes (add, update, delete)\n\tinformer.AddEventHandler(cache.ResourceEventHandlerFuncs{\n\t\tAddFunc: func(obj interface{}) {\n\t\t\tk.handleEndpointChange(obj)\n\t\t},\n\t\tUpdateFunc: func(oldObj, newObj interface{}) {\n\t\t\tk.handleEndpointChange(newObj)\n\t\t},\n\t\tDeleteFunc: func(obj interface{}) {\n\t\t\tk.handleEndpointChange(obj)\n\t\t},\n\t})\n\n\tinformerFactory.Start(context.Background().Done())\n\t<-context.Background().Done() // Block forever\n}\n\nfunc (k *KubernetesConnManager) handleEndpointChange(obj interface{}) {\n\tendpoint, ok := obj.(*v1.Endpoints)\n\tif !ok {\n\t\treturn\n\t}\n\tserviceName := endpoint.Name\n\tif err := k.initializeConns(serviceName); err != nil {\n\t\tfmt.Printf(\"Error initializing connections for %s: %v\\n\", serviceName, err)\n\t}\n}\n"
  },
  {
    "path": "pkg/common/ginprometheus/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage ginprometheus // import \"github.com/openimsdk/open-im-server/v3/pkg/common/ginprometheus\"\n"
  },
  {
    "path": "pkg/common/ginprometheus/ginprometheus.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage ginprometheus\n\n//\n//import (\n//\t\"bytes\"\n//\t\"fmt\"\n//\t\"io\"\n//\t\"net/http\"\n//\t\"os\"\n//\t\"strconv\"\n//\t\"time\"\n//\n//\t\"github.com/gin-gonic/gin\"\n//\t\"github.com/prometheus/client_golang/prometheus\"\n//\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n//)\n//\n//var defaultMetricPath = \"/metrics\"\n//\n//// counter, counter_vec, gauge, gauge_vec,\n//// histogram, histogram_vec, summary, summary_vec.\n//var (\n//\treqCounter = &Metric{\n//\t\tID:          \"reqCnt\",\n//\t\tName:        \"requests_total\",\n//\t\tDescription: \"How many HTTP requests processed, partitioned by status code and HTTP method.\",\n//\t\tType:        \"counter_vec\",\n//\t\tArgs:        []string{\"code\", \"method\", \"handler\", \"host\", \"url\"}}\n//\n//\treqDuration = &Metric{\n//\t\tID:          \"reqDur\",\n//\t\tName:        \"request_duration_seconds\",\n//\t\tDescription: \"The HTTP request latencies in seconds.\",\n//\t\tType:        \"histogram_vec\",\n//\t\tArgs:        []string{\"code\", \"method\", \"url\"},\n//\t}\n//\n//\tresSize = &Metric{\n//\t\tID:          \"resSz\",\n//\t\tName:        \"response_size_bytes\",\n//\t\tDescription: \"The HTTP response sizes in bytes.\",\n//\t\tType:        \"summary\"}\n//\n//\treqSize = &Metric{\n//\t\tID:          \"reqSz\",\n//\t\tName:        \"request_size_bytes\",\n//\t\tDescription: \"The HTTP request sizes in bytes.\",\n//\t\tType:        \"summary\"}\n//\n//\tstandardMetrics = []*Metric{\n//\t\treqCounter,\n//\t\treqDuration,\n//\t\tresSize,\n//\t\treqSize,\n//\t}\n//)\n//\n///*\n//RequestCounterURLLabelMappingFn is a function which can be supplied to the middleware to control\n//the cardinality of the request counter's \"url\" label, which might be required in some contexts.\n//For instance, if for a \"/customer/:name\" route you don't want to generate a time series for every\n//possible customer name, you could use this function:\n//\n//\tfunc(c *gin.Context) string {\n//\t\turl := c.Request.URL.Path\n//\t\tfor _, p := range c.Params {\n//\t\t\tif p.Key == \"name\" {\n//\t\t\t\turl = strings.Replace(url, p.Value, \":name\", 1)\n//\t\t\t\tbreak\n//\t\t\t}\n//\t\t}\n//\t\treturn url\n//\t}\n//\n//which would map \"/customer/alice\" and \"/customer/bob\" to their template \"/customer/:name\".\n//*/\n//type RequestCounterURLLabelMappingFn func(c *gin.Context) string\n//\n//// Metric is a definition for the name, description, type, ID, and\n//// prometheus.Collector type (i.e. CounterVec, Summary, etc) of each metric.\n//type Metric struct {\n//\tMetricCollector prometheus.Collector\n//\tID              string\n//\tName            string\n//\tDescription     string\n//\tType            string\n//\tArgs            []string\n//}\n//\n//// Prometheus contains the metrics gathered by the instance and its path.\n//type Prometheus struct {\n//\treqCnt        *prometheus.CounterVec\n//\treqDur        *prometheus.HistogramVec\n//\treqSz, resSz  prometheus.Summary\n//\trouter        *gin.Engine\n//\tlistenAddress string\n//\tPpg           PrometheusPushGateway\n//\n//\tMetricsList []*Metric\n//\tMetricsPath string\n//\n//\tReqCntURLLabelMappingFn RequestCounterURLLabelMappingFn\n//\n//\t// gin.Context string to use as a prometheus URL label\n//\tURLLabelFromContext string\n//}\n//\n//// PrometheusPushGateway contains the configuration for pushing to a Prometheus pushgateway (optional).\n//type PrometheusPushGateway struct {\n//\n//\t// Push interval in seconds\n//\tPushIntervalSeconds time.Duration\n//\n//\t// Push Gateway URL in format http://domain:port\n//\t// where JOBNAME can be any string of your choice\n//\tPushGatewayURL string\n//\n//\t// Local metrics URL where metrics are fetched from, this could be omitted in the future\n//\t// if implemented using prometheus common/expfmt instead\n//\tMetricsURL string\n//\n//\t// pushgateway job name, defaults to \"gin\"\n//\tJob string\n//}\n//\n//// NewPrometheus generates a new set of metrics with a certain subsystem name.\n//func NewPrometheus(subsystem string, customMetricsList ...[]*Metric) *Prometheus {\n//\tif subsystem == \"\" {\n//\t\tsubsystem = \"app\"\n//\t}\n//\n//\tvar metricsList []*Metric\n//\n//\tif len(customMetricsList) > 1 {\n//\t\tpanic(\"Too many args. NewPrometheus( string, <optional []*Metric> ).\")\n//\t} else if len(customMetricsList) == 1 {\n//\t\tmetricsList = customMetricsList[0]\n//\t}\n//\tmetricsList = append(metricsList, standardMetrics...)\n//\n//\tp := &Prometheus{\n//\t\tMetricsList: metricsList,\n//\t\tMetricsPath: defaultMetricPath,\n//\t\tReqCntURLLabelMappingFn: func(c *gin.Context) string {\n//\t\t\treturn c.FullPath() // e.g. /user/:id , /user/:id/info\n//\t\t},\n//\t}\n//\n//\tp.registerMetrics(subsystem)\n//\n//\treturn p\n//}\n//\n//// SetPushGateway sends metrics to a remote pushgateway exposed on pushGatewayURL\n//// every pushIntervalSeconds. Metrics are fetched from metricsURL.\n//func (p *Prometheus) SetPushGateway(pushGatewayURL, metricsURL string, pushIntervalSeconds time.Duration) {\n//\tp.Ppg.PushGatewayURL = pushGatewayURL\n//\tp.Ppg.MetricsURL = metricsURL\n//\tp.Ppg.PushIntervalSeconds = pushIntervalSeconds\n//\tp.startPushTicker()\n//}\n//\n//// SetPushGatewayJob job name, defaults to \"gin\".\n//func (p *Prometheus) SetPushGatewayJob(j string) {\n//\tp.Ppg.Job = j\n//}\n//\n//// SetListenAddress for exposing metrics on address. If not set, it will be exposed at the\n//// same address of the gin engine that is being used.\n//func (p *Prometheus) SetListenAddress(address string) {\n//\tp.listenAddress = address\n//\tif p.listenAddress != \"\" {\n//\t\tp.router = gin.Default()\n//\t}\n//}\n//\n//// SetListenAddressWithRouter for using a separate router to expose metrics. (this keeps things like GET /metrics out of\n//// your content's access log).\n//func (p *Prometheus) SetListenAddressWithRouter(listenAddress string, r *gin.Engine) {\n//\tp.listenAddress = listenAddress\n//\tif len(p.listenAddress) > 0 {\n//\t\tp.router = r\n//\t}\n//}\n//\n//// SetMetricsPath set metrics paths.\n//func (p *Prometheus) SetMetricsPath(e *gin.Engine) error {\n//\n//\tif p.listenAddress != \"\" {\n//\t\tp.router.GET(p.MetricsPath, prometheusHandler())\n//\t\treturn p.runServer()\n//\t} else {\n//\t\te.GET(p.MetricsPath, prometheusHandler())\n//\t\treturn nil\n//\t}\n//}\n//\n//// SetMetricsPathWithAuth set metrics paths with authentication.\n//func (p *Prometheus) SetMetricsPathWithAuth(e *gin.Engine, accounts gin.Accounts) error {\n//\n//\tif p.listenAddress != \"\" {\n//\t\tp.router.GET(p.MetricsPath, gin.BasicAuth(accounts), prometheusHandler())\n//\t\treturn p.runServer()\n//\t} else {\n//\t\te.GET(p.MetricsPath, gin.BasicAuth(accounts), prometheusHandler())\n//\t\treturn nil\n//\t}\n//\n//}\n//\n//func (p *Prometheus) runServer() error {\n//\treturn p.router.Run(p.listenAddress)\n//}\n//\n//func (p *Prometheus) getMetrics() []byte {\n//\tresponse, err := http.Get(p.Ppg.MetricsURL)\n//\tif err != nil {\n//\t\treturn nil\n//\t}\n//\n//\tdefer response.Body.Close()\n//\n//\tbody, _ := io.ReadAll(response.Body)\n//\treturn body\n//}\n//\n//var hostname, _ = os.Hostname()\n//\n//func (p *Prometheus) getPushGatewayURL() string {\n//\tif p.Ppg.Job == \"\" {\n//\t\tp.Ppg.Job = \"gin\"\n//\t}\n//\treturn p.Ppg.PushGatewayURL + \"/metrics/job/\" + p.Ppg.Job + \"/instance/\" + hostname\n//}\n//\n//func (p *Prometheus) sendMetricsToPushGateway(metrics []byte) {\n//\treq, err := http.NewRequest(\"POST\", p.getPushGatewayURL(), bytes.NewBuffer(metrics))\n//\tif err != nil {\n//\t\treturn\n//\t}\n//\n//\tclient := &http.Client{}\n//\tresp, err := client.Do(req)\n//\tif err != nil {\n//\t\tfmt.Println(\"Error sending to push gateway error:\", err.Error())\n//\t}\n//\n//\tresp.Body.Close()\n//}\n//\n//func (p *Prometheus) startPushTicker() {\n//\tticker := time.NewTicker(time.Second * p.Ppg.PushIntervalSeconds)\n//\tgo func() {\n//\t\tfor range ticker.C {\n//\t\t\tp.sendMetricsToPushGateway(p.getMetrics())\n//\t\t}\n//\t}()\n//}\n//\n//// NewMetric associates prometheus.Collector based on Metric.Type.\n//func NewMetric(m *Metric, subsystem string) prometheus.Collector {\n//\tvar metric prometheus.Collector\n//\tswitch m.Type {\n//\tcase \"counter_vec\":\n//\t\tmetric = prometheus.NewCounterVec(\n//\t\t\tprometheus.CounterOpts{\n//\t\t\t\tSubsystem: subsystem,\n//\t\t\t\tName:      m.Name,\n//\t\t\t\tHelp:      m.Description,\n//\t\t\t},\n//\t\t\tm.Args,\n//\t\t)\n//\tcase \"counter\":\n//\t\tmetric = prometheus.NewCounter(\n//\t\t\tprometheus.CounterOpts{\n//\t\t\t\tSubsystem: subsystem,\n//\t\t\t\tName:      m.Name,\n//\t\t\t\tHelp:      m.Description,\n//\t\t\t},\n//\t\t)\n//\tcase \"gauge_vec\":\n//\t\tmetric = prometheus.NewGaugeVec(\n//\t\t\tprometheus.GaugeOpts{\n//\t\t\t\tSubsystem: subsystem,\n//\t\t\t\tName:      m.Name,\n//\t\t\t\tHelp:      m.Description,\n//\t\t\t},\n//\t\t\tm.Args,\n//\t\t)\n//\tcase \"gauge\":\n//\t\tmetric = prometheus.NewGauge(\n//\t\t\tprometheus.GaugeOpts{\n//\t\t\t\tSubsystem: subsystem,\n//\t\t\t\tName:      m.Name,\n//\t\t\t\tHelp:      m.Description,\n//\t\t\t},\n//\t\t)\n//\tcase \"histogram_vec\":\n//\t\tmetric = prometheus.NewHistogramVec(\n//\t\t\tprometheus.HistogramOpts{\n//\t\t\t\tSubsystem: subsystem,\n//\t\t\t\tName:      m.Name,\n//\t\t\t\tHelp:      m.Description,\n//\t\t\t},\n//\t\t\tm.Args,\n//\t\t)\n//\tcase \"histogram\":\n//\t\tmetric = prometheus.NewHistogram(\n//\t\t\tprometheus.HistogramOpts{\n//\t\t\t\tSubsystem: subsystem,\n//\t\t\t\tName:      m.Name,\n//\t\t\t\tHelp:      m.Description,\n//\t\t\t},\n//\t\t)\n//\tcase \"summary_vec\":\n//\t\tmetric = prometheus.NewSummaryVec(\n//\t\t\tprometheus.SummaryOpts{\n//\t\t\t\tSubsystem: subsystem,\n//\t\t\t\tName:      m.Name,\n//\t\t\t\tHelp:      m.Description,\n//\t\t\t},\n//\t\t\tm.Args,\n//\t\t)\n//\tcase \"summary\":\n//\t\tmetric = prometheus.NewSummary(\n//\t\t\tprometheus.SummaryOpts{\n//\t\t\t\tSubsystem: subsystem,\n//\t\t\t\tName:      m.Name,\n//\t\t\t\tHelp:      m.Description,\n//\t\t\t},\n//\t\t)\n//\t}\n//\treturn metric\n//}\n//\n//func (p *Prometheus) registerMetrics(subsystem string) {\n//\tfor _, metricDef := range p.MetricsList {\n//\t\tmetric := NewMetric(metricDef, subsystem)\n//\t\tif err := prometheus.Register(metric); err != nil {\n//\t\t\tfmt.Println(\"could not be registered in Prometheus,metricDef.Name:\", metricDef.Name, \"   error:\", err.Error())\n//\t\t}\n//\n//\t\tswitch metricDef {\n//\t\tcase reqCounter:\n//\t\t\tp.reqCnt = metric.(*prometheus.CounterVec)\n//\t\tcase reqDuration:\n//\t\t\tp.reqDur = metric.(*prometheus.HistogramVec)\n//\t\tcase resSize:\n//\t\t\tp.resSz = metric.(prometheus.Summary)\n//\t\tcase reqSize:\n//\t\t\tp.reqSz = metric.(prometheus.Summary)\n//\t\t}\n//\t\tmetricDef.MetricCollector = metric\n//\t}\n//}\n//\n//// Use adds the middleware to a gin engine.\n//func (p *Prometheus) Use(e *gin.Engine) error {\n//\te.Use(p.HandlerFunc())\n//\treturn p.SetMetricsPath(e)\n//}\n//\n//// UseWithAuth adds the middleware to a gin engine with BasicAuth.\n//func (p *Prometheus) UseWithAuth(e *gin.Engine, accounts gin.Accounts) error {\n//\te.Use(p.HandlerFunc())\n//\treturn p.SetMetricsPathWithAuth(e, accounts)\n//}\n//\n//// HandlerFunc defines handler function for middleware.\n//func (p *Prometheus) HandlerFunc() gin.HandlerFunc {\n//\treturn func(c *gin.Context) {\n//\t\tif c.Request.URL.Path == p.MetricsPath {\n//\t\t\tc.Next()\n//\t\t\treturn\n//\t\t}\n//\n//\t\tstart := time.Now()\n//\t\treqSz := computeApproximateRequestSize(c.Request)\n//\n//\t\tc.Next()\n//\n//\t\tstatus := strconv.Itoa(c.Writer.Status())\n//\t\telapsed := float64(time.Since(start)) / float64(time.Second)\n//\t\tresSz := float64(c.Writer.Size())\n//\n//\t\turl := p.ReqCntURLLabelMappingFn(c)\n//\t\tif len(p.URLLabelFromContext) > 0 {\n//\t\t\tu, found := c.Get(p.URLLabelFromContext)\n//\t\t\tif !found {\n//\t\t\t\tu = \"unknown\"\n//\t\t\t}\n//\t\t\turl = u.(string)\n//\t\t}\n//\t\tp.reqDur.WithLabelValues(status, c.Request.Method, url).Observe(elapsed)\n//\t\tp.reqCnt.WithLabelValues(status, c.Request.Method, c.HandlerName(), c.Request.Host, url).Inc()\n//\t\tp.reqSz.Observe(float64(reqSz))\n//\t\tp.resSz.Observe(resSz)\n//\t}\n//}\n//\n//func prometheusHandler() gin.HandlerFunc {\n//\th := promhttp.Handler()\n//\treturn func(c *gin.Context) {\n//\t\th.ServeHTTP(c.Writer, c.Request)\n//\t}\n//}\n//\n//func computeApproximateRequestSize(r *http.Request) int {\n//\tvar s int\n//\tif r.URL != nil {\n//\t\ts = len(r.URL.Path)\n//\t}\n//\n//\ts += len(r.Method)\n//\ts += len(r.Proto)\n//\tfor name, values := range r.Header {\n//\t\ts += len(name)\n//\t\tfor _, value := range values {\n//\t\t\ts += len(value)\n//\t\t}\n//\t}\n//\ts += len(r.Host)\n//\n//\t// r.FormData and r.MultipartForm are assumed to be included in r.URL.\n//\n//\tif r.ContentLength != -1 {\n//\t\ts += int(r.ContentLength)\n//\t}\n//\treturn s\n//}\n"
  },
  {
    "path": "pkg/common/prommetrics/api.go",
    "content": "package prommetrics\n\nimport (\n\t\"net\"\n\t\"strconv\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\nvar (\n\tapiCounter = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tName: \"api_count\",\n\t\t\tHelp: \"Total number of API calls\",\n\t\t},\n\t\t[]string{\"path\", \"method\", \"code\"},\n\t)\n\thttpCounter = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tName: \"http_count\",\n\t\t\tHelp: \"Total number of HTTP calls\",\n\t\t},\n\t\t[]string{\"path\", \"method\", \"status\"},\n\t)\n)\n\nfunc RegistryApi() {\n\tregistry.MustRegister(apiCounter, httpCounter)\n}\n\nfunc ApiInit(listener net.Listener) error {\n\tapiRegistry := prometheus.NewRegistry()\n\tcs := append(\n\t\tbaseCollector,\n\t\tapiCounter,\n\t\thttpCounter,\n\t)\n\treturn Init(apiRegistry, listener, commonPath, promhttp.HandlerFor(apiRegistry, promhttp.HandlerOpts{}), cs...)\n}\n\nfunc APICall(path string, method string, apiCode int) {\n\tapiCounter.With(prometheus.Labels{\"path\": path, \"method\": method, \"code\": strconv.Itoa(apiCode)}).Inc()\n}\n\nfunc HttpCall(path string, method string, status int) {\n\thttpCounter.With(prometheus.Labels{\"path\": path, \"method\": method, \"status\": strconv.Itoa(status)}).Inc()\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/grpc_auth.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage prommetrics\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar (\n\tUserLoginCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"user_login_total\",\n\t\tHelp: \"The number of user login\",\n\t})\n)\n\nfunc RegistryAuth() {\n\tregistry.MustRegister(UserLoginCounter)\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/grpc_msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage prommetrics\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar (\n\tSingleChatMsgProcessSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"single_chat_msg_process_success_total\",\n\t\tHelp: \"The number of single chat msg successful processed\",\n\t})\n\tSingleChatMsgProcessFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"single_chat_msg_process_failed_total\",\n\t\tHelp: \"The number of single chat msg failed processed\",\n\t})\n\tGroupChatMsgProcessSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"group_chat_msg_process_success_total\",\n\t\tHelp: \"The number of group chat msg successful processed\",\n\t})\n\tGroupChatMsgProcessFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"group_chat_msg_process_failed_total\",\n\t\tHelp: \"The number of group chat msg failed processed\",\n\t})\n)\n\nfunc RegistryMsg() {\n\tregistry.MustRegister(\n\t\tSingleChatMsgProcessSuccessCounter,\n\t\tSingleChatMsgProcessFailedCounter,\n\t\tGroupChatMsgProcessSuccessCounter,\n\t\tGroupChatMsgProcessFailedCounter,\n\t)\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/grpc_msggateway.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage prommetrics\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar (\n\tOnlineUserGauge = prometheus.NewGauge(prometheus.GaugeOpts{\n\t\tName: \"online_user_num\",\n\t\tHelp: \"The number of online user num\",\n\t})\n)\n\nfunc RegistryMsgGateway() {\n\tregistry.MustRegister(OnlineUserGauge)\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/grpc_push.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage prommetrics\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar (\n\tMsgOfflinePushFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"msg_offline_push_failed_total\",\n\t\tHelp: \"The number of msg failed offline pushed\",\n\t})\n\tMsgLoneTimePushCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"msg_long_time_push_total\",\n\t\tHelp: \"The number of messages with a push time exceeding 10 seconds\",\n\t})\n)\n\nfunc RegistryPush() {\n\tregistry.MustRegister(\n\t\tMsgOfflinePushFailedCounter,\n\t\tMsgLoneTimePushCounter,\n\t)\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/grpc_user.go",
    "content": "package prommetrics\n\nimport \"github.com/prometheus/client_golang/prometheus\"\n\nvar (\n\tUserRegisterCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"user_register_total\",\n\t\tHelp: \"The number of user login\",\n\t})\n)\n\nfunc RegistryUser() {\n\tregistry.MustRegister(UserRegisterCounter)\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/prommetrics.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage prommetrics\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/collectors\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\nconst commonPath = \"/metrics\"\n\nvar registry = &prometheusRegistry{prometheus.NewRegistry()}\n\ntype prometheusRegistry struct {\n\t*prometheus.Registry\n}\n\nfunc (x *prometheusRegistry) MustRegister(cs ...prometheus.Collector) {\n\tfor _, c := range cs {\n\t\tif err := x.Registry.Register(c); err != nil {\n\t\t\tif errors.As(err, &prometheus.AlreadyRegisteredError{}) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc init() {\n\tregistry.MustRegister(\n\t\tcollectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),\n\t\tcollectors.NewGoCollector(),\n\t)\n}\n\nvar (\n\tbaseCollector = []prometheus.Collector{\n\t\tcollectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),\n\t\tcollectors.NewGoCollector(),\n\t}\n)\n\nfunc Init(registry *prometheus.Registry, listener net.Listener, path string, handler http.Handler, cs ...prometheus.Collector) error {\n\tregistry.MustRegister(cs...)\n\tsrv := http.NewServeMux()\n\tsrv.Handle(path, handler)\n\treturn http.Serve(listener, srv)\n}\n\nfunc RegistryAll() {\n\tRegistryApi()\n\tRegistryAuth()\n\tRegistryMsg()\n\tRegistryMsgGateway()\n\tRegistryPush()\n\tRegistryUser()\n\tRegistryRpc()\n\tRegistryTransfer()\n}\n\nfunc Start(listener net.Listener) error {\n\tsrv := http.NewServeMux()\n\tsrv.Handle(commonPath, promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))\n\treturn http.Serve(listener, srv)\n}\n\nconst (\n\tAPIKeyName             = \"api\"\n\tMessageTransferKeyName = \"message-transfer\"\n\n\tTTL = 300\n)\n\ntype Target struct {\n\tTarget string            `json:\"target\"`\n\tLabels map[string]string `json:\"labels\"`\n}\n\ntype RespTarget struct {\n\tTargets []string          `json:\"targets\"`\n\tLabels  map[string]string `json:\"labels\"`\n}\n\nfunc BuildDiscoveryKeyPrefix(name string) string {\n\treturn fmt.Sprintf(\"%s/%s/%s\", \"openim\", \"prometheus_discovery\", name)\n}\n\nfunc BuildDiscoveryKey(name string, index int) string {\n\treturn fmt.Sprintf(\"%s/%s/%s/%d\", \"openim\", \"prometheus_discovery\", name, index)\n}\n\nfunc BuildDefaultTarget(host string, ip int) Target {\n\treturn Target{\n\t\tTarget: fmt.Sprintf(\"%s:%d\", host, ip),\n\t\tLabels: map[string]string{\n\t\t\t\"namespace\": \"default\",\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/prommetrics_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage prommetrics\n\nimport \"testing\"\n\n//func TestNewGrpcPromObj(t *testing.T) {\n//\t// Create a custom metric to pass into the NewGrpcPromObj function.\n//\tcustomMetric := prometheus.NewCounter(prometheus.CounterOpts{\n//\t\tName: \"test_metric\",\n//\t\tHelp: \"This is a test metric.\",\n//\t})\n//\tcusMetrics := []prometheus.Collector{customMetric}\n//\n//\t// Call NewGrpcPromObj with the custom metrics.\n//\treg, grpcMetrics, err := NewGrpcPromObj(cusMetrics)\n//\n//\t// Assert no error was returned.\n//\tassert.NoError(t, err)\n//\n//\t// Assert the registry was correctly initialized.\n//\tassert.NotNil(t, reg)\n//\n//\t// Assert the grpcMetrics was correctly initialized.\n//\tassert.NotNil(t, grpcMetrics)\n//\n//\t// Assert that the custom metric is registered.\n//\tmfs, err := reg.Gather()\n//\tassert.NoError(t, err)\n//\tassert.NotEmpty(t, mfs) // Ensure some metrics are present.\n//\tfound := false\n//\tfor _, mf := range mfs {\n//\t\tif *mf.Name == \"test_metric\" {\n//\t\t\tfound = true\n//\t\t\tbreak\n//\t\t}\n//\t}\n//\tassert.True(t, found, \"Custom metric not found in registry\")\n//}\n\n//func TestGetGrpcCusMetrics(t *testing.T) {\n//\tconf := config2.NewGlobalConfig()\n//\n//\tconfig2.InitConfig(conf, \"../../config\")\n//\t// Test various cases based on the switch statement in the GetGrpcCusMetrics function.\n//\ttestCases := []struct {\n//\t\tname     string\n//\t\texpected int // The expected number of metrics for each case.\n//\t}{\n//\t\t{conf.RpcRegisterName.OpenImMessageGatewayName, 1},\n//\t}\n//\n//\tfor _, tc := range testCases {\n//\t\tt.Run(tc.name, func(t *testing.T) {\n//\t\t\tmetrics := GetGrpcCusMetrics(tc.name, &conf.RpcRegisterName)\n//\t\t\tassert.Len(t, metrics, tc.expected)\n//\t\t})\n//\t}\n//}\n\nfunc TestName(t *testing.T) {\n\tRegistryApi()\n\tRegistryApi()\n\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/rpc.go",
    "content": "package prommetrics\n\nimport (\n\t\"net\"\n\t\"strconv\"\n\n\tgp \"github.com/grpc-ecosystem/go-grpc-prometheus\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\nconst rpcPath = commonPath\n\nvar (\n\tgrpcMetrics *gp.ServerMetrics\n\trpcCounter  = prometheus.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tName: \"rpc_count\",\n\t\t\tHelp: \"Total number of RPC calls\",\n\t\t},\n\t\t[]string{\"name\", \"path\", \"code\"},\n\t)\n)\n\nfunc RegistryRpc() {\n\tregistry.MustRegister(rpcCounter)\n}\n\nfunc RpcInit(cs []prometheus.Collector, listener net.Listener) error {\n\treg := prometheus.NewRegistry()\n\tcs = append(append(\n\t\tbaseCollector,\n\t\trpcCounter,\n\t), cs...)\n\treturn Init(reg, listener, rpcPath, promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}), cs...)\n}\n\nfunc RPCCall(name string, path string, code int) {\n\trpcCounter.With(prometheus.Labels{\"name\": name, \"path\": path, \"code\": strconv.Itoa(code)}).Inc()\n}\n\nfunc GetGrpcServerMetrics() *gp.ServerMetrics {\n\tif grpcMetrics == nil {\n\t\tgrpcMetrics = gp.NewServerMetrics()\n\t\tgrpcMetrics.EnableHandlingTimeHistogram()\n\t}\n\treturn grpcMetrics\n}\n\nfunc GetGrpcCusMetrics(registerName string, discovery *config.Discovery) []prometheus.Collector {\n\tswitch registerName {\n\tcase discovery.RpcService.MessageGateway:\n\t\treturn []prometheus.Collector{OnlineUserGauge}\n\tcase discovery.RpcService.Msg:\n\t\treturn []prometheus.Collector{\n\t\t\tSingleChatMsgProcessSuccessCounter,\n\t\t\tSingleChatMsgProcessFailedCounter,\n\t\t\tGroupChatMsgProcessSuccessCounter,\n\t\t\tGroupChatMsgProcessFailedCounter,\n\t\t}\n\tcase discovery.RpcService.Push:\n\t\treturn []prometheus.Collector{\n\t\t\tMsgOfflinePushFailedCounter,\n\t\t\tMsgLoneTimePushCounter,\n\t\t}\n\tcase discovery.RpcService.Auth:\n\t\treturn []prometheus.Collector{UserLoginCounter}\n\tcase discovery.RpcService.User:\n\t\treturn []prometheus.Collector{UserRegisterCounter}\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "pkg/common/prommetrics/transfer.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage prommetrics\n\nimport (\n\t\"net\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\nvar (\n\tMsgInsertRedisSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"msg_insert_redis_success_total\",\n\t\tHelp: \"The number of successful insert msg to redis\",\n\t})\n\tMsgInsertRedisFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"msg_insert_redis_failed_total\",\n\t\tHelp: \"The number of failed insert msg to redis\",\n\t})\n\tMsgInsertMongoSuccessCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"msg_insert_mongo_success_total\",\n\t\tHelp: \"The number of successful insert msg to mongo\",\n\t})\n\tMsgInsertMongoFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"msg_insert_mongo_failed_total\",\n\t\tHelp: \"The number of failed insert msg to mongo\",\n\t})\n\tSeqSetFailedCounter = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"seq_set_failed_total\",\n\t\tHelp: \"The number of failed set seq\",\n\t})\n)\n\nfunc RegistryTransfer() {\n\tregistry.MustRegister(\n\t\tMsgInsertRedisSuccessCounter,\n\t\tMsgInsertRedisFailedCounter,\n\t\tMsgInsertMongoSuccessCounter,\n\t\tMsgInsertMongoFailedCounter,\n\t\tSeqSetFailedCounter,\n\t)\n}\n\nfunc TransferInit(listener net.Listener) error {\n\treg := prometheus.NewRegistry()\n\tcs := append(\n\t\tbaseCollector,\n\t\tMsgInsertRedisSuccessCounter,\n\t\tMsgInsertRedisFailedCounter,\n\t\tMsgInsertMongoSuccessCounter,\n\t\tMsgInsertMongoFailedCounter,\n\t\tSeqSetFailedCounter,\n\t)\n\treturn Init(reg, listener, commonPath, promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}), cs...)\n}\n"
  },
  {
    "path": "pkg/common/servererrs/code.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage servererrs\n\n// UnknownCode represents the error code when code is not parsed or parsed code equals 0.\nconst UnknownCode = 1000\n\n// Error codes for various error scenarios.\nconst (\n\tFormattingError      = 10001 // Error in formatting\n\tHasRegistered        = 10002 // user has already registered\n\tNotRegistered        = 10003 // user is not registered\n\tPasswordErr          = 10004 // Password error\n\tGetIMTokenErr        = 10005 // Error in getting IM token\n\tRepeatSendCode       = 10006 // Repeat sending code\n\tMailSendCodeErr      = 10007 // Error in sending code via email\n\tSmsSendCodeErr       = 10008 // Error in sending code via SMS\n\tCodeInvalidOrExpired = 10009 // Code is invalid or expired\n\tRegisterFailed       = 10010 // Registration failed\n\tResetPasswordFailed  = 10011 // Resetting password failed\n\tRegisterLimit        = 10012 // Registration limit exceeded\n\tLoginLimit           = 10013 // Login limit exceeded\n\tInvitationError      = 10014 // Error in invitation\n)\n\n// General error codes.\nconst (\n\tNoError = 0 // No error\n\n\tDatabaseError = 90002 // Database error (redis/mysql, etc.)\n\tNetworkError  = 90004 // Network error\n\tDataError     = 90007 // Data error\n\n\tCallbackError = 80000\n\n\t// General error codes.\n\tServerInternalError   = 500  // Server internal error\n\tArgsError             = 1001 // Input parameter error\n\tNoPermissionError     = 1002 // Insufficient permission\n\tDuplicateKeyError     = 1003\n\tRecordNotFoundError   = 1004 // Record does not exist\n\tSecretNotChangedError = 1050 // secret not changed\n\n\t// Account error codes.\n\tUserIDNotFoundError    = 1101 // UserID does not exist or is not registered\n\tRegisteredAlreadyError = 1102 // user is already registered\n\n\t// Group error codes.\n\tGroupIDNotFoundError  = 1201 // GroupID does not exist\n\tGroupIDExisted        = 1202 // GroupID already exists\n\tNotInGroupYetError    = 1203 // Not in the group yet\n\tDismissedAlreadyError = 1204 // Group has already been dismissed\n\tGroupTypeNotSupport   = 1205\n\tGroupRequestHandled   = 1206\n\n\t// Relationship error codes.\n\tCanNotAddYourselfError   = 1301 // Cannot add yourself as a friend\n\tBlockedByPeer            = 1302 // Blocked by the peer\n\tNotPeersFriend           = 1303 // Not the peer's friend\n\tRelationshipAlreadyError = 1304 // Already in a friend relationship\n\tFriendRequestHandled     = 1305 // Friend request has already been handled\n\n\t// Message error codes.\n\tMessageHasReadDisable = 1401\n\tMutedInGroup          = 1402 // Member muted in the group\n\tMutedGroup            = 1403 // Group is muted\n\tMsgAlreadyRevoke      = 1404 // Message already revoked\n\n\t// Token error codes.\n\tTokenExpiredError     = 1501\n\tTokenInvalidError     = 1502\n\tTokenMalformedError   = 1503\n\tTokenNotValidYetError = 1504\n\tTokenUnknownError     = 1505\n\tTokenKickedError      = 1506\n\tTokenNotExistError    = 1507\n\n\t// Long connection gateway error codes.\n\tConnOverMaxNumLimit  = 1601\n\tConnArgsErr          = 1602\n\tPushMsgErr           = 1603\n\tIOSBackgroundPushErr = 1604\n\n\t// S3 error codes.\n\tFileUploadedExpiredError = 1701 // Upload expired\n)\n"
  },
  {
    "path": "pkg/common/servererrs/doc.go",
    "content": "package servererrs // import \"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n"
  },
  {
    "path": "pkg/common/servererrs/predefine.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage servererrs\n\nimport \"github.com/openimsdk/tools/errs\"\n\nvar (\n\tErrSecretNotChanged = errs.NewCodeError(SecretNotChangedError, \"secret not changed, please change secret in config/share.yml for security reasons\")\n\n\tErrDatabase         = errs.NewCodeError(DatabaseError, \"DatabaseError\")\n\tErrNetwork          = errs.NewCodeError(NetworkError, \"NetworkError\")\n\tErrCallback         = errs.NewCodeError(CallbackError, \"CallbackError\")\n\tErrCallbackContinue = errs.NewCodeError(CallbackError, \"ErrCallbackContinue\")\n\n\tErrInternalServer = errs.NewCodeError(ServerInternalError, \"ServerInternalError\")\n\tErrArgs           = errs.NewCodeError(ArgsError, \"ArgsError\")\n\tErrNoPermission   = errs.NewCodeError(NoPermissionError, \"NoPermissionError\")\n\tErrDuplicateKey   = errs.NewCodeError(DuplicateKeyError, \"DuplicateKeyError\")\n\tErrRecordNotFound = errs.NewCodeError(RecordNotFoundError, \"RecordNotFoundError\")\n\n\tErrUserIDNotFound  = errs.NewCodeError(UserIDNotFoundError, \"UserIDNotFoundError\")\n\tErrGroupIDNotFound = errs.NewCodeError(GroupIDNotFoundError, \"GroupIDNotFoundError\")\n\tErrGroupIDExisted  = errs.NewCodeError(GroupIDExisted, \"GroupIDExisted\")\n\n\tErrNotInGroupYet       = errs.NewCodeError(NotInGroupYetError, \"NotInGroupYetError\")\n\tErrDismissedAlready    = errs.NewCodeError(DismissedAlreadyError, \"DismissedAlreadyError\")\n\tErrRegisteredAlready   = errs.NewCodeError(RegisteredAlreadyError, \"RegisteredAlreadyError\")\n\tErrGroupTypeNotSupport = errs.NewCodeError(GroupTypeNotSupport, \"\")\n\tErrGroupRequestHandled = errs.NewCodeError(GroupRequestHandled, \"GroupRequestHandled\")\n\n\tErrData             = errs.NewCodeError(DataError, \"DataError\")\n\tErrTokenExpired     = errs.NewCodeError(TokenExpiredError, \"TokenExpiredError\")\n\tErrTokenInvalid     = errs.NewCodeError(TokenInvalidError, \"TokenInvalidError\")         //\n\tErrTokenMalformed   = errs.NewCodeError(TokenMalformedError, \"TokenMalformedError\")     //\n\tErrTokenNotValidYet = errs.NewCodeError(TokenNotValidYetError, \"TokenNotValidYetError\") //\n\tErrTokenUnknown     = errs.NewCodeError(TokenUnknownError, \"TokenUnknownError\")         //\n\tErrTokenKicked      = errs.NewCodeError(TokenKickedError, \"TokenKickedError\")\n\tErrTokenNotExist    = errs.NewCodeError(TokenNotExistError, \"TokenNotExistError\") //\n\n\tErrMessageHasReadDisable = errs.NewCodeError(MessageHasReadDisable, \"MessageHasReadDisable\")\n\n\tErrCanNotAddYourself    = errs.NewCodeError(CanNotAddYourselfError, \"CanNotAddYourselfError\")\n\tErrBlockedByPeer        = errs.NewCodeError(BlockedByPeer, \"BlockedByPeer\")\n\tErrNotPeersFriend       = errs.NewCodeError(NotPeersFriend, \"NotPeersFriend\")\n\tErrRelationshipAlready  = errs.NewCodeError(RelationshipAlreadyError, \"RelationshipAlreadyError\")\n\tErrFriendRequestHandled = errs.NewCodeError(FriendRequestHandled, \"FriendRequestHandled\")\n\n\tErrMutedInGroup     = errs.NewCodeError(MutedInGroup, \"MutedInGroup\")\n\tErrMutedGroup       = errs.NewCodeError(MutedGroup, \"MutedGroup\")\n\tErrMsgAlreadyRevoke = errs.NewCodeError(MsgAlreadyRevoke, \"MsgAlreadyRevoke\")\n\n\tErrConnOverMaxNumLimit = errs.NewCodeError(ConnOverMaxNumLimit, \"ConnOverMaxNumLimit\")\n\n\tErrConnArgsErr          = errs.NewCodeError(ConnArgsErr, \"args err, need token, sendID, platformID\")\n\tErrPushMsgErr           = errs.NewCodeError(PushMsgErr, \"push msg err\")\n\tErrIOSBackgroundPushErr = errs.NewCodeError(IOSBackgroundPushErr, \"ios background push err\")\n\n\tErrFileUploadedExpired = errs.NewCodeError(FileUploadedExpiredError, \"FileUploadedExpiredError\")\n)\n"
  },
  {
    "path": "pkg/common/servererrs/relation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage servererrs\n\nimport \"github.com/openimsdk/tools/errs\"\n\nvar Relation = &relation{m: make(map[int]map[int]struct{})}\n\nfunc init() {\n\tRelation.Add(errs.RecordNotFoundError, UserIDNotFoundError)\n\tRelation.Add(errs.RecordNotFoundError, GroupIDNotFoundError)\n\tRelation.Add(errs.DuplicateKeyError, GroupIDExisted)\n}\n\ntype relation struct {\n\tm map[int]map[int]struct{}\n}\n\nfunc (r *relation) Add(codes ...int) {\n\tif len(codes) < 2 {\n\t\tpanic(\"codes length must be greater than 2\")\n\t}\n\tfor i := 1; i < len(codes); i++ {\n\t\tparent := codes[i-1]\n\t\ts, ok := r.m[parent]\n\t\tif !ok {\n\t\t\ts = make(map[int]struct{})\n\t\t\tr.m[parent] = s\n\t\t}\n\t\tfor _, code := range codes[i:] {\n\t\t\ts[code] = struct{}{}\n\t\t}\n\t}\n}\n\nfunc (r *relation) Is(parent, child int) bool {\n\tif parent == child {\n\t\treturn true\n\t}\n\ts, ok := r.m[parent]\n\tif !ok {\n\t\treturn false\n\t}\n\t_, ok = s[child]\n\treturn ok\n}\n"
  },
  {
    "path": "pkg/common/startrpc/circuitbreaker.go",
    "content": "package startrpc\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/stability/circuitbreaker\"\n\t\"github.com/openimsdk/tools/stability/circuitbreaker/sre\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n)\n\ntype CircuitBreaker struct {\n\tEnable  bool          `yaml:\"enable\"`\n\tSuccess float64       `yaml:\"success\"` // success rate threshold (0.0-1.0)\n\tRequest int64         `yaml:\"request\"` // request threshold\n\tBucket  int           `yaml:\"bucket\"`  // number of buckets\n\tWindow  time.Duration `yaml:\"window\"`  // time window for statistics\n}\n\nfunc NewCircuitBreaker(config *CircuitBreaker) circuitbreaker.CircuitBreaker {\n\tif !config.Enable {\n\t\treturn nil\n\t}\n\n\treturn sre.NewSREBraker(\n\t\tsre.WithWindow(config.Window),\n\t\tsre.WithBucket(config.Bucket),\n\t\tsre.WithSuccess(config.Success),\n\t\tsre.WithRequest(config.Request),\n\t)\n}\n\nfunc UnaryCircuitBreakerInterceptor(breaker circuitbreaker.CircuitBreaker) grpc.ServerOption {\n\tif breaker == nil {\n\t\treturn grpc.ChainUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {\n\t\t\treturn handler(ctx, req)\n\t\t})\n\t}\n\n\treturn grpc.ChainUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {\n\t\tif err := breaker.Allow(); err != nil {\n\t\t\tlog.ZWarn(ctx, \"rpc circuit breaker open\", err, \"method\", info.FullMethod)\n\t\t\treturn nil, status.Error(codes.Unavailable, \"service unavailable due to circuit breaker\")\n\t\t}\n\n\t\tresp, err = handler(ctx, req)\n\n\t\tif err != nil {\n\t\t\tif st, ok := status.FromError(err); ok {\n\t\t\t\tswitch st.Code() {\n\t\t\t\tcase codes.OK:\n\t\t\t\t\tbreaker.MarkSuccess()\n\t\t\t\tcase codes.InvalidArgument, codes.NotFound, codes.AlreadyExists, codes.PermissionDenied:\n\t\t\t\t\tbreaker.MarkSuccess()\n\t\t\t\tdefault:\n\t\t\t\t\tbreaker.MarkFailed()\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbreaker.MarkFailed()\n\t\t\t}\n\t\t} else {\n\t\t\tbreaker.MarkSuccess()\n\t\t}\n\n\t\treturn resp, err\n\n\t})\n}\n\nfunc StreamCircuitBreakerInterceptor(breaker circuitbreaker.CircuitBreaker) grpc.ServerOption {\n\tif breaker == nil {\n\t\treturn grpc.ChainStreamInterceptor(func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {\n\t\t\treturn handler(srv, ss)\n\t\t})\n\t}\n\n\treturn grpc.ChainStreamInterceptor(func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {\n\t\tif err := breaker.Allow(); err != nil {\n\t\t\tlog.ZWarn(ss.Context(), \"rpc circuit breaker open\", err, \"method\", info.FullMethod)\n\t\t\treturn status.Error(codes.Unavailable, \"service unavailable due to circuit breaker\")\n\t\t}\n\n\t\terr := handler(srv, ss)\n\n\t\tif err != nil {\n\t\t\tif st, ok := status.FromError(err); ok {\n\t\t\t\tswitch st.Code() {\n\t\t\t\tcase codes.OK:\n\t\t\t\t\tbreaker.MarkSuccess()\n\t\t\t\tcase codes.InvalidArgument, codes.NotFound, codes.AlreadyExists, codes.PermissionDenied:\n\t\t\t\t\tbreaker.MarkSuccess()\n\t\t\t\tdefault:\n\t\t\t\t\tbreaker.MarkFailed()\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbreaker.MarkFailed()\n\t\t\t}\n\t\t} else {\n\t\t\tbreaker.MarkSuccess()\n\t\t}\n\n\t\treturn err\n\t})\n}\n"
  },
  {
    "path": "pkg/common/startrpc/mw.go",
    "content": "package startrpc\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc grpcServerIMAdminUserID(imAdminUserID []string) grpc.ServerOption {\n\treturn grpc.ChainUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {\n\t\tctx = authverify.WithIMAdminUserIDs(ctx, imAdminUserID)\n\t\treturn handler(ctx, req)\n\t})\n}\n"
  },
  {
    "path": "pkg/common/startrpc/ratelimit.go",
    "content": "package startrpc\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/stability/ratelimit\"\n\t\"github.com/openimsdk/tools/stability/ratelimit/bbr\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n)\n\ntype RateLimiter struct {\n\tEnable       bool\n\tWindow       time.Duration\n\tBucket       int\n\tCPUThreshold int64\n}\n\nfunc NewRateLimiter(config *RateLimiter) ratelimit.Limiter {\n\tif !config.Enable {\n\t\treturn nil\n\t}\n\n\treturn bbr.NewBBRLimiter(\n\t\tbbr.WithWindow(config.Window),\n\t\tbbr.WithBucket(config.Bucket),\n\t\tbbr.WithCPUThreshold(config.CPUThreshold),\n\t)\n}\n\nfunc UnaryRateLimitInterceptor(limiter ratelimit.Limiter) grpc.ServerOption {\n\tif limiter == nil {\n\t\treturn grpc.ChainUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {\n\t\t\treturn handler(ctx, req)\n\t\t})\n\t}\n\n\treturn grpc.ChainUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {\n\t\tdone, err := limiter.Allow()\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"rpc rate limited\", err, \"method\", info.FullMethod)\n\t\t\treturn nil, status.Errorf(codes.ResourceExhausted, \"rpc request rate limit exceeded: %v, please try again later\", err)\n\t\t}\n\n\t\tdefer done(ratelimit.DoneInfo{})\n\t\treturn handler(ctx, req)\n\t})\n}\n\nfunc StreamRateLimitInterceptor(limiter ratelimit.Limiter) grpc.ServerOption {\n\tif limiter == nil {\n\t\treturn grpc.ChainStreamInterceptor(func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {\n\t\t\treturn handler(srv, ss)\n\t\t})\n\t}\n\n\treturn grpc.ChainStreamInterceptor(func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {\n\t\tdone, err := limiter.Allow()\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ss.Context(), \"rpc rate limited\", err, \"method\", info.FullMethod)\n\t\t\treturn status.Errorf(codes.ResourceExhausted, \"rpc request rate limit exceeded: %v, please try again later\", err)\n\t\t}\n\t\tdefer done(ratelimit.DoneInfo{})\n\n\t\treturn handler(srv, ss)\n\t})\n}\n"
  },
  {
    "path": "pkg/common/startrpc/start.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage startrpc\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"syscall\"\n\t\"time\"\n\n\tconf \"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n\t\"github.com/openimsdk/tools/utils/network\"\n\t\"google.golang.org/grpc/status\"\n\n\tkdisc \"github.com/openimsdk/open-im-server/v3/pkg/common/discovery\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics\"\n\t\"github.com/openimsdk/tools/discovery\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\tgrpccli \"github.com/openimsdk/tools/mw/grpc/client\"\n\tgrpcsrv \"github.com/openimsdk/tools/mw/grpc/server\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\nfunc init() {\n\tprommetrics.RegistryAll()\n}\n\nfunc Start[T any](ctx context.Context, disc *conf.Discovery, circuitBreakerConfig *conf.CircuitBreaker, rateLimiterConfig *conf.RateLimiter, prometheusConfig *conf.Prometheus, listenIP,\n\tregisterIP string, autoSetPorts bool, rpcPorts []int, index int, rpcRegisterName string, notification *conf.Notification, config T,\n\twatchConfigNames []string, watchServiceNames []string,\n\trpcFn func(ctx context.Context, config T, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error,\n\toptions ...grpc.ServerOption) error {\n\n\tif notification != nil {\n\t\tconf.InitNotification(notification)\n\t}\n\n\tmaxRequestBody := getConfigRpcMaxRequestBody(reflect.ValueOf(config))\n\tshareConfig := getConfigShare(reflect.ValueOf(config))\n\n\tlog.ZDebug(ctx, \"rpc start\", \"rpcMaxRequestBody\", maxRequestBody, \"rpcRegisterName\", rpcRegisterName, \"registerIP\", registerIP, \"listenIP\", listenIP)\n\n\toptions = append(options,\n\t\tgrpcsrv.GrpcServerMetadataContext(),\n\t\tgrpcsrv.GrpcServerErrorConvert(),\n\t\tgrpcsrv.GrpcServerLogger(),\n\t\tgrpcsrv.GrpcServerRequestValidate(),\n\t\tgrpcsrv.GrpcServerPanicCapture(),\n\t)\n\tif shareConfig != nil && len(shareConfig.IMAdminUser.UserIDs) > 0 {\n\t\toptions = append(options, grpcServerIMAdminUserID(shareConfig.IMAdminUser.UserIDs))\n\t}\n\tvar clientOptions []grpc.DialOption\n\tif maxRequestBody != nil {\n\t\tif maxRequestBody.RequestMaxBodySize > 0 {\n\t\t\toptions = append(options, grpc.MaxRecvMsgSize(maxRequestBody.RequestMaxBodySize))\n\t\t\tclientOptions = append(clientOptions, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(maxRequestBody.RequestMaxBodySize)))\n\t\t}\n\t\tif maxRequestBody.ResponseMaxBodySize > 0 {\n\t\t\toptions = append(options, grpc.MaxSendMsgSize(maxRequestBody.ResponseMaxBodySize))\n\t\t\tclientOptions = append(clientOptions, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxRequestBody.ResponseMaxBodySize)))\n\t\t}\n\t}\n\n\tif circuitBreakerConfig != nil && circuitBreakerConfig.Enable {\n\t\tcb := &CircuitBreaker{\n\t\t\tEnable:  circuitBreakerConfig.Enable,\n\t\t\tSuccess: circuitBreakerConfig.Success,\n\t\t\tRequest: circuitBreakerConfig.Request,\n\t\t\tBucket:  circuitBreakerConfig.Bucket,\n\t\t\tWindow:  circuitBreakerConfig.Window,\n\t\t}\n\n\t\tbreaker := NewCircuitBreaker(cb)\n\n\t\toptions = append(options,\n\t\t\tUnaryCircuitBreakerInterceptor(breaker),\n\t\t\tStreamCircuitBreakerInterceptor(breaker),\n\t\t)\n\n\t\tlog.ZInfo(ctx, \"RPC circuit breaker enabled\",\n\t\t\t\"service\", rpcRegisterName,\n\t\t\t\"window\", circuitBreakerConfig.Window,\n\t\t\t\"bucket\", circuitBreakerConfig.Bucket,\n\t\t\t\"success\", circuitBreakerConfig.Success,\n\t\t\t\"requestThreshold\", circuitBreakerConfig.Request)\n\t}\n\n\tif rateLimiterConfig != nil && rateLimiterConfig.Enable {\n\t\tlimiter := NewRateLimiter((*RateLimiter)(rateLimiterConfig))\n\n\t\toptions = append(options,\n\t\t\tUnaryRateLimitInterceptor(limiter),\n\t\t\tStreamRateLimitInterceptor(limiter),\n\t\t)\n\n\t\tlog.ZInfo(ctx, \"RPC rate limiter enabled\",\n\t\t\t\"service\", rpcRegisterName,\n\t\t\t\"window\", rateLimiterConfig.Window,\n\t\t\t\"bucket\", rateLimiterConfig.Bucket,\n\t\t\t\"cpuThreshold\", rateLimiterConfig.CPUThreshold)\n\t}\n\n\tregisterIP, err := network.GetRpcRegisterIP(registerIP)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar prometheusListenAddr string\n\tif autoSetPorts {\n\t\tprometheusListenAddr = net.JoinHostPort(listenIP, \"0\")\n\t} else {\n\t\tprometheusPort, err := datautil.GetElemByIndex(prometheusConfig.Ports, index)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprometheusListenAddr = net.JoinHostPort(listenIP, strconv.Itoa(prometheusPort))\n\t}\n\n\twatchConfigNames = append(watchConfigNames, conf.LogConfigFileName)\n\n\tclient, err := kdisc.NewDiscoveryRegister(disc, watchServiceNames)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer client.Close()\n\tclient.AddOption(\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\tgrpc.WithDefaultServiceConfig(fmt.Sprintf(`{\"LoadBalancingPolicy\": \"%s\"}`, \"round_robin\")),\n\n\t\tgrpccli.GrpcClientLogger(),\n\t\tgrpccli.GrpcClientContext(),\n\t\tgrpccli.GrpcClientErrorConvert(),\n\t)\n\tif len(clientOptions) > 0 {\n\t\tclient.AddOption(clientOptions...)\n\t}\n\n\tctx, cancel := context.WithCancelCause(ctx)\n\n\tgo func() {\n\t\tsigs := make(chan os.Signal, 1)\n\t\tsignal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase val := <-sigs:\n\t\t\tlog.ZDebug(ctx, \"recv signal\", \"signal\", val.String())\n\t\t\tcancel(fmt.Errorf(\"signal %s\", val.String()))\n\t\t}\n\t}()\n\n\tif prometheusListenAddr != \"\" {\n\t\toptions = append(\n\t\t\toptions,\n\t\t\tprommetricsUnaryInterceptor(rpcRegisterName),\n\t\t\tprommetricsStreamInterceptor(rpcRegisterName),\n\t\t)\n\t\tprometheusListener, prometheusPort, err := listenTCP(prometheusListenAddr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.ZDebug(ctx, \"prometheus start\", \"addr\", prometheusListener.Addr(), \"rpcRegisterName\", rpcRegisterName)\n\t\ttarget, err := jsonutil.JsonMarshal(prommetrics.BuildDefaultTarget(registerIP, prometheusPort))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif autoSetPorts {\n\t\t\tif err = client.SetWithLease(ctx, prommetrics.BuildDiscoveryKey(rpcRegisterName, index), target, prommetrics.TTL); err != nil {\n\t\t\t\tif !errors.Is(err, discovery.ErrNotSupported) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tgo func() {\n\t\t\terr := prommetrics.Start(prometheusListener)\n\t\t\tif err == nil {\n\t\t\t\terr = fmt.Errorf(\"listener done\")\n\t\t\t}\n\t\t\tcancel(fmt.Errorf(\"prommetrics %s %w\", rpcRegisterName, err))\n\t\t}()\n\t}\n\n\tvar (\n\t\trpcServer       *grpc.Server\n\t\trpcGracefulStop chan struct{}\n\t)\n\n\tonGrpcServiceRegistrar := func(desc *grpc.ServiceDesc, impl any) {\n\t\tif rpcServer != nil {\n\t\t\trpcServer.RegisterService(desc, impl)\n\t\t\treturn\n\t\t}\n\t\tvar rpcListenAddr string\n\t\tif autoSetPorts {\n\t\t\trpcListenAddr = net.JoinHostPort(listenIP, \"0\")\n\t\t} else {\n\t\t\trpcPort, err := datautil.GetElemByIndex(rpcPorts, index)\n\t\t\tif err != nil {\n\t\t\t\tcancel(fmt.Errorf(\"rpcPorts index out of range %s %w\", rpcRegisterName, err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\trpcListenAddr = net.JoinHostPort(listenIP, strconv.Itoa(rpcPort))\n\t\t}\n\t\trpcListener, err := net.Listen(\"tcp\", rpcListenAddr)\n\t\tif err != nil {\n\t\t\tcancel(fmt.Errorf(\"listen rpc %s %s %w\", rpcRegisterName, rpcListenAddr, err))\n\t\t\treturn\n\t\t}\n\n\t\trpcServer = grpc.NewServer(options...)\n\t\trpcServer.RegisterService(desc, impl)\n\t\trpcGracefulStop = make(chan struct{})\n\t\trpcPort := rpcListener.Addr().(*net.TCPAddr).Port\n\t\tlog.ZDebug(ctx, \"rpc start register\", \"rpcRegisterName\", rpcRegisterName, \"registerIP\", registerIP, \"rpcPort\", rpcPort)\n\t\tgrpcOpt := grpc.WithTransportCredentials(insecure.NewCredentials())\n\t\trpcGracefulStop = make(chan struct{})\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\trpcServer.GracefulStop()\n\t\t\tclose(rpcGracefulStop)\n\t\t}()\n\t\tif err := client.Register(ctx, rpcRegisterName, registerIP, rpcListener.Addr().(*net.TCPAddr).Port, grpcOpt); err != nil {\n\t\t\tcancel(fmt.Errorf(\"rpc register %s %w\", rpcRegisterName, err))\n\t\t\treturn\n\t\t}\n\n\t\tgo func() {\n\t\t\terr := rpcServer.Serve(rpcListener)\n\t\t\tif err == nil {\n\t\t\t\terr = fmt.Errorf(\"serve end\")\n\t\t\t}\n\t\t\tcancel(fmt.Errorf(\"rpc %s %w\", rpcRegisterName, err))\n\t\t}()\n\t}\n\n\terr = rpcFn(ctx, config, client, &grpcServiceRegistrar{onRegisterService: onGrpcServiceRegistrar})\n\tif err != nil {\n\t\treturn err\n\t}\n\t<-ctx.Done()\n\tlog.ZDebug(ctx, \"cmd wait done\", \"err\", context.Cause(ctx))\n\tif rpcGracefulStop != nil {\n\t\ttimeout := time.NewTimer(time.Second * 15)\n\t\tdefer timeout.Stop()\n\t\tselect {\n\t\tcase <-timeout.C:\n\t\t\tlog.ZWarn(ctx, \"rcp graceful stop timeout\", nil)\n\t\tcase <-rpcGracefulStop:\n\t\t\tlog.ZDebug(ctx, \"rcp graceful stop done\")\n\t\t}\n\t}\n\treturn context.Cause(ctx)\n}\n\nfunc listenTCP(addr string) (net.Listener, int, error) {\n\tlistener, err := net.Listen(\"tcp\", addr)\n\tif err != nil {\n\t\treturn nil, 0, errs.WrapMsg(err, \"listen err\", \"addr\", addr)\n\t}\n\treturn listener, listener.Addr().(*net.TCPAddr).Port, nil\n}\n\nfunc prommetricsUnaryInterceptor(rpcRegisterName string) grpc.ServerOption {\n\tgetCode := func(err error) int {\n\t\tif err == nil {\n\t\t\treturn 0\n\t\t}\n\t\trpcErr, ok := err.(interface{ GRPCStatus() *status.Status })\n\t\tif !ok {\n\t\t\treturn -1\n\t\t}\n\t\treturn int(rpcErr.GRPCStatus().Code())\n\t}\n\treturn grpc.ChainUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {\n\t\tresp, err := handler(ctx, req)\n\t\tprommetrics.RPCCall(rpcRegisterName, info.FullMethod, getCode(err))\n\t\treturn resp, err\n\t})\n}\n\nfunc prommetricsStreamInterceptor(rpcRegisterName string) grpc.ServerOption {\n\treturn grpc.ChainStreamInterceptor()\n}\n\ntype grpcServiceRegistrar struct {\n\tonRegisterService func(desc *grpc.ServiceDesc, impl any)\n}\n\nfunc (x *grpcServiceRegistrar) RegisterService(desc *grpc.ServiceDesc, impl any) {\n\tx.onRegisterService(desc, impl)\n}\n"
  },
  {
    "path": "pkg/common/startrpc/tools.go",
    "content": "package startrpc\n\nimport (\n\t\"reflect\"\n\n\tconf \"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n)\n\nfunc getConfig[T any](value reflect.Value) *T {\n\tfor value.Kind() == reflect.Pointer {\n\t\tvalue = value.Elem()\n\t}\n\tif value.Kind() == reflect.Struct {\n\t\tnum := value.NumField()\n\t\tfor i := 0; i < num; i++ {\n\t\t\tfield := value.Field(i)\n\t\t\tfor field.Kind() == reflect.Pointer {\n\t\t\t\tfield = field.Elem()\n\t\t\t}\n\t\t\tif field.Kind() == reflect.Struct {\n\t\t\t\tif elem, ok := field.Interface().(T); ok {\n\t\t\t\t\treturn &elem\n\t\t\t\t}\n\t\t\t\tif elem := getConfig[T](field); elem != nil {\n\t\t\t\t\treturn elem\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getConfigRpcMaxRequestBody(value reflect.Value) *conf.MaxRequestBody {\n\treturn getConfig[conf.MaxRequestBody](value)\n}\n\nfunc getConfigShare(value reflect.Value) *conf.Share {\n\treturn getConfig[conf.Share](value)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/batch_handler.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n)\n\n// BatchDeleter interface defines a set of methods for batch deleting cache and publishing deletion information.\ntype BatchDeleter interface {\n\t//ChainExecDel method is used for chain calls and must call Clone to prevent memory pollution.\n\tChainExecDel(ctx context.Context) error\n\t//ExecDelWithKeys method directly takes keys for deletion.\n\tExecDelWithKeys(ctx context.Context, keys []string) error\n\t//Clone method creates a copy of the BatchDeleter to avoid modifying the original object.\n\tClone() BatchDeleter\n\t//AddKeys method adds keys to be deleted.\n\tAddKeys(keys ...string)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/black.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cache\n\nimport (\n\t\"context\"\n)\n\ntype BlackCache interface {\n\tBatchDeleter\n\tCloneBlackCache() BlackCache\n\tGetBlackIDs(ctx context.Context, userID string) (blackIDs []string, err error)\n\t// del user's blackIDs msgCache, exec when a user's black list changed\n\tDelBlackIDs(ctx context.Context, userID string) BlackCache\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/black.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey\n\nconst (\n\tBlackIDsKey = \"BLACK_IDS:\"\n\tIsBlackKey  = \"IS_BLACK:\" // local cache\n)\n\nfunc GetBlackIDsKey(ownerUserID string) string {\n\treturn BlackIDsKey + ownerUserID\n\n}\n\nfunc GetIsBlackIDsKey(possibleBlackUserID, userID string) string {\n\treturn IsBlackKey + userID + \"-\" + possibleBlackUserID\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/client_config.go",
    "content": "package cachekey\n\nconst ClientConfig = \"CLIENT_CONFIG\"\n\nfunc GetClientConfigKey(userID string) string {\n\tif userID == \"\" {\n\t\treturn ClientConfig\n\t}\n\treturn ClientConfig + \":\" + userID\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/conversation.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey\n\nconst (\n\tConversationKey                          = \"CONVERSATION:\"\n\tConversationIDsKey                       = \"CONVERSATION_IDS:\"\n\tNotNotifyConversationIDsKey              = \"NOT_NOTIFY_CONVERSATION_IDS:\"\n\tPinnedConversationIDsKey                 = \"PINNED_CONVERSATION_IDS:\"\n\tConversationIDsHashKey                   = \"CONVERSATION_IDS_HASH:\"\n\tConversationHasReadSeqKey                = \"CONVERSATION_HAS_READ_SEQ:\"\n\tRecvMsgOptKey                            = \"RECV_MSG_OPT:\"\n\tSuperGroupRecvMsgNotNotifyUserIDsKey     = \"SUPER_GROUP_RECV_MSG_NOT_NOTIFY_USER_IDS:\"\n\tSuperGroupRecvMsgNotNotifyUserIDsHashKey = \"SUPER_GROUP_RECV_MSG_NOT_NOTIFY_USER_IDS_HASH:\"\n\tConversationNotReceiveMessageUserIDsKey  = \"CONVERSATION_NOT_RECEIVE_MESSAGE_USER_IDS:\"\n\tConversationUserMaxKey                   = \"CONVERSATION_USER_MAX:\"\n)\n\nfunc GetConversationKey(ownerUserID, conversationID string) string {\n\treturn ConversationKey + ownerUserID + \":\" + conversationID\n}\n\nfunc GetConversationIDsKey(ownerUserID string) string {\n\treturn ConversationIDsKey + ownerUserID\n}\n\nfunc GetNotNotifyConversationIDsKey(ownerUserID string) string {\n\treturn NotNotifyConversationIDsKey + ownerUserID\n}\n\nfunc GetPinnedConversationIDs(ownerUserID string) string {\n\treturn PinnedConversationIDsKey + ownerUserID\n}\n\nfunc GetSuperGroupRecvNotNotifyUserIDsKey(groupID string) string {\n\treturn SuperGroupRecvMsgNotNotifyUserIDsKey + groupID\n}\n\nfunc GetRecvMsgOptKey(ownerUserID, conversationID string) string {\n\treturn RecvMsgOptKey + ownerUserID + \":\" + conversationID\n}\n\nfunc GetSuperGroupRecvNotNotifyUserIDsHashKey(groupID string) string {\n\treturn SuperGroupRecvMsgNotNotifyUserIDsHashKey + groupID\n}\n\nfunc GetConversationHasReadSeqKey(ownerUserID, conversationID string) string {\n\treturn ConversationHasReadSeqKey + ownerUserID + \":\" + conversationID\n}\n\nfunc GetConversationNotReceiveMessageUserIDsKey(conversationID string) string {\n\treturn ConversationNotReceiveMessageUserIDsKey + conversationID\n}\n\nfunc GetUserConversationIDsHashKey(ownerUserID string) string {\n\treturn ConversationIDsHashKey + ownerUserID\n}\n\nfunc GetConversationUserMaxVersionKey(userID string) string {\n\treturn ConversationUserMaxKey + userID\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey // import \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cachekey\"\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/friend.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey\n\nconst (\n\tFriendIDsKey        = \"FRIEND_IDS:\"\n\tTwoWayFriendsIDsKey = \"COMMON_FRIENDS_IDS:\"\n\tFriendKey           = \"FRIEND_INFO:\"\n\tIsFriendKey         = \"IS_FRIEND:\" // local cache key\n\t//FriendSyncSortUserIDsKey = \"FRIEND_SYNC_SORT_USER_IDS:\"\n\tFriendMaxVersionKey = \"FRIEND_MAX_VERSION:\"\n)\n\nfunc GetFriendIDsKey(ownerUserID string) string {\n\treturn FriendIDsKey + ownerUserID\n}\n\nfunc GetTwoWayFriendsIDsKey(ownerUserID string) string {\n\treturn TwoWayFriendsIDsKey + ownerUserID\n}\n\nfunc GetFriendKey(ownerUserID, friendUserID string) string {\n\treturn FriendKey + ownerUserID + \"-\" + friendUserID\n}\n\nfunc GetFriendMaxVersionKey(ownerUserID string) string {\n\treturn FriendMaxVersionKey + ownerUserID\n}\n\nfunc GetIsFriendKey(possibleFriendUserID, userID string) string {\n\treturn IsFriendKey + possibleFriendUserID + \"-\" + userID\n}\n\n//func GetFriendSyncSortUserIDsKey(ownerUserID string, count int) string {\n//\treturn FriendSyncSortUserIDsKey + strconv.Itoa(count) + \":\" + ownerUserID\n//}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/group.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\nconst (\n\tgroupExpireTime             = time.Second * 60 * 60 * 12\n\tGroupInfoKey                = \"GROUP_INFO:\"\n\tGroupMemberIDsKey           = \"GROUP_MEMBER_IDS:\"\n\tGroupMembersHashKey         = \"GROUP_MEMBERS_HASH2:\"\n\tGroupMemberInfoKey          = \"GROUP_MEMBER_INFO:\"\n\tJoinedGroupsKey             = \"JOIN_GROUPS_KEY:\"\n\tGroupMemberNumKey           = \"GROUP_MEMBER_NUM_CACHE:\"\n\tGroupRoleLevelMemberIDsKey  = \"GROUP_ROLE_LEVEL_MEMBER_IDS:\"\n\tGroupAdminLevelMemberIDsKey = \"GROUP_ADMIN_LEVEL_MEMBER_IDS:\"\n\tGroupMemberMaxVersionKey    = \"GROUP_MEMBER_MAX_VERSION:\"\n\tGroupJoinMaxVersionKey      = \"GROUP_JOIN_MAX_VERSION:\"\n)\n\nfunc GetGroupInfoKey(groupID string) string {\n\treturn GroupInfoKey + groupID\n}\n\nfunc GetJoinedGroupsKey(userID string) string {\n\treturn JoinedGroupsKey + userID\n}\n\nfunc GetGroupMembersHashKey(groupID string) string {\n\treturn GroupMembersHashKey + groupID\n}\n\nfunc GetGroupMemberIDsKey(groupID string) string {\n\treturn GroupMemberIDsKey + groupID\n}\n\nfunc GetGroupMemberInfoKey(groupID, userID string) string {\n\treturn GroupMemberInfoKey + groupID + \"-\" + userID\n}\n\nfunc GetGroupMemberNumKey(groupID string) string {\n\treturn GroupMemberNumKey + groupID\n}\n\nfunc GetGroupRoleLevelMemberIDsKey(groupID string, roleLevel int32) string {\n\treturn GroupRoleLevelMemberIDsKey + groupID + \"-\" + strconv.Itoa(int(roleLevel))\n}\n\nfunc GetGroupMemberMaxVersionKey(groupID string) string {\n\treturn GroupMemberMaxVersionKey + groupID\n}\n\nfunc GetJoinGroupMaxVersionKey(userID string) string {\n\treturn GroupJoinMaxVersionKey + userID\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/msg.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey\n\nimport (\n\t\"strconv\"\n)\n\nconst (\n\tsendMsgFailedFlag = \"SEND_MSG_FAILED_FLAG:\"\n\tmessageCache      = \"MSG_CACHE:\"\n)\n\nfunc GetMsgCacheKey(conversationID string, seq int64) string {\n\treturn messageCache + conversationID + \":\" + strconv.Itoa(int(seq))\n}\n\nfunc GetSendMsgKey(id string) string {\n\treturn sendMsgFailedFlag + id\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/online.go",
    "content": "package cachekey\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\tOnlineKey     = \"ONLINE:\"\n\tOnlineChannel = \"online_change\"\n\tOnlineExpire  = time.Hour / 2\n)\n\nfunc GetOnlineKey(userID string) string {\n\treturn OnlineKey + userID\n}\n\nfunc GetOnlineKeyUserID(key string) string {\n\treturn strings.TrimPrefix(key, OnlineKey)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/s3.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey\n\nimport \"strconv\"\n\nconst (\n\tobject         = \"OBJECT:\"\n\ts3             = \"S3:\"\n\tminioImageInfo = \"MINIO:IMAGE:\"\n\tminioThumbnail = \"MINIO:THUMBNAIL:\"\n)\n\nfunc GetObjectKey(engine string, name string) string {\n\treturn object + engine + \":\" + name\n}\n\nfunc GetS3Key(engine string, name string) string {\n\treturn s3 + engine + \":\" + name\n}\n\nfunc GetObjectImageInfoKey(key string) string {\n\treturn minioImageInfo + key\n}\n\nfunc GetMinioImageThumbnailKey(key string, format string, width int, height int) string {\n\treturn minioThumbnail + format + \":w\" + strconv.Itoa(width) + \":h\" + strconv.Itoa(height) + \":\" + key\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/seq.go",
    "content": "package cachekey\n\nconst (\n\tMallocSeq        = \"MALLOC_SEQ:\"\n\tMallocMinSeqLock = \"MALLOC_MIN_SEQ:\"\n\n\tSeqUserMaxSeq  = \"SEQ_USER_MAX:\"\n\tSeqUserMinSeq  = \"SEQ_USER_MIN:\"\n\tSeqUserReadSeq = \"SEQ_USER_READ:\"\n)\n\nfunc GetMallocSeqKey(conversationID string) string {\n\treturn MallocSeq + conversationID\n}\n\nfunc GetMallocMinSeqKey(conversationID string) string {\n\treturn MallocMinSeqLock + conversationID\n}\n\nfunc GetSeqUserMaxSeqKey(conversationID string, userID string) string {\n\treturn SeqUserMaxSeq + conversationID + \":\" + userID\n}\n\nfunc GetSeqUserMinSeqKey(conversationID string, userID string) string {\n\treturn SeqUserMinSeq + conversationID + \":\" + userID\n}\n\nfunc GetSeqUserReadSeqKey(conversationID string, userID string) string {\n\treturn SeqUserReadSeq + conversationID + \":\" + userID\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/third.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey\n\nimport (\n\t\"strconv\"\n)\n\nconst (\n\tgetuiToken              = \"GETUI_TOKEN\"\n\tgetuiTaskID             = \"GETUI_TASK_ID\"\n\tfmcToken                = \"FCM_TOKEN:\"\n\tuserBadgeUnreadCountSum = \"USER_BADGE_UNREAD_COUNT_SUM:\"\n)\n\nfunc GetFcmAccountTokenKey(account string, platformID int) string {\n\treturn fmcToken + account + \":\" + strconv.Itoa(platformID)\n}\n\nfunc GetUserBadgeUnreadCountSumKey(userID string) string {\n\treturn userBadgeUnreadCountSum + userID\n}\n\nfunc GetGetuiTokenKey() string {\n\treturn getuiToken\n}\nfunc GetGetuiTaskIDKey() string {\n\treturn getuiTaskID\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/token.go",
    "content": "package cachekey\n\nimport (\n\t\"strings\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n)\n\nconst (\n\tUidPidToken = \"UID_PID_TOKEN_STATUS:\"\n)\n\nfunc GetTokenKey(userID string, platformID int) string {\n\treturn UidPidToken + userID + \":\" + constant.PlatformIDToName(platformID)\n}\n\nfunc GetTemporaryTokenKey(userID string, platformID int, token string) string {\n\treturn UidPidToken + \":TEMPORARY:\" + userID + \":\" + constant.PlatformIDToName(platformID) + \":\" + token\n}\n\nfunc GetAllPlatformTokenKey(userID string) []string {\n\tres := make([]string, len(constant.PlatformID2Name))\n\tfor k := range constant.PlatformID2Name {\n\t\tres[k-1] = GetTokenKey(userID, k)\n\t}\n\treturn res\n}\n\nfunc GetPlatformIDByTokenKey(key string) int {\n\tsplitKey := strings.Split(key, \":\")\n\tplatform := splitKey[len(splitKey)-1]\n\treturn constant.PlatformNameToID(platform)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/cachekey/user.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cachekey\n\nconst (\n\tUserInfoKey             = \"USER_INFO:\"\n\tUserGlobalRecvMsgOptKey = \"USER_GLOBAL_RECV_MSG_OPT_KEY:\"\n)\n\nfunc GetUserInfoKey(userID string) string {\n\treturn UserInfoKey + userID\n}\n\nfunc GetUserGlobalRecvMsgOptKey(userID string) string {\n\treturn UserGlobalRecvMsgOptKey + userID\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/client_config.go",
    "content": "package cache\n\nimport \"context\"\n\ntype ClientConfigCache interface {\n\tDeleteUserCache(ctx context.Context, userIDs []string) error\n\tGetUserConfig(ctx context.Context, userID string) (map[string]string, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cache\n\nimport (\n\t\"context\"\n\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n)\n\n// arg fn will exec when no data in msgCache.\ntype ConversationCache interface {\n\tBatchDeleter\n\tCloneConversationCache() ConversationCache\n\t// get user's conversationIDs from msgCache\n\tGetUserConversationIDs(ctx context.Context, ownerUserID string) ([]string, error)\n\tGetUserNotNotifyConversationIDs(ctx context.Context, userID string) ([]string, error)\n\tGetPinnedConversationIDs(ctx context.Context, userID string) ([]string, error)\n\tDelConversationIDs(userIDs ...string) ConversationCache\n\n\tGetUserConversationIDsHash(ctx context.Context, ownerUserID string) (hash uint64, err error)\n\tDelUserConversationIDsHash(ownerUserIDs ...string) ConversationCache\n\n\t// get one conversation from msgCache\n\tGetConversation(ctx context.Context, ownerUserID, conversationID string) (*relationtb.Conversation, error)\n\tDelConversations(ownerUserID string, conversationIDs ...string) ConversationCache\n\tDelUsersConversation(conversationID string, ownerUserIDs ...string) ConversationCache\n\t// get one conversation from msgCache\n\tGetConversations(ctx context.Context, ownerUserID string,\n\t\tconversationIDs []string) ([]*relationtb.Conversation, error)\n\t// get one user's all conversations from msgCache\n\tGetUserAllConversations(ctx context.Context, ownerUserID string) ([]*relationtb.Conversation, error)\n\t// get user conversation recv msg from msgCache\n\tGetUserRecvMsgOpt(ctx context.Context, ownerUserID, conversationID string) (opt int, err error)\n\tDelUserRecvMsgOpt(ownerUserID, conversationID string) ConversationCache\n\t// get one super group recv msg but do not notification userID list\n\t// GetSuperGroupRecvMsgNotNotifyUserIDs(ctx context.Context, groupID string) (userIDs []string, err error)\n\tDelSuperGroupRecvMsgNotNotifyUserIDs(groupID string) ConversationCache\n\t// get one super group recv msg but do not notification userID list hash\n\t// GetSuperGroupRecvMsgNotNotifyUserIDsHash(ctx context.Context, groupID string) (hash uint64, err error)\n\tDelSuperGroupRecvMsgNotNotifyUserIDsHash(groupID string) ConversationCache\n\n\t// GetUserAllHasReadSeqs(ctx context.Context, ownerUserID string) (map[string]int64, error)\n\tDelUserAllHasReadSeqs(ownerUserID string, conversationIDs ...string) ConversationCache\n\n\tGetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error)\n\tDelConversationNotReceiveMessageUserIDs(conversationIDs ...string) ConversationCache\n\tDelConversationNotNotifyMessageUserIDs(userIDs ...string) ConversationCache\n\tDelUserPinnedConversations(userIDs ...string) ConversationCache\n\tDelConversationVersionUserIDs(userIDs ...string) ConversationCache\n\n\tFindMaxConversationUserVersion(ctx context.Context, userID string) (*relationtb.VersionLog, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage cache // import \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n"
  },
  {
    "path": "pkg/common/storage/cache/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cache\n\nimport (\n\t\"context\"\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n)\n\n// FriendCache is an interface for caching friend-related data.\ntype FriendCache interface {\n\tBatchDeleter\n\tCloneFriendCache() FriendCache\n\tGetFriendIDs(ctx context.Context, ownerUserID string) (friendIDs []string, err error)\n\t// Called when friendID list changed\n\tDelFriendIDs(ownerUserID ...string) FriendCache\n\t// Get single friendInfo from the cache\n\tGetFriend(ctx context.Context, ownerUserID, friendUserID string) (friend *relationtb.Friend, err error)\n\t// Delete friend when friend info changed\n\tDelFriend(ownerUserID, friendUserID string) FriendCache\n\t// Delete friends when friends' info changed\n\tDelFriends(ownerUserID string, friendUserIDs []string) FriendCache\n\n\tDelOwner(friendUserID string, ownerUserIDs []string) FriendCache\n\n\tDelMaxFriendVersion(ownerUserIDs ...string) FriendCache\n\n\t//DelSortFriendUserIDs(ownerUserIDs ...string) FriendCache\n\n\t//FindSortFriendUserIDs(ctx context.Context, ownerUserID string) ([]string, error)\n\n\t//FindFriendIncrVersion(ctx context.Context, ownerUserID string, version uint, limit int) (*relationtb.VersionLog, error)\n\n\tFindMaxFriendVersion(ctx context.Context, ownerUserID string) (*relationtb.VersionLog, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cache\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/common\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n)\n\ntype GroupHash interface {\n\tGetGroupHash(ctx context.Context, groupID string) (uint64, error)\n}\n\ntype GroupCache interface {\n\tBatchDeleter\n\tCloneGroupCache() GroupCache\n\tGetGroupsInfo(ctx context.Context, groupIDs []string) (groups []*model.Group, err error)\n\tGetGroupInfo(ctx context.Context, groupID string) (group *model.Group, err error)\n\tDelGroupsInfo(groupIDs ...string) GroupCache\n\n\tGetGroupMembersHash(ctx context.Context, groupID string) (hashCode uint64, err error)\n\tGetGroupMemberHashMap(ctx context.Context, groupIDs []string) (map[string]*common.GroupSimpleUserID, error)\n\tDelGroupMembersHash(groupID string) GroupCache\n\n\tGetGroupMemberIDs(ctx context.Context, groupID string) (groupMemberIDs []string, err error)\n\n\tDelGroupMemberIDs(groupID string) GroupCache\n\n\tGetJoinedGroupIDs(ctx context.Context, userID string) (joinedGroupIDs []string, err error)\n\tDelJoinedGroupID(userID ...string) GroupCache\n\n\tGetGroupMemberInfo(ctx context.Context, groupID, userID string) (groupMember *model.GroupMember, err error)\n\tGetGroupMembersInfo(ctx context.Context, groupID string, userID []string) (groupMembers []*model.GroupMember, err error)\n\tGetAllGroupMembersInfo(ctx context.Context, groupID string) (groupMembers []*model.GroupMember, err error)\n\tFindGroupMemberUser(ctx context.Context, groupIDs []string, userID string) ([]*model.GroupMember, error)\n\n\tGetGroupRoleLevelMemberIDs(ctx context.Context, groupID string, roleLevel int32) ([]string, error)\n\tGetGroupOwner(ctx context.Context, groupID string) (*model.GroupMember, error)\n\tGetGroupsOwner(ctx context.Context, groupIDs []string) ([]*model.GroupMember, error)\n\tDelGroupRoleLevel(groupID string, roleLevel []int32) GroupCache\n\tDelGroupAllRoleLevel(groupID string) GroupCache\n\tDelGroupMembersInfo(groupID string, userID ...string) GroupCache\n\tGetGroupRoleLevelMemberInfo(ctx context.Context, groupID string, roleLevel int32) ([]*model.GroupMember, error)\n\tGetGroupRolesLevelMemberInfo(ctx context.Context, groupID string, roleLevels []int32) ([]*model.GroupMember, error)\n\tGetGroupMemberNum(ctx context.Context, groupID string) (memberNum int64, err error)\n\tDelGroupsMemberNum(groupID ...string) GroupCache\n\n\t//FindSortGroupMemberUserIDs(ctx context.Context, groupID string) ([]string, error)\n\t//FindSortJoinGroupIDs(ctx context.Context, userID string) ([]string, error)\n\n\tDelMaxGroupMemberVersion(groupIDs ...string) GroupCache\n\tDelMaxJoinGroupVersion(userIDs ...string) GroupCache\n\tFindMaxGroupMemberVersion(ctx context.Context, groupID string) (*model.VersionLog, error)\n\tBatchFindMaxGroupMemberVersion(ctx context.Context, groupIDs []string) ([]*model.VersionLog, error)\n\tFindMaxJoinGroupVersion(ctx context.Context, userID string) (*model.VersionLog, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/mcache/minio.go",
    "content": "package mcache\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/tools/s3/minio\"\n)\n\nfunc NewMinioCache(cache database.Cache) minio.Cache {\n\treturn &minioCache{\n\t\tcache:      cache,\n\t\texpireTime: time.Hour * 24 * 7,\n\t}\n}\n\ntype minioCache struct {\n\tcache      database.Cache\n\texpireTime time.Duration\n}\n\nfunc (g *minioCache) getObjectImageInfoKey(key string) string {\n\treturn cachekey.GetObjectImageInfoKey(key)\n}\n\nfunc (g *minioCache) getMinioImageThumbnailKey(key string, format string, width int, height int) string {\n\treturn cachekey.GetMinioImageThumbnailKey(key, format, width, height)\n}\n\nfunc (g *minioCache) DelObjectImageInfoKey(ctx context.Context, keys ...string) error {\n\tks := make([]string, 0, len(keys))\n\tfor _, key := range keys {\n\t\tks = append(ks, g.getObjectImageInfoKey(key))\n\t}\n\treturn g.cache.Del(ctx, ks)\n}\n\nfunc (g *minioCache) DelImageThumbnailKey(ctx context.Context, key string, format string, width int, height int) error {\n\treturn g.cache.Del(ctx, []string{g.getMinioImageThumbnailKey(key, format, width, height)})\n}\n\nfunc (g *minioCache) GetImageObjectKeyInfo(ctx context.Context, key string, fn func(ctx context.Context) (*minio.ImageInfo, error)) (*minio.ImageInfo, error) {\n\treturn getCache[*minio.ImageInfo](ctx, g.cache, g.getObjectImageInfoKey(key), g.expireTime, fn)\n}\n\nfunc (g *minioCache) GetThumbnailKey(ctx context.Context, key string, format string, width int, height int, minioCache func(ctx context.Context) (string, error)) (string, error) {\n\treturn getCache[string](ctx, g.cache, g.getMinioImageThumbnailKey(key, format, width, height), g.expireTime, minioCache)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/mcache/msg_cache.go",
    "content": "package mcache\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache/lru\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar (\n\tmemMsgCache     lru.LRU[string, *model.MsgInfoModel]\n\tinitMemMsgCache sync.Once\n)\n\nfunc NewMsgCache(cache database.Cache, msgDocDatabase database.Msg) cache.MsgCache {\n\tinitMemMsgCache.Do(func() {\n\t\tmemMsgCache = lru.NewLazyLRU[string, *model.MsgInfoModel](1024*8, time.Hour, time.Second*10, localcache.EmptyTarget{}, nil)\n\t})\n\treturn &msgCache{\n\t\tcache:          cache,\n\t\tmsgDocDatabase: msgDocDatabase,\n\t\tmemMsgCache:    memMsgCache,\n\t}\n}\n\ntype msgCache struct {\n\tcache          database.Cache\n\tmsgDocDatabase database.Msg\n\tmemMsgCache    lru.LRU[string, *model.MsgInfoModel]\n}\n\nfunc (x *msgCache) getSendMsgKey(id string) string {\n\treturn cachekey.GetSendMsgKey(id)\n}\n\nfunc (x *msgCache) SetSendMsgStatus(ctx context.Context, id string, status int32) error {\n\treturn x.cache.Set(ctx, x.getSendMsgKey(id), strconv.Itoa(int(status)), time.Hour*24)\n}\n\nfunc (x *msgCache) GetSendMsgStatus(ctx context.Context, id string) (int32, error) {\n\tkey := x.getSendMsgKey(id)\n\tres, err := x.cache.Get(ctx, []string{key})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tval, ok := res[key]\n\tif !ok {\n\t\treturn 0, errs.Wrap(redis.Nil)\n\t}\n\tstatus, err := strconv.Atoi(val)\n\tif err != nil {\n\t\treturn 0, errs.WrapMsg(err, \"GetSendMsgStatus strconv.Atoi error\", \"val\", val)\n\t}\n\treturn int32(status), nil\n}\n\nfunc (x *msgCache) getMsgCacheKey(conversationID string, seq int64) string {\n\treturn cachekey.GetMsgCacheKey(conversationID, seq)\n\n}\n\nfunc (x *msgCache) GetMessageBySeqs(ctx context.Context, conversationID string, seqs []int64) ([]*model.MsgInfoModel, error) {\n\tif len(seqs) == 0 {\n\t\treturn nil, nil\n\t}\n\tkeys := make([]string, 0, len(seqs))\n\tkeySeq := make(map[string]int64, len(seqs))\n\tfor _, seq := range seqs {\n\t\tkey := x.getMsgCacheKey(conversationID, seq)\n\t\tkeys = append(keys, key)\n\t\tkeySeq[key] = seq\n\t}\n\tres, err := x.memMsgCache.GetBatch(keys, func(keys []string) (map[string]*model.MsgInfoModel, error) {\n\t\tfindSeqs := make([]int64, 0, len(keys))\n\t\tfor _, key := range keys {\n\t\t\tseq, ok := keySeq[key]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfindSeqs = append(findSeqs, seq)\n\t\t}\n\t\tres, err := x.msgDocDatabase.FindSeqs(ctx, conversationID, seqs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tkv := make(map[string]*model.MsgInfoModel)\n\t\tfor i := range res {\n\t\t\tmsg := res[i]\n\t\t\tif msg == nil || msg.Msg == nil || msg.Msg.Seq <= 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := x.getMsgCacheKey(conversationID, msg.Msg.Seq)\n\t\t\tkv[key] = msg\n\t\t}\n\t\treturn kv, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn datautil.Values(res), nil\n}\n\nfunc (x msgCache) DelMessageBySeqs(ctx context.Context, conversationID string, seqs []int64) error {\n\tif len(seqs) == 0 {\n\t\treturn nil\n\t}\n\tfor _, seq := range seqs {\n\t\tx.memMsgCache.Del(x.getMsgCacheKey(conversationID, seq))\n\t}\n\treturn nil\n}\n\nfunc (x *msgCache) SetMessageBySeqs(ctx context.Context, conversationID string, msgs []*model.MsgInfoModel) error {\n\tfor i := range msgs {\n\t\tmsg := msgs[i]\n\t\tif msg == nil || msg.Msg == nil || msg.Msg.Seq <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tx.memMsgCache.Set(x.getMsgCacheKey(conversationID, msg.Msg.Seq), msg)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/mcache/online.go",
    "content": "package mcache\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n)\n\nvar (\n\tglobalOnlineCache cache.OnlineCache\n\tglobalOnlineOnce  sync.Once\n)\n\nfunc NewOnlineCache() cache.OnlineCache {\n\tglobalOnlineOnce.Do(func() {\n\t\tglobalOnlineCache = &onlineCache{\n\t\t\tuser: make(map[string]map[int32]struct{}),\n\t\t}\n\t})\n\treturn globalOnlineCache\n}\n\ntype onlineCache struct {\n\tlock sync.RWMutex\n\tuser map[string]map[int32]struct{}\n}\n\nfunc (x *onlineCache) GetOnline(ctx context.Context, userID string) ([]int32, error) {\n\tx.lock.RLock()\n\tdefer x.lock.RUnlock()\n\tpSet, ok := x.user[userID]\n\tif !ok {\n\t\treturn nil, nil\n\t}\n\tres := make([]int32, 0, len(pSet))\n\tfor k := range pSet {\n\t\tres = append(res, k)\n\t}\n\treturn res, nil\n}\n\nfunc (x *onlineCache) SetUserOnline(ctx context.Context, userID string, online, offline []int32) error {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tpSet, ok := x.user[userID]\n\tif ok {\n\t\tfor _, p := range offline {\n\t\t\tdelete(pSet, p)\n\t\t}\n\t}\n\tif len(online) > 0 {\n\t\tif !ok {\n\t\t\tpSet = make(map[int32]struct{})\n\t\t\tx.user[userID] = pSet\n\t\t}\n\t\tfor _, p := range online {\n\t\t\tpSet[p] = struct{}{}\n\t\t}\n\t}\n\tif len(pSet) == 0 {\n\t\tdelete(x.user, userID)\n\t}\n\treturn nil\n}\n\nfunc (x *onlineCache) GetAllOnlineUsers(ctx context.Context, cursor uint64) (map[string][]int32, uint64, error) {\n\tif cursor != 0 {\n\t\treturn nil, 0, nil\n\t}\n\tx.lock.RLock()\n\tdefer x.lock.RUnlock()\n\tres := make(map[string][]int32)\n\tfor k, v := range x.user {\n\t\tpSet := make([]int32, 0, len(v))\n\t\tfor p := range v {\n\t\t\tpSet = append(pSet, p)\n\t\t}\n\t\tres[k] = pSet\n\t}\n\treturn res, 0, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/mcache/seq_conversation.go",
    "content": "package mcache\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n)\n\nfunc NewSeqConversationCache(sc database.SeqConversation) cache.SeqConversationCache {\n\treturn &seqConversationCache{\n\t\tsc: sc,\n\t}\n}\n\ntype seqConversationCache struct {\n\tsc database.SeqConversation\n}\n\nfunc (x *seqConversationCache) Malloc(ctx context.Context, conversationID string, size int64) (int64, error) {\n\treturn x.sc.Malloc(ctx, conversationID, size)\n}\n\nfunc (x *seqConversationCache) SetMinSeq(ctx context.Context, conversationID string, seq int64) error {\n\treturn x.sc.SetMinSeq(ctx, conversationID, seq)\n}\n\nfunc (x *seqConversationCache) GetMinSeq(ctx context.Context, conversationID string) (int64, error) {\n\treturn x.sc.GetMinSeq(ctx, conversationID)\n}\n\nfunc (x *seqConversationCache) GetMaxSeqs(ctx context.Context, conversationIDs []string) (map[string]int64, error) {\n\tres := make(map[string]int64)\n\tfor _, conversationID := range conversationIDs {\n\t\tseq, err := x.GetMinSeq(ctx, conversationID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres[conversationID] = seq\n\t}\n\treturn res, nil\n}\n\nfunc (x *seqConversationCache) GetMaxSeqsWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error) {\n\tres := make(map[string]database.SeqTime)\n\tfor _, conversationID := range conversationIDs {\n\t\tseq, err := x.GetMinSeq(ctx, conversationID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres[conversationID] = database.SeqTime{Seq: seq}\n\t}\n\treturn res, nil\n}\n\nfunc (x *seqConversationCache) GetMaxSeq(ctx context.Context, conversationID string) (int64, error) {\n\treturn x.sc.GetMaxSeq(ctx, conversationID)\n}\n\nfunc (x *seqConversationCache) GetMaxSeqWithTime(ctx context.Context, conversationID string) (database.SeqTime, error) {\n\tseq, err := x.GetMinSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn database.SeqTime{}, err\n\t}\n\treturn database.SeqTime{Seq: seq}, nil\n}\n\nfunc (x *seqConversationCache) SetMinSeqs(ctx context.Context, seqs map[string]int64) error {\n\tfor conversationID, seq := range seqs {\n\t\tif err := x.sc.SetMinSeq(ctx, conversationID, seq); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *seqConversationCache) GetCacheMaxSeqWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error) {\n\treturn x.GetMaxSeqsWithTime(ctx, conversationIDs)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/mcache/third.go",
    "content": "package mcache\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewThirdCache(cache database.Cache) cache.ThirdCache {\n\treturn &thirdCache{\n\t\tcache: cache,\n\t}\n}\n\ntype thirdCache struct {\n\tcache database.Cache\n}\n\nfunc (c *thirdCache) getGetuiTokenKey() string {\n\treturn cachekey.GetGetuiTokenKey()\n}\n\nfunc (c *thirdCache) getGetuiTaskIDKey() string {\n\treturn cachekey.GetGetuiTaskIDKey()\n}\n\nfunc (c *thirdCache) getUserBadgeUnreadCountSumKey(userID string) string {\n\treturn cachekey.GetUserBadgeUnreadCountSumKey(userID)\n}\n\nfunc (c *thirdCache) getFcmAccountTokenKey(account string, platformID int) string {\n\treturn cachekey.GetFcmAccountTokenKey(account, platformID)\n}\n\nfunc (c *thirdCache) get(ctx context.Context, key string) (string, error) {\n\tres, err := c.cache.Get(ctx, []string{key})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif val, ok := res[key]; ok {\n\t\treturn val, nil\n\t}\n\treturn \"\", errs.Wrap(redis.Nil)\n}\n\nfunc (c *thirdCache) SetFcmToken(ctx context.Context, account string, platformID int, fcmToken string, expireTime int64) (err error) {\n\treturn errs.Wrap(c.cache.Set(ctx, c.getFcmAccountTokenKey(account, platformID), fcmToken, time.Duration(expireTime)*time.Second))\n}\n\nfunc (c *thirdCache) GetFcmToken(ctx context.Context, account string, platformID int) (string, error) {\n\treturn c.get(ctx, c.getFcmAccountTokenKey(account, platformID))\n}\n\nfunc (c *thirdCache) DelFcmToken(ctx context.Context, account string, platformID int) error {\n\treturn c.cache.Del(ctx, []string{c.getFcmAccountTokenKey(account, platformID)})\n}\n\nfunc (c *thirdCache) IncrUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error) {\n\treturn c.cache.Incr(ctx, c.getUserBadgeUnreadCountSumKey(userID), 1)\n}\n\nfunc (c *thirdCache) SetUserBadgeUnreadCountSum(ctx context.Context, userID string, value int) error {\n\treturn c.cache.Set(ctx, c.getUserBadgeUnreadCountSumKey(userID), strconv.Itoa(value), 0)\n}\n\nfunc (c *thirdCache) GetUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error) {\n\tstr, err := c.get(ctx, c.getUserBadgeUnreadCountSumKey(userID))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tval, err := strconv.Atoi(str)\n\tif err != nil {\n\t\treturn 0, errs.WrapMsg(err, \"strconv.Atoi\", \"str\", str)\n\t}\n\treturn val, nil\n}\n\nfunc (c *thirdCache) SetGetuiToken(ctx context.Context, token string, expireTime int64) error {\n\treturn c.cache.Set(ctx, c.getGetuiTokenKey(), token, time.Duration(expireTime)*time.Second)\n}\n\nfunc (c *thirdCache) GetGetuiToken(ctx context.Context) (string, error) {\n\treturn c.get(ctx, c.getGetuiTokenKey())\n}\n\nfunc (c *thirdCache) SetGetuiTaskID(ctx context.Context, taskID string, expireTime int64) error {\n\treturn c.cache.Set(ctx, c.getGetuiTaskIDKey(), taskID, time.Duration(expireTime)*time.Second)\n}\n\nfunc (c *thirdCache) GetGetuiTaskID(ctx context.Context) (string, error) {\n\treturn c.get(ctx, c.getGetuiTaskIDKey())\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/mcache/token.go",
    "content": "package mcache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n)\n\nfunc NewTokenCacheModel(cache database.Cache, accessExpire int64) cache.TokenModel {\n\tc := &tokenCache{cache: cache}\n\tc.accessExpire = c.getExpireTime(accessExpire)\n\treturn c\n}\n\ntype tokenCache struct {\n\tcache        database.Cache\n\taccessExpire time.Duration\n}\n\nfunc (x *tokenCache) getTokenKey(userID string, platformID int, token string) string {\n\treturn cachekey.GetTokenKey(userID, platformID) + \":\" + token\n}\n\nfunc (x *tokenCache) SetTokenFlag(ctx context.Context, userID string, platformID int, token string, flag int) error {\n\treturn x.cache.Set(ctx, x.getTokenKey(userID, platformID, token), strconv.Itoa(flag), x.accessExpire)\n}\n\n// SetTokenFlagEx set token and flag with expire time\nfunc (x *tokenCache) SetTokenFlagEx(ctx context.Context, userID string, platformID int, token string, flag int) error {\n\treturn x.SetTokenFlag(ctx, userID, platformID, token, flag)\n}\n\nfunc (x *tokenCache) GetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error) {\n\tprefix := x.getTokenKey(userID, platformID, \"\")\n\tm, err := x.cache.Prefix(ctx, prefix)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\tmm := make(map[string]int)\n\tfor k, v := range m {\n\t\tstate, err := strconv.Atoi(v)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"token value is not int\", err, \"value\", v, \"userID\", userID, \"platformID\", platformID)\n\t\t\tcontinue\n\t\t}\n\t\tmm[strings.TrimPrefix(k, prefix)] = state\n\t}\n\treturn mm, nil\n}\n\nfunc (x *tokenCache) HasTemporaryToken(ctx context.Context, userID string, platformID int, token string) error {\n\tkey := cachekey.GetTemporaryTokenKey(userID, platformID, token)\n\tif _, err := x.cache.Get(ctx, []string{key}); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (x *tokenCache) GetAllTokensWithoutError(ctx context.Context, userID string) (map[int]map[string]int, error) {\n\tprefix := cachekey.UidPidToken + userID + \":\"\n\ttokens, err := x.cache.Prefix(ctx, prefix)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make(map[int]map[string]int)\n\tfor key, flagStr := range tokens {\n\t\tflag, err := strconv.Atoi(flagStr)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"token value is not int\", err, \"key\", key, \"value\", flagStr, \"userID\", userID)\n\t\t\tcontinue\n\t\t}\n\t\tarr := strings.SplitN(strings.TrimPrefix(key, prefix), \":\", 2)\n\t\tif len(arr) != 2 {\n\t\t\tlog.ZError(ctx, \"token value is not int\", err, \"key\", key, \"value\", flagStr, \"userID\", userID)\n\t\t\tcontinue\n\t\t}\n\t\tplatformID, err := strconv.Atoi(arr[0])\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"token value is not int\", err, \"key\", key, \"value\", flagStr, \"userID\", userID)\n\t\t\tcontinue\n\t\t}\n\t\ttoken := arr[1]\n\t\tif token == \"\" {\n\t\t\tlog.ZError(ctx, \"token value is not int\", err, \"key\", key, \"value\", flagStr, \"userID\", userID)\n\t\t\tcontinue\n\t\t}\n\t\ttk, ok := res[platformID]\n\t\tif !ok {\n\t\t\ttk = make(map[string]int)\n\t\t\tres[platformID] = tk\n\t\t}\n\t\ttk[token] = flag\n\t}\n\treturn res, nil\n}\n\nfunc (x *tokenCache) SetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error {\n\tfor token, flag := range m {\n\t\terr := x.SetTokenFlag(ctx, userID, platformID, token, flag)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *tokenCache) BatchSetTokenMapByUidPid(ctx context.Context, tokens map[string]map[string]any) error {\n\tfor prefix, tokenFlag := range tokens {\n\t\tfor token, flag := range tokenFlag {\n\t\t\tflagStr := fmt.Sprintf(\"%v\", flag)\n\t\t\tif err := x.cache.Set(ctx, prefix+\":\"+token, flagStr, x.accessExpire); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *tokenCache) DeleteTokenByUidPid(ctx context.Context, userID string, platformID int, fields []string) error {\n\tkeys := make([]string, 0, len(fields))\n\tfor _, token := range fields {\n\t\tkeys = append(keys, x.getTokenKey(userID, platformID, token))\n\t}\n\treturn x.cache.Del(ctx, keys)\n}\n\nfunc (x *tokenCache) getExpireTime(t int64) time.Duration {\n\treturn time.Hour * 24 * time.Duration(t)\n}\n\nfunc (x *tokenCache) DeleteTokenByTokenMap(ctx context.Context, userID string, tokens map[int][]string) error {\n\tkeys := make([]string, 0, len(tokens))\n\tfor platformID, ts := range tokens {\n\t\tfor _, t := range ts {\n\t\t\tkeys = append(keys, x.getTokenKey(userID, platformID, t))\n\t\t}\n\t}\n\treturn x.cache.Del(ctx, keys)\n}\n\nfunc (x *tokenCache) DeleteAndSetTemporary(ctx context.Context, userID string, platformID int, fields []string) error {\n\tkeys := make([]string, 0, len(fields))\n\tfor _, f := range fields {\n\t\tkeys = append(keys, x.getTokenKey(userID, platformID, f))\n\t}\n\tif err := x.cache.Del(ctx, keys); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, f := range fields {\n\t\tk := cachekey.GetTemporaryTokenKey(userID, platformID, f)\n\t\tif err := x.cache.Set(ctx, k, \"\", time.Minute*5); err != nil {\n\t\t\treturn errs.Wrap(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/mcache/tools.go",
    "content": "package mcache\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/tools/log\"\n)\n\nfunc getCache[V any](ctx context.Context, cache database.Cache, key string, expireTime time.Duration, fn func(ctx context.Context) (V, error)) (V, error) {\n\tgetDB := func() (V, bool, error) {\n\t\tres, err := cache.Get(ctx, []string{key})\n\t\tif err != nil {\n\t\t\tvar val V\n\t\t\treturn val, false, err\n\t\t}\n\t\tvar val V\n\t\tif str, ok := res[key]; ok {\n\t\t\tif json.Unmarshal([]byte(str), &val) != nil {\n\t\t\t\treturn val, false, err\n\t\t\t}\n\t\t\treturn val, true, nil\n\t\t}\n\t\treturn val, false, nil\n\t}\n\tdbVal, ok, err := getDB()\n\tif err != nil {\n\t\treturn dbVal, err\n\t}\n\tif ok {\n\t\treturn dbVal, nil\n\t}\n\tlockValue, err := cache.Lock(ctx, key, time.Minute)\n\tif err != nil {\n\t\treturn dbVal, err\n\t}\n\tdefer func() {\n\t\tif err := cache.Unlock(ctx, key, lockValue); err != nil {\n\t\t\tlog.ZError(ctx, \"unlock cache key\", err, \"key\", key, \"value\", lockValue)\n\t\t}\n\t}()\n\tdbVal, ok, err = getDB()\n\tif err != nil {\n\t\treturn dbVal, err\n\t}\n\tif ok {\n\t\treturn dbVal, nil\n\t}\n\tval, err := fn(ctx)\n\tif err != nil {\n\t\treturn val, err\n\t}\n\tdata, err := json.Marshal(val)\n\tif err != nil {\n\t\treturn val, err\n\t}\n\tif err := cache.Set(ctx, key, string(data), expireTime); err != nil {\n\t\treturn val, err\n\t}\n\treturn val, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cache\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n)\n\ntype MsgCache interface {\n\tSetSendMsgStatus(ctx context.Context, id string, status int32) error\n\tGetSendMsgStatus(ctx context.Context, id string) (int32, error)\n\n\tGetMessageBySeqs(ctx context.Context, conversationID string, seqs []int64) ([]*model.MsgInfoModel, error)\n\tDelMessageBySeqs(ctx context.Context, conversationID string, seqs []int64) error\n\tSetMessageBySeqs(ctx context.Context, conversationID string, msgs []*model.MsgInfoModel) error\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/online.go",
    "content": "package cache\n\nimport \"context\"\n\ntype OnlineCache interface {\n\tGetOnline(ctx context.Context, userID string) ([]int32, error)\n\tSetUserOnline(ctx context.Context, userID string, online, offline []int32) error\n\tGetAllOnlineUsers(ctx context.Context, cursor uint64) (map[string][]int32, uint64, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/batch.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/dtm-labs/rockscache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\n// GetRocksCacheOptions returns the default configuration options for RocksCache.\nfunc GetRocksCacheOptions() *rockscache.Options {\n\topts := rockscache.NewDefaultOptions()\n\topts.LockExpire = rocksCacheTimeout\n\topts.WaitReplicasTimeout = rocksCacheTimeout\n\topts.StrongConsistency = true\n\topts.RandomExpireAdjustment = 0.2\n\n\treturn &opts\n}\n\nfunc newRocksCacheClient(rdb redis.UniversalClient) *rocksCacheClient {\n\tif rdb == nil {\n\t\treturn &rocksCacheClient{}\n\t}\n\trc := &rocksCacheClient{\n\t\trdb:    rdb,\n\t\tclient: rockscache.NewClient(rdb, *GetRocksCacheOptions()),\n\t}\n\treturn rc\n}\n\ntype rocksCacheClient struct {\n\trdb    redis.UniversalClient\n\tclient *rockscache.Client\n}\n\nfunc (x *rocksCacheClient) GetClient() *rockscache.Client {\n\treturn x.client\n}\n\nfunc (x *rocksCacheClient) Disable() bool {\n\treturn x.client == nil\n}\n\nfunc (x *rocksCacheClient) GetRedis() redis.UniversalClient {\n\treturn x.rdb\n}\n\nfunc (x *rocksCacheClient) GetBatchDeleter(topics ...string) cache.BatchDeleter {\n\treturn NewBatchDeleterRedis(x, topics)\n}\n\nfunc batchGetCache2[K comparable, V any](ctx context.Context, rcClient *rocksCacheClient, expire time.Duration, ids []K, idKey func(id K) string, vId func(v *V) K, fn func(ctx context.Context, ids []K) ([]*V, error)) ([]*V, error) {\n\tif len(ids) == 0 {\n\t\treturn nil, nil\n\t}\n\tif rcClient.Disable() {\n\t\treturn fn(ctx, ids)\n\t}\n\tfindKeys := make([]string, 0, len(ids))\n\tkeyId := make(map[string]K)\n\tfor _, id := range ids {\n\t\tkey := idKey(id)\n\t\tif _, ok := keyId[key]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tkeyId[key] = id\n\t\tfindKeys = append(findKeys, key)\n\t}\n\tslotKeys, err := groupKeysBySlot(ctx, rcClient.GetRedis(), findKeys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := make([]*V, 0, len(findKeys))\n\tfor _, keys := range slotKeys {\n\t\tindexCache, err := rcClient.GetClient().FetchBatch2(ctx, keys, expire, func(idx []int) (map[int]string, error) {\n\t\t\tqueryIds := make([]K, 0, len(idx))\n\t\t\tidIndex := make(map[K]int)\n\t\t\tfor _, index := range idx {\n\t\t\t\tid := keyId[keys[index]]\n\t\t\t\tidIndex[id] = index\n\t\t\t\tqueryIds = append(queryIds, id)\n\t\t\t}\n\t\t\tvalues, err := fn(ctx, queryIds)\n\t\t\tif err != nil {\n\t\t\t\tlog.ZError(ctx, \"batchGetCache query database failed\", err, \"keys\", keys, \"queryIds\", queryIds)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif len(values) == 0 {\n\t\t\t\treturn map[int]string{}, nil\n\t\t\t}\n\t\t\tcacheIndex := make(map[int]string)\n\t\t\tfor _, value := range values {\n\t\t\t\tid := vId(value)\n\t\t\t\tindex, ok := idIndex[id]\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tbs, err := json.Marshal(value)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.ZError(ctx, \"marshal failed\", err)\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tcacheIndex[index] = string(bs)\n\t\t\t}\n\t\t\treturn cacheIndex, nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errs.WrapMsg(err, \"FetchBatch2 failed\")\n\t\t}\n\t\tfor index, data := range indexCache {\n\t\t\tif data == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar value V\n\t\t\tif err := json.Unmarshal([]byte(data), &value); err != nil {\n\t\t\t\treturn nil, errs.WrapMsg(err, \"Unmarshal failed\")\n\t\t\t}\n\t\t\tif cb, ok := any(&value).(BatchCacheCallback[K]); ok {\n\t\t\t\tcb.BatchCache(keyId[keys[index]])\n\t\t\t}\n\t\t\tresult = append(result, &value)\n\t\t}\n\t}\n\treturn result, nil\n}\n\ntype BatchCacheCallback[K comparable] interface {\n\tBatchCache(id K)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/batch_handler.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/dtm-labs/rockscache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\trocksCacheTimeout = 11 * time.Second\n)\n\n// BatchDeleterRedis is a concrete implementation of the BatchDeleter interface based on Redis and RocksCache.\ntype BatchDeleterRedis struct {\n\tredisClient    redis.UniversalClient\n\tkeys           []string\n\trocksClient    *rockscache.Client\n\tredisPubTopics []string\n}\n\n// NewBatchDeleterRedis creates a new BatchDeleterRedis instance.\nfunc NewBatchDeleterRedis(rcClient *rocksCacheClient, redisPubTopics []string) *BatchDeleterRedis {\n\treturn &BatchDeleterRedis{\n\t\tredisClient:    rcClient.GetRedis(),\n\t\trocksClient:    rcClient.GetClient(),\n\t\tredisPubTopics: redisPubTopics,\n\t}\n}\n\n// ExecDelWithKeys directly takes keys for batch deletion and publishes deletion information.\nfunc (c *BatchDeleterRedis) ExecDelWithKeys(ctx context.Context, keys []string) error {\n\tdistinctKeys := datautil.Distinct(keys)\n\treturn c.execDel(ctx, distinctKeys)\n}\n\n// ChainExecDel is used for chain calls for batch deletion. It must call Clone to prevent memory pollution.\nfunc (c *BatchDeleterRedis) ChainExecDel(ctx context.Context) error {\n\tdistinctKeys := datautil.Distinct(c.keys)\n\treturn c.execDel(ctx, distinctKeys)\n}\n\n// execDel performs batch deletion and publishes the keys that have been deleted to update the local cache information of other nodes.\nfunc (c *BatchDeleterRedis) execDel(ctx context.Context, keys []string) error {\n\tif len(keys) > 0 {\n\t\tlog.ZDebug(ctx, \"delete cache\", \"topic\", c.redisPubTopics, \"keys\", keys)\n\t\t// Batch delete keys\n\t\terr := ProcessKeysBySlot(ctx, c.redisClient, keys, func(ctx context.Context, slot int64, keys []string) error {\n\t\t\treturn c.rocksClient.TagAsDeletedBatch2(ctx, keys)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// Publish the keys that have been deleted to Redis to update the local cache information of other nodes\n\t\tif len(c.redisPubTopics) > 0 && len(keys) > 0 {\n\t\t\tkeysByTopic := localcache.GetPublishKeysByTopic(c.redisPubTopics, keys)\n\t\t\tfor topic, keys := range keysByTopic {\n\t\t\t\tif len(keys) > 0 {\n\t\t\t\t\tdata, err := json.Marshal(keys)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.ZWarn(ctx, \"keys json marshal failed\", err, \"topic\", topic, \"keys\", keys)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif err := c.redisClient.Publish(ctx, topic, string(data)).Err(); err != nil {\n\t\t\t\t\t\t\tlog.ZWarn(ctx, \"redis publish cache delete error\", err, \"topic\", topic, \"keys\", keys)\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\treturn nil\n}\n\n// Clone creates a copy of BatchDeleterRedis for chain calls to prevent memory pollution.\nfunc (c *BatchDeleterRedis) Clone() cache.BatchDeleter {\n\treturn &BatchDeleterRedis{\n\t\tredisClient:    c.redisClient,\n\t\tkeys:           c.keys,\n\t\trocksClient:    c.rocksClient,\n\t\tredisPubTopics: c.redisPubTopics,\n\t}\n}\n\n// AddKeys adds keys to be deleted.\nfunc (c *BatchDeleterRedis) AddKeys(keys ...string) {\n\tc.keys = append(c.keys, keys...)\n}\n\ntype disableBatchDeleter struct{}\n\nfunc (x disableBatchDeleter) ChainExecDel(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (x disableBatchDeleter) ExecDelWithKeys(ctx context.Context, keys []string) error {\n\treturn nil\n}\n\nfunc (x disableBatchDeleter) Clone() cache.BatchDeleter {\n\treturn x\n}\n\nfunc (x disableBatchDeleter) AddKeys(keys ...string) {}\n\nfunc getCache[T any](ctx context.Context, rcClient *rocksCacheClient, key string, expire time.Duration, fn func(ctx context.Context) (T, error)) (T, error) {\n\tif rcClient.Disable() {\n\t\treturn fn(ctx)\n\t}\n\tvar t T\n\tvar write bool\n\tv, err := rcClient.GetClient().Fetch2(ctx, key, expire, func() (s string, err error) {\n\t\tt, err = fn(ctx)\n\t\tif err != nil {\n\t\t\t//log.ZError(ctx, \"getCache query database failed\", err, \"key\", key)\n\t\t\treturn \"\", err\n\t\t}\n\t\tbs, err := json.Marshal(t)\n\t\tif err != nil {\n\t\t\treturn \"\", errs.WrapMsg(err, \"marshal failed\")\n\t\t}\n\t\twrite = true\n\n\t\treturn string(bs), nil\n\t})\n\tif err != nil {\n\t\treturn t, errs.Wrap(err)\n\t}\n\tif write {\n\t\treturn t, nil\n\t}\n\tif v == \"\" {\n\t\treturn t, errs.ErrRecordNotFound.WrapMsg(\"cache is not found\")\n\t}\n\terr = json.Unmarshal([]byte(v), &t)\n\tif err != nil {\n\t\terrInfo := fmt.Sprintf(\"cache json.Unmarshal failed, key:%s, value:%s, expire:%s\", key, v, expire)\n\t\treturn t, errs.WrapMsg(err, errInfo)\n\t}\n\n\treturn t, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/batch_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/redisutil\"\n\t\"testing\"\n)\n\nfunc TestName(t *testing.T) {\n\t//var rocks rockscache.Client\n\t//rdb := getRocksCacheRedisClient(&rocks)\n\t//t.Log(rdb == nil)\n\n\tctx := context.Background()\n\trdb, err := redisutil.NewRedisClient(ctx, (&config.Redis{\n\t\tAddress:  []string{\"172.16.8.48:16379\"},\n\t\tPassword: \"openIM123\",\n\t\tDB:       3,\n\t}).Build())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tmgocli, err := mongoutil.NewMongoDB(ctx, (&config.Mongo{\n\t\tAddress:     []string{\"172.16.8.48:37017\"},\n\t\tDatabase:    \"openim_v3\",\n\t\tUsername:    \"openIM\",\n\t\tPassword:    \"openIM123\",\n\t\tMaxPoolSize: 100,\n\t\tMaxRetry:    1,\n\t}).Build())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t//userMgo, err := mgo.NewUserMongo(mgocli.GetDB())\n\t//if err != nil {\n\t//\tpanic(err)\n\t//}\n\t//rock := rockscache.NewClient(rdb, rockscache.NewDefaultOptions())\n\tmgoSeqUser, err := mgo.NewSeqUserMongo(mgocli.GetDB())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tseqUser := NewSeqUserCacheRedis(rdb, mgoSeqUser)\n\n\tres, err := seqUser.GetUserReadSeqs(ctx, \"2110910952\", []string{\"sg_2920732023\", \"sg_345762580\"})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tt.Log(res)\n\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/black.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tblackExpireTime = time.Second * 60 * 60 * 12\n)\n\ntype BlackCacheRedis struct {\n\tcache.BatchDeleter\n\texpireTime time.Duration\n\trcClient   *rocksCacheClient\n\tblackDB    database.Black\n}\n\nfunc NewBlackCacheRedis(rdb redis.UniversalClient, localCache *config.LocalCache, blackDB database.Black) cache.BlackCache {\n\trc := newRocksCacheClient(rdb)\n\treturn &BlackCacheRedis{\n\t\tBatchDeleter: rc.GetBatchDeleter(localCache.Friend.Topic),\n\t\texpireTime:   blackExpireTime,\n\t\trcClient:     rc,\n\t\tblackDB:      blackDB,\n\t}\n}\n\nfunc (b *BlackCacheRedis) CloneBlackCache() cache.BlackCache {\n\treturn &BlackCacheRedis{\n\t\tBatchDeleter: b.BatchDeleter.Clone(),\n\t\texpireTime:   b.expireTime,\n\t\trcClient:     b.rcClient,\n\t\tblackDB:      b.blackDB,\n\t}\n}\n\nfunc (b *BlackCacheRedis) getBlackIDsKey(ownerUserID string) string {\n\treturn cachekey.GetBlackIDsKey(ownerUserID)\n}\n\nfunc (b *BlackCacheRedis) GetBlackIDs(ctx context.Context, userID string) (blackIDs []string, err error) {\n\treturn getCache(\n\t\tctx,\n\t\tb.rcClient,\n\t\tb.getBlackIDsKey(userID),\n\t\tb.expireTime,\n\t\tfunc(ctx context.Context) ([]string, error) {\n\t\t\treturn b.blackDB.FindBlackUserIDs(ctx, userID)\n\t\t},\n\t)\n}\n\nfunc (b *BlackCacheRedis) DelBlackIDs(_ context.Context, userID string) cache.BlackCache {\n\tcache := b.CloneBlackCache()\n\tcache.AddKeys(b.getBlackIDsKey(userID))\n\n\treturn cache\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/client_config.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewClientConfigCache(rdb redis.UniversalClient, mgo database.ClientConfig) cache.ClientConfigCache {\n\trc := newRocksCacheClient(rdb)\n\treturn &ClientConfigCache{\n\t\tmgo:      mgo,\n\t\trcClient: rc,\n\t\tdelete:   rc.GetBatchDeleter(),\n\t}\n}\n\ntype ClientConfigCache struct {\n\tmgo      database.ClientConfig\n\trcClient *rocksCacheClient\n\tdelete   cache.BatchDeleter\n}\n\nfunc (x *ClientConfigCache) getExpireTime(userID string) time.Duration {\n\tif userID == \"\" {\n\t\treturn time.Hour * 24\n\t} else {\n\t\treturn time.Hour\n\t}\n}\n\nfunc (x *ClientConfigCache) getClientConfigKey(userID string) string {\n\treturn cachekey.GetClientConfigKey(userID)\n}\n\nfunc (x *ClientConfigCache) GetConfig(ctx context.Context, userID string) (map[string]string, error) {\n\treturn getCache(ctx, x.rcClient, x.getClientConfigKey(userID), x.getExpireTime(userID), func(ctx context.Context) (map[string]string, error) {\n\t\treturn x.mgo.Get(ctx, userID)\n\t})\n}\n\nfunc (x *ClientConfigCache) DeleteUserCache(ctx context.Context, userIDs []string) error {\n\tkeys := make([]string, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tkeys = append(keys, x.getClientConfigKey(userID))\n\t}\n\treturn x.delete.ExecDelWithKeys(ctx, keys)\n}\n\nfunc (x *ClientConfigCache) GetUserConfig(ctx context.Context, userID string) (map[string]string, error) {\n\tconfig, err := x.GetConfig(ctx, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif userID != \"\" {\n\t\tuserConfig, err := x.GetConfig(ctx, userID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor k, v := range userConfig {\n\t\t\tconfig[k] = v\n\t\t}\n\t}\n\treturn config, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/conversation.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"math/big\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/encrypt\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tconversationExpireTime = time.Second * 60 * 60 * 12\n)\n\nfunc NewConversationRedis(rdb redis.UniversalClient, localCache *config.LocalCache, db database.Conversation) cache.ConversationCache {\n\trc := newRocksCacheClient(rdb)\n\treturn &ConversationRedisCache{\n\t\tBatchDeleter:   rc.GetBatchDeleter(localCache.Conversation.Topic),\n\t\trcClient:       rc,\n\t\tconversationDB: db,\n\t\texpireTime:     conversationExpireTime,\n\t}\n}\n\ntype ConversationRedisCache struct {\n\tcache.BatchDeleter\n\trcClient       *rocksCacheClient\n\tconversationDB database.Conversation\n\texpireTime     time.Duration\n}\n\nfunc (c *ConversationRedisCache) CloneConversationCache() cache.ConversationCache {\n\treturn &ConversationRedisCache{\n\t\tBatchDeleter:   c.BatchDeleter.Clone(),\n\t\trcClient:       c.rcClient,\n\t\tconversationDB: c.conversationDB,\n\t\texpireTime:     c.expireTime,\n\t}\n}\n\nfunc (c *ConversationRedisCache) getConversationKey(ownerUserID, conversationID string) string {\n\treturn cachekey.GetConversationKey(ownerUserID, conversationID)\n}\n\nfunc (c *ConversationRedisCache) getConversationIDsKey(ownerUserID string) string {\n\treturn cachekey.GetConversationIDsKey(ownerUserID)\n}\n\nfunc (c *ConversationRedisCache) getNotNotifyConversationIDsKey(ownerUserID string) string {\n\treturn cachekey.GetNotNotifyConversationIDsKey(ownerUserID)\n}\n\nfunc (c *ConversationRedisCache) getPinnedConversationIDsKey(ownerUserID string) string {\n\treturn cachekey.GetPinnedConversationIDs(ownerUserID)\n}\n\nfunc (c *ConversationRedisCache) getSuperGroupRecvNotNotifyUserIDsKey(groupID string) string {\n\treturn cachekey.GetSuperGroupRecvNotNotifyUserIDsKey(groupID)\n}\n\nfunc (c *ConversationRedisCache) getRecvMsgOptKey(ownerUserID, conversationID string) string {\n\treturn cachekey.GetRecvMsgOptKey(ownerUserID, conversationID)\n}\n\nfunc (c *ConversationRedisCache) getSuperGroupRecvNotNotifyUserIDsHashKey(groupID string) string {\n\treturn cachekey.GetSuperGroupRecvNotNotifyUserIDsHashKey(groupID)\n}\n\nfunc (c *ConversationRedisCache) getConversationHasReadSeqKey(ownerUserID, conversationID string) string {\n\treturn cachekey.GetConversationHasReadSeqKey(ownerUserID, conversationID)\n}\n\nfunc (c *ConversationRedisCache) getConversationNotReceiveMessageUserIDsKey(conversationID string) string {\n\treturn cachekey.GetConversationNotReceiveMessageUserIDsKey(conversationID)\n}\n\nfunc (c *ConversationRedisCache) getUserConversationIDsHashKey(ownerUserID string) string {\n\treturn cachekey.GetUserConversationIDsHashKey(ownerUserID)\n}\n\nfunc (c *ConversationRedisCache) getConversationUserMaxVersionKey(ownerUserID string) string {\n\treturn cachekey.GetConversationUserMaxVersionKey(ownerUserID)\n}\n\nfunc (c *ConversationRedisCache) GetUserConversationIDs(ctx context.Context, ownerUserID string) ([]string, error) {\n\treturn getCache(ctx, c.rcClient, c.getConversationIDsKey(ownerUserID), c.expireTime, func(ctx context.Context) ([]string, error) {\n\t\treturn c.conversationDB.FindUserIDAllConversationID(ctx, ownerUserID)\n\t})\n}\n\nfunc (c *ConversationRedisCache) GetUserNotNotifyConversationIDs(ctx context.Context, userID string) ([]string, error) {\n\treturn getCache(ctx, c.rcClient, c.getNotNotifyConversationIDsKey(userID), c.expireTime, func(ctx context.Context) ([]string, error) {\n\t\treturn c.conversationDB.FindUserIDAllNotNotifyConversationID(ctx, userID)\n\t})\n}\n\nfunc (c *ConversationRedisCache) GetPinnedConversationIDs(ctx context.Context, userID string) ([]string, error) {\n\treturn getCache(ctx, c.rcClient, c.getPinnedConversationIDsKey(userID), c.expireTime, func(ctx context.Context) ([]string, error) {\n\t\treturn c.conversationDB.FindUserIDAllPinnedConversationID(ctx, userID)\n\t})\n}\n\nfunc (c *ConversationRedisCache) DelConversationIDs(userIDs ...string) cache.ConversationCache {\n\tkeys := make([]string, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tkeys = append(keys, c.getConversationIDsKey(userID))\n\t}\n\tcache := c.CloneConversationCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) GetUserConversationIDsHash(ctx context.Context, ownerUserID string) (hash uint64, err error) {\n\treturn getCache(\n\t\tctx,\n\t\tc.rcClient,\n\t\tc.getUserConversationIDsHashKey(ownerUserID),\n\t\tc.expireTime,\n\t\tfunc(ctx context.Context) (uint64, error) {\n\t\t\tconversationIDs, err := c.GetUserConversationIDs(ctx, ownerUserID)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tdatautil.Sort(conversationIDs, true)\n\t\t\tbi := big.NewInt(0)\n\t\t\tbi.SetString(encrypt.Md5(strings.Join(conversationIDs, \";\"))[0:8], 16)\n\t\t\treturn bi.Uint64(), nil\n\t\t},\n\t)\n}\n\nfunc (c *ConversationRedisCache) DelUserConversationIDsHash(ownerUserIDs ...string) cache.ConversationCache {\n\tkeys := make([]string, 0, len(ownerUserIDs))\n\tfor _, ownerUserID := range ownerUserIDs {\n\t\tkeys = append(keys, c.getUserConversationIDsHashKey(ownerUserID))\n\t}\n\tcache := c.CloneConversationCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) GetConversation(ctx context.Context, ownerUserID, conversationID string) (*model.Conversation, error) {\n\treturn getCache(ctx, c.rcClient, c.getConversationKey(ownerUserID, conversationID), c.expireTime, func(ctx context.Context) (*model.Conversation, error) {\n\t\treturn c.conversationDB.Take(ctx, ownerUserID, conversationID)\n\t})\n}\n\nfunc (c *ConversationRedisCache) DelConversations(ownerUserID string, conversationIDs ...string) cache.ConversationCache {\n\tkeys := make([]string, 0, len(conversationIDs))\n\tfor _, conversationID := range conversationIDs {\n\t\tkeys = append(keys, c.getConversationKey(ownerUserID, conversationID))\n\t}\n\tcache := c.CloneConversationCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) GetConversations(ctx context.Context, ownerUserID string, conversationIDs []string) ([]*model.Conversation, error) {\n\treturn batchGetCache2(ctx, c.rcClient, c.expireTime, conversationIDs, func(conversationID string) string {\n\t\treturn c.getConversationKey(ownerUserID, conversationID)\n\t}, func(conversation *model.Conversation) string {\n\t\treturn conversation.ConversationID\n\t}, func(ctx context.Context, conversationIDs []string) ([]*model.Conversation, error) {\n\t\treturn c.conversationDB.Find(ctx, ownerUserID, conversationIDs)\n\t})\n}\n\nfunc (c *ConversationRedisCache) GetUserAllConversations(ctx context.Context, ownerUserID string) ([]*model.Conversation, error) {\n\tconversationIDs, err := c.GetUserConversationIDs(ctx, ownerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.GetConversations(ctx, ownerUserID, conversationIDs)\n}\n\nfunc (c *ConversationRedisCache) GetUserRecvMsgOpt(ctx context.Context, ownerUserID, conversationID string) (opt int, err error) {\n\treturn getCache(ctx, c.rcClient, c.getRecvMsgOptKey(ownerUserID, conversationID), c.expireTime, func(ctx context.Context) (opt int, err error) {\n\t\treturn c.conversationDB.GetUserRecvMsgOpt(ctx, ownerUserID, conversationID)\n\t})\n}\n\nfunc (c *ConversationRedisCache) DelUsersConversation(conversationID string, ownerUserIDs ...string) cache.ConversationCache {\n\tkeys := make([]string, 0, len(ownerUserIDs))\n\tfor _, ownerUserID := range ownerUserIDs {\n\t\tkeys = append(keys, c.getConversationKey(ownerUserID, conversationID))\n\t}\n\tcache := c.CloneConversationCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) DelUserRecvMsgOpt(ownerUserID, conversationID string) cache.ConversationCache {\n\tcache := c.CloneConversationCache()\n\tcache.AddKeys(c.getRecvMsgOptKey(ownerUserID, conversationID))\n\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) DelSuperGroupRecvMsgNotNotifyUserIDs(groupID string) cache.ConversationCache {\n\tcache := c.CloneConversationCache()\n\tcache.AddKeys(c.getSuperGroupRecvNotNotifyUserIDsKey(groupID))\n\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) DelSuperGroupRecvMsgNotNotifyUserIDsHash(groupID string) cache.ConversationCache {\n\tcache := c.CloneConversationCache()\n\tcache.AddKeys(c.getSuperGroupRecvNotNotifyUserIDsHashKey(groupID))\n\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) DelUserAllHasReadSeqs(ownerUserID string, conversationIDs ...string) cache.ConversationCache {\n\tcache := c.CloneConversationCache()\n\tfor _, conversationID := range conversationIDs {\n\t\tcache.AddKeys(c.getConversationHasReadSeqKey(ownerUserID, conversationID))\n\t}\n\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) GetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error) {\n\treturn getCache(ctx, c.rcClient, c.getConversationNotReceiveMessageUserIDsKey(conversationID), c.expireTime, func(ctx context.Context) ([]string, error) {\n\t\treturn c.conversationDB.GetConversationNotReceiveMessageUserIDs(ctx, conversationID)\n\t})\n}\n\nfunc (c *ConversationRedisCache) DelConversationNotReceiveMessageUserIDs(conversationIDs ...string) cache.ConversationCache {\n\tcache := c.CloneConversationCache()\n\tfor _, conversationID := range conversationIDs {\n\t\tcache.AddKeys(c.getConversationNotReceiveMessageUserIDsKey(conversationID))\n\t}\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) DelConversationNotNotifyMessageUserIDs(userIDs ...string) cache.ConversationCache {\n\tcache := c.CloneConversationCache()\n\tfor _, userID := range userIDs {\n\t\tcache.AddKeys(c.getNotNotifyConversationIDsKey(userID))\n\t}\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) DelUserPinnedConversations(userIDs ...string) cache.ConversationCache {\n\tcache := c.CloneConversationCache()\n\tfor _, userID := range userIDs {\n\t\tcache.AddKeys(c.getPinnedConversationIDsKey(userID))\n\t}\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) DelConversationVersionUserIDs(userIDs ...string) cache.ConversationCache {\n\tcache := c.CloneConversationCache()\n\tfor _, userID := range userIDs {\n\t\tcache.AddKeys(c.getConversationUserMaxVersionKey(userID))\n\t}\n\treturn cache\n}\n\nfunc (c *ConversationRedisCache) FindMaxConversationUserVersion(ctx context.Context, userID string) (*model.VersionLog, error) {\n\treturn getCache(ctx, c.rcClient, c.getConversationUserMaxVersionKey(userID), c.expireTime, func(ctx context.Context) (*model.VersionLog, error) {\n\t\treturn c.conversationDB.FindConversationUserVersion(ctx, userID, 0, 0)\n\t})\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/friend.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tfriendExpireTime = time.Second * 60 * 60 * 12\n)\n\n// FriendCacheRedis is an implementation of the FriendCache interface using Redis.\ntype FriendCacheRedis struct {\n\tcache.BatchDeleter\n\tfriendDB   database.Friend\n\texpireTime time.Duration\n\trcClient   *rocksCacheClient\n\tsyncCount  int\n}\n\n// NewFriendCacheRedis creates a new instance of FriendCacheRedis.\nfunc NewFriendCacheRedis(rdb redis.UniversalClient, localCache *config.LocalCache, friendDB database.Friend) cache.FriendCache {\n\trc := newRocksCacheClient(rdb)\n\treturn &FriendCacheRedis{\n\t\tBatchDeleter: rc.GetBatchDeleter(localCache.Friend.Topic),\n\t\tfriendDB:     friendDB,\n\t\texpireTime:   friendExpireTime,\n\t\trcClient:     rc,\n\t}\n}\n\nfunc (f *FriendCacheRedis) CloneFriendCache() cache.FriendCache {\n\treturn &FriendCacheRedis{\n\t\tBatchDeleter: f.BatchDeleter.Clone(),\n\t\tfriendDB:     f.friendDB,\n\t\texpireTime:   f.expireTime,\n\t\trcClient:     f.rcClient,\n\t}\n}\n\n// getFriendIDsKey returns the key for storing friend IDs in the cache.\nfunc (f *FriendCacheRedis) getFriendIDsKey(ownerUserID string) string {\n\treturn cachekey.GetFriendIDsKey(ownerUserID)\n}\n\nfunc (f *FriendCacheRedis) getFriendMaxVersionKey(ownerUserID string) string {\n\treturn cachekey.GetFriendMaxVersionKey(ownerUserID)\n}\n\n// getTwoWayFriendsIDsKey returns the key for storing two-way friend IDs in the cache.\nfunc (f *FriendCacheRedis) getTwoWayFriendsIDsKey(ownerUserID string) string {\n\treturn cachekey.GetTwoWayFriendsIDsKey(ownerUserID)\n}\n\n// getFriendKey returns the key for storing friend info in the cache.\nfunc (f *FriendCacheRedis) getFriendKey(ownerUserID, friendUserID string) string {\n\treturn cachekey.GetFriendKey(ownerUserID, friendUserID)\n}\n\n// GetFriendIDs retrieves friend IDs from the cache or the database if not found.\nfunc (f *FriendCacheRedis) GetFriendIDs(ctx context.Context, ownerUserID string) (friendIDs []string, err error) {\n\treturn getCache(ctx, f.rcClient, f.getFriendIDsKey(ownerUserID), f.expireTime, func(ctx context.Context) ([]string, error) {\n\t\treturn f.friendDB.FindFriendUserIDs(ctx, ownerUserID)\n\t})\n}\n\n// DelFriendIDs deletes friend IDs from the cache.\nfunc (f *FriendCacheRedis) DelFriendIDs(ownerUserIDs ...string) cache.FriendCache {\n\tnewFriendCache := f.CloneFriendCache()\n\tkeys := make([]string, 0, len(ownerUserIDs))\n\tfor _, userID := range ownerUserIDs {\n\t\tkeys = append(keys, f.getFriendIDsKey(userID))\n\t}\n\tnewFriendCache.AddKeys(keys...)\n\n\treturn newFriendCache\n}\n\n// GetTwoWayFriendIDs retrieves two-way friend IDs from the cache.\nfunc (f *FriendCacheRedis) GetTwoWayFriendIDs(ctx context.Context, ownerUserID string) (twoWayFriendIDs []string, err error) {\n\tfriendIDs, err := f.GetFriendIDs(ctx, ownerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, friendID := range friendIDs {\n\t\tfriendFriendID, err := f.GetFriendIDs(ctx, friendID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif datautil.Contain(ownerUserID, friendFriendID...) {\n\t\t\ttwoWayFriendIDs = append(twoWayFriendIDs, ownerUserID)\n\t\t}\n\t}\n\n\treturn twoWayFriendIDs, nil\n}\n\n// DelTwoWayFriendIDs deletes two-way friend IDs from the cache.\nfunc (f *FriendCacheRedis) DelTwoWayFriendIDs(ctx context.Context, ownerUserID string) cache.FriendCache {\n\tnewFriendCache := f.CloneFriendCache()\n\tnewFriendCache.AddKeys(f.getTwoWayFriendsIDsKey(ownerUserID))\n\n\treturn newFriendCache\n}\n\n// GetFriend retrieves friend info from the cache or the database if not found.\nfunc (f *FriendCacheRedis) GetFriend(ctx context.Context, ownerUserID, friendUserID string) (friend *model.Friend, err error) {\n\treturn getCache(ctx, f.rcClient, f.getFriendKey(ownerUserID,\n\t\tfriendUserID), f.expireTime, func(ctx context.Context) (*model.Friend, error) {\n\t\treturn f.friendDB.Take(ctx, ownerUserID, friendUserID)\n\t})\n}\n\n// DelFriend deletes friend info from the cache.\nfunc (f *FriendCacheRedis) DelFriend(ownerUserID, friendUserID string) cache.FriendCache {\n\tnewFriendCache := f.CloneFriendCache()\n\tnewFriendCache.AddKeys(f.getFriendKey(ownerUserID, friendUserID))\n\n\treturn newFriendCache\n}\n\n// DelFriends deletes multiple friend infos from the cache.\nfunc (f *FriendCacheRedis) DelFriends(ownerUserID string, friendUserIDs []string) cache.FriendCache {\n\tnewFriendCache := f.CloneFriendCache()\n\n\tfor _, friendUserID := range friendUserIDs {\n\t\tkey := f.getFriendKey(ownerUserID, friendUserID)\n\t\tnewFriendCache.AddKeys(key) // Assuming AddKeys marks the keys for deletion\n\t}\n\n\treturn newFriendCache\n}\n\nfunc (f *FriendCacheRedis) DelOwner(friendUserID string, ownerUserIDs []string) cache.FriendCache {\n\tnewFriendCache := f.CloneFriendCache()\n\n\tfor _, ownerUserID := range ownerUserIDs {\n\t\tkey := f.getFriendKey(ownerUserID, friendUserID)\n\t\tnewFriendCache.AddKeys(key) // Assuming AddKeys marks the keys for deletion\n\t}\n\n\treturn newFriendCache\n}\n\nfunc (f *FriendCacheRedis) DelMaxFriendVersion(ownerUserIDs ...string) cache.FriendCache {\n\tnewFriendCache := f.CloneFriendCache()\n\tfor _, ownerUserID := range ownerUserIDs {\n\t\tkey := f.getFriendMaxVersionKey(ownerUserID)\n\t\tnewFriendCache.AddKeys(key) // Assuming AddKeys marks the keys for deletion\n\t}\n\n\treturn newFriendCache\n}\n\nfunc (f *FriendCacheRedis) FindMaxFriendVersion(ctx context.Context, ownerUserID string) (*model.VersionLog, error) {\n\treturn getCache(ctx, f.rcClient, f.getFriendMaxVersionKey(ownerUserID), f.expireTime, func(ctx context.Context) (*model.VersionLog, error) {\n\t\treturn f.friendDB.FindIncrVersion(ctx, ownerUserID, 0, 0)\n\t})\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/group.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/common\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tgroupExpireTime = time.Second * 60 * 60 * 12\n)\n\ntype GroupCacheRedis struct {\n\tcache.BatchDeleter\n\tgroupDB        database.Group\n\tgroupMemberDB  database.GroupMember\n\tgroupRequestDB database.GroupRequest\n\texpireTime     time.Duration\n\trcClient       *rocksCacheClient\n\tgroupHash      cache.GroupHash\n}\n\nfunc NewGroupCacheRedis(rdb redis.UniversalClient, localCache *config.LocalCache, groupDB database.Group, groupMemberDB database.GroupMember, groupRequestDB database.GroupRequest, hashCode cache.GroupHash) cache.GroupCache {\n\trc := newRocksCacheClient(rdb)\n\treturn &GroupCacheRedis{\n\t\tBatchDeleter:   rc.GetBatchDeleter(localCache.Group.Topic),\n\t\trcClient:       rc,\n\t\texpireTime:     groupExpireTime,\n\t\tgroupDB:        groupDB,\n\t\tgroupMemberDB:  groupMemberDB,\n\t\tgroupRequestDB: groupRequestDB,\n\t\tgroupHash:      hashCode,\n\t}\n}\n\nfunc (g *GroupCacheRedis) CloneGroupCache() cache.GroupCache {\n\treturn &GroupCacheRedis{\n\t\tBatchDeleter:   g.BatchDeleter.Clone(),\n\t\trcClient:       g.rcClient,\n\t\texpireTime:     g.expireTime,\n\t\tgroupDB:        g.groupDB,\n\t\tgroupMemberDB:  g.groupMemberDB,\n\t\tgroupRequestDB: g.groupRequestDB,\n\t}\n}\n\nfunc (g *GroupCacheRedis) getGroupInfoKey(groupID string) string {\n\treturn cachekey.GetGroupInfoKey(groupID)\n}\n\nfunc (g *GroupCacheRedis) getJoinedGroupsKey(userID string) string {\n\treturn cachekey.GetJoinedGroupsKey(userID)\n}\n\nfunc (g *GroupCacheRedis) getGroupMembersHashKey(groupID string) string {\n\treturn cachekey.GetGroupMembersHashKey(groupID)\n}\n\nfunc (g *GroupCacheRedis) getGroupMemberIDsKey(groupID string) string {\n\treturn cachekey.GetGroupMemberIDsKey(groupID)\n}\n\nfunc (g *GroupCacheRedis) getGroupMemberInfoKey(groupID, userID string) string {\n\treturn cachekey.GetGroupMemberInfoKey(groupID, userID)\n}\n\nfunc (g *GroupCacheRedis) getGroupMemberNumKey(groupID string) string {\n\treturn cachekey.GetGroupMemberNumKey(groupID)\n}\n\nfunc (g *GroupCacheRedis) getGroupRoleLevelMemberIDsKey(groupID string, roleLevel int32) string {\n\treturn cachekey.GetGroupRoleLevelMemberIDsKey(groupID, roleLevel)\n}\n\nfunc (g *GroupCacheRedis) getGroupMemberMaxVersionKey(groupID string) string {\n\treturn cachekey.GetGroupMemberMaxVersionKey(groupID)\n}\n\nfunc (g *GroupCacheRedis) getJoinGroupMaxVersionKey(userID string) string {\n\treturn cachekey.GetJoinGroupMaxVersionKey(userID)\n}\n\nfunc (g *GroupCacheRedis) getGroupID(group *model.Group) string {\n\treturn group.GroupID\n}\n\nfunc (g *GroupCacheRedis) GetGroupsInfo(ctx context.Context, groupIDs []string) (groups []*model.Group, err error) {\n\treturn batchGetCache2(ctx, g.rcClient, g.expireTime, groupIDs, g.getGroupInfoKey, g.getGroupID, g.groupDB.Find)\n}\n\nfunc (g *GroupCacheRedis) GetGroupInfo(ctx context.Context, groupID string) (group *model.Group, err error) {\n\treturn getCache(ctx, g.rcClient, g.getGroupInfoKey(groupID), g.expireTime, func(ctx context.Context) (*model.Group, error) {\n\t\treturn g.groupDB.Take(ctx, groupID)\n\t})\n}\n\nfunc (g *GroupCacheRedis) DelGroupsInfo(groupIDs ...string) cache.GroupCache {\n\tnewGroupCache := g.CloneGroupCache()\n\tkeys := make([]string, 0, len(groupIDs))\n\tfor _, groupID := range groupIDs {\n\t\tkeys = append(keys, g.getGroupInfoKey(groupID))\n\t}\n\tnewGroupCache.AddKeys(keys...)\n\n\treturn newGroupCache\n}\n\nfunc (g *GroupCacheRedis) DelGroupsOwner(groupIDs ...string) cache.GroupCache {\n\tnewGroupCache := g.CloneGroupCache()\n\tkeys := make([]string, 0, len(groupIDs))\n\tfor _, groupID := range groupIDs {\n\t\tkeys = append(keys, g.getGroupRoleLevelMemberIDsKey(groupID, constant.GroupOwner))\n\t}\n\tnewGroupCache.AddKeys(keys...)\n\n\treturn newGroupCache\n}\n\nfunc (g *GroupCacheRedis) DelGroupRoleLevel(groupID string, roleLevels []int32) cache.GroupCache {\n\tnewGroupCache := g.CloneGroupCache()\n\tkeys := make([]string, 0, len(roleLevels))\n\tfor _, roleLevel := range roleLevels {\n\t\tkeys = append(keys, g.getGroupRoleLevelMemberIDsKey(groupID, roleLevel))\n\t}\n\tnewGroupCache.AddKeys(keys...)\n\treturn newGroupCache\n}\n\nfunc (g *GroupCacheRedis) DelGroupAllRoleLevel(groupID string) cache.GroupCache {\n\treturn g.DelGroupRoleLevel(groupID, []int32{constant.GroupOwner, constant.GroupAdmin, constant.GroupOrdinaryUsers})\n}\n\nfunc (g *GroupCacheRedis) GetGroupMembersHash(ctx context.Context, groupID string) (hashCode uint64, err error) {\n\tif g.groupHash == nil {\n\t\treturn 0, errs.ErrInternalServer.WrapMsg(\"group hash is nil\")\n\t}\n\treturn getCache(ctx, g.rcClient, g.getGroupMembersHashKey(groupID), g.expireTime, func(ctx context.Context) (uint64, error) {\n\t\treturn g.groupHash.GetGroupHash(ctx, groupID)\n\t})\n}\n\nfunc (g *GroupCacheRedis) GetGroupMemberHashMap(ctx context.Context, groupIDs []string) (map[string]*common.GroupSimpleUserID, error) {\n\tif g.groupHash == nil {\n\t\treturn nil, errs.ErrInternalServer.WrapMsg(\"group hash is nil\")\n\t}\n\tres := make(map[string]*common.GroupSimpleUserID)\n\tfor _, groupID := range groupIDs {\n\t\thash, err := g.GetGroupMembersHash(ctx, groupID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.ZDebug(ctx, \"GetGroupMemberHashMap\", \"groupID\", groupID, \"hash\", hash)\n\t\tnum, err := g.GetGroupMemberNum(ctx, groupID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres[groupID] = &common.GroupSimpleUserID{Hash: hash, MemberNum: uint32(num)}\n\t}\n\n\treturn res, nil\n}\n\nfunc (g *GroupCacheRedis) DelGroupMembersHash(groupID string) cache.GroupCache {\n\tcache := g.CloneGroupCache()\n\tcache.AddKeys(g.getGroupMembersHashKey(groupID))\n\n\treturn cache\n}\n\nfunc (g *GroupCacheRedis) GetGroupMemberIDs(ctx context.Context, groupID string) (groupMemberIDs []string, err error) {\n\treturn getCache(ctx, g.rcClient, g.getGroupMemberIDsKey(groupID), g.expireTime, func(ctx context.Context) ([]string, error) {\n\t\treturn g.groupMemberDB.FindMemberUserID(ctx, groupID)\n\t})\n}\n\nfunc (g *GroupCacheRedis) DelGroupMemberIDs(groupID string) cache.GroupCache {\n\tcache := g.CloneGroupCache()\n\tcache.AddKeys(g.getGroupMemberIDsKey(groupID))\n\n\treturn cache\n}\n\nfunc (g *GroupCacheRedis) findUserJoinedGroupID(ctx context.Context, userID string) ([]string, error) {\n\tgroupIDs, err := g.groupMemberDB.FindUserJoinedGroupID(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn g.groupDB.FindJoinSortGroupID(ctx, groupIDs)\n}\n\nfunc (g *GroupCacheRedis) GetJoinedGroupIDs(ctx context.Context, userID string) (joinedGroupIDs []string, err error) {\n\treturn getCache(ctx, g.rcClient, g.getJoinedGroupsKey(userID), g.expireTime, func(ctx context.Context) ([]string, error) {\n\t\treturn g.findUserJoinedGroupID(ctx, userID)\n\t})\n}\n\nfunc (g *GroupCacheRedis) DelJoinedGroupID(userIDs ...string) cache.GroupCache {\n\tkeys := make([]string, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tkeys = append(keys, g.getJoinedGroupsKey(userID))\n\t}\n\tcache := g.CloneGroupCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n\nfunc (g *GroupCacheRedis) GetGroupMemberInfo(ctx context.Context, groupID, userID string) (groupMember *model.GroupMember, err error) {\n\treturn getCache(ctx, g.rcClient, g.getGroupMemberInfoKey(groupID, userID), g.expireTime, func(ctx context.Context) (*model.GroupMember, error) {\n\t\treturn g.groupMemberDB.Take(ctx, groupID, userID)\n\t})\n}\n\nfunc (g *GroupCacheRedis) GetGroupMembersInfo(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupMember, error) {\n\treturn batchGetCache2(ctx, g.rcClient, g.expireTime, userIDs, func(userID string) string {\n\t\treturn g.getGroupMemberInfoKey(groupID, userID)\n\t}, func(member *model.GroupMember) string {\n\t\treturn member.UserID\n\t}, func(ctx context.Context, userIDs []string) ([]*model.GroupMember, error) {\n\t\treturn g.groupMemberDB.Find(ctx, groupID, userIDs)\n\t})\n}\n\nfunc (g *GroupCacheRedis) GetAllGroupMembersInfo(ctx context.Context, groupID string) (groupMembers []*model.GroupMember, err error) {\n\tgroupMemberIDs, err := g.GetGroupMemberIDs(ctx, groupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn g.GetGroupMembersInfo(ctx, groupID, groupMemberIDs)\n}\n\nfunc (g *GroupCacheRedis) DelGroupMembersInfo(groupID string, userIDs ...string) cache.GroupCache {\n\tkeys := make([]string, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tkeys = append(keys, g.getGroupMemberInfoKey(groupID, userID))\n\t}\n\tcache := g.CloneGroupCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n\nfunc (g *GroupCacheRedis) GetGroupMemberNum(ctx context.Context, groupID string) (memberNum int64, err error) {\n\treturn getCache(ctx, g.rcClient, g.getGroupMemberNumKey(groupID), g.expireTime, func(ctx context.Context) (int64, error) {\n\t\treturn g.groupMemberDB.TakeGroupMemberNum(ctx, groupID)\n\t})\n}\n\nfunc (g *GroupCacheRedis) DelGroupsMemberNum(groupID ...string) cache.GroupCache {\n\tkeys := make([]string, 0, len(groupID))\n\tfor _, groupID := range groupID {\n\t\tkeys = append(keys, g.getGroupMemberNumKey(groupID))\n\t}\n\tcache := g.CloneGroupCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n\nfunc (g *GroupCacheRedis) GetGroupOwner(ctx context.Context, groupID string) (*model.GroupMember, error) {\n\tmembers, err := g.GetGroupRoleLevelMemberInfo(ctx, groupID, constant.GroupOwner)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(members) == 0 {\n\t\treturn nil, errs.ErrRecordNotFound.WrapMsg(fmt.Sprintf(\"group %s owner not found\", groupID))\n\t}\n\treturn members[0], nil\n}\n\nfunc (g *GroupCacheRedis) GetGroupsOwner(ctx context.Context, groupIDs []string) ([]*model.GroupMember, error) {\n\tmembers := make([]*model.GroupMember, 0, len(groupIDs))\n\tfor _, groupID := range groupIDs {\n\t\titems, err := g.GetGroupRoleLevelMemberInfo(ctx, groupID, constant.GroupOwner)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(items) > 0 {\n\t\t\tmembers = append(members, items[0])\n\t\t}\n\t}\n\treturn members, nil\n}\n\nfunc (g *GroupCacheRedis) GetGroupRoleLevelMemberIDs(ctx context.Context, groupID string, roleLevel int32) ([]string, error) {\n\treturn getCache(ctx, g.rcClient, g.getGroupRoleLevelMemberIDsKey(groupID, roleLevel), g.expireTime, func(ctx context.Context) ([]string, error) {\n\t\treturn g.groupMemberDB.FindRoleLevelUserIDs(ctx, groupID, roleLevel)\n\t})\n}\n\nfunc (g *GroupCacheRedis) GetGroupRoleLevelMemberInfo(ctx context.Context, groupID string, roleLevel int32) ([]*model.GroupMember, error) {\n\tuserIDs, err := g.GetGroupRoleLevelMemberIDs(ctx, groupID, roleLevel)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn g.GetGroupMembersInfo(ctx, groupID, userIDs)\n}\n\nfunc (g *GroupCacheRedis) GetGroupRolesLevelMemberInfo(ctx context.Context, groupID string, roleLevels []int32) ([]*model.GroupMember, error) {\n\tvar userIDs []string\n\tfor _, roleLevel := range roleLevels {\n\t\tids, err := g.GetGroupRoleLevelMemberIDs(ctx, groupID, roleLevel)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tuserIDs = append(userIDs, ids...)\n\t}\n\treturn g.GetGroupMembersInfo(ctx, groupID, userIDs)\n}\n\nfunc (g *GroupCacheRedis) FindGroupMemberUser(ctx context.Context, groupIDs []string, userID string) ([]*model.GroupMember, error) {\n\tif len(groupIDs) == 0 {\n\t\tvar err error\n\t\tgroupIDs, err = g.GetJoinedGroupIDs(ctx, userID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn batchGetCache2(ctx, g.rcClient, g.expireTime, groupIDs, func(groupID string) string {\n\t\treturn g.getGroupMemberInfoKey(groupID, userID)\n\t}, func(member *model.GroupMember) string {\n\t\treturn member.GroupID\n\t}, func(ctx context.Context, groupIDs []string) ([]*model.GroupMember, error) {\n\t\treturn g.groupMemberDB.FindInGroup(ctx, userID, groupIDs)\n\t})\n}\n\nfunc (g *GroupCacheRedis) DelMaxGroupMemberVersion(groupIDs ...string) cache.GroupCache {\n\tkeys := make([]string, 0, len(groupIDs))\n\tfor _, groupID := range groupIDs {\n\t\tkeys = append(keys, g.getGroupMemberMaxVersionKey(groupID))\n\t}\n\tcache := g.CloneGroupCache()\n\tcache.AddKeys(keys...)\n\treturn cache\n}\n\nfunc (g *GroupCacheRedis) DelMaxJoinGroupVersion(userIDs ...string) cache.GroupCache {\n\tkeys := make([]string, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tkeys = append(keys, g.getJoinGroupMaxVersionKey(userID))\n\t}\n\tcache := g.CloneGroupCache()\n\tcache.AddKeys(keys...)\n\treturn cache\n}\n\nfunc (g *GroupCacheRedis) FindMaxGroupMemberVersion(ctx context.Context, groupID string) (*model.VersionLog, error) {\n\treturn getCache(ctx, g.rcClient, g.getGroupMemberMaxVersionKey(groupID), g.expireTime, func(ctx context.Context) (*model.VersionLog, error) {\n\t\treturn g.groupMemberDB.FindMemberIncrVersion(ctx, groupID, 0, 0)\n\t})\n}\n\nfunc (g *GroupCacheRedis) BatchFindMaxGroupMemberVersion(ctx context.Context, groupIDs []string) ([]*model.VersionLog, error) {\n\treturn batchGetCache2(ctx, g.rcClient, g.expireTime, groupIDs,\n\t\tfunc(groupID string) string {\n\t\t\treturn g.getGroupMemberMaxVersionKey(groupID)\n\t\t}, func(versionLog *model.VersionLog) string {\n\t\t\treturn versionLog.DID\n\t\t}, func(ctx context.Context, groupIDs []string) ([]*model.VersionLog, error) {\n\t\t\t// create two slices with len is groupIDs, just need 0\n\t\t\tversions := make([]uint, len(groupIDs))\n\t\t\tlimits := make([]int, len(groupIDs))\n\n\t\t\treturn g.groupMemberDB.BatchFindMemberIncrVersion(ctx, groupIDs, versions, limits)\n\t\t})\n}\n\nfunc (g *GroupCacheRedis) FindMaxJoinGroupVersion(ctx context.Context, userID string) (*model.VersionLog, error) {\n\treturn getCache(ctx, g.rcClient, g.getJoinGroupMaxVersionKey(userID), g.expireTime, func(ctx context.Context) (*model.VersionLog, error) {\n\t\treturn g.groupMemberDB.FindJoinIncrVersion(ctx, userID, 0, 0)\n\t})\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/lua_script.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nvar (\n\tsetBatchWithCommonExpireScript = redis.NewScript(`\nlocal expire = tonumber(ARGV[1])\nfor i, key in ipairs(KEYS) do\n    redis.call('SET', key, ARGV[i + 1])\n    redis.call('EXPIRE', key, expire)\nend\nreturn #KEYS\n`)\n\n\tsetBatchWithIndividualExpireScript = redis.NewScript(`\nlocal n = #KEYS\nfor i = 1, n do\n    redis.call('SET', KEYS[i], ARGV[i])\n    redis.call('EXPIRE', KEYS[i], ARGV[i + n])\nend\nreturn n\n`)\n\n\tdeleteBatchScript = redis.NewScript(`\nfor i, key in ipairs(KEYS) do\n    redis.call('DEL', key)\nend\nreturn #KEYS\n`)\n\n\tgetBatchScript = redis.NewScript(`\nlocal values = {}\nfor i, key in ipairs(KEYS) do\n    local value = redis.call('GET', key)\n    table.insert(values, value)\nend\nreturn values\n`)\n)\n\nfunc callLua(ctx context.Context, rdb redis.Scripter, script *redis.Script, keys []string, args []any) (any, error) {\n\tlog.ZDebug(ctx, \"callLua args\", \"scriptHash\", script.Hash(), \"keys\", keys, \"args\", args)\n\tr := script.EvalSha(ctx, rdb, keys, args)\n\tif redis.HasErrorPrefix(r.Err(), \"NOSCRIPT\") {\n\t\tif err := script.Load(ctx, rdb).Err(); err != nil {\n\t\t\tr = script.Eval(ctx, rdb, keys, args)\n\t\t} else {\n\t\t\tr = script.EvalSha(ctx, rdb, keys, args)\n\t\t}\n\t}\n\tv, err := r.Result()\n\tif errors.Is(err, redis.Nil) {\n\t\terr = nil\n\t}\n\treturn v, errs.WrapMsg(err, \"call lua err\", \"scriptHash\", script.Hash(), \"keys\", keys, \"args\", args)\n}\n\nfunc LuaSetBatchWithCommonExpire(ctx context.Context, rdb redis.Scripter, keys []string, values []string, expire int) error {\n\t// Check if the lengths of keys and values match\n\tif len(keys) != len(values) {\n\t\treturn errs.New(\"keys and values length mismatch\").Wrap()\n\t}\n\n\t// Ensure allocation size does not overflow\n\tmaxAllowedLen := (1 << 31) - 1 // 2GB limit (maximum address space for 32-bit systems)\n\n\tif len(values) > maxAllowedLen-1 {\n\t\treturn fmt.Errorf(\"values length is too large, causing overflow\")\n\t}\n\tvar vals = make([]any, 0, 1+len(values))\n\tvals = append(vals, expire)\n\tfor _, v := range values {\n\t\tvals = append(vals, v)\n\t}\n\t_, err := callLua(ctx, rdb, setBatchWithCommonExpireScript, keys, vals)\n\treturn err\n}\n\nfunc LuaSetBatchWithIndividualExpire(ctx context.Context, rdb redis.Scripter, keys []string, values []string, expires []int) error {\n\t// Check if the lengths of keys, values, and expires match\n\tif len(keys) != len(values) || len(keys) != len(expires) {\n\t\treturn errs.New(\"keys and values length mismatch\").Wrap()\n\t}\n\n\t// Ensure the allocation size does not overflow\n\tmaxAllowedLen := (1 << 31) - 1 // 2GB limit (maximum address space for 32-bit systems)\n\n\tif len(values) > maxAllowedLen-1 {\n\t\treturn errs.New(fmt.Sprintf(\"values length %d exceeds the maximum allowed length %d\", len(values), maxAllowedLen-1)).Wrap()\n\t}\n\tvar vals = make([]any, 0, len(values)+len(expires))\n\tfor _, v := range values {\n\t\tvals = append(vals, v)\n\t}\n\tfor _, ex := range expires {\n\t\tvals = append(vals, ex)\n\t}\n\t_, err := callLua(ctx, rdb, setBatchWithIndividualExpireScript, keys, vals)\n\treturn err\n}\n\nfunc LuaDeleteBatch(ctx context.Context, rdb redis.Scripter, keys []string) error {\n\t_, err := callLua(ctx, rdb, deleteBatchScript, keys, nil)\n\treturn err\n}\n\nfunc LuaGetBatch(ctx context.Context, rdb redis.Scripter, keys []string) ([]any, error) {\n\tv, err := callLua(ctx, rdb, getBatchScript, keys, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvalues, ok := v.([]any)\n\tif !ok {\n\t\treturn nil, servererrs.ErrArgs.WrapMsg(\"invalid lua get batch result\")\n\t}\n\treturn values, nil\n\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/lua_script_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"github.com/go-redis/redismock/v9\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"testing\"\n)\n\nfunc TestLuaSetBatchWithCommonExpire(t *testing.T) {\n\trdb, mock := redismock.NewClientMock()\n\tctx := context.Background()\n\n\tkeys := []string{\"key1\", \"key2\"}\n\tvalues := []string{\"value1\", \"value2\"}\n\texpire := 10\n\n\tmock.ExpectEvalSha(setBatchWithCommonExpireScript.Hash(), keys, []any{expire, \"value1\", \"value2\"}).SetVal(int64(len(keys)))\n\n\terr := LuaSetBatchWithCommonExpire(ctx, rdb, keys, values, expire)\n\trequire.NoError(t, err)\n\tassert.NoError(t, mock.ExpectationsWereMet())\n}\n\nfunc TestLuaSetBatchWithIndividualExpire(t *testing.T) {\n\trdb, mock := redismock.NewClientMock()\n\tctx := context.Background()\n\n\tkeys := []string{\"key1\", \"key2\"}\n\tvalues := []string{\"value1\", \"value2\"}\n\texpires := []int{10, 20}\n\n\targs := make([]any, 0, len(values)+len(expires))\n\tfor _, v := range values {\n\t\targs = append(args, v)\n\t}\n\tfor _, ex := range expires {\n\t\targs = append(args, ex)\n\t}\n\n\tmock.ExpectEvalSha(setBatchWithIndividualExpireScript.Hash(), keys, args).SetVal(int64(len(keys)))\n\n\terr := LuaSetBatchWithIndividualExpire(ctx, rdb, keys, values, expires)\n\trequire.NoError(t, err)\n\tassert.NoError(t, mock.ExpectationsWereMet())\n}\n\nfunc TestLuaDeleteBatch(t *testing.T) {\n\trdb, mock := redismock.NewClientMock()\n\tctx := context.Background()\n\n\tkeys := []string{\"key1\", \"key2\"}\n\n\tmock.ExpectEvalSha(deleteBatchScript.Hash(), keys, []any{}).SetVal(int64(len(keys)))\n\n\terr := LuaDeleteBatch(ctx, rdb, keys)\n\trequire.NoError(t, err)\n\tassert.NoError(t, mock.ExpectationsWereMet())\n}\n\nfunc TestLuaGetBatch(t *testing.T) {\n\trdb, mock := redismock.NewClientMock()\n\tctx := context.Background()\n\n\tkeys := []string{\"key1\", \"key2\"}\n\texpectedValues := []any{\"value1\", \"value2\"}\n\n\tmock.ExpectEvalSha(getBatchScript.Hash(), keys, []any{}).SetVal(expectedValues)\n\n\tvalues, err := LuaGetBatch(ctx, rdb, keys)\n\trequire.NoError(t, err)\n\tassert.NoError(t, mock.ExpectationsWereMet())\n\tassert.Equal(t, expectedValues, values)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/minio.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/tools/s3/minio\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewMinioCache(rdb redis.UniversalClient) minio.Cache {\n\trc := newRocksCacheClient(rdb)\n\treturn &minioCacheRedis{\n\t\tBatchDeleter: rc.GetBatchDeleter(),\n\t\trcClient:     rc,\n\t\texpireTime:   time.Hour * 24 * 7,\n\t}\n}\n\ntype minioCacheRedis struct {\n\tcache.BatchDeleter\n\trcClient   *rocksCacheClient\n\texpireTime time.Duration\n}\n\nfunc (g *minioCacheRedis) getObjectImageInfoKey(key string) string {\n\treturn cachekey.GetObjectImageInfoKey(key)\n}\n\nfunc (g *minioCacheRedis) getMinioImageThumbnailKey(key string, format string, width int, height int) string {\n\treturn cachekey.GetMinioImageThumbnailKey(key, format, width, height)\n}\n\nfunc (g *minioCacheRedis) DelObjectImageInfoKey(ctx context.Context, keys ...string) error {\n\tks := make([]string, 0, len(keys))\n\tfor _, key := range keys {\n\t\tks = append(ks, g.getObjectImageInfoKey(key))\n\t}\n\treturn g.BatchDeleter.ExecDelWithKeys(ctx, ks)\n}\n\nfunc (g *minioCacheRedis) DelImageThumbnailKey(ctx context.Context, key string, format string, width int, height int) error {\n\treturn g.BatchDeleter.ExecDelWithKeys(ctx, []string{g.getMinioImageThumbnailKey(key, format, width, height)})\n\n}\n\nfunc (g *minioCacheRedis) GetImageObjectKeyInfo(ctx context.Context, key string, fn func(ctx context.Context) (*minio.ImageInfo, error)) (*minio.ImageInfo, error) {\n\tinfo, err := getCache(ctx, g.rcClient, g.getObjectImageInfoKey(key), g.expireTime, fn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn info, nil\n}\n\nfunc (g *minioCacheRedis) GetThumbnailKey(ctx context.Context, key string, format string, width int, height int, minioCache func(ctx context.Context) (string, error)) (string, error) {\n\treturn getCache(ctx, g.rcClient, g.getMinioImageThumbnailKey(key, format, width, height), g.expireTime, minioCache)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/msg.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n) //\n\n// msgCacheTimeout is  expiration time of message cache, 86400 seconds\nconst msgCacheTimeout = time.Hour * 24\n\nfunc NewMsgCache(client redis.UniversalClient, db database.Msg) cache.MsgCache {\n\treturn &msgCache{\n\t\trcClient:       newRocksCacheClient(client),\n\t\tmsgDocDatabase: db,\n\t}\n}\n\ntype msgCache struct {\n\trcClient       *rocksCacheClient\n\tmsgDocDatabase database.Msg\n}\n\nfunc (c *msgCache) getSendMsgKey(id string) string {\n\treturn cachekey.GetSendMsgKey(id)\n}\n\nfunc (c *msgCache) SetSendMsgStatus(ctx context.Context, id string, status int32) error {\n\treturn errs.Wrap(c.rcClient.GetRedis().Set(ctx, c.getSendMsgKey(id), status, time.Hour*24).Err())\n}\n\nfunc (c *msgCache) GetSendMsgStatus(ctx context.Context, id string) (int32, error) {\n\tresult, err := c.rcClient.GetRedis().Get(ctx, c.getSendMsgKey(id)).Int()\n\treturn int32(result), errs.Wrap(err)\n}\n\nfunc (c *msgCache) GetMessageBySeqs(ctx context.Context, conversationID string, seqs []int64) ([]*model.MsgInfoModel, error) {\n\tif len(seqs) == 0 {\n\t\treturn nil, nil\n\t}\n\tgetKey := func(seq int64) string {\n\t\treturn cachekey.GetMsgCacheKey(conversationID, seq)\n\t}\n\tgetMsgID := func(msg *model.MsgInfoModel) int64 {\n\t\treturn msg.Msg.Seq\n\t}\n\tfind := func(ctx context.Context, seqs []int64) ([]*model.MsgInfoModel, error) {\n\t\treturn c.msgDocDatabase.FindSeqs(ctx, conversationID, seqs)\n\t}\n\treturn batchGetCache2(ctx, c.rcClient, msgCacheTimeout, seqs, getKey, getMsgID, find)\n}\n\nfunc (c *msgCache) DelMessageBySeqs(ctx context.Context, conversationID string, seqs []int64) error {\n\tif len(seqs) == 0 {\n\t\treturn nil\n\t}\n\tkeys := datautil.Slice(seqs, func(seq int64) string {\n\t\treturn cachekey.GetMsgCacheKey(conversationID, seq)\n\t})\n\tslotKeys, err := groupKeysBySlot(ctx, c.rcClient.GetRedis(), keys)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, keys := range slotKeys {\n\t\tif err := c.rcClient.GetClient().TagAsDeletedBatch2(ctx, keys); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *msgCache) SetMessageBySeqs(ctx context.Context, conversationID string, msgs []*model.MsgInfoModel) error {\n\tfor _, msg := range msgs {\n\t\tif msg == nil || msg.Msg == nil || msg.Msg.Seq <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tdata, err := json.Marshal(msg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := c.rcClient.GetClient().RawSet(ctx, cachekey.GetMsgCacheKey(conversationID, msg.Msg.Seq), string(data), msgCacheTimeout); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/online.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewUserOnline(rdb redis.UniversalClient) cache.OnlineCache {\n\tif rdb == nil || config.Standalone() {\n\t\treturn mcache.NewOnlineCache()\n\t}\n\treturn &userOnline{\n\t\trdb:         rdb,\n\t\texpire:      cachekey.OnlineExpire,\n\t\tchannelName: cachekey.OnlineChannel,\n\t}\n}\n\ntype userOnline struct {\n\trdb         redis.UniversalClient\n\texpire      time.Duration\n\tchannelName string\n}\n\nfunc (s *userOnline) getUserOnlineKey(userID string) string {\n\treturn cachekey.GetOnlineKey(userID)\n}\n\nfunc (s *userOnline) GetOnline(ctx context.Context, userID string) ([]int32, error) {\n\tmembers, err := s.rdb.ZRangeByScore(ctx, s.getUserOnlineKey(userID), &redis.ZRangeBy{\n\t\tMin: strconv.FormatInt(time.Now().Unix(), 10),\n\t\tMax: \"+inf\",\n\t}).Result()\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\tplatformIDs := make([]int32, 0, len(members))\n\tfor _, member := range members {\n\t\tval, err := strconv.Atoi(member)\n\t\tif err != nil {\n\t\t\treturn nil, errs.Wrap(err)\n\t\t}\n\t\tplatformIDs = append(platformIDs, int32(val))\n\t}\n\treturn platformIDs, nil\n}\n\nfunc (s *userOnline) GetAllOnlineUsers(ctx context.Context, cursor uint64) (map[string][]int32, uint64, error) {\n\tresult := make(map[string][]int32)\n\n\tkeys, nextCursor, err := s.rdb.Scan(ctx, cursor, fmt.Sprintf(\"%s*\", cachekey.OnlineKey), constant.ParamMaxLength).Result()\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tfor _, key := range keys {\n\t\tuserID := cachekey.GetOnlineKeyUserID(key)\n\t\tstrValues, err := s.rdb.ZRange(ctx, key, 0, -1).Result()\n\t\tif err != nil {\n\t\t\treturn nil, 0, err\n\t\t}\n\n\t\tvalues := make([]int32, 0, len(strValues))\n\t\tfor _, value := range strValues {\n\t\t\tintValue, err := strconv.Atoi(value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, errs.Wrap(err)\n\t\t\t}\n\t\t\tvalues = append(values, int32(intValue))\n\t\t}\n\n\t\tresult[userID] = values\n\t}\n\n\treturn result, nextCursor, nil\n}\n\nfunc (s *userOnline) SetUserOnline(ctx context.Context, userID string, online, offline []int32) error {\n\tscript := `\n\tlocal key = KEYS[1]\n\tlocal score = ARGV[3]\n\tlocal num1 = redis.call(\"ZCARD\", key)\n\tredis.call(\"ZREMRANGEBYSCORE\", key, \"-inf\", ARGV[2])\n\tfor i = 5, tonumber(ARGV[4])+4 do\n\t\tredis.call(\"ZREM\", key, ARGV[i])\n\tend\n\tlocal num2 = redis.call(\"ZCARD\", key)\n\tfor i = 5+tonumber(ARGV[4]), #ARGV do\n\t\tredis.call(\"ZADD\", key, score, ARGV[i])\n\tend\n\tredis.call(\"EXPIRE\", key, ARGV[1])\n\tlocal num3 = redis.call(\"ZCARD\", key)\n\tlocal change = (num1 ~= num2) or (num2 ~= num3)\n\tif change then\n\t\tlocal members = redis.call(\"ZRANGE\", key, 0, -1)\n\t\ttable.insert(members, \"1\")\n\t\treturn members\n\telse\n\t\treturn {\"0\"}\n\tend\n`\n\tnow := time.Now()\n\targv := make([]any, 0, 2+len(online)+len(offline))\n\targv = append(argv, int32(s.expire/time.Second), now.Unix(), now.Add(s.expire).Unix(), int32(len(offline)))\n\tfor _, platformID := range offline {\n\t\targv = append(argv, platformID)\n\t}\n\tfor _, platformID := range online {\n\t\targv = append(argv, platformID)\n\t}\n\tkeys := []string{s.getUserOnlineKey(userID)}\n\tplatformIDs, err := s.rdb.Eval(ctx, script, keys, argv).StringSlice()\n\tif err != nil {\n\t\tlog.ZError(ctx, \"redis SetUserOnline\", err, \"userID\", userID, \"online\", online, \"offline\", offline)\n\t\treturn err\n\t}\n\tif len(platformIDs) == 0 {\n\t\treturn errs.ErrInternalServer.WrapMsg(\"SetUserOnline redis lua invalid return value\")\n\t}\n\tif platformIDs[len(platformIDs)-1] != \"0\" {\n\t\tlog.ZDebug(ctx, \"redis SetUserOnline push\", \"userID\", userID, \"online\", online, \"offline\", offline, \"platformIDs\", platformIDs[:len(platformIDs)-1])\n\t\tplatformIDs[len(platformIDs)-1] = userID\n\t\tmsg := strings.Join(platformIDs, \":\")\n\t\tif err := s.rdb.Publish(ctx, s.channelName, msg).Err(); err != nil {\n\t\t\treturn errs.Wrap(err)\n\t\t}\n\t} else {\n\t\tlog.ZDebug(ctx, \"redis SetUserOnline not push\", \"userID\", userID, \"online\", online, \"offline\", offline)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/online_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/db/redisutil\"\n\t\"testing\"\n\t\"time\"\n)\n\n/*\naddress: [ 172.16.8.48:7001, 172.16.8.48:7002, 172.16.8.48:7003, 172.16.8.48:7004, 172.16.8.48:7005, 172.16.8.48:7006 ]\nusername:\npassword: passwd123\nclusterMode: true\ndb: 0\nmaxRetry: 10\n*/\nfunc TestName111111(t *testing.T) {\n\tconf := config.Redis{\n\t\tAddress: []string{\n\t\t\t\"172.16.8.124:7001\",\n\t\t\t\"172.16.8.124:7002\",\n\t\t\t\"172.16.8.124:7003\",\n\t\t\t\"172.16.8.124:7004\",\n\t\t\t\"172.16.8.124:7005\",\n\t\t\t\"172.16.8.124:7006\",\n\t\t},\n\t\tRedisMode: \"cluster\",\n\t\tPassword:    \"passwd123\",\n\t\t//Address:  []string{\"localhost:16379\"},\n\t\t//Password: \"openIM123\",\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*1000)\n\tdefer cancel()\n\trdb, err := redisutil.NewRedisClient(ctx, conf.Build())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tonline := NewUserOnline(rdb)\n\n\tuserID := \"a123456\"\n\tt.Log(online.GetOnline(ctx, userID))\n\tt.Log(online.SetUserOnline(ctx, userID, []int32{1, 2, 3, 4}, nil))\n\tt.Log(online.GetOnline(ctx, userID))\n\n}\n\nfunc TestName111(t *testing.T) {\n\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/redis_shard_manager.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nconst (\n\tdefaultBatchSize       = 50\n\tdefaultConcurrentLimit = 3\n)\n\n// RedisShardManager is a class for sharding and processing keys\ntype RedisShardManager struct {\n\tredisClient redis.UniversalClient\n\tconfig      *Config\n}\ntype Config struct {\n\tbatchSize       int\n\tcontinueOnError bool\n\tconcurrentLimit int\n}\n\n// Option is a function type for configuring Config\ntype Option func(c *Config)\n\n//// NewRedisShardManager creates a new RedisShardManager instance\n//func NewRedisShardManager(redisClient redis.UniversalClient, opts ...Option) *RedisShardManager {\n//\tconfig := &Config{\n//\t\tbatchSize:       defaultBatchSize, // Default batch size is 50 keys\n//\t\tcontinueOnError: false,\n//\t\tconcurrentLimit: defaultConcurrentLimit, // Default concurrent limit is 3\n//\t}\n//\tfor _, opt := range opts {\n//\t\topt(config)\n//\t}\n//\trsm := &RedisShardManager{\n//\t\tredisClient: redisClient,\n//\t\tconfig:      config,\n//\t}\n//\treturn rsm\n//}\n//\n//// WithBatchSize sets the number of keys to process per batch\n//func WithBatchSize(size int) Option {\n//\treturn func(c *Config) {\n//\t\tc.batchSize = size\n//\t}\n//}\n//\n//// WithContinueOnError sets whether to continue processing on error\n//func WithContinueOnError(continueOnError bool) Option {\n//\treturn func(c *Config) {\n//\t\tc.continueOnError = continueOnError\n//\t}\n//}\n//\n//// WithConcurrentLimit sets the concurrency limit\n//func WithConcurrentLimit(limit int) Option {\n//\treturn func(c *Config) {\n//\t\tc.concurrentLimit = limit\n//\t}\n//}\n//\n//// ProcessKeysBySlot groups keys by their Redis cluster hash slots and processes them using the provided function.\n//func (rsm *RedisShardManager) ProcessKeysBySlot(\n//\tctx context.Context,\n//\tkeys []string,\n//\tprocessFunc func(ctx context.Context, slot int64, keys []string) error,\n//) error {\n//\n//\t// Group keys by slot\n//\tslots, err := groupKeysBySlot(ctx, rsm.redisClient, keys)\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\n//\tg, ctx := errgroup.WithContext(ctx)\n//\tg.SetLimit(rsm.config.concurrentLimit)\n//\n//\t// Process keys in each slot using the provided function\n//\tfor slot, singleSlotKeys := range slots {\n//\t\tbatches := splitIntoBatches(singleSlotKeys, rsm.config.batchSize)\n//\t\tfor _, batch := range batches {\n//\t\t\tslot, batch := slot, batch // Avoid closure capture issue\n//\t\t\tg.Go(func() error {\n//\t\t\t\terr := processFunc(ctx, slot, batch)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tlog.ZWarn(ctx, \"Batch processFunc failed\", err, \"slot\", slot, \"keys\", batch)\n//\t\t\t\t\tif !rsm.config.continueOnError {\n//\t\t\t\t\t\treturn err\n//\t\t\t\t\t}\n//\t\t\t\t}\n//\t\t\t\treturn nil\n//\t\t\t})\n//\t\t}\n//\t}\n//\n//\tif err := g.Wait(); err != nil {\n//\t\treturn err\n//\t}\n//\treturn nil\n//}\n\n// groupKeysBySlot groups keys by their Redis cluster hash slots.\nfunc groupKeysBySlot(ctx context.Context, redisClient redis.UniversalClient, keys []string) (map[int64][]string, error) {\n\tslots := make(map[int64][]string)\n\tclusterClient, isCluster := redisClient.(*redis.ClusterClient)\n\tif isCluster && len(keys) > 1 {\n\t\tpipe := clusterClient.Pipeline()\n\t\tcmds := make([]*redis.IntCmd, len(keys))\n\t\tfor i, key := range keys {\n\t\t\tcmds[i] = pipe.ClusterKeySlot(ctx, key)\n\t\t}\n\t\t_, err := pipe.Exec(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, errs.WrapMsg(err, \"get slot err\")\n\t\t}\n\n\t\tfor i, cmd := range cmds {\n\t\t\tslot, err := cmd.Result()\n\t\t\tif err != nil {\n\t\t\t\tlog.ZWarn(ctx, \"some key get slot err\", err, \"key\", keys[i])\n\t\t\t\treturn nil, errs.WrapMsg(err, \"get slot err\", \"key\", keys[i])\n\t\t\t}\n\t\t\tslots[slot] = append(slots[slot], keys[i])\n\t\t}\n\t} else {\n\t\t// If not a cluster client, put all keys in the same slot (0)\n\t\tslots[0] = keys\n\t}\n\n\treturn slots, nil\n}\n\n// splitIntoBatches splits keys into batches of the specified size\nfunc splitIntoBatches(keys []string, batchSize int) [][]string {\n\tvar batches [][]string\n\tfor batchSize < len(keys) {\n\t\tkeys, batches = keys[batchSize:], append(batches, keys[0:batchSize:batchSize])\n\t}\n\treturn append(batches, keys)\n}\n\n// ProcessKeysBySlot groups keys by their Redis cluster hash slots and processes them using the provided function.\nfunc ProcessKeysBySlot(\n\tctx context.Context,\n\tredisClient redis.UniversalClient,\n\tkeys []string,\n\tprocessFunc func(ctx context.Context, slot int64, keys []string) error,\n\topts ...Option,\n) error {\n\n\tconfig := &Config{\n\t\tbatchSize:       defaultBatchSize,\n\t\tcontinueOnError: false,\n\t\tconcurrentLimit: defaultConcurrentLimit,\n\t}\n\tfor _, opt := range opts {\n\t\topt(config)\n\t}\n\n\t// Group keys by slot\n\tslots, err := groupKeysBySlot(ctx, redisClient, keys)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(config.concurrentLimit)\n\n\t// Process keys in each slot using the provided function\n\tfor slot, singleSlotKeys := range slots {\n\t\tbatches := splitIntoBatches(singleSlotKeys, config.batchSize)\n\t\tfor _, batch := range batches {\n\t\t\tslot, batch := slot, batch // Avoid closure capture issue\n\t\t\tg.Go(func() error {\n\t\t\t\terr := processFunc(ctx, slot, batch)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.ZWarn(ctx, \"Batch processFunc failed\", err, \"slot\", slot, \"keys\", batch)\n\t\t\t\t\tif !config.continueOnError {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc DeleteCacheBySlot(ctx context.Context, rcClient *rocksCacheClient, keys []string) error {\n\tswitch len(keys) {\n\tcase 0:\n\t\treturn nil\n\tcase 1:\n\t\treturn rcClient.GetClient().TagAsDeletedBatch2(ctx, keys)\n\tdefault:\n\t\treturn ProcessKeysBySlot(ctx, rcClient.GetRedis(), keys, func(ctx context.Context, slot int64, keys []string) error {\n\t\t\treturn rcClient.GetClient().TagAsDeletedBatch2(ctx, keys)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/s3.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/s3\"\n\t\"github.com/openimsdk/tools/s3/cont\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewObjectCacheRedis(rdb redis.UniversalClient, objDB database.ObjectInfo) cache.ObjectCache {\n\trc := newRocksCacheClient(rdb)\n\treturn &objectCacheRedis{\n\t\tBatchDeleter: rc.GetBatchDeleter(),\n\t\trcClient:     rc,\n\t\texpireTime:   time.Hour * 12,\n\t\tobjDB:        objDB,\n\t}\n}\n\ntype objectCacheRedis struct {\n\tcache.BatchDeleter\n\tobjDB      database.ObjectInfo\n\trcClient   *rocksCacheClient\n\texpireTime time.Duration\n}\n\nfunc (g *objectCacheRedis) getObjectKey(engine string, name string) string {\n\treturn cachekey.GetObjectKey(engine, name)\n}\n\nfunc (g *objectCacheRedis) CloneObjectCache() cache.ObjectCache {\n\treturn &objectCacheRedis{\n\t\tBatchDeleter: g.BatchDeleter.Clone(),\n\t\trcClient:     g.rcClient,\n\t\texpireTime:   g.expireTime,\n\t\tobjDB:        g.objDB,\n\t}\n}\n\nfunc (g *objectCacheRedis) DelObjectName(engine string, names ...string) cache.ObjectCache {\n\tobjectCache := g.CloneObjectCache()\n\tkeys := make([]string, 0, len(names))\n\tfor _, name := range names {\n\t\tkeys = append(keys, g.getObjectKey(name, engine))\n\t}\n\tobjectCache.AddKeys(keys...)\n\treturn objectCache\n}\n\nfunc (g *objectCacheRedis) GetName(ctx context.Context, engine string, name string) (*model.Object, error) {\n\treturn getCache(ctx, g.rcClient, g.getObjectKey(name, engine), g.expireTime, func(ctx context.Context) (*model.Object, error) {\n\t\treturn g.objDB.Take(ctx, engine, name)\n\t})\n}\n\nfunc NewS3Cache(rdb redis.UniversalClient, s3 s3.Interface) cont.S3Cache {\n\trc := newRocksCacheClient(rdb)\n\treturn &s3CacheRedis{\n\t\tBatchDeleter: rc.GetBatchDeleter(),\n\t\trcClient:     rc,\n\t\texpireTime:   time.Hour * 12,\n\t\ts3:           s3,\n\t}\n}\n\ntype s3CacheRedis struct {\n\tcache.BatchDeleter\n\ts3         s3.Interface\n\trcClient   *rocksCacheClient\n\texpireTime time.Duration\n}\n\nfunc (g *s3CacheRedis) getS3Key(engine string, name string) string {\n\treturn cachekey.GetS3Key(engine, name)\n}\n\nfunc (g *s3CacheRedis) DelS3Key(ctx context.Context, engine string, keys ...string) error {\n\tks := make([]string, 0, len(keys))\n\tfor _, key := range keys {\n\t\tks = append(ks, g.getS3Key(engine, key))\n\t}\n\treturn g.BatchDeleter.ExecDelWithKeys(ctx, ks)\n}\n\nfunc (g *s3CacheRedis) GetKey(ctx context.Context, engine string, name string) (*s3.ObjectInfo, error) {\n\treturn getCache(ctx, g.rcClient, g.getS3Key(engine, name), g.expireTime, func(ctx context.Context) (*s3.ObjectInfo, error) {\n\t\treturn g.s3.StatObject(ctx, name)\n\t})\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/seq_conversation.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewSeqConversationCacheRedis(rdb redis.UniversalClient, mgo database.SeqConversation) cache.SeqConversationCache {\n\tif rdb == nil {\n\t\treturn mcache.NewSeqConversationCache(mgo)\n\t}\n\treturn &seqConversationCacheRedis{\n\t\tmgo:              mgo,\n\t\tlockTime:         time.Second * 3,\n\t\tdataTime:         time.Hour * 24 * 365,\n\t\tminSeqExpireTime: time.Hour,\n\t\trcClient:         newRocksCacheClient(rdb),\n\t}\n}\n\ntype seqConversationCacheRedis struct {\n\tmgo              database.SeqConversation\n\trcClient         *rocksCacheClient\n\tlockTime         time.Duration\n\tdataTime         time.Duration\n\tminSeqExpireTime time.Duration\n}\n\nfunc (s *seqConversationCacheRedis) getMinSeqKey(conversationID string) string {\n\treturn cachekey.GetMallocMinSeqKey(conversationID)\n}\n\nfunc (s *seqConversationCacheRedis) SetMinSeq(ctx context.Context, conversationID string, seq int64) error {\n\treturn s.SetMinSeqs(ctx, map[string]int64{conversationID: seq})\n}\n\nfunc (s *seqConversationCacheRedis) GetMinSeq(ctx context.Context, conversationID string) (int64, error) {\n\treturn getCache(ctx, s.rcClient, s.getMinSeqKey(conversationID), s.minSeqExpireTime, func(ctx context.Context) (int64, error) {\n\t\treturn s.mgo.GetMinSeq(ctx, conversationID)\n\t})\n}\n\nfunc (s *seqConversationCacheRedis) getSingleMaxSeq(ctx context.Context, conversationID string) (map[string]int64, error) {\n\tseq, err := s.GetMaxSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn map[string]int64{conversationID: seq}, nil\n}\n\nfunc (s *seqConversationCacheRedis) getSingleMaxSeqWithTime(ctx context.Context, conversationID string) (map[string]database.SeqTime, error) {\n\tseq, err := s.GetMaxSeqWithTime(ctx, conversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn map[string]database.SeqTime{conversationID: seq}, nil\n}\n\nfunc (s *seqConversationCacheRedis) batchGetMaxSeq(ctx context.Context, keys []string, keyConversationID map[string]string, seqs map[string]int64) error {\n\tresult := make([]*redis.StringCmd, len(keys))\n\tpipe := s.rcClient.GetRedis().Pipeline()\n\tfor i, key := range keys {\n\t\tresult[i] = pipe.HGet(ctx, key, \"CURR\")\n\t}\n\tif _, err := pipe.Exec(ctx); err != nil && !errors.Is(err, redis.Nil) {\n\t\treturn errs.Wrap(err)\n\t}\n\tvar notFoundKey []string\n\tfor i, r := range result {\n\t\treq, err := r.Int64()\n\t\tif err == nil {\n\t\t\tseqs[keyConversationID[keys[i]]] = req\n\t\t} else if errors.Is(err, redis.Nil) {\n\t\t\tnotFoundKey = append(notFoundKey, keys[i])\n\t\t} else {\n\t\t\treturn errs.Wrap(err)\n\t\t}\n\t}\n\tfor _, key := range notFoundKey {\n\t\tconversationID := keyConversationID[key]\n\t\tseq, err := s.GetMaxSeq(ctx, conversationID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tseqs[conversationID] = seq\n\t}\n\treturn nil\n}\n\nfunc (s *seqConversationCacheRedis) batchGetMaxSeqWithTime(ctx context.Context, keys []string, keyConversationID map[string]string, seqs map[string]database.SeqTime) error {\n\tresult := make([]*redis.SliceCmd, len(keys))\n\tpipe := s.rcClient.GetRedis().Pipeline()\n\tfor i, key := range keys {\n\t\tresult[i] = pipe.HMGet(ctx, key, \"CURR\", \"TIME\")\n\t}\n\tif _, err := pipe.Exec(ctx); err != nil && !errors.Is(err, redis.Nil) {\n\t\treturn errs.Wrap(err)\n\t}\n\tvar notFoundKey []string\n\tfor i, r := range result {\n\t\tval, err := r.Result()\n\t\tif len(val) != 2 {\n\t\t\treturn errs.WrapMsg(err, \"batchGetMaxSeqWithTime invalid result\", \"key\", keys[i], \"res\", val)\n\t\t}\n\t\tif val[0] == nil {\n\t\t\tnotFoundKey = append(notFoundKey, keys[i])\n\t\t\tcontinue\n\t\t}\n\t\tseq, err := s.parseInt64(val[0])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmill, err := s.parseInt64(val[1])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tseqs[keyConversationID[keys[i]]] = database.SeqTime{Seq: seq, Time: mill}\n\t}\n\tfor _, key := range notFoundKey {\n\t\tconversationID := keyConversationID[key]\n\t\tseq, err := s.GetMaxSeqWithTime(ctx, conversationID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tseqs[conversationID] = seq\n\t}\n\treturn nil\n}\n\nfunc (s *seqConversationCacheRedis) GetMaxSeqs(ctx context.Context, conversationIDs []string) (map[string]int64, error) {\n\tswitch len(conversationIDs) {\n\tcase 0:\n\t\treturn map[string]int64{}, nil\n\tcase 1:\n\t\treturn s.getSingleMaxSeq(ctx, conversationIDs[0])\n\t}\n\tkeys := make([]string, 0, len(conversationIDs))\n\tkeyConversationID := make(map[string]string, len(conversationIDs))\n\tfor _, conversationID := range conversationIDs {\n\t\tkey := s.getSeqMallocKey(conversationID)\n\t\tif _, ok := keyConversationID[key]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tkeys = append(keys, key)\n\t\tkeyConversationID[key] = conversationID\n\t}\n\tif len(keys) == 1 {\n\t\treturn s.getSingleMaxSeq(ctx, conversationIDs[0])\n\t}\n\tslotKeys, err := groupKeysBySlot(ctx, s.rcClient.GetRedis(), keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tseqs := make(map[string]int64, len(conversationIDs))\n\tfor _, keys := range slotKeys {\n\t\tif err := s.batchGetMaxSeq(ctx, keys, keyConversationID, seqs); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn seqs, nil\n}\n\nfunc (s *seqConversationCacheRedis) GetMaxSeqsWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error) {\n\tswitch len(conversationIDs) {\n\tcase 0:\n\t\treturn map[string]database.SeqTime{}, nil\n\tcase 1:\n\t\treturn s.getSingleMaxSeqWithTime(ctx, conversationIDs[0])\n\t}\n\tkeys := make([]string, 0, len(conversationIDs))\n\tkeyConversationID := make(map[string]string, len(conversationIDs))\n\tfor _, conversationID := range conversationIDs {\n\t\tkey := s.getSeqMallocKey(conversationID)\n\t\tif _, ok := keyConversationID[key]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tkeys = append(keys, key)\n\t\tkeyConversationID[key] = conversationID\n\t}\n\tif len(keys) == 1 {\n\t\treturn s.getSingleMaxSeqWithTime(ctx, conversationIDs[0])\n\t}\n\tslotKeys, err := groupKeysBySlot(ctx, s.rcClient.GetRedis(), keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tseqs := make(map[string]database.SeqTime, len(conversationIDs))\n\tfor _, keys := range slotKeys {\n\t\tif err := s.batchGetMaxSeqWithTime(ctx, keys, keyConversationID, seqs); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn seqs, nil\n}\n\nfunc (s *seqConversationCacheRedis) getSeqMallocKey(conversationID string) string {\n\treturn cachekey.GetMallocSeqKey(conversationID)\n}\n\nfunc (s *seqConversationCacheRedis) setSeq(ctx context.Context, key string, owner int64, currSeq int64, lastSeq int64, mill int64) (int64, error) {\n\tif lastSeq < currSeq {\n\t\treturn 0, errs.New(\"lastSeq must be greater than currSeq\")\n\t}\n\t// 0: success\n\t// 1: success the lock has expired, but has not been locked by anyone else\n\t// 2: already locked, but not by yourself\n\tscript := `\nlocal key = KEYS[1]\nlocal lockValue = ARGV[1]\nlocal dataSecond = ARGV[2]\nlocal curr_seq = tonumber(ARGV[3])\nlocal last_seq = tonumber(ARGV[4])\nlocal mallocTime = ARGV[5]\nif redis.call(\"EXISTS\", key) == 0 then\n\tredis.call(\"HSET\", key, \"CURR\", curr_seq, \"LAST\", last_seq, \"TIME\", mallocTime)\n\tredis.call(\"EXPIRE\", key, dataSecond)\n\treturn 1\nend\nif redis.call(\"HGET\", key, \"LOCK\") ~= lockValue then\n\treturn 2\nend\nredis.call(\"HDEL\", key, \"LOCK\")\nredis.call(\"HSET\", key, \"CURR\", curr_seq, \"LAST\", last_seq, \"TIME\", mallocTime)\nredis.call(\"EXPIRE\", key, dataSecond)\nreturn 0\n`\n\tresult, err := s.rcClient.GetRedis().Eval(ctx, script, []string{key}, owner, int64(s.dataTime/time.Second), currSeq, lastSeq, mill).Int64()\n\tif err != nil {\n\t\treturn 0, errs.Wrap(err)\n\t}\n\treturn result, nil\n}\n\n// malloc size=0 is to get the current seq size>0 is to allocate seq\nfunc (s *seqConversationCacheRedis) malloc(ctx context.Context, key string, size int64) ([]int64, error) {\n\t// 0: success\n\t// 1: need to obtain and lock\n\t// 2: already locked\n\t// 3: exceeded the maximum value and locked\n\tscript := `\nlocal key = KEYS[1]\nlocal size = tonumber(ARGV[1])\nlocal lockSecond = ARGV[2]\nlocal dataSecond = ARGV[3]\nlocal mallocTime = ARGV[4]\nlocal result = {}\nif redis.call(\"EXISTS\", key) == 0 then\n\tlocal lockValue = math.random(0, 999999999)\n\tredis.call(\"HSET\", key, \"LOCK\", lockValue)\n\tredis.call(\"EXPIRE\", key, lockSecond)\n\ttable.insert(result, 1)\n\ttable.insert(result, lockValue)\n\ttable.insert(result, mallocTime)\n\treturn result\nend\nif redis.call(\"HEXISTS\", key, \"LOCK\") == 1 then\n\ttable.insert(result, 2)\n\treturn result\nend\nlocal curr_seq = tonumber(redis.call(\"HGET\", key, \"CURR\"))\nlocal last_seq = tonumber(redis.call(\"HGET\", key, \"LAST\"))\nif size == 0 then\n\tredis.call(\"EXPIRE\", key, dataSecond)\n\ttable.insert(result, 0)\n\ttable.insert(result, curr_seq)\n\ttable.insert(result, last_seq)\n\tlocal setTime = redis.call(\"HGET\", key, \"TIME\")\n\tif setTime then\n\t\ttable.insert(result, setTime)\t\n\telse\n\t\ttable.insert(result, 0)\n\tend\n\treturn result\nend\nlocal max_seq = curr_seq + size\nif max_seq > last_seq then\n\tlocal lockValue = math.random(0, 999999999)\n\tredis.call(\"HSET\", key, \"LOCK\", lockValue)\n\tredis.call(\"HSET\", key, \"CURR\", last_seq)\n\tredis.call(\"HSET\", key, \"TIME\", mallocTime)\n\tredis.call(\"EXPIRE\", key, lockSecond)\n\ttable.insert(result, 3)\n\ttable.insert(result, curr_seq)\n\ttable.insert(result, last_seq)\n\ttable.insert(result, lockValue)\n\ttable.insert(result, mallocTime)\n\treturn result\nend\nredis.call(\"HSET\", key, \"CURR\", max_seq)\nredis.call(\"HSET\", key, \"TIME\", ARGV[4])\nredis.call(\"EXPIRE\", key, dataSecond)\ntable.insert(result, 0)\ntable.insert(result, curr_seq)\ntable.insert(result, last_seq)\ntable.insert(result, mallocTime)\nreturn result\n`\n\tresult, err := s.rcClient.GetRedis().Eval(ctx, script, []string{key}, size, int64(s.lockTime/time.Second), int64(s.dataTime/time.Second), time.Now().UnixMilli()).Int64Slice()\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn result, nil\n}\n\nfunc (s *seqConversationCacheRedis) wait(ctx context.Context) error {\n\ttimer := time.NewTimer(time.Second / 4)\n\tdefer timer.Stop()\n\tselect {\n\tcase <-timer.C:\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\t}\n}\n\nfunc (s *seqConversationCacheRedis) setSeqRetry(ctx context.Context, key string, owner int64, currSeq int64, lastSeq int64, mill int64) {\n\tfor i := 0; i < 10; i++ {\n\t\tstate, err := s.setSeq(ctx, key, owner, currSeq, lastSeq, mill)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"set seq cache failed\", err, \"key\", key, \"owner\", owner, \"currSeq\", currSeq, \"lastSeq\", lastSeq, \"count\", i+1)\n\t\t\tif err := s.wait(ctx); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tswitch state {\n\t\tcase 0: // ideal state\n\t\tcase 1:\n\t\t\tlog.ZWarn(ctx, \"set seq cache lock not found\", nil, \"key\", key, \"owner\", owner, \"currSeq\", currSeq, \"lastSeq\", lastSeq)\n\t\tcase 2:\n\t\t\tlog.ZWarn(ctx, \"set seq cache lock to be held by someone else\", nil, \"key\", key, \"owner\", owner, \"currSeq\", currSeq, \"lastSeq\", lastSeq)\n\t\tdefault:\n\t\t\tlog.ZError(ctx, \"set seq cache lock unknown state\", nil, \"key\", key, \"owner\", owner, \"currSeq\", currSeq, \"lastSeq\", lastSeq)\n\t\t}\n\t\treturn\n\t}\n\tlog.ZError(ctx, \"set seq cache retrying still failed\", nil, \"key\", key, \"owner\", owner, \"currSeq\", currSeq, \"lastSeq\", lastSeq)\n}\n\nfunc (s *seqConversationCacheRedis) getMallocSize(conversationID string, size int64) int64 {\n\tif size == 0 {\n\t\treturn 0\n\t}\n\tvar basicSize int64\n\tif msgprocessor.IsGroupConversationID(conversationID) {\n\t\tbasicSize = 100\n\t} else {\n\t\tbasicSize = 50\n\t}\n\tbasicSize += size\n\treturn basicSize\n}\n\nfunc (s *seqConversationCacheRedis) Malloc(ctx context.Context, conversationID string, size int64) (int64, error) {\n\tseq, _, err := s.mallocTime(ctx, conversationID, size)\n\treturn seq, err\n}\n\nfunc (s *seqConversationCacheRedis) mallocTime(ctx context.Context, conversationID string, size int64) (int64, int64, error) {\n\tif size < 0 {\n\t\treturn 0, 0, errs.New(\"size must be greater than 0\")\n\t}\n\tkey := s.getSeqMallocKey(conversationID)\n\tfor i := 0; i < 10; i++ {\n\t\tstates, err := s.malloc(ctx, key, size)\n\t\tif err != nil {\n\t\t\treturn 0, 0, err\n\t\t}\n\t\tswitch states[0] {\n\t\tcase 0: // success\n\t\t\treturn states[1], states[3], nil\n\t\tcase 1: // not found\n\t\t\tmallocSize := s.getMallocSize(conversationID, size)\n\t\t\tseq, err := s.mgo.Malloc(ctx, conversationID, mallocSize)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, 0, err\n\t\t\t}\n\t\t\ts.setSeqRetry(ctx, key, states[1], seq+size, seq+mallocSize, states[2])\n\t\t\treturn seq, 0, nil\n\t\tcase 2: // locked\n\t\t\tif err := s.wait(ctx); err != nil {\n\t\t\t\treturn 0, 0, err\n\t\t\t}\n\t\t\tcontinue\n\t\tcase 3: // exceeded cache max value\n\t\t\tcurrSeq := states[1]\n\t\t\tlastSeq := states[2]\n\t\t\tmill := states[4]\n\t\t\tmallocSize := s.getMallocSize(conversationID, size)\n\t\t\tseq, err := s.mgo.Malloc(ctx, conversationID, mallocSize)\n\t\t\tif err != nil {\n\t\t\t\treturn 0, 0, err\n\t\t\t}\n\t\t\tif lastSeq == seq {\n\t\t\t\ts.setSeqRetry(ctx, key, states[3], currSeq+size, seq+mallocSize, mill)\n\t\t\t\treturn currSeq, states[4], nil\n\t\t\t} else {\n\t\t\t\tlog.ZWarn(ctx, \"malloc seq not equal cache last seq\", nil, \"conversationID\", conversationID, \"currSeq\", currSeq, \"lastSeq\", lastSeq, \"mallocSeq\", seq)\n\t\t\t\ts.setSeqRetry(ctx, key, states[3], seq+size, seq+mallocSize, mill)\n\t\t\t\treturn seq, mill, nil\n\t\t\t}\n\t\tdefault:\n\t\t\tlog.ZError(ctx, \"malloc seq unknown state\", nil, \"state\", states[0], \"conversationID\", conversationID, \"size\", size)\n\t\t\treturn 0, 0, errs.New(fmt.Sprintf(\"unknown state: %d\", states[0]))\n\t\t}\n\t}\n\tlog.ZError(ctx, \"malloc seq retrying still failed\", nil, \"conversationID\", conversationID, \"size\", size)\n\treturn 0, 0, errs.New(\"malloc seq waiting for lock timeout\", \"conversationID\", conversationID, \"size\", size)\n}\n\nfunc (s *seqConversationCacheRedis) GetMaxSeq(ctx context.Context, conversationID string) (int64, error) {\n\treturn s.Malloc(ctx, conversationID, 0)\n}\n\nfunc (s *seqConversationCacheRedis) GetMaxSeqWithTime(ctx context.Context, conversationID string) (database.SeqTime, error) {\n\tseq, mill, err := s.mallocTime(ctx, conversationID, 0)\n\tif err != nil {\n\t\treturn database.SeqTime{}, err\n\t}\n\treturn database.SeqTime{Seq: seq, Time: mill}, nil\n}\n\nfunc (s *seqConversationCacheRedis) SetMinSeqs(ctx context.Context, seqs map[string]int64) error {\n\tkeys := make([]string, 0, len(seqs))\n\tfor conversationID, seq := range seqs {\n\t\tkeys = append(keys, s.getMinSeqKey(conversationID))\n\t\tif err := s.mgo.SetMinSeq(ctx, conversationID, seq); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn DeleteCacheBySlot(ctx, s.rcClient, keys)\n}\n\n// GetCacheMaxSeqWithTime only get the existing cache, if there is no cache, no cache will be generated\nfunc (s *seqConversationCacheRedis) GetCacheMaxSeqWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error) {\n\tif len(conversationIDs) == 0 {\n\t\treturn map[string]database.SeqTime{}, nil\n\t}\n\tkey2conversationID := make(map[string]string)\n\tkeys := make([]string, 0, len(conversationIDs))\n\tfor _, conversationID := range conversationIDs {\n\t\tkey := s.getSeqMallocKey(conversationID)\n\t\tif _, ok := key2conversationID[key]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tkey2conversationID[key] = conversationID\n\t\tkeys = append(keys, key)\n\t}\n\tslotKeys, err := groupKeysBySlot(ctx, s.rcClient.GetRedis(), keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make(map[string]database.SeqTime)\n\tfor _, keys := range slotKeys {\n\t\tif len(keys) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tpipe := s.rcClient.GetRedis().Pipeline()\n\t\tcmds := make([]*redis.SliceCmd, 0, len(keys))\n\t\tfor _, key := range keys {\n\t\t\tcmds = append(cmds, pipe.HMGet(ctx, key, \"CURR\", \"TIME\"))\n\t\t}\n\t\tif _, err := pipe.Exec(ctx); err != nil {\n\t\t\treturn nil, errs.Wrap(err)\n\t\t}\n\t\tfor i, cmd := range cmds {\n\t\t\tval, err := cmd.Result()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif len(val) != 2 {\n\t\t\t\treturn nil, errs.WrapMsg(err, \"GetCacheMaxSeqWithTime invalid result\", \"key\", keys[i], \"res\", val)\n\t\t\t}\n\t\t\tif val[0] == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseq, err := s.parseInt64(val[0])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tmill, err := s.parseInt64(val[1])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tconversationID := key2conversationID[keys[i]]\n\t\t\tres[conversationID] = database.SeqTime{Seq: seq, Time: mill}\n\t\t}\n\t}\n\treturn res, nil\n}\n\nfunc (s *seqConversationCacheRedis) parseInt64(val any) (int64, error) {\n\tswitch v := val.(type) {\n\tcase nil:\n\t\treturn 0, nil\n\tcase int:\n\t\treturn int64(v), nil\n\tcase int64:\n\t\treturn v, nil\n\tcase string:\n\t\tres, err := strconv.ParseInt(v, 10, 64)\n\t\tif err != nil {\n\t\t\treturn 0, errs.WrapMsg(err, \"invalid string not int64\", \"value\", v)\n\t\t}\n\t\treturn res, nil\n\tdefault:\n\t\treturn 0, errs.New(\"invalid result not int64\", \"resType\", fmt.Sprintf(\"%T\", v), \"value\", v)\n\t}\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/seq_conversation_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc newTestSeq() *seqConversationCacheRedis {\n\tmgocli, err := mongo.Connect(context.Background(), options.Client().ApplyURI(\"mongodb://openIM:openIM123@127.0.0.1:37017/openim_v3?maxPoolSize=100\").SetConnectTimeout(5*time.Second))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tmodel, err := mgo.NewSeqConversationMongo(mgocli.Database(\"openim_v3\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\topt := &redis.Options{\n\t\tAddr:     \"127.0.0.1:16379\",\n\t\tPassword: \"openIM123\",\n\t\tDB:       1,\n\t}\n\trdb := redis.NewClient(opt)\n\tif err := rdb.Ping(context.Background()).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\treturn NewSeqConversationCacheRedis(rdb, model).(*seqConversationCacheRedis)\n}\n\nfunc TestSeq(t *testing.T) {\n\tts := newTestSeq()\n\tvar (\n\t\twg    sync.WaitGroup\n\t\tspeed atomic.Int64\n\t)\n\n\tconst count = 128\n\twg.Add(count)\n\tfor i := 0; i < count; i++ {\n\t\tindex := i + 1\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tvar size int64 = 10\n\t\t\tcID := strconv.Itoa(index * 1)\n\t\t\tfor i := 1; ; i++ {\n\t\t\t\t//first, err := ts.mgo.Malloc(context.Background(), cID, size) // mongo\n\t\t\t\tfirst, err := ts.Malloc(context.Background(), cID, size) // redis\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"[%d-%d] %s %s\", index, i, cID, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tspeed.Add(size)\n\t\t\t\t_ = first\n\t\t\t\t//t.Logf(\"[%d] %d -> %d\", i, first+1, first+size)\n\t\t\t}\n\t\t}()\n\t}\n\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(done)\n\t}()\n\n\tticker := time.NewTicker(time.Second)\n\n\tfor {\n\t\tselect {\n\t\tcase <-done:\n\t\t\tticker.Stop()\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\tvalue := speed.Swap(0)\n\t\t\tt.Logf(\"speed: %d/s\", value)\n\t\t}\n\t}\n}\n\nfunc TestDel(t *testing.T) {\n\tts := newTestSeq()\n\tfor i := 1; i < 100; i++ {\n\t\tvar size int64 = 100\n\t\tfirst, err := ts.Malloc(context.Background(), \"100\", size)\n\t\tif err != nil {\n\t\t\tt.Logf(\"[%d] %s\", i, err)\n\t\t\treturn\n\t\t}\n\t\tt.Logf(\"[%d] %d -> %d\", i, first+1, first+size)\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc TestSeqMalloc(t *testing.T) {\n\tts := newTestSeq()\n\tt.Log(ts.GetMaxSeq(context.Background(), \"100\"))\n}\n\nfunc TestMinSeq(t *testing.T) {\n\tts := newTestSeq()\n\tt.Log(ts.GetMinSeq(context.Background(), \"10000000\"))\n}\n\nfunc TestMalloc(t *testing.T) {\n\tts := newTestSeq()\n\tt.Log(ts.mallocTime(context.Background(), \"10000000\", 100))\n}\n\nfunc TestHMGET(t *testing.T) {\n\tts := newTestSeq()\n\tres, err := ts.GetCacheMaxSeqWithTime(context.Background(), []string{\"10000000\", \"123456\"})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(res)\n}\n\nfunc TestGetMaxSeqWithTime(t *testing.T) {\n\tts := newTestSeq()\n\tt.Log(ts.GetMaxSeqWithTime(context.Background(), \"10000000\"))\n}\n\nfunc TestGetMaxSeqWithTime1(t *testing.T) {\n\tts := newTestSeq()\n\tt.Log(ts.GetMaxSeqsWithTime(context.Background(), []string{\"10000000\", \"12345\", \"111\"}))\n}\n\n//\n//func TestHMGET(t *testing.T) {\n//\tts := newTestSeq()\n//\tres, err := ts.rdb.HMGet(context.Background(), \"MALLOC_SEQ:1\", \"CURR\", \"TIME1\").Result()\n//\tif err != nil {\n//\t\tpanic(err)\n//\t}\n//\tt.Log(res)\n//}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/seq_user.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewSeqUserCacheRedis(rdb redis.UniversalClient, mgo database.SeqUser) cache.SeqUser {\n\treturn &seqUserCacheRedis{\n\t\tmgo:               mgo,\n\t\treadSeqWriteRatio: 100,\n\t\texpireTime:        time.Hour * 24 * 7,\n\t\treadExpireTime:    time.Hour * 24 * 30,\n\t\trocks:             newRocksCacheClient(rdb),\n\t}\n}\n\ntype seqUserCacheRedis struct {\n\tmgo               database.SeqUser\n\trocks             *rocksCacheClient\n\texpireTime        time.Duration\n\treadExpireTime    time.Duration\n\treadSeqWriteRatio int64\n}\n\nfunc (s *seqUserCacheRedis) getSeqUserMaxSeqKey(conversationID string, userID string) string {\n\treturn cachekey.GetSeqUserMaxSeqKey(conversationID, userID)\n}\n\nfunc (s *seqUserCacheRedis) getSeqUserMinSeqKey(conversationID string, userID string) string {\n\treturn cachekey.GetSeqUserMinSeqKey(conversationID, userID)\n}\n\nfunc (s *seqUserCacheRedis) getSeqUserReadSeqKey(conversationID string, userID string) string {\n\treturn cachekey.GetSeqUserReadSeqKey(conversationID, userID)\n}\n\nfunc (s *seqUserCacheRedis) GetUserMaxSeq(ctx context.Context, conversationID string, userID string) (int64, error) {\n\treturn getCache(ctx, s.rocks, s.getSeqUserMaxSeqKey(conversationID, userID), s.expireTime, func(ctx context.Context) (int64, error) {\n\t\treturn s.mgo.GetUserMaxSeq(ctx, conversationID, userID)\n\t})\n}\n\nfunc (s *seqUserCacheRedis) SetUserMaxSeq(ctx context.Context, conversationID string, userID string, seq int64) error {\n\tif err := s.mgo.SetUserMaxSeq(ctx, conversationID, userID, seq); err != nil {\n\t\treturn err\n\t}\n\treturn s.rocks.GetClient().TagAsDeleted2(ctx, s.getSeqUserMaxSeqKey(conversationID, userID))\n}\n\nfunc (s *seqUserCacheRedis) GetUserMinSeq(ctx context.Context, conversationID string, userID string) (int64, error) {\n\treturn getCache(ctx, s.rocks, s.getSeqUserMinSeqKey(conversationID, userID), s.expireTime, func(ctx context.Context) (int64, error) {\n\t\treturn s.mgo.GetUserMinSeq(ctx, conversationID, userID)\n\t})\n}\n\nfunc (s *seqUserCacheRedis) SetUserMinSeq(ctx context.Context, conversationID string, userID string, seq int64) error {\n\treturn s.SetUserMinSeqs(ctx, userID, map[string]int64{conversationID: seq})\n}\n\nfunc (s *seqUserCacheRedis) GetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error) {\n\treturn getCache(ctx, s.rocks, s.getSeqUserReadSeqKey(conversationID, userID), s.readExpireTime, func(ctx context.Context) (int64, error) {\n\t\treturn s.mgo.GetUserReadSeq(ctx, conversationID, userID)\n\t})\n}\n\nfunc (s *seqUserCacheRedis) SetUserReadSeq(ctx context.Context, conversationID string, userID string, seq int64) error {\n\tif s.rocks.GetRedis() == nil {\n\t\treturn s.SetUserReadSeqToDB(ctx, conversationID, userID, seq)\n\t}\n\tdbSeq, err := s.GetUserReadSeq(ctx, conversationID, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif dbSeq < seq {\n\t\tif err := s.rocks.GetClient().RawSet(ctx, s.getSeqUserReadSeqKey(conversationID, userID), strconv.Itoa(int(seq)), s.readExpireTime); err != nil {\n\t\t\treturn errs.Wrap(err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *seqUserCacheRedis) SetUserReadSeqToDB(ctx context.Context, conversationID string, userID string, seq int64) error {\n\treturn s.mgo.SetUserReadSeq(ctx, conversationID, userID, seq)\n}\n\nfunc (s *seqUserCacheRedis) SetUserMinSeqs(ctx context.Context, userID string, seqs map[string]int64) error {\n\tkeys := make([]string, 0, len(seqs))\n\tfor conversationID, seq := range seqs {\n\t\tif err := s.mgo.SetUserMinSeq(ctx, conversationID, userID, seq); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tkeys = append(keys, s.getSeqUserMinSeqKey(conversationID, userID))\n\t}\n\treturn DeleteCacheBySlot(ctx, s.rocks, keys)\n}\n\nfunc (s *seqUserCacheRedis) setUserRedisReadSeqs(ctx context.Context, userID string, seqs map[string]int64) error {\n\tkeys := make([]string, 0, len(seqs))\n\tkeySeq := make(map[string]int64)\n\tfor conversationID, seq := range seqs {\n\t\tkey := s.getSeqUserReadSeqKey(conversationID, userID)\n\t\tkeys = append(keys, key)\n\t\tkeySeq[key] = seq\n\t}\n\tslotKeys, err := groupKeysBySlot(ctx, s.rocks.GetRedis(), keys)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, keys := range slotKeys {\n\t\tpipe := s.rocks.GetRedis().Pipeline()\n\t\tfor _, key := range keys {\n\t\t\tpipe.HSet(ctx, key, \"value\", strconv.FormatInt(keySeq[key], 10))\n\t\t\tpipe.Expire(ctx, key, s.readExpireTime)\n\t\t}\n\t\tif _, err := pipe.Exec(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *seqUserCacheRedis) SetUserReadSeqs(ctx context.Context, userID string, seqs map[string]int64) error {\n\tif len(seqs) == 0 {\n\t\treturn nil\n\t}\n\tif err := s.setUserRedisReadSeqs(ctx, userID, seqs); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (s *seqUserCacheRedis) GetUserReadSeqs(ctx context.Context, userID string, conversationIDs []string) (map[string]int64, error) {\n\tres, err := batchGetCache2(ctx, s.rocks, s.readExpireTime, conversationIDs, func(conversationID string) string {\n\t\treturn s.getSeqUserReadSeqKey(conversationID, userID)\n\t}, func(v *readSeqModel) string {\n\t\treturn v.ConversationID\n\t}, func(ctx context.Context, conversationIDs []string) ([]*readSeqModel, error) {\n\t\tseqs, err := s.mgo.GetUserReadSeqs(ctx, userID, conversationIDs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres := make([]*readSeqModel, 0, len(seqs))\n\t\tfor conversationID, seq := range seqs {\n\t\t\tres = append(res, &readSeqModel{ConversationID: conversationID, Seq: seq})\n\t\t}\n\t\treturn res, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdata := make(map[string]int64)\n\tfor _, v := range res {\n\t\tdata[v.ConversationID] = v.Seq\n\t}\n\treturn data, nil\n}\n\nvar _ BatchCacheCallback[string] = (*readSeqModel)(nil)\n\ntype readSeqModel struct {\n\tConversationID string\n\tSeq            int64\n}\n\nfunc (r *readSeqModel) BatchCache(conversationID string) {\n\tr.ConversationID = conversationID\n}\n\nfunc (r *readSeqModel) UnmarshalJSON(bytes []byte) (err error) {\n\tr.Seq, err = strconv.ParseInt(string(bytes), 10, 64)\n\treturn\n}\n\nfunc (r *readSeqModel) MarshalJSON() ([]byte, error) {\n\treturn []byte(strconv.FormatInt(r.Seq, 10)), nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/seq_user_test.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\tmgo2 \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\t\"log\"\n\t\"strconv\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc newTestOnline() *userOnline {\n\topt := &redis.Options{\n\t\tAddr:     \"172.16.8.48:16379\",\n\t\tPassword: \"openIM123\",\n\t\tDB:       0,\n\t}\n\trdb := redis.NewClient(opt)\n\tif err := rdb.Ping(context.Background()).Err(); err != nil {\n\t\tpanic(err)\n\t}\n\treturn &userOnline{rdb: rdb, expire: time.Hour, channelName: \"user_online\"}\n}\n\nfunc TestOnline(t *testing.T) {\n\tts := newTestOnline()\n\tvar count atomic.Int64\n\tfor i := 0; i < 64; i++ {\n\t\tgo func(userID string) {\n\t\t\tvar err error\n\t\t\tfor i := 0; ; i++ {\n\t\t\t\tif i%2 == 0 {\n\t\t\t\t\terr = ts.SetUserOnline(context.Background(), userID, []int32{5, 6}, []int32{7, 8, 9})\n\t\t\t\t} else {\n\t\t\t\t\terr = ts.SetUserOnline(context.Background(), userID, []int32{1, 2, 3}, []int32{4, 5, 6})\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t\tcount.Add(1)\n\t\t\t}\n\t\t}(strconv.Itoa(10000 + i))\n\t}\n\n\tticker := time.NewTicker(time.Second)\n\tfor range ticker.C {\n\t\tt.Log(count.Swap(0))\n\t}\n}\n\nfunc TestGetOnline(t *testing.T) {\n\tts := newTestOnline()\n\tctx := context.Background()\n\tpIDs, err := ts.GetOnline(ctx, \"10000\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(pIDs)\n}\n\nfunc TestRecvOnline(t *testing.T) {\n\tts := newTestOnline()\n\tctx := context.Background()\n\tpubsub := ts.rdb.Subscribe(ctx, cachekey.OnlineChannel)\n\n\t_, err := pubsub.Receive(ctx)\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not subscribe: %v\", err)\n\t}\n\n\tch := pubsub.Channel()\n\n\tfor msg := range ch {\n\t\tfmt.Printf(\"Received message from channel %s: %s\\n\", msg.Channel, msg.Payload)\n\t}\n}\n\nfunc TestName1(t *testing.T) {\n\topt := &redis.Options{\n\t\tAddr:     \"172.16.8.48:16379\",\n\t\tPassword: \"openIM123\",\n\t\tDB:       0,\n\t}\n\trdb := redis.NewClient(opt)\n\n\tmgo, err := mongo.Connect(context.Background(),\n\t\toptions.Client().\n\t\t\tApplyURI(\"mongodb://openIM:openIM123@172.16.8.48:37017/openim_v3?maxPoolSize=100\").\n\t\t\tSetConnectTimeout(5*time.Second))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tmodel, err := mgo2.NewSeqUserMongo(mgo.Database(\"openim_v3\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tseq := NewSeqUserCacheRedis(rdb, model)\n\n\tres, err := seq.GetUserReadSeqs(context.Background(), \"2110910952\", []string{\"sg_345762580\", \"2000\", \"3000\"})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(res)\n\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/third.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewThirdCache(rdb redis.UniversalClient) cache.ThirdCache {\n\treturn &thirdCache{rdb: rdb}\n}\n\ntype thirdCache struct {\n\trdb redis.UniversalClient\n}\n\nfunc (c *thirdCache) getGetuiTokenKey() string {\n\treturn cachekey.GetGetuiTokenKey()\n}\n\nfunc (c *thirdCache) getGetuiTaskIDKey() string {\n\treturn cachekey.GetGetuiTaskIDKey()\n}\n\nfunc (c *thirdCache) getUserBadgeUnreadCountSumKey(userID string) string {\n\treturn cachekey.GetUserBadgeUnreadCountSumKey(userID)\n}\n\nfunc (c *thirdCache) getFcmAccountTokenKey(account string, platformID int) string {\n\treturn cachekey.GetFcmAccountTokenKey(account, platformID)\n}\n\nfunc (c *thirdCache) SetFcmToken(ctx context.Context, account string, platformID int, fcmToken string, expireTime int64) (err error) {\n\treturn errs.Wrap(c.rdb.Set(ctx, c.getFcmAccountTokenKey(account, platformID), fcmToken, time.Duration(expireTime)*time.Second).Err())\n}\n\nfunc (c *thirdCache) GetFcmToken(ctx context.Context, account string, platformID int) (string, error) {\n\tval, err := c.rdb.Get(ctx, c.getFcmAccountTokenKey(account, platformID)).Result()\n\tif err != nil {\n\t\treturn \"\", errs.Wrap(err)\n\t}\n\treturn val, nil\n}\n\nfunc (c *thirdCache) DelFcmToken(ctx context.Context, account string, platformID int) error {\n\treturn errs.Wrap(c.rdb.Del(ctx, c.getFcmAccountTokenKey(account, platformID)).Err())\n}\n\nfunc (c *thirdCache) IncrUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error) {\n\tseq, err := c.rdb.Incr(ctx, c.getUserBadgeUnreadCountSumKey(userID)).Result()\n\n\treturn int(seq), errs.Wrap(err)\n}\n\nfunc (c *thirdCache) SetUserBadgeUnreadCountSum(ctx context.Context, userID string, value int) error {\n\treturn errs.Wrap(c.rdb.Set(ctx, c.getUserBadgeUnreadCountSumKey(userID), value, 0).Err())\n}\n\nfunc (c *thirdCache) GetUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error) {\n\tval, err := c.rdb.Get(ctx, c.getUserBadgeUnreadCountSumKey(userID)).Int()\n\treturn val, errs.Wrap(err)\n}\n\nfunc (c *thirdCache) SetGetuiToken(ctx context.Context, token string, expireTime int64) error {\n\treturn errs.Wrap(c.rdb.Set(ctx, c.getGetuiTokenKey(), token, time.Duration(expireTime)*time.Second).Err())\n}\n\nfunc (c *thirdCache) GetGetuiToken(ctx context.Context) (string, error) {\n\tval, err := c.rdb.Get(ctx, c.getGetuiTokenKey()).Result()\n\tif err != nil {\n\t\treturn \"\", errs.Wrap(err)\n\t}\n\treturn val, nil\n}\n\nfunc (c *thirdCache) SetGetuiTaskID(ctx context.Context, taskID string, expireTime int64) error {\n\treturn errs.Wrap(c.rdb.Set(ctx, c.getGetuiTaskIDKey(), taskID, time.Duration(expireTime)*time.Second).Err())\n}\n\nfunc (c *thirdCache) GetGetuiTaskID(ctx context.Context) (string, error) {\n\tval, err := c.rdb.Get(ctx, c.getGetuiTaskIDKey()).Result()\n\tif err != nil {\n\t\treturn \"\", errs.Wrap(err)\n\t}\n\treturn val, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/token.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype tokenCache struct {\n\trdb          redis.UniversalClient\n\taccessExpire time.Duration\n\tlocalCache   *config.LocalCache\n}\n\nfunc NewTokenCacheModel(rdb redis.UniversalClient, localCache *config.LocalCache, accessExpire int64) cache.TokenModel {\n\tc := &tokenCache{rdb: rdb, localCache: localCache}\n\tc.accessExpire = c.getExpireTime(accessExpire)\n\treturn c\n}\n\nfunc (c *tokenCache) SetTokenFlag(ctx context.Context, userID string, platformID int, token string, flag int) error {\n\tkey := cachekey.GetTokenKey(userID, platformID)\n\tif err := c.rdb.HSet(ctx, key, token, flag).Err(); err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\n\tif c.localCache != nil {\n\t\tc.removeLocalTokenCache(ctx, key)\n\t}\n\n\treturn nil\n}\n\n// SetTokenFlagEx set token and flag with expire time\nfunc (c *tokenCache) SetTokenFlagEx(ctx context.Context, userID string, platformID int, token string, flag int) error {\n\tkey := cachekey.GetTokenKey(userID, platformID)\n\tif err := c.rdb.HSet(ctx, key, token, flag).Err(); err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\tif err := c.rdb.Expire(ctx, key, c.accessExpire).Err(); err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\n\tif c.localCache != nil {\n\t\tc.removeLocalTokenCache(ctx, key)\n\t}\n\n\treturn nil\n}\n\nfunc (c *tokenCache) GetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error) {\n\tm, err := c.rdb.HGetAll(ctx, cachekey.GetTokenKey(userID, platformID)).Result()\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\tmm := make(map[string]int)\n\tfor k, v := range m {\n\t\tstate, err := strconv.Atoi(v)\n\t\tif err != nil {\n\t\t\treturn nil, errs.WrapMsg(err, \"redis token value is not int\", \"value\", v, \"userID\", userID, \"platformID\", platformID)\n\t\t}\n\t\tmm[k] = state\n\t}\n\treturn mm, nil\n}\n\nfunc (c *tokenCache) HasTemporaryToken(ctx context.Context, userID string, platformID int, token string) error {\n\terr := c.rdb.Get(ctx, cachekey.GetTemporaryTokenKey(userID, platformID, token)).Err()\n\tif err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\treturn nil\n}\n\nfunc (c *tokenCache) GetAllTokensWithoutError(ctx context.Context, userID string) (map[int]map[string]int, error) {\n\tvar (\n\t\tres     = make(map[int]map[string]int)\n\t\tresLock = sync.Mutex{}\n\t)\n\n\tkeys := cachekey.GetAllPlatformTokenKey(userID)\n\tif err := ProcessKeysBySlot(ctx, c.rdb, keys, func(ctx context.Context, slot int64, keys []string) error {\n\t\tpipe := c.rdb.Pipeline()\n\t\tmapRes := make([]*redis.MapStringStringCmd, len(keys))\n\t\tfor i, key := range keys {\n\t\t\tmapRes[i] = pipe.HGetAll(ctx, key)\n\t\t}\n\t\t_, err := pipe.Exec(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor i, m := range mapRes {\n\t\t\tmm := make(map[string]int)\n\t\t\tfor k, v := range m.Val() {\n\t\t\t\tstate, err := strconv.Atoi(v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn errs.WrapMsg(err, \"redis token value is not int\", \"value\", v, \"userID\", userID)\n\t\t\t\t}\n\t\t\t\tmm[k] = state\n\t\t\t}\n\t\t\tresLock.Lock()\n\t\t\tres[cachekey.GetPlatformIDByTokenKey(keys[i])] = mm\n\t\t\tresLock.Unlock()\n\t\t}\n\t\treturn nil\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\treturn res, nil\n}\n\nfunc (c *tokenCache) SetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error {\n\tmm := make(map[string]any)\n\tfor k, v := range m {\n\t\tmm[k] = v\n\t}\n\n\terr := c.rdb.HSet(ctx, cachekey.GetTokenKey(userID, platformID), mm).Err()\n\tif err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\n\tif c.localCache != nil {\n\t\tc.removeLocalTokenCache(ctx, cachekey.GetTokenKey(userID, platformID))\n\t}\n\n\treturn nil\n}\n\nfunc (c *tokenCache) BatchSetTokenMapByUidPid(ctx context.Context, tokens map[string]map[string]any) error {\n\tkeys := datautil.Keys(tokens)\n\tif err := ProcessKeysBySlot(ctx, c.rdb, keys, func(ctx context.Context, slot int64, keys []string) error {\n\t\tpipe := c.rdb.Pipeline()\n\t\tfor k, v := range tokens {\n\t\t\tpipe.HSet(ctx, k, v)\n\t\t}\n\t\t_, err := pipe.Exec(ctx)\n\t\tif err != nil {\n\t\t\treturn errs.Wrap(err)\n\t\t}\n\t\treturn nil\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tif c.localCache != nil {\n\t\tc.removeLocalTokenCache(ctx, keys...)\n\t}\n\treturn nil\n}\n\nfunc (c *tokenCache) DeleteTokenByUidPid(ctx context.Context, userID string, platformID int, fields []string) error {\n\tkey := cachekey.GetTokenKey(userID, platformID)\n\tif err := c.rdb.HDel(ctx, key, fields...).Err(); err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\n\tif c.localCache != nil {\n\t\tc.removeLocalTokenCache(ctx, key)\n\t}\n\treturn nil\n}\n\nfunc (c *tokenCache) getExpireTime(t int64) time.Duration {\n\treturn time.Hour * 24 * time.Duration(t)\n}\n\n// DeleteTokenByTokenMap tokens key is platformID, value is token slice\nfunc (c *tokenCache) DeleteTokenByTokenMap(ctx context.Context, userID string, tokens map[int][]string) error {\n\tvar (\n\t\tkeys   = make([]string, 0, len(tokens))\n\t\tkeyMap = make(map[string][]string)\n\t)\n\tfor k, v := range tokens {\n\t\tk1 := cachekey.GetTokenKey(userID, k)\n\t\tkeys = append(keys, k1)\n\t\tkeyMap[k1] = v\n\t}\n\n\tif err := ProcessKeysBySlot(ctx, c.rdb, keys, func(ctx context.Context, slot int64, keys []string) error {\n\t\tpipe := c.rdb.Pipeline()\n\t\tfor k, v := range tokens {\n\t\t\tpipe.HDel(ctx, cachekey.GetTokenKey(userID, k), v...)\n\t\t}\n\t\t_, err := pipe.Exec(ctx)\n\t\tif err != nil {\n\t\t\treturn errs.Wrap(err)\n\t\t}\n\t\treturn nil\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\t// Remove local cache for the token\n\tif c.localCache != nil {\n\t\tc.removeLocalTokenCache(ctx, keys...)\n\t}\n\n\treturn nil\n}\n\nfunc (c *tokenCache) DeleteAndSetTemporary(ctx context.Context, userID string, platformID int, fields []string) error {\n\tfor _, f := range fields {\n\t\tk := cachekey.GetTemporaryTokenKey(userID, platformID, f)\n\t\tif err := c.rdb.Set(ctx, k, \"\", time.Minute*5).Err(); err != nil {\n\t\t\treturn errs.Wrap(err)\n\t\t}\n\t}\n\tkey := cachekey.GetTokenKey(userID, platformID)\n\tif err := c.rdb.HDel(ctx, key, fields...).Err(); err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\tif c.localCache != nil {\n\t\tc.removeLocalTokenCache(ctx, key)\n\t}\n\treturn nil\n}\n\nfunc (c *tokenCache) removeLocalTokenCache(ctx context.Context, keys ...string) {\n\tif len(keys) == 0 {\n\t\treturn\n\t}\n\n\ttopic := c.localCache.Auth.Topic\n\tif topic == \"\" {\n\t\treturn\n\t}\n\n\tdata, err := json.Marshal(keys)\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"keys json marshal failed\", err, \"topic\", topic, \"keys\", keys)\n\t} else {\n\t\tif err := c.rdb.Publish(ctx, topic, string(data)).Err(); err != nil {\n\t\t\tlog.ZWarn(ctx, \"redis publish cache delete error\", err, \"topic\", topic, \"keys\", keys)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/redis/user.go",
    "content": "package redis\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/dtm-labs/rockscache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tuserExpireTime            = time.Second * 60 * 60 * 12\n\tuserOlineStatusExpireTime = time.Second * 60 * 60 * 24\n\tstatusMod                 = 501\n)\n\ntype UserCacheRedis struct {\n\tcache.BatchDeleter\n\trdb        redis.UniversalClient\n\tuserDB     database.User\n\texpireTime time.Duration\n\trcClient   *rocksCacheClient\n}\n\nfunc NewUserCacheRedis(rdb redis.UniversalClient, localCache *config.LocalCache, userDB database.User, options *rockscache.Options) cache.UserCache {\n\trc := newRocksCacheClient(rdb)\n\treturn &UserCacheRedis{\n\t\tBatchDeleter: rc.GetBatchDeleter(localCache.User.Topic),\n\t\trdb:          rdb,\n\t\tuserDB:       userDB,\n\t\texpireTime:   userExpireTime,\n\t\trcClient:     rc,\n\t}\n}\n\nfunc (u *UserCacheRedis) getUserID(user *model.User) string {\n\treturn user.UserID\n}\n\nfunc (u *UserCacheRedis) CloneUserCache() cache.UserCache {\n\treturn &UserCacheRedis{\n\t\tBatchDeleter: u.BatchDeleter.Clone(),\n\t\trdb:          u.rdb,\n\t\tuserDB:       u.userDB,\n\t\texpireTime:   u.expireTime,\n\t\trcClient:     u.rcClient,\n\t}\n}\n\nfunc (u *UserCacheRedis) getUserInfoKey(userID string) string {\n\treturn cachekey.GetUserInfoKey(userID)\n}\n\nfunc (u *UserCacheRedis) getUserGlobalRecvMsgOptKey(userID string) string {\n\treturn cachekey.GetUserGlobalRecvMsgOptKey(userID)\n}\n\nfunc (u *UserCacheRedis) GetUserInfo(ctx context.Context, userID string) (userInfo *model.User, err error) {\n\treturn getCache(ctx, u.rcClient, u.getUserInfoKey(userID), u.expireTime, func(ctx context.Context) (*model.User, error) {\n\t\treturn u.userDB.Take(ctx, userID)\n\t})\n}\n\nfunc (u *UserCacheRedis) GetUsersInfo(ctx context.Context, userIDs []string) ([]*model.User, error) {\n\treturn batchGetCache2(ctx, u.rcClient, u.expireTime, userIDs, u.getUserInfoKey, u.getUserID, u.userDB.Find)\n}\n\nfunc (u *UserCacheRedis) DelUsersInfo(userIDs ...string) cache.UserCache {\n\tkeys := make([]string, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tkeys = append(keys, u.getUserInfoKey(userID))\n\t}\n\tcache := u.CloneUserCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n\nfunc (u *UserCacheRedis) GetUserGlobalRecvMsgOpt(ctx context.Context, userID string) (opt int, err error) {\n\treturn getCache(\n\t\tctx,\n\t\tu.rcClient,\n\t\tu.getUserGlobalRecvMsgOptKey(userID),\n\t\tu.expireTime,\n\t\tfunc(ctx context.Context) (int, error) {\n\t\t\treturn u.userDB.GetUserGlobalRecvMsgOpt(ctx, userID)\n\t\t},\n\t)\n}\n\nfunc (u *UserCacheRedis) DelUsersGlobalRecvMsgOpt(userIDs ...string) cache.UserCache {\n\tkeys := make([]string, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tkeys = append(keys, u.getUserGlobalRecvMsgOptKey(userID))\n\t}\n\tcache := u.CloneUserCache()\n\tcache.AddKeys(keys...)\n\n\treturn cache\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/s3.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cache\n\nimport (\n\t\"context\"\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/s3\"\n)\n\ntype ObjectCache interface {\n\tBatchDeleter\n\tCloneObjectCache() ObjectCache\n\tGetName(ctx context.Context, engine string, name string) (*relationtb.Object, error)\n\tDelObjectName(engine string, names ...string) ObjectCache\n}\n\ntype S3Cache interface {\n\tBatchDeleter\n\tGetKey(ctx context.Context, engine string, key string) (*s3.ObjectInfo, error)\n\tDelS3Key(engine string, keys ...string) S3Cache\n}\n\n// TODO integrating minio.Cache and MinioCache interfaces.\ntype MinioCache interface {\n\tBatchDeleter\n\tGetImageObjectKeyInfo(ctx context.Context, key string, fn func(ctx context.Context) (*MinioImageInfo, error)) (*MinioImageInfo, error)\n\tGetThumbnailKey(ctx context.Context, key string, format string, width int, height int, minioCache func(ctx context.Context) (string, error)) (string, error)\n\tDelObjectImageInfoKey(keys ...string) MinioCache\n\tDelImageThumbnailKey(key string, format string, width int, height int) MinioCache\n}\n\ntype MinioImageInfo struct {\n\tIsImg  bool   `json:\"isImg\"`\n\tWidth  int    `json:\"width\"`\n\tHeight int    `json:\"height\"`\n\tFormat string `json:\"format\"`\n\tEtag   string `json:\"etag\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/seq_conversation.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n)\n\ntype SeqConversationCache interface {\n\tMalloc(ctx context.Context, conversationID string, size int64) (int64, error)\n\tGetMaxSeq(ctx context.Context, conversationID string) (int64, error)\n\tSetMinSeq(ctx context.Context, conversationID string, seq int64) error\n\tGetMinSeq(ctx context.Context, conversationID string) (int64, error)\n\tGetMaxSeqs(ctx context.Context, conversationIDs []string) (map[string]int64, error)\n\tSetMinSeqs(ctx context.Context, seqs map[string]int64) error\n\tGetCacheMaxSeqWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error)\n\tGetMaxSeqsWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error)\n\tGetMaxSeqWithTime(ctx context.Context, conversationID string) (database.SeqTime, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/seq_user.go",
    "content": "package cache\n\nimport \"context\"\n\ntype SeqUser interface {\n\tGetUserMaxSeq(ctx context.Context, conversationID string, userID string) (int64, error)\n\tSetUserMaxSeq(ctx context.Context, conversationID string, userID string, seq int64) error\n\tGetUserMinSeq(ctx context.Context, conversationID string, userID string) (int64, error)\n\tSetUserMinSeq(ctx context.Context, conversationID string, userID string, seq int64) error\n\tGetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error)\n\tSetUserReadSeq(ctx context.Context, conversationID string, userID string, seq int64) error\n\tSetUserReadSeqToDB(ctx context.Context, conversationID string, userID string, seq int64) error\n\tSetUserMinSeqs(ctx context.Context, userID string, seqs map[string]int64) error\n\tSetUserReadSeqs(ctx context.Context, userID string, seqs map[string]int64) error\n\tGetUserReadSeqs(ctx context.Context, userID string, conversationIDs []string) (map[string]int64, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/third.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n)\n\ntype ThirdCache interface {\n\tSetFcmToken(ctx context.Context, account string, platformID int, fcmToken string, expireTime int64) (err error)\n\tGetFcmToken(ctx context.Context, account string, platformID int) (string, error)\n\tDelFcmToken(ctx context.Context, account string, platformID int) error\n\tIncrUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error)\n\tSetUserBadgeUnreadCountSum(ctx context.Context, userID string, value int) error\n\tGetUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error)\n\tSetGetuiToken(ctx context.Context, token string, expireTime int64) error\n\tGetGetuiToken(ctx context.Context) (string, error)\n\tSetGetuiTaskID(ctx context.Context, taskID string, expireTime int64) error\n\tGetGetuiTaskID(ctx context.Context) (string, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/token.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n)\n\ntype TokenModel interface {\n\tSetTokenFlag(ctx context.Context, userID string, platformID int, token string, flag int) error\n\t// SetTokenFlagEx set token and flag with expire time\n\tSetTokenFlagEx(ctx context.Context, userID string, platformID int, token string, flag int) error\n\tGetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error)\n\tHasTemporaryToken(ctx context.Context, userID string, platformID int, token string) error\n\tGetAllTokensWithoutError(ctx context.Context, userID string) (map[int]map[string]int, error)\n\tSetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error\n\tBatchSetTokenMapByUidPid(ctx context.Context, tokens map[string]map[string]any) error\n\tDeleteTokenByUidPid(ctx context.Context, userID string, platformID int, fields []string) error\n\tDeleteTokenByTokenMap(ctx context.Context, userID string, tokens map[int][]string) error\n\tDeleteAndSetTemporary(ctx context.Context, userID string, platformID int, fields []string) error\n}\n"
  },
  {
    "path": "pkg/common/storage/cache/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage cache\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n)\n\ntype UserCache interface {\n\tBatchDeleter\n\tCloneUserCache() UserCache\n\tGetUserInfo(ctx context.Context, userID string) (userInfo *model.User, err error)\n\tGetUsersInfo(ctx context.Context, userIDs []string) ([]*model.User, error)\n\tDelUsersInfo(userIDs ...string) UserCache\n\tGetUserGlobalRecvMsgOpt(ctx context.Context, userID string) (opt int, err error)\n\tDelUsersGlobalRecvMsgOpt(userIDs ...string) UserCache\n\t//GetUserStatus(ctx context.Context, userIDs []string) ([]*user.OnlineStatus, error)\n\t//SetUserStatus(ctx context.Context, userID string, status, platformID int32) error\n}\n"
  },
  {
    "path": "pkg/common/storage/common/types.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage common\n\ntype BatchUpdateGroupMember struct {\n\tGroupID string\n\tUserID  string\n\tMap     map[string]any\n}\n\ntype GroupSimpleUserID struct {\n\tHash      uint64\n\tMemberNum uint32\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/auth.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\n\t\"github.com/golang-jwt/jwt/v4\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/tokenverify\"\n)\n\ntype AuthDatabase interface {\n\t// If the result is empty, no error is returned.\n\tGetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error)\n\n\tGetTemporaryTokensWithoutError(ctx context.Context, userID string, platformID int, token string) error\n\t// Create token\n\tCreateToken(ctx context.Context, userID string, platformID int) (string, error)\n\n\tBatchSetTokenMapByUidPid(ctx context.Context, tokens []string) error\n\n\tSetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error\n}\n\ntype multiLoginConfig struct {\n\tPolicy       int\n\tMaxNumOneEnd int\n}\n\ntype authDatabase struct {\n\tcache        cache.TokenModel\n\taccessSecret string\n\taccessExpire int64\n\tmultiLogin   multiLoginConfig\n\tadminUserIDs []string\n}\n\nfunc NewAuthDatabase(cache cache.TokenModel, accessSecret string, accessExpire int64, multiLogin config.MultiLogin, adminUserIDs []string) AuthDatabase {\n\treturn &authDatabase{cache: cache, accessSecret: accessSecret, accessExpire: accessExpire, multiLogin: multiLoginConfig{\n\t\tPolicy:       multiLogin.Policy,\n\t\tMaxNumOneEnd: multiLogin.MaxNumOneEnd,\n\t},\n\t\tadminUserIDs: adminUserIDs,\n\t}\n}\n\n// If the result is empty.\nfunc (a *authDatabase) GetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error) {\n\treturn a.cache.GetTokensWithoutError(ctx, userID, platformID)\n}\n\nfunc (a *authDatabase) GetTemporaryTokensWithoutError(ctx context.Context, userID string, platformID int, token string) error {\n\treturn a.cache.HasTemporaryToken(ctx, userID, platformID, token)\n}\n\nfunc (a *authDatabase) SetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error {\n\treturn a.cache.SetTokenMapByUidPid(ctx, userID, platformID, m)\n}\n\nfunc (a *authDatabase) BatchSetTokenMapByUidPid(ctx context.Context, tokens []string) error {\n\tsetMap := make(map[string]map[string]any)\n\tfor _, token := range tokens {\n\t\tclaims, err := tokenverify.GetClaimFromToken(token, authverify.Secret(a.accessSecret))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tkey := cachekey.GetTokenKey(claims.UserID, claims.PlatformID)\n\t\tif v, ok := setMap[key]; ok {\n\t\t\tv[token] = constant.KickedToken\n\t\t} else {\n\t\t\tsetMap[key] = map[string]any{\n\t\t\t\ttoken: constant.KickedToken,\n\t\t\t}\n\t\t}\n\t}\n\tif err := a.cache.BatchSetTokenMapByUidPid(ctx, setMap); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// Create Token.\nfunc (a *authDatabase) CreateToken(ctx context.Context, userID string, platformID int) (string, error) {\n\ttokens, err := a.cache.GetAllTokensWithoutError(ctx, userID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdeleteTokenKey, kickedTokenKey, adminTokens, err := a.checkToken(ctx, tokens, platformID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(deleteTokenKey) != 0 {\n\t\terr = a.cache.DeleteTokenByTokenMap(ctx, userID, deleteTokenKey)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif len(kickedTokenKey) != 0 {\n\t\tfor plt, ks := range kickedTokenKey {\n\t\t\tfor _, k := range ks {\n\t\t\t\terr := a.cache.SetTokenFlagEx(ctx, userID, plt, k, constant.KickedToken)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\tlog.ZDebug(ctx, \"kicked token in create token\", \"token\", k)\n\t\t\t}\n\t\t}\n\t}\n\tif len(adminTokens) != 0 {\n\t\tif err = a.cache.DeleteAndSetTemporary(ctx, userID, constant.AdminPlatformID, adminTokens); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tclaims := tokenverify.BuildClaims(userID, platformID, a.accessExpire)\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\ttokenString, err := token.SignedString([]byte(a.accessSecret))\n\tif err != nil {\n\t\treturn \"\", errs.WrapMsg(err, \"token.SignedString\")\n\t}\n\n\tif err = a.cache.SetTokenFlagEx(ctx, userID, platformID, tokenString, constant.NormalToken); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn tokenString, nil\n}\n\n// checkToken will check token by tokenPolicy and return deleteToken,kickToken,deleteAdminToken\nfunc (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string]int, platformID int) (map[int][]string, map[int][]string, []string, error) {\n\t// todo: Asynchronous deletion of old data.\n\tvar (\n\t\tloginTokenMap  = make(map[int][]string) // The length of the value of the map must be greater than 0\n\t\tdeleteToken    = make(map[int][]string)\n\t\tkickToken      = make(map[int][]string)\n\t\tadminToken     = make([]string, 0)\n\t\tunkickTerminal = \"\"\n\t)\n\n\tfor plfID, tks := range tokens {\n\t\tfor k, v := range tks {\n\t\t\t_, err := tokenverify.GetClaimFromToken(k, authverify.Secret(a.accessSecret))\n\t\t\tif err != nil || v != constant.NormalToken {\n\t\t\t\tdeleteToken[plfID] = append(deleteToken[plfID], k)\n\t\t\t} else {\n\t\t\t\tif plfID != constant.AdminPlatformID {\n\t\t\t\t\tloginTokenMap[plfID] = append(loginTokenMap[plfID], k)\n\t\t\t\t} else {\n\t\t\t\t\tadminToken = append(adminToken, k)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch a.multiLogin.Policy {\n\tcase constant.DefalutNotKick:\n\t\tfor plt, ts := range loginTokenMap {\n\t\t\tl := len(ts)\n\t\t\tif platformID == plt {\n\t\t\t\tl++\n\t\t\t}\n\t\t\tlimit := a.multiLogin.MaxNumOneEnd\n\t\t\tif l > limit {\n\t\t\t\tkickToken[plt] = ts[:l-limit]\n\t\t\t}\n\t\t}\n\tcase constant.AllLoginButSameTermKick:\n\t\tfor plt, ts := range loginTokenMap {\n\t\t\tkickToken[plt] = ts[:len(ts)-1]\n\n\t\t\tif plt == platformID {\n\t\t\t\tkickToken[plt] = append(kickToken[plt], ts[len(ts)-1])\n\t\t\t}\n\t\t}\n\tcase constant.PCAndOther:\n\t\tunkickTerminal = constant.TerminalPC\n\t\tif constant.PlatformIDToClass(platformID) != unkickTerminal {\n\t\t\tfor plt, ts := range loginTokenMap {\n\t\t\t\tif constant.PlatformIDToClass(plt) != unkickTerminal {\n\t\t\t\t\tkickToken[plt] = ts\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tvar (\n\t\t\t\tpreKickToken string\n\t\t\t\tpreKickPlt   int\n\t\t\t\treserveToken = false\n\t\t\t)\n\t\t\tfor plt, ts := range loginTokenMap {\n\t\t\t\tif constant.PlatformIDToClass(plt) != unkickTerminal {\n\t\t\t\t\t// Keep a token from another end\n\t\t\t\t\tif !reserveToken {\n\t\t\t\t\t\treserveToken = true\n\t\t\t\t\t\tkickToken[plt] = ts[:len(ts)-1]\n\t\t\t\t\t\tpreKickToken = ts[len(ts)-1]\n\t\t\t\t\t\tpreKickPlt = plt\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Prioritize keeping Android\n\t\t\t\t\t\tif plt == constant.AndroidPlatformID {\n\t\t\t\t\t\t\tif preKickToken != \"\" {\n\t\t\t\t\t\t\t\tkickToken[preKickPlt] = append(kickToken[preKickPlt], preKickToken)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tkickToken[plt] = ts[:len(ts)-1]\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tkickToken[plt] = ts\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\tcase constant.AllLoginButSameClassKick:\n\t\tvar (\n\t\t\treserved = make(map[string]struct{})\n\t\t)\n\n\t\tfor plt, ts := range loginTokenMap {\n\t\t\tif constant.PlatformIDToClass(plt) == constant.PlatformIDToClass(platformID) {\n\t\t\t\tkickToken[plt] = ts\n\t\t\t} else {\n\t\t\t\tif _, ok := reserved[constant.PlatformIDToClass(plt)]; !ok {\n\t\t\t\t\treserved[constant.PlatformIDToClass(plt)] = struct{}{}\n\t\t\t\t\tkickToken[plt] = ts[:len(ts)-1]\n\t\t\t\t\tcontinue\n\t\t\t\t} else {\n\t\t\t\t\tkickToken[plt] = ts\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn nil, nil, nil, errs.New(\"unknown multiLogin policy\").Wrap()\n\t}\n\n\t//var adminTokenMaxNum = a.multiLogin.MaxNumOneEnd\n\t//l := len(adminToken)\n\t//if platformID == constant.AdminPlatformID {\n\t//\tl++\n\t//}\n\t//if l > adminTokenMaxNum {\n\t//\tkickToken = append(kickToken, adminToken[:l-adminTokenMaxNum]...)\n\t//}\n\tvar deleteAdminToken []string\n\tif platformID == constant.AdminPlatformID {\n\t\tdeleteAdminToken = adminToken\n\t}\n\treturn deleteToken, kickToken, deleteAdminToken, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/black.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\ntype BlackDatabase interface {\n\t// Create add BlackList\n\tCreate(ctx context.Context, blacks []*model.Black) (err error)\n\t// Delete delete BlackList\n\tDelete(ctx context.Context, blacks []*model.Black) (err error)\n\t// FindOwnerBlacks get BlackList list\n\tFindOwnerBlacks(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, blacks []*model.Black, err error)\n\tFindBlackInfos(ctx context.Context, ownerUserID string, userIDs []string) (blacks []*model.Black, err error)\n\t// CheckIn Check whether user2 is in the black list of user1 (inUser1Blacks==true) Check whether user1 is in the black list of user2 (inUser2Blacks==true)\n\tCheckIn(ctx context.Context, userID1, userID2 string) (inUser1Blacks bool, inUser2Blacks bool, err error)\n}\n\ntype blackDatabase struct {\n\tblack database.Black\n\tcache cache.BlackCache\n}\n\nfunc NewBlackDatabase(black database.Black, cache cache.BlackCache) BlackDatabase {\n\treturn &blackDatabase{black, cache}\n}\n\n// Create Add Blacklist.\nfunc (b *blackDatabase) Create(ctx context.Context, blacks []*model.Black) (err error) {\n\tif err := b.black.Create(ctx, blacks); err != nil {\n\t\treturn err\n\t}\n\treturn b.deleteBlackIDsCache(ctx, blacks)\n}\n\n// Delete Delete Blacklist.\nfunc (b *blackDatabase) Delete(ctx context.Context, blacks []*model.Black) (err error) {\n\tif err := b.black.Delete(ctx, blacks); err != nil {\n\t\treturn err\n\t}\n\treturn b.deleteBlackIDsCache(ctx, blacks)\n}\n\n// FindOwnerBlacks Get Blacklist List.\nfunc (b *blackDatabase) deleteBlackIDsCache(ctx context.Context, blacks []*model.Black) (err error) {\n\tcache := b.cache.CloneBlackCache()\n\tfor _, black := range blacks {\n\t\tcache = cache.DelBlackIDs(ctx, black.OwnerUserID)\n\t}\n\treturn cache.ChainExecDel(ctx)\n}\n\n// FindOwnerBlacks Get Blacklist List.\nfunc (b *blackDatabase) FindOwnerBlacks(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, blacks []*model.Black, err error) {\n\treturn b.black.FindOwnerBlacks(ctx, ownerUserID, pagination)\n}\n\n// FindOwnerBlacks Get Blacklist List.\nfunc (b *blackDatabase) CheckIn(ctx context.Context, userID1, userID2 string) (inUser1Blacks bool, inUser2Blacks bool, err error) {\n\tuserID1BlackIDs, err := b.cache.GetBlackIDs(ctx, userID1)\n\tif err != nil {\n\t\treturn\n\t}\n\tuserID2BlackIDs, err := b.cache.GetBlackIDs(ctx, userID2)\n\tif err != nil {\n\t\treturn\n\t}\n\tlog.ZDebug(ctx, \"blackIDs\", \"user1BlackIDs\", userID1BlackIDs, \"user2BlackIDs\", userID2BlackIDs)\n\treturn datautil.Contain(userID2, userID1BlackIDs...), datautil.Contain(userID1, userID2BlackIDs...), nil\n}\n\n// FindBlackIDs Get Blacklist List.\nfunc (b *blackDatabase) FindBlackIDs(ctx context.Context, ownerUserID string) (blackIDs []string, err error) {\n\treturn b.cache.GetBlackIDs(ctx, ownerUserID)\n}\n\n// FindBlackInfos Get Blacklist List.\nfunc (b *blackDatabase) FindBlackInfos(ctx context.Context, ownerUserID string, userIDs []string) (blacks []*model.Black, err error) {\n\treturn b.black.FindOwnerBlackInfos(ctx, ownerUserID, userIDs)\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/client_config.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/db/tx\"\n)\n\ntype ClientConfigDatabase interface {\n\tSetUserConfig(ctx context.Context, userID string, config map[string]string) error\n\tGetUserConfig(ctx context.Context, userID string) (map[string]string, error)\n\tDelUserConfig(ctx context.Context, userID string, keys []string) error\n\tGetUserConfigPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error)\n}\n\nfunc NewClientConfigDatabase(db database.ClientConfig, cache cache.ClientConfigCache, tx tx.Tx) ClientConfigDatabase {\n\treturn &clientConfigDatabase{\n\t\ttx:    tx,\n\t\tdb:    db,\n\t\tcache: cache,\n\t}\n}\n\ntype clientConfigDatabase struct {\n\ttx    tx.Tx\n\tdb    database.ClientConfig\n\tcache cache.ClientConfigCache\n}\n\nfunc (x *clientConfigDatabase) SetUserConfig(ctx context.Context, userID string, config map[string]string) error {\n\treturn x.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := x.db.Set(ctx, userID, config); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn x.cache.DeleteUserCache(ctx, []string{userID})\n\t})\n}\n\nfunc (x *clientConfigDatabase) GetUserConfig(ctx context.Context, userID string) (map[string]string, error) {\n\treturn x.cache.GetUserConfig(ctx, userID)\n}\n\nfunc (x *clientConfigDatabase) DelUserConfig(ctx context.Context, userID string, keys []string) error {\n\treturn x.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := x.db.Del(ctx, userID, keys); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn x.cache.DeleteUserCache(ctx, []string{userID})\n\t})\n}\n\nfunc (x *clientConfigDatabase) GetUserConfigPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) {\n\treturn x.db.GetPage(ctx, userID, key, pagination)\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\trelationtb \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/db/tx\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/stringutil\"\n)\n\ntype ConversationDatabase interface {\n\t// UpdateUsersConversationField updates the properties of a conversation for specified users.\n\tUpdateUsersConversationField(ctx context.Context, userIDs []string, conversationID string, args map[string]any) error\n\t// CreateConversation creates a batch of new conversations.\n\tCreateConversation(ctx context.Context, conversations []*relationtb.Conversation) error\n\t// SyncPeerUserPrivateConversationTx ensures transactional operation while syncing private conversations between peers.\n\tSyncPeerUserPrivateConversationTx(ctx context.Context, conversation []*relationtb.Conversation) error\n\t// FindConversations retrieves multiple conversations of a user by conversation IDs.\n\tFindConversations(ctx context.Context, ownerUserID string, conversationIDs []string) ([]*relationtb.Conversation, error)\n\t// GetUserAllConversation fetches all conversations of a user on the server.\n\tGetUserAllConversation(ctx context.Context, ownerUserID string) ([]*relationtb.Conversation, error)\n\t// SetUserConversations sets multiple conversation properties for a user, creates new conversations if they do not exist, or updates them otherwise. This operation is atomic.\n\tSetUserConversations(ctx context.Context, ownerUserID string, conversations []*relationtb.Conversation) error\n\t// SetUsersConversationFieldTx updates a specific field for multiple users' conversations, creating new conversations if they do not exist, or updates them otherwise. This operation is\n\t// transactional.\n\tSetUsersConversationFieldTx(ctx context.Context, userIDs []string, conversation *relationtb.Conversation, fieldMap map[string]any) error\n\t// UpdateUserConversations updates all conversations related to a specified user.\n\t// This function does NOT update the user's own conversations but rather the conversations where this user is involved (e.g., other users' conversations referencing this user).\n\tUpdateUserConversations(ctx context.Context, userID string, args map[string]any) error\n\t// CreateGroupChatConversation creates a group chat conversation for the specified group ID and user IDs.\n\tCreateGroupChatConversation(ctx context.Context, groupID string, userIDs []string, conversations *relationtb.Conversation) error\n\t// GetConversationIDs retrieves conversation IDs for a given user.\n\tGetConversationIDs(ctx context.Context, userID string) ([]string, error)\n\t// GetUserConversationIDsHash gets the hash of conversation IDs for a given user.\n\tGetUserConversationIDsHash(ctx context.Context, ownerUserID string) (hash uint64, err error)\n\t// GetAllConversationIDs fetches all conversation IDs.\n\tGetAllConversationIDs(ctx context.Context) ([]string, error)\n\t// GetAllConversationIDsNumber returns the number of all conversation IDs.\n\tGetAllConversationIDsNumber(ctx context.Context) (int64, error)\n\t// PageConversationIDs paginates through conversation IDs based on the specified pagination settings.\n\tPageConversationIDs(ctx context.Context, pagination pagination.Pagination) (conversationIDs []string, err error)\n\t// GetConversationIDsNeedDestruct fetches conversations that need to be destructed based on specific criteria.\n\tGetConversationIDsNeedDestruct(ctx context.Context) ([]*relationtb.Conversation, error)\n\t// GetConversationNotReceiveMessageUserIDs gets user IDs for users in a conversation who have not received messages.\n\tGetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error)\n\t// GetUserAllHasReadSeqs(ctx context.Context, ownerUserID string) (map[string]int64, error)\n\t// FindRecvMsgNotNotifyUserIDs(ctx context.Context, groupID string) ([]string, error)\n\tFindConversationUserVersion(ctx context.Context, userID string, version uint, limit int) (*relationtb.VersionLog, error)\n\tFindMaxConversationUserVersionCache(ctx context.Context, userID string) (*relationtb.VersionLog, error)\n\tGetOwnerConversation(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (int64, []*relationtb.Conversation, error)\n\t// GetNotNotifyConversationIDs gets not notify conversationIDs by userID\n\tGetNotNotifyConversationIDs(ctx context.Context, userID string) ([]string, error)\n\t// GetPinnedConversationIDs gets pinned conversationIDs by userID\n\tGetPinnedConversationIDs(ctx context.Context, userID string) ([]string, error)\n\t// FindRandConversation finds random conversations based on the specified timestamp and limit.\n\tFindRandConversation(ctx context.Context, ts int64, limit int) ([]*relationtb.Conversation, error)\n\n\tDeleteUsersConversations(ctx context.Context, userID string, conversationIDs []string) (err error)\n}\n\nfunc NewConversationDatabase(conversation database.Conversation, cache cache.ConversationCache, tx tx.Tx) ConversationDatabase {\n\treturn &conversationDatabase{\n\t\tconversationDB: conversation,\n\t\tcache:          cache,\n\t\ttx:             tx,\n\t}\n}\n\ntype conversationDatabase struct {\n\tconversationDB database.Conversation\n\tcache          cache.ConversationCache\n\ttx             tx.Tx\n}\n\nfunc (c *conversationDatabase) SetUsersConversationFieldTx(ctx context.Context, userIDs []string, conversation *relationtb.Conversation, fieldMap map[string]any) (err error) {\n\treturn c.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tcache := c.cache.CloneConversationCache()\n\t\tif conversation.GroupID != \"\" {\n\t\t\tcache = cache.DelSuperGroupRecvMsgNotNotifyUserIDs(conversation.GroupID).DelSuperGroupRecvMsgNotNotifyUserIDsHash(conversation.GroupID)\n\t\t}\n\t\thaveUserIDs, err := c.conversationDB.FindUserID(ctx, userIDs, []string{conversation.ConversationID})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(haveUserIDs) > 0 {\n\t\t\t_, err = c.conversationDB.UpdateByMap(ctx, haveUserIDs, conversation.ConversationID, fieldMap)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcache = cache.DelUsersConversation(conversation.ConversationID, haveUserIDs...)\n\t\t\tif _, ok := fieldMap[\"has_read_seq\"]; ok {\n\t\t\t\tfor _, userID := range haveUserIDs {\n\t\t\t\t\tcache = cache.DelUserAllHasReadSeqs(userID, conversation.ConversationID)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif _, ok := fieldMap[\"recv_msg_opt\"]; ok {\n\t\t\t\tcache = cache.DelConversationNotReceiveMessageUserIDs(conversation.ConversationID)\n\t\t\t\tcache = cache.DelConversationNotNotifyMessageUserIDs(userIDs...)\n\t\t\t}\n\t\t\tif _, ok := fieldMap[\"is_pinned\"]; ok {\n\t\t\t\tcache = cache.DelUserPinnedConversations(userIDs...)\n\t\t\t}\n\t\t\tcache = cache.DelConversationVersionUserIDs(haveUserIDs...)\n\t\t}\n\t\tNotUserIDs := stringutil.DifferenceString(haveUserIDs, userIDs)\n\t\tlog.ZDebug(ctx, \"SetUsersConversationFieldTx\", \"NotUserIDs\", NotUserIDs, \"haveUserIDs\", haveUserIDs, \"userIDs\", userIDs)\n\t\tvar conversations []*relationtb.Conversation\n\t\tnow := time.Now()\n\t\tfor _, v := range NotUserIDs {\n\t\t\ttemp := new(relationtb.Conversation)\n\t\t\tif err = datautil.CopyStructFields(temp, conversation); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttemp.OwnerUserID = v\n\t\t\ttemp.CreateTime = now\n\t\t\tconversations = append(conversations, temp)\n\t\t}\n\t\tif len(conversations) > 0 {\n\t\t\terr = c.conversationDB.Create(ctx, conversations)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcache = cache.DelConversationIDs(NotUserIDs...).DelUserConversationIDsHash(NotUserIDs...).DelConversations(conversation.ConversationID, NotUserIDs...)\n\t\t}\n\t\treturn cache.ChainExecDel(ctx)\n\t})\n}\n\nfunc (c *conversationDatabase) UpdateUserConversations(ctx context.Context, userID string, args map[string]any) error {\n\tconversations, err := c.conversationDB.UpdateUserConversations(ctx, userID, args)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcache := c.cache.CloneConversationCache()\n\tfor _, conversation := range conversations {\n\t\tcache = cache.DelUsersConversation(conversation.ConversationID, conversation.OwnerUserID).DelConversationVersionUserIDs(conversation.OwnerUserID)\n\t}\n\treturn cache.ChainExecDel(ctx)\n}\n\nfunc (c *conversationDatabase) UpdateUsersConversationField(ctx context.Context, userIDs []string, conversationID string, args map[string]any) error {\n\t_, err := c.conversationDB.UpdateByMap(ctx, userIDs, conversationID, args)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcache := c.cache.CloneConversationCache()\n\tcache = cache.DelUsersConversation(conversationID, userIDs...).DelConversationVersionUserIDs(userIDs...)\n\tif _, ok := args[\"recv_msg_opt\"]; ok {\n\t\tcache = cache.DelConversationNotReceiveMessageUserIDs(conversationID)\n\t\tcache = cache.DelConversationNotNotifyMessageUserIDs(userIDs...)\n\t}\n\tif _, ok := args[\"is_pinned\"]; ok {\n\t\tcache = cache.DelUserPinnedConversations(userIDs...)\n\t}\n\treturn cache.ChainExecDel(ctx)\n}\n\nfunc (c *conversationDatabase) CreateConversation(ctx context.Context, conversations []*relationtb.Conversation) error {\n\tif err := c.conversationDB.Create(ctx, conversations); err != nil {\n\t\treturn err\n\t}\n\tvar (\n\t\tuserIDs          []string\n\t\tnotNotifyUserIDs []string\n\t\tpinnedUserIDs    []string\n\t)\n\n\tcache := c.cache.CloneConversationCache()\n\tfor _, conversation := range conversations {\n\t\tcache = cache.DelConversations(conversation.OwnerUserID, conversation.ConversationID)\n\t\tcache = cache.DelConversationNotReceiveMessageUserIDs(conversation.ConversationID)\n\t\tuserIDs = append(userIDs, conversation.OwnerUserID)\n\t\tif conversation.RecvMsgOpt == constant.ReceiveNotNotifyMessage {\n\t\t\tnotNotifyUserIDs = append(notNotifyUserIDs, conversation.OwnerUserID)\n\t\t}\n\t\tif conversation.IsPinned {\n\t\t\tpinnedUserIDs = append(pinnedUserIDs, conversation.OwnerUserID)\n\t\t}\n\t}\n\treturn cache.DelConversationIDs(userIDs...).\n\t\tDelUserConversationIDsHash(userIDs...).\n\t\tDelConversationVersionUserIDs(userIDs...).\n\t\tDelConversationNotNotifyMessageUserIDs(notNotifyUserIDs...).\n\t\tDelUserPinnedConversations(pinnedUserIDs...).\n\t\tChainExecDel(ctx)\n}\n\nfunc (c *conversationDatabase) SyncPeerUserPrivateConversationTx(ctx context.Context, conversations []*relationtb.Conversation) error {\n\treturn c.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tcache := c.cache.CloneConversationCache()\n\t\tfor _, conversation := range conversations {\n\t\t\tcache = cache.DelConversationVersionUserIDs(conversation.OwnerUserID, conversation.UserID)\n\t\t\tfor _, v := range [][2]string{{conversation.OwnerUserID, conversation.UserID}, {conversation.UserID, conversation.OwnerUserID}} {\n\t\t\t\townerUserID := v[0]\n\t\t\t\tuserID := v[1]\n\t\t\t\thaveUserIDs, err := c.conversationDB.FindUserID(ctx, []string{ownerUserID}, []string{conversation.ConversationID})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif len(haveUserIDs) > 0 {\n\t\t\t\t\t_, err := c.conversationDB.UpdateByMap(ctx, []string{ownerUserID}, conversation.ConversationID, map[string]any{\"is_private_chat\": conversation.IsPrivateChat})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tcache = cache.DelUsersConversation(conversation.ConversationID, ownerUserID)\n\t\t\t\t} else {\n\t\t\t\t\tnewConversation := *conversation\n\t\t\t\t\tnewConversation.OwnerUserID = ownerUserID\n\t\t\t\t\tnewConversation.UserID = userID\n\t\t\t\t\tnewConversation.ConversationID = conversation.ConversationID\n\t\t\t\t\tnewConversation.IsPrivateChat = conversation.IsPrivateChat\n\t\t\t\t\tif err := c.conversationDB.Create(ctx, []*relationtb.Conversation{&newConversation}); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tcache = cache.DelConversationIDs(ownerUserID).DelUserConversationIDsHash(ownerUserID)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn cache.ChainExecDel(ctx)\n\t})\n}\n\nfunc (c *conversationDatabase) FindConversations(ctx context.Context, ownerUserID string, conversationIDs []string) ([]*relationtb.Conversation, error) {\n\treturn c.cache.GetConversations(ctx, ownerUserID, conversationIDs)\n}\n\nfunc (c *conversationDatabase) GetConversation(ctx context.Context, ownerUserID string, conversationID string) (*relationtb.Conversation, error) {\n\treturn c.cache.GetConversation(ctx, ownerUserID, conversationID)\n}\n\nfunc (c *conversationDatabase) GetUserAllConversation(ctx context.Context, ownerUserID string) ([]*relationtb.Conversation, error) {\n\treturn c.cache.GetUserAllConversations(ctx, ownerUserID)\n}\n\nfunc (c *conversationDatabase) SetUserConversations(ctx context.Context, ownerUserID string, conversations []*relationtb.Conversation) error {\n\treturn c.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tcache := c.cache.CloneConversationCache()\n\t\tcache = cache.DelConversationVersionUserIDs(ownerUserID).\n\t\t\tDelConversationNotNotifyMessageUserIDs(ownerUserID).\n\t\t\tDelUserPinnedConversations(ownerUserID)\n\n\t\tgroupIDs := datautil.Distinct(datautil.Filter(conversations, func(e *relationtb.Conversation) (string, bool) {\n\t\t\treturn e.GroupID, e.GroupID != \"\"\n\t\t}))\n\t\tfor _, groupID := range groupIDs {\n\t\t\tcache = cache.DelSuperGroupRecvMsgNotNotifyUserIDs(groupID).DelSuperGroupRecvMsgNotNotifyUserIDsHash(groupID)\n\t\t}\n\t\tvar conversationIDs []string\n\t\tfor _, conversation := range conversations {\n\t\t\tconversationIDs = append(conversationIDs, conversation.ConversationID)\n\t\t\tcache = cache.DelConversations(conversation.OwnerUserID, conversation.ConversationID)\n\t\t}\n\t\texistConversations, err := c.conversationDB.Find(ctx, ownerUserID, conversationIDs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(existConversations) > 0 {\n\t\t\tfor _, conversation := range conversations {\n\t\t\t\terr = c.conversationDB.Update(ctx, conversation)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tvar existConversationIDs []string\n\t\tfor _, conversation := range existConversations {\n\t\t\texistConversationIDs = append(existConversationIDs, conversation.ConversationID)\n\t\t}\n\n\t\tvar notExistConversations []*relationtb.Conversation\n\t\tfor _, conversation := range conversations {\n\t\t\tif !datautil.Contain(conversation.ConversationID, existConversationIDs...) {\n\t\t\t\tnotExistConversations = append(notExistConversations, conversation)\n\t\t\t}\n\t\t}\n\t\tif len(notExistConversations) > 0 {\n\t\t\terr = c.conversationDB.Create(ctx, notExistConversations)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcache = cache.DelConversationIDs(ownerUserID).\n\t\t\t\tDelUserConversationIDsHash(ownerUserID).\n\t\t\t\tDelConversationNotReceiveMessageUserIDs(datautil.Slice(notExistConversations, func(e *relationtb.Conversation) string { return e.ConversationID })...)\n\t\t}\n\t\treturn cache.ChainExecDel(ctx)\n\t})\n}\n\n// func (c *conversationDatabase) FindRecvMsgNotNotifyUserIDs(ctx context.Context, groupID string) ([]string, error) {\n//\treturn c.cache.GetSuperGroupRecvMsgNotNotifyUserIDs(ctx, groupID)\n//}\n\nfunc (c *conversationDatabase) CreateGroupChatConversation(ctx context.Context, groupID string, userIDs []string, conversation *relationtb.Conversation) error {\n\treturn c.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tcache := c.cache.CloneConversationCache()\n\t\tconversationID := conversation.ConversationID\n\t\texistConversationUserIDs, err := c.conversationDB.FindUserID(ctx, userIDs, []string{conversationID})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnotExistUserIDs := stringutil.DifferenceString(userIDs, existConversationUserIDs)\n\t\tvar conversations []*relationtb.Conversation\n\t\tfor _, v := range notExistUserIDs {\n\t\t\tconversation := relationtb.Conversation{\n\t\t\t\tConversationType: conversation.ConversationType, GroupID: groupID, OwnerUserID: v, ConversationID: conversationID,\n\t\t\t\t// the parameters have default value\n\t\t\t\tRecvMsgOpt: conversation.RecvMsgOpt, IsPinned: conversation.IsPinned, IsPrivateChat: conversation.IsPrivateChat,\n\t\t\t\tBurnDuration: conversation.BurnDuration, GroupAtType: conversation.GroupAtType, AttachedInfo: conversation.AttachedInfo,\n\t\t\t\tEx: conversation.Ex, MaxSeq: conversation.MaxSeq, MinSeq: conversation.MinSeq, CreateTime: conversation.CreateTime,\n\t\t\t\tMsgDestructTime: conversation.MsgDestructTime, IsMsgDestruct: conversation.IsMsgDestruct, LatestMsgDestructTime: conversation.LatestMsgDestructTime,\n\t\t\t}\n\n\t\t\tconversations = append(conversations, &conversation)\n\t\t\tcache = cache.DelConversations(v, conversationID).DelConversationNotReceiveMessageUserIDs(conversationID)\n\t\t}\n\t\tcache = cache.DelConversationIDs(notExistUserIDs...).DelUserConversationIDsHash(notExistUserIDs...)\n\t\tif len(conversations) > 0 {\n\t\t\terr = c.conversationDB.Create(ctx, conversations)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t_, err = c.conversationDB.UpdateByMap(ctx, existConversationUserIDs, conversationID, map[string]any{\"max_seq\": conversation.MaxSeq})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, v := range existConversationUserIDs {\n\t\t\tcache = cache.DelConversations(v, conversationID)\n\t\t}\n\t\treturn cache.ChainExecDel(ctx)\n\t})\n}\n\nfunc (c *conversationDatabase) GetConversationIDs(ctx context.Context, userID string) ([]string, error) {\n\treturn c.cache.GetUserConversationIDs(ctx, userID)\n}\n\nfunc (c *conversationDatabase) GetUserConversationIDsHash(ctx context.Context, ownerUserID string) (hash uint64, err error) {\n\treturn c.cache.GetUserConversationIDsHash(ctx, ownerUserID)\n}\n\nfunc (c *conversationDatabase) GetAllConversationIDs(ctx context.Context) ([]string, error) {\n\treturn c.conversationDB.GetAllConversationIDs(ctx)\n}\n\nfunc (c *conversationDatabase) GetAllConversationIDsNumber(ctx context.Context) (int64, error) {\n\treturn c.conversationDB.GetAllConversationIDsNumber(ctx)\n}\n\nfunc (c *conversationDatabase) PageConversationIDs(ctx context.Context, pagination pagination.Pagination) ([]string, error) {\n\treturn c.conversationDB.PageConversationIDs(ctx, pagination)\n}\n\nfunc (c *conversationDatabase) GetConversationIDsNeedDestruct(ctx context.Context) ([]*relationtb.Conversation, error) {\n\treturn c.conversationDB.GetConversationIDsNeedDestruct(ctx)\n}\n\nfunc (c *conversationDatabase) GetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error) {\n\treturn c.cache.GetConversationNotReceiveMessageUserIDs(ctx, conversationID)\n}\n\nfunc (c *conversationDatabase) FindConversationUserVersion(ctx context.Context, userID string, version uint, limit int) (*relationtb.VersionLog, error) {\n\treturn c.conversationDB.FindConversationUserVersion(ctx, userID, version, limit)\n}\n\nfunc (c *conversationDatabase) FindMaxConversationUserVersionCache(ctx context.Context, userID string) (*relationtb.VersionLog, error) {\n\treturn c.cache.FindMaxConversationUserVersion(ctx, userID)\n}\n\nfunc (c *conversationDatabase) GetOwnerConversation(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (int64, []*relationtb.Conversation, error) {\n\tconversationIDs, err := c.cache.GetUserConversationIDs(ctx, ownerUserID)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\tfindConversationIDs := datautil.Paginate(conversationIDs, int(pagination.GetPageNumber()), int(pagination.GetShowNumber()))\n\tconversations := make([]*relationtb.Conversation, 0, len(findConversationIDs))\n\tfor _, conversationID := range findConversationIDs {\n\t\tconversation, err := c.cache.GetConversation(ctx, ownerUserID, conversationID)\n\t\tif err != nil {\n\t\t\treturn 0, nil, err\n\t\t}\n\t\tconversations = append(conversations, conversation)\n\t}\n\treturn int64(len(conversationIDs)), conversations, nil\n}\n\nfunc (c *conversationDatabase) GetNotNotifyConversationIDs(ctx context.Context, userID string) ([]string, error) {\n\tconversationIDs, err := c.cache.GetUserNotNotifyConversationIDs(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn conversationIDs, nil\n}\n\nfunc (c *conversationDatabase) GetPinnedConversationIDs(ctx context.Context, userID string) ([]string, error) {\n\tconversationIDs, err := c.cache.GetPinnedConversationIDs(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn conversationIDs, nil\n}\n\nfunc (c *conversationDatabase) FindRandConversation(ctx context.Context, ts int64, limit int) ([]*relationtb.Conversation, error) {\n\treturn c.conversationDB.FindRandConversation(ctx, ts, limit)\n}\n\nfunc (c *conversationDatabase) DeleteUsersConversations(ctx context.Context, userID string, conversationIDs []string) (err error) {\n\treturn c.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\terr = c.conversationDB.DeleteUsersConversations(ctx, userID, conversationIDs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcache := c.cache.CloneConversationCache()\n\t\tcache = cache.DelConversations(userID, conversationIDs...).\n\t\t\tDelConversationVersionUserIDs(userID).\n\t\t\tDelConversationIDs(userID).\n\t\t\tDelUserConversationIDsHash(userID).\n\t\t\tDelConversationNotNotifyMessageUserIDs(userID).\n\t\t\tDelUserPinnedConversations(userID)\n\n\t\treturn cache.ChainExecDel(ctx)\n\t})\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage controller // import \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller\"\n"
  },
  {
    "path": "pkg/common/storage/controller/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/db/tx\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\ntype FriendDatabase interface {\n\t// CheckIn checks if user2 is in user1's friend list (inUser1Friends==true) and if user1 is in user2's friend list (inUser2Friends==true)\n\tCheckIn(ctx context.Context, user1, user2 string) (inUser1Friends bool, inUser2Friends bool, err error)\n\n\t// AddFriendRequest adds or updates a friend request\n\tAddFriendRequest(ctx context.Context, fromUserID, toUserID string, reqMsg string, ex string) (err error)\n\n\t// BecomeFriends first checks if the users are already in the friends model; if not, it inserts them as friends\n\tBecomeFriends(ctx context.Context, ownerUserID string, friendUserIDs []string, addSource int32) (err error)\n\n\t// RefuseFriendRequest refuses a friend request\n\tRefuseFriendRequest(ctx context.Context, friendRequest *model.FriendRequest) (err error)\n\n\t// AgreeFriendRequest accepts a friend request\n\tAgreeFriendRequest(ctx context.Context, friendRequest *model.FriendRequest) (err error)\n\n\t// Delete removes a friend or friends from the owner's friend list\n\tDelete(ctx context.Context, ownerUserID string, friendUserIDs []string) (err error)\n\n\t// UpdateRemark updates the remark for a friend\n\tUpdateRemark(ctx context.Context, ownerUserID, friendUserID, remark string) (err error)\n\n\t// PageOwnerFriends retrieves the friend list of ownerUserID with pagination\n\tPageOwnerFriends(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, friends []*model.Friend, err error)\n\n\t// PageInWhoseFriends finds the users who have friendUserID in their friend list with pagination\n\tPageInWhoseFriends(ctx context.Context, friendUserID string, pagination pagination.Pagination) (total int64, friends []*model.Friend, err error)\n\n\t// PageFriendRequestFromMe retrieves the friend requests sent by the user with pagination\n\tPageFriendRequestFromMe(ctx context.Context, userID string, handleResults []int, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error)\n\n\t// PageFriendRequestToMe retrieves the friend requests received by the user with pagination\n\tPageFriendRequestToMe(ctx context.Context, userID string, handleResults []int, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error)\n\n\t// FindFriendsWithError fetches specified friends of a user and returns an error if any do not exist\n\tFindFriendsWithError(ctx context.Context, ownerUserID string, friendUserIDs []string) (friends []*model.Friend, err error)\n\n\t// FindFriendUserIDs retrieves the friend IDs of a user\n\tFindFriendUserIDs(ctx context.Context, ownerUserID string) (friendUserIDs []string, err error)\n\n\t// FindBothFriendRequests finds friend requests sent and received\n\tFindBothFriendRequests(ctx context.Context, fromUserID, toUserID string) (friends []*model.FriendRequest, err error)\n\n\t// UpdateFriends updates fields for friends\n\tUpdateFriends(ctx context.Context, ownerUserID string, friendUserIDs []string, val map[string]any) (err error)\n\n\t//FindSortFriendUserIDs(ctx context.Context, ownerUserID string) ([]string, error)\n\n\tFindFriendIncrVersion(ctx context.Context, ownerUserID string, version uint, limit int) (*model.VersionLog, error)\n\n\tFindMaxFriendVersionCache(ctx context.Context, ownerUserID string) (*model.VersionLog, error)\n\n\tFindFriendUserID(ctx context.Context, friendUserID string) ([]string, error)\n\n\tOwnerIncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error\n\n\tGetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error)\n}\n\ntype friendDatabase struct {\n\tfriend        database.Friend\n\tfriendRequest database.FriendRequest\n\ttx            tx.Tx\n\tcache         cache.FriendCache\n}\n\nfunc NewFriendDatabase(friend database.Friend, friendRequest database.FriendRequest, cache cache.FriendCache, tx tx.Tx) FriendDatabase {\n\treturn &friendDatabase{friend: friend, friendRequest: friendRequest, cache: cache, tx: tx}\n}\n\n// CheckIn verifies if user2 is in user1's friend list (inUser1Friends returns true) and\n// if user1 is in user2's friend list (inUser2Friends returns true).\nfunc (f *friendDatabase) CheckIn(ctx context.Context, userID1, userID2 string) (inUser1Friends bool, inUser2Friends bool, err error) {\n\t// Retrieve friend IDs of userID1 from the cache\n\tuserID1FriendIDs, err := f.cache.GetFriendIDs(ctx, userID1)\n\tif err != nil {\n\t\treturn false, false, err\n\t}\n\n\t// Retrieve friend IDs of userID2 from the cache\n\tuserID2FriendIDs, err := f.cache.GetFriendIDs(ctx, userID2)\n\tif err != nil {\n\t\treturn false, false, err\n\t}\n\n\t// Check if userID2 is in userID1's friend list and vice versa\n\tinUser1Friends = datautil.Contain(userID2, userID1FriendIDs...)\n\tinUser2Friends = datautil.Contain(userID1, userID2FriendIDs...)\n\treturn inUser1Friends, inUser2Friends, nil\n}\n\n// AddFriendRequest adds or updates a friend request.\nfunc (f *friendDatabase) AddFriendRequest(ctx context.Context, fromUserID, toUserID string, reqMsg string, ex string) (err error) {\n\treturn f.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\t_, err := f.friendRequest.Take(ctx, fromUserID, toUserID)\n\t\tswitch {\n\t\tcase err == nil:\n\t\t\tm := make(map[string]any, 1)\n\t\t\tm[\"handle_result\"] = 0\n\t\t\tm[\"handle_msg\"] = \"\"\n\t\t\tm[\"req_msg\"] = reqMsg\n\t\t\tm[\"ex\"] = ex\n\t\t\tm[\"create_time\"] = time.Now()\n\t\t\treturn f.friendRequest.UpdateByMap(ctx, fromUserID, toUserID, m)\n\t\tcase mgo.IsNotFound(err):\n\t\t\treturn f.friendRequest.Create(\n\t\t\t\tctx,\n\t\t\t\t[]*model.FriendRequest{{FromUserID: fromUserID, ToUserID: toUserID, ReqMsg: reqMsg, Ex: ex, CreateTime: time.Now(), HandleTime: time.Unix(0, 0)}},\n\t\t\t)\n\t\tdefault:\n\t\t\treturn err\n\t\t}\n\t})\n}\n\n// (1) First determine whether it is in the friends list (in or out does not return an error) (2) for not in the friends list can be inserted.\nfunc (f *friendDatabase) BecomeFriends(ctx context.Context, ownerUserID string, friendUserIDs []string, addSource int32) (err error) {\n\treturn f.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tcache := f.cache.CloneFriendCache()\n\t\t// user find friends\n\t\tmyFriends, err := f.friend.FindFriends(ctx, ownerUserID, friendUserIDs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\taddOwners, err := f.friend.FindReversalFriends(ctx, ownerUserID, friendUserIDs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\topUserID := mcontext.GetOpUserID(ctx)\n\t\tfriends := make([]*model.Friend, 0, len(friendUserIDs)*2)\n\t\tmyFriendsSet := datautil.SliceSetAny(myFriends, func(friend *model.Friend) string {\n\t\t\treturn friend.FriendUserID\n\t\t})\n\t\taddOwnersSet := datautil.SliceSetAny(addOwners, func(friend *model.Friend) string {\n\t\t\treturn friend.OwnerUserID\n\t\t})\n\t\tnewMyFriendIDs := make([]string, 0, len(friendUserIDs))\n\t\tnewMyOwnerIDs := make([]string, 0, len(friendUserIDs))\n\t\tfor _, userID := range friendUserIDs {\n\t\t\tif ownerUserID == userID {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := myFriendsSet[userID]; !ok {\n\t\t\t\tmyFriendsSet[userID] = struct{}{}\n\t\t\t\tnewMyFriendIDs = append(newMyFriendIDs, userID)\n\t\t\t\tfriends = append(friends, &model.Friend{OwnerUserID: ownerUserID, FriendUserID: userID, AddSource: addSource, OperatorUserID: opUserID})\n\t\t\t}\n\t\t\tif _, ok := addOwnersSet[userID]; !ok {\n\t\t\t\taddOwnersSet[userID] = struct{}{}\n\t\t\t\tnewMyOwnerIDs = append(newMyOwnerIDs, userID)\n\t\t\t\tfriends = append(friends, &model.Friend{OwnerUserID: userID, FriendUserID: ownerUserID, AddSource: addSource, OperatorUserID: opUserID})\n\t\t\t}\n\t\t}\n\t\tif len(friends) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\terr = f.friend.Create(ctx, friends)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcache = cache.DelFriendIDs(ownerUserID).DelMaxFriendVersion(ownerUserID)\n\t\tif len(newMyFriendIDs) > 0 {\n\t\t\tcache = cache.DelFriendIDs(newMyFriendIDs...)\n\t\t\tcache = cache.DelFriends(ownerUserID, newMyFriendIDs).DelMaxFriendVersion(newMyFriendIDs...)\n\t\t}\n\t\tif len(newMyOwnerIDs) > 0 {\n\t\t\tcache = cache.DelFriendIDs(newMyOwnerIDs...)\n\t\t\tcache = cache.DelOwner(ownerUserID, newMyOwnerIDs).DelMaxFriendVersion(newMyOwnerIDs...)\n\t\t}\n\t\treturn cache.ChainExecDel(ctx)\n\t})\n}\n\n// RefuseFriendRequest rejects a friend request. It first checks for an existing, unprocessed request.\n// If no such request exists, it returns an error. Otherwise, it marks the request as refused.\nfunc (f *friendDatabase) RefuseFriendRequest(ctx context.Context, friendRequest *model.FriendRequest) error {\n\t// Attempt to retrieve the friend request from the database.\n\tfr, err := f.friendRequest.Take(ctx, friendRequest.FromUserID, friendRequest.ToUserID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if the friend request has already been handled.\n\tif fr.HandleResult != 0 {\n\t\treturn servererrs.ErrFriendRequestHandled.WrapMsg(\"friend request has already been processed\", \"from\", friendRequest.FromUserID, \"to\", friendRequest.ToUserID)\n\t}\n\n\t// Log the action of refusing the friend request for debugging and auditing purposes.\n\tlog.ZDebug(ctx, \"Refusing friend request\", map[string]interface{}{\n\t\t\"DB_FriendRequest\":  fr,\n\t\t\"Arg_FriendRequest\": friendRequest,\n\t})\n\n\t// Mark the friend request as refused and update the handle time.\n\tfriendRequest.HandleResult = constant.FriendResponseRefuse\n\tfriendRequest.HandleTime = time.Now()\n\tif err := f.friendRequest.Update(ctx, friendRequest); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// AgreeFriendRequest accepts a friend request. It first checks for an existing, unprocessed request.\nfunc (f *friendDatabase) AgreeFriendRequest(ctx context.Context, friendRequest *model.FriendRequest) (err error) {\n\treturn f.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tnow := time.Now()\n\t\tfr, err := f.friendRequest.Take(ctx, friendRequest.FromUserID, friendRequest.ToUserID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fr.HandleResult != 0 {\n\t\t\treturn errs.ErrArgs.WrapMsg(\"the friend request has been processed\")\n\t\t}\n\t\tfriendRequest.HandlerUserID = mcontext.GetOpUserID(ctx)\n\t\tfriendRequest.HandleResult = constant.FriendResponseAgree\n\t\tfriendRequest.HandleTime = now\n\t\terr = f.friendRequest.Update(ctx, friendRequest)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfr2, err := f.friendRequest.Take(ctx, friendRequest.ToUserID, friendRequest.FromUserID)\n\t\tif err == nil && fr2.HandleResult == constant.FriendResponseNotHandle {\n\t\t\tfr2.HandlerUserID = mcontext.GetOpUserID(ctx)\n\t\t\tfr2.HandleResult = constant.FriendResponseAgree\n\t\t\tfr2.HandleTime = now\n\t\t\terr = f.friendRequest.Update(ctx, fr2)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else if err != nil && (!mgo.IsNotFound(err)) {\n\t\t\treturn err\n\t\t}\n\n\t\texists, err := f.friend.FindUserState(ctx, friendRequest.FromUserID, friendRequest.ToUserID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\texistsMap := datautil.SliceSet(datautil.Slice(exists, func(friend *model.Friend) [2]string {\n\t\t\treturn [...]string{friend.OwnerUserID, friend.FriendUserID} // My - Friend\n\t\t}))\n\t\tvar adds []*model.Friend\n\t\tif _, ok := existsMap[[...]string{friendRequest.ToUserID, friendRequest.FromUserID}]; !ok { // My - Friend\n\t\t\tadds = append(\n\t\t\t\tadds,\n\t\t\t\t&model.Friend{\n\t\t\t\t\tOwnerUserID:    friendRequest.ToUserID,\n\t\t\t\t\tFriendUserID:   friendRequest.FromUserID,\n\t\t\t\t\tAddSource:      int32(constant.BecomeFriendByApply),\n\t\t\t\t\tOperatorUserID: friendRequest.FromUserID,\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t\tif _, ok := existsMap[[...]string{friendRequest.FromUserID, friendRequest.ToUserID}]; !ok { // My - Friend\n\t\t\tadds = append(\n\t\t\t\tadds,\n\t\t\t\t&model.Friend{\n\t\t\t\t\tOwnerUserID:    friendRequest.FromUserID,\n\t\t\t\t\tFriendUserID:   friendRequest.ToUserID,\n\t\t\t\t\tAddSource:      int32(constant.BecomeFriendByApply),\n\t\t\t\t\tOperatorUserID: friendRequest.FromUserID,\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t\tif len(adds) > 0 {\n\t\t\tif err := f.friend.Create(ctx, adds); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn f.cache.DelFriendIDs(friendRequest.ToUserID, friendRequest.FromUserID).DelMaxFriendVersion(friendRequest.ToUserID, friendRequest.FromUserID).ChainExecDel(ctx)\n\t})\n}\n\n// Delete removes a friend relationship. It is assumed that the external caller has verified the friendship status.\nfunc (f *friendDatabase) Delete(ctx context.Context, ownerUserID string, friendUserIDs []string) (err error) {\n\tif err := f.friend.Delete(ctx, ownerUserID, friendUserIDs); err != nil {\n\t\treturn err\n\t}\n\tuserIds := append(friendUserIDs, ownerUserID)\n\treturn f.cache.DelFriendIDs(userIds...).DelMaxFriendVersion(userIds...).ChainExecDel(ctx)\n}\n\n// UpdateRemark updates the remark for a friend. Zero value for remark is also supported.\nfunc (f *friendDatabase) UpdateRemark(ctx context.Context, ownerUserID, friendUserID, remark string) (err error) {\n\tif err := f.friend.UpdateRemark(ctx, ownerUserID, friendUserID, remark); err != nil {\n\t\treturn err\n\t}\n\treturn f.cache.DelFriend(ownerUserID, friendUserID).DelMaxFriendVersion(ownerUserID).ChainExecDel(ctx)\n}\n\n// PageOwnerFriends retrieves the list of friends for the ownerUserID. It does not return an error if the result is empty.\nfunc (f *friendDatabase) PageOwnerFriends(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, friends []*model.Friend, err error) {\n\treturn f.friend.FindOwnerFriends(ctx, ownerUserID, pagination)\n}\n\n// PageInWhoseFriends identifies in whose friend lists the friendUserID appears.\nfunc (f *friendDatabase) PageInWhoseFriends(ctx context.Context, friendUserID string, pagination pagination.Pagination) (total int64, friends []*model.Friend, err error) {\n\treturn f.friend.FindInWhoseFriends(ctx, friendUserID, pagination)\n}\n\n// PageFriendRequestFromMe retrieves friend requests sent by me. It does not return an error if the result is empty.\nfunc (f *friendDatabase) PageFriendRequestFromMe(ctx context.Context, userID string, handleResults []int, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) {\n\treturn f.friendRequest.FindFromUserID(ctx, userID, handleResults, pagination)\n}\n\n// PageFriendRequestToMe retrieves friend requests received by me. It does not return an error if the result is empty.\nfunc (f *friendDatabase) PageFriendRequestToMe(ctx context.Context, userID string, handleResults []int, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) {\n\treturn f.friendRequest.FindToUserID(ctx, userID, handleResults, pagination)\n}\n\n// FindFriendsWithError retrieves specified friends' information for ownerUserID. Returns an error if any friend does not exist.\nfunc (f *friendDatabase) FindFriendsWithError(ctx context.Context, ownerUserID string, friendUserIDs []string) (friends []*model.Friend, err error) {\n\tfriends, err = f.friend.FindFriends(ctx, ownerUserID, friendUserIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn friends, nil\n}\n\nfunc (f *friendDatabase) FindFriendUserIDs(ctx context.Context, ownerUserID string) (friendUserIDs []string, err error) {\n\treturn f.cache.GetFriendIDs(ctx, ownerUserID)\n}\n\nfunc (f *friendDatabase) FindBothFriendRequests(ctx context.Context, fromUserID, toUserID string) (friends []*model.FriendRequest, err error) {\n\treturn f.friendRequest.FindBothFriendRequests(ctx, fromUserID, toUserID)\n}\nfunc (f *friendDatabase) UpdateFriends(ctx context.Context, ownerUserID string, friendUserIDs []string, val map[string]any) (err error) {\n\tif len(val) == 0 {\n\t\treturn nil\n\t}\n\treturn f.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := f.friend.UpdateFriends(ctx, ownerUserID, friendUserIDs, val); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn f.cache.DelFriends(ownerUserID, friendUserIDs).DelMaxFriendVersion(ownerUserID).ChainExecDel(ctx)\n\t})\n}\n\n//func (f *friendDatabase) FindSortFriendUserIDs(ctx context.Context, ownerUserID string) ([]string, error) {\n//\treturn f.cache.FindSortFriendUserIDs(ctx, ownerUserID)\n//}\n\nfunc (f *friendDatabase) FindFriendIncrVersion(ctx context.Context, ownerUserID string, version uint, limit int) (*model.VersionLog, error) {\n\treturn f.friend.FindIncrVersion(ctx, ownerUserID, version, limit)\n}\n\nfunc (f *friendDatabase) FindMaxFriendVersionCache(ctx context.Context, ownerUserID string) (*model.VersionLog, error) {\n\treturn f.cache.FindMaxFriendVersion(ctx, ownerUserID)\n}\n\nfunc (f *friendDatabase) FindFriendUserID(ctx context.Context, friendUserID string) ([]string, error) {\n\treturn f.friend.FindFriendUserID(ctx, friendUserID)\n}\n\n//func (f *friendDatabase) SearchFriend(ctx context.Context, ownerUserID, keyword string, pagination pagination.Pagination) (int64, []*model.Friend, error) {\n//\treturn f.friend.SearchFriend(ctx, ownerUserID, keyword, pagination)\n//}\n\nfunc (f *friendDatabase) OwnerIncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error {\n\tif err := f.friend.IncrVersion(ctx, ownerUserID, friendUserIDs, state); err != nil {\n\t\treturn err\n\t}\n\treturn f.cache.DelMaxFriendVersion(ownerUserID).ChainExecDel(ctx)\n}\n\nfunc (f *friendDatabase) GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) {\n\treturn f.friendRequest.GetUnhandledCount(ctx, userID, ts)\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\tredis2 \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/common\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/db/tx\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype GroupDatabase interface {\n\t// CreateGroup creates new groups along with their members.\n\tCreateGroup(ctx context.Context, groups []*model.Group, groupMembers []*model.GroupMember) error\n\t// TakeGroup retrieves a single group by its ID.\n\tTakeGroup(ctx context.Context, groupID string) (group *model.Group, err error)\n\t// FindGroup retrieves multiple groups by their IDs.\n\tFindGroup(ctx context.Context, groupIDs []string) (groups []*model.Group, err error)\n\t// SearchGroup searches for groups based on a keyword and pagination settings, returns total count and groups.\n\tSearchGroup(ctx context.Context, keyword string, pagination pagination.Pagination) (int64, []*model.Group, error)\n\t// UpdateGroup updates the properties of a group identified by its ID.\n\tUpdateGroup(ctx context.Context, groupID string, data map[string]any) error\n\t// DismissGroup disbands a group and optionally removes its members based on the deleteMember flag.\n\tDismissGroup(ctx context.Context, groupID string, deleteMember bool) error\n\n\t// TakeGroupMember retrieves a specific group member by group ID and user ID.\n\tTakeGroupMember(ctx context.Context, groupID string, userID string) (groupMember *model.GroupMember, err error)\n\t// TakeGroupOwner retrieves the owner of a group by group ID.\n\tTakeGroupOwner(ctx context.Context, groupID string) (*model.GroupMember, error)\n\t// FindGroupMembers retrieves members of a group filtered by user IDs.\n\tFindGroupMembers(ctx context.Context, groupID string, userIDs []string) (groupMembers []*model.GroupMember, err error)\n\t// FindGroupMemberUser retrieves groups that a user is a member of, filtered by group IDs.\n\tFindGroupMemberUser(ctx context.Context, groupIDs []string, userID string) (groupMembers []*model.GroupMember, err error)\n\t// FindGroupMemberRoleLevels retrieves group members filtered by their role levels within a group.\n\tFindGroupMemberRoleLevels(ctx context.Context, groupID string, roleLevels []int32) (groupMembers []*model.GroupMember, err error)\n\t// FindGroupMemberAll retrieves all members of a group.\n\tFindGroupMemberAll(ctx context.Context, groupID string) (groupMembers []*model.GroupMember, err error)\n\t// FindGroupsOwner retrieves the owners for multiple groups.\n\tFindGroupsOwner(ctx context.Context, groupIDs []string) ([]*model.GroupMember, error)\n\t// FindGroupMemberUserID retrieves the user IDs of all members in a group.\n\tFindGroupMemberUserID(ctx context.Context, groupID string) ([]string, error)\n\t// FindGroupMemberNum retrieves the number of members in a group.\n\tFindGroupMemberNum(ctx context.Context, groupID string) (uint32, error)\n\t// FindUserManagedGroupID retrieves group IDs managed by a user.\n\tFindUserManagedGroupID(ctx context.Context, userID string) (groupIDs []string, err error)\n\t// PageGroupRequest paginates through group requests for specified groups.\n\tPageGroupRequest(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (int64, []*model.GroupRequest, error)\n\t// GetGroupRoleLevelMemberIDs retrieves user IDs of group members with a specific role level.\n\tGetGroupRoleLevelMemberIDs(ctx context.Context, groupID string, roleLevel int32) ([]string, error)\n\n\t// PageGetJoinGroup paginates through groups that a user has joined.\n\tPageGetJoinGroup(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, totalGroupMembers []*model.GroupMember, err error)\n\t// PageGetGroupMember paginates through members of a group.\n\tPageGetGroupMember(ctx context.Context, groupID string, pagination pagination.Pagination) (total int64, totalGroupMembers []*model.GroupMember, err error)\n\t// SearchGroupMember searches for group members based on a keyword, group ID, and pagination settings.\n\tSearchGroupMember(ctx context.Context, keyword string, groupID string, pagination pagination.Pagination) (int64, []*model.GroupMember, error)\n\t// HandlerGroupRequest processes a group join request with a specified result.\n\tHandlerGroupRequest(ctx context.Context, groupID string, userID string, handledMsg string, handleResult int32, member *model.GroupMember) error\n\t// DeleteGroupMember removes specified users from a group.\n\tDeleteGroupMember(ctx context.Context, groupID string, userIDs []string) error\n\t// MapGroupMemberUserID maps group IDs to their members' simplified user IDs.\n\tMapGroupMemberUserID(ctx context.Context, groupIDs []string) (map[string]*common.GroupSimpleUserID, error)\n\t// MapGroupMemberNum maps group IDs to their member count.\n\tMapGroupMemberNum(ctx context.Context, groupIDs []string) (map[string]uint32, error)\n\t// TransferGroupOwner transfers the ownership of a group to another user.\n\tTransferGroupOwner(ctx context.Context, groupID string, oldOwnerUserID, newOwnerUserID string, roleLevel int32) error\n\t// UpdateGroupMember updates properties of a group member.\n\tUpdateGroupMember(ctx context.Context, groupID string, userID string, data map[string]any) error\n\t// UpdateGroupMembers batch updates properties of group members.\n\tUpdateGroupMembers(ctx context.Context, data []*common.BatchUpdateGroupMember) error\n\n\t// CreateGroupRequest creates new group join requests.\n\tCreateGroupRequest(ctx context.Context, requests []*model.GroupRequest) error\n\t// TakeGroupRequest retrieves a specific group join request.\n\tTakeGroupRequest(ctx context.Context, groupID string, userID string) (*model.GroupRequest, error)\n\t// FindGroupRequests retrieves multiple group join requests.\n\tFindGroupRequests(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupRequest, error)\n\t// PageGroupRequestUser paginates through group join requests made by a user.\n\tPageGroupRequestUser(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (int64, []*model.GroupRequest, error)\n\n\t// CountTotal counts the total number of groups as of a certain date.\n\tCountTotal(ctx context.Context, before *time.Time) (count int64, err error)\n\t// CountRangeEverydayTotal counts the daily group creation total within a specified date range.\n\tCountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error)\n\t// DeleteGroupMemberHash deletes the hash entries for group members in specified groups.\n\tDeleteGroupMemberHash(ctx context.Context, groupIDs []string) error\n\n\tFindMemberIncrVersion(ctx context.Context, groupID string, version uint, limit int) (*model.VersionLog, error)\n\tBatchFindMemberIncrVersion(ctx context.Context, groupIDs []string, versions []uint64, limits []int) (map[string]*model.VersionLog, error)\n\tFindJoinIncrVersion(ctx context.Context, userID string, version uint, limit int) (*model.VersionLog, error)\n\tMemberGroupIncrVersion(ctx context.Context, groupID string, userIDs []string, state int32) error\n\n\t//FindSortGroupMemberUserIDs(ctx context.Context, groupID string) ([]string, error)\n\t//FindSortJoinGroupIDs(ctx context.Context, userID string) ([]string, error)\n\n\tFindMaxGroupMemberVersionCache(ctx context.Context, groupID string) (*model.VersionLog, error)\n\tBatchFindMaxGroupMemberVersionCache(ctx context.Context, groupIDs []string) (map[string]*model.VersionLog, error)\n\tFindMaxJoinGroupVersionCache(ctx context.Context, userID string) (*model.VersionLog, error)\n\n\tSearchJoinGroup(ctx context.Context, userID string, keyword string, pagination pagination.Pagination) (int64, []*model.Group, error)\n\n\tFindJoinGroupID(ctx context.Context, userID string) ([]string, error)\n\n\tGetGroupApplicationUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error)\n}\n\nfunc NewGroupDatabase(\n\trdb redis.UniversalClient,\n\tlocalCache *config.LocalCache,\n\tgroupDB database.Group,\n\tgroupMemberDB database.GroupMember,\n\tgroupRequestDB database.GroupRequest,\n\tctxTx tx.Tx,\n\tgroupHash cache.GroupHash,\n) GroupDatabase {\n\treturn &groupDatabase{\n\t\tgroupDB:        groupDB,\n\t\tgroupMemberDB:  groupMemberDB,\n\t\tgroupRequestDB: groupRequestDB,\n\t\tctxTx:          ctxTx,\n\t\tcache:          redis2.NewGroupCacheRedis(rdb, localCache, groupDB, groupMemberDB, groupRequestDB, groupHash),\n\t}\n}\n\ntype groupDatabase struct {\n\tgroupDB        database.Group\n\tgroupMemberDB  database.GroupMember\n\tgroupRequestDB database.GroupRequest\n\tctxTx          tx.Tx\n\tcache          cache.GroupCache\n}\n\nfunc (g *groupDatabase) FindJoinGroupID(ctx context.Context, userID string) ([]string, error) {\n\treturn g.cache.GetJoinedGroupIDs(ctx, userID)\n}\n\nfunc (g *groupDatabase) FindGroupMembers(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupMember, error) {\n\treturn g.cache.GetGroupMembersInfo(ctx, groupID, userIDs)\n}\n\nfunc (g *groupDatabase) FindGroupMemberUser(ctx context.Context, groupIDs []string, userID string) ([]*model.GroupMember, error) {\n\treturn g.cache.FindGroupMemberUser(ctx, groupIDs, userID)\n}\n\nfunc (g *groupDatabase) FindGroupMemberRoleLevels(ctx context.Context, groupID string, roleLevels []int32) ([]*model.GroupMember, error) {\n\treturn g.cache.GetGroupRolesLevelMemberInfo(ctx, groupID, roleLevels)\n}\n\nfunc (g *groupDatabase) FindGroupMemberAll(ctx context.Context, groupID string) ([]*model.GroupMember, error) {\n\treturn g.cache.GetAllGroupMembersInfo(ctx, groupID)\n}\n\nfunc (g *groupDatabase) FindGroupsOwner(ctx context.Context, groupIDs []string) ([]*model.GroupMember, error) {\n\treturn g.cache.GetGroupsOwner(ctx, groupIDs)\n}\n\nfunc (g *groupDatabase) GetGroupRoleLevelMemberIDs(ctx context.Context, groupID string, roleLevel int32) ([]string, error) {\n\treturn g.cache.GetGroupRoleLevelMemberIDs(ctx, groupID, roleLevel)\n}\n\nfunc (g *groupDatabase) CreateGroup(ctx context.Context, groups []*model.Group, groupMembers []*model.GroupMember) error {\n\tif len(groups)+len(groupMembers) == 0 {\n\t\treturn nil\n\t}\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tc := g.cache.CloneGroupCache()\n\t\tif len(groups) > 0 {\n\t\t\tif err := g.groupDB.Create(ctx, groups); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor _, group := range groups {\n\t\t\t\tc = c.DelGroupsInfo(group.GroupID).\n\t\t\t\t\tDelGroupMembersHash(group.GroupID).\n\t\t\t\t\tDelGroupsMemberNum(group.GroupID).\n\t\t\t\t\tDelGroupMemberIDs(group.GroupID).\n\t\t\t\t\tDelGroupAllRoleLevel(group.GroupID).\n\t\t\t\t\tDelMaxGroupMemberVersion(group.GroupID)\n\t\t\t}\n\t\t}\n\t\tif len(groupMembers) > 0 {\n\t\t\tif err := g.groupMemberDB.Create(ctx, groupMembers); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor _, groupMember := range groupMembers {\n\t\t\t\tc = c.DelGroupMembersHash(groupMember.GroupID).\n\t\t\t\t\tDelGroupsMemberNum(groupMember.GroupID).\n\t\t\t\t\tDelGroupMemberIDs(groupMember.GroupID).\n\t\t\t\t\tDelJoinedGroupID(groupMember.UserID).\n\t\t\t\t\tDelGroupMembersInfo(groupMember.GroupID, groupMember.UserID).\n\t\t\t\t\tDelGroupAllRoleLevel(groupMember.GroupID).\n\t\t\t\t\tDelMaxJoinGroupVersion(groupMember.UserID).\n\t\t\t\t\tDelMaxGroupMemberVersion(groupMember.GroupID)\n\t\t\t}\n\t\t}\n\t\treturn c.ChainExecDel(ctx)\n\t})\n}\n\nfunc (g *groupDatabase) FindGroupMemberUserID(ctx context.Context, groupID string) ([]string, error) {\n\treturn g.cache.GetGroupMemberIDs(ctx, groupID)\n}\n\nfunc (g *groupDatabase) FindGroupMemberNum(ctx context.Context, groupID string) (uint32, error) {\n\tnum, err := g.cache.GetGroupMemberNum(ctx, groupID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn uint32(num), nil\n}\n\nfunc (g *groupDatabase) TakeGroup(ctx context.Context, groupID string) (*model.Group, error) {\n\treturn g.cache.GetGroupInfo(ctx, groupID)\n}\n\nfunc (g *groupDatabase) FindGroup(ctx context.Context, groupIDs []string) ([]*model.Group, error) {\n\treturn g.cache.GetGroupsInfo(ctx, groupIDs)\n}\n\nfunc (g *groupDatabase) SearchGroup(ctx context.Context, keyword string, pagination pagination.Pagination) (int64, []*model.Group, error) {\n\treturn g.groupDB.Search(ctx, keyword, pagination)\n}\n\nfunc (g *groupDatabase) UpdateGroup(ctx context.Context, groupID string, data map[string]any) error {\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := g.groupDB.UpdateMap(ctx, groupID, data); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := g.groupMemberDB.MemberGroupIncrVersion(ctx, groupID, []string{\"\"}, model.VersionStateUpdate); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn g.cache.CloneGroupCache().DelGroupsInfo(groupID).DelMaxGroupMemberVersion(groupID).ChainExecDel(ctx)\n\t})\n}\n\nfunc (g *groupDatabase) DismissGroup(ctx context.Context, groupID string, deleteMember bool) error {\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tc := g.cache.CloneGroupCache()\n\t\tif err := g.groupDB.UpdateStatus(ctx, groupID, constant.GroupStatusDismissed); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif deleteMember {\n\t\t\tuserIDs, err := g.cache.GetGroupMemberIDs(ctx, groupID)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := g.groupMemberDB.Delete(ctx, groupID, nil); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tc = c.DelJoinedGroupID(userIDs...).\n\t\t\t\tDelGroupMemberIDs(groupID).\n\t\t\t\tDelGroupsMemberNum(groupID).\n\t\t\t\tDelGroupMembersHash(groupID).\n\t\t\t\tDelGroupAllRoleLevel(groupID).\n\t\t\t\tDelGroupMembersInfo(groupID, userIDs...).\n\t\t\t\tDelMaxGroupMemberVersion(groupID).\n\t\t\t\tDelMaxJoinGroupVersion(userIDs...)\n\t\t\tfor _, userID := range userIDs {\n\t\t\t\tif err := g.groupMemberDB.JoinGroupIncrVersion(ctx, userID, []string{groupID}, model.VersionStateDelete); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif err := g.groupMemberDB.MemberGroupIncrVersion(ctx, groupID, []string{\"\"}, model.VersionStateUpdate); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tc = c.DelMaxGroupMemberVersion(groupID)\n\t\t}\n\t\treturn c.DelGroupsInfo(groupID).ChainExecDel(ctx)\n\t})\n}\n\nfunc (g *groupDatabase) TakeGroupMember(ctx context.Context, groupID string, userID string) (*model.GroupMember, error) {\n\treturn g.cache.GetGroupMemberInfo(ctx, groupID, userID)\n}\n\nfunc (g *groupDatabase) TakeGroupOwner(ctx context.Context, groupID string) (*model.GroupMember, error) {\n\treturn g.cache.GetGroupOwner(ctx, groupID)\n}\n\nfunc (g *groupDatabase) FindUserManagedGroupID(ctx context.Context, userID string) (groupIDs []string, err error) {\n\treturn g.groupMemberDB.FindUserManagedGroupID(ctx, userID)\n}\n\nfunc (g *groupDatabase) PageGroupRequest(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) {\n\treturn g.groupRequestDB.PageGroup(ctx, groupIDs, handleResults, pagination)\n}\n\nfunc (g *groupDatabase) PageGetJoinGroup(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, totalGroupMembers []*model.GroupMember, err error) {\n\tgroupIDs, err := g.cache.GetJoinedGroupIDs(ctx, userID)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\tfor _, groupID := range datautil.Paginate(groupIDs, int(pagination.GetPageNumber()), int(pagination.GetShowNumber())) {\n\t\tgroupMembers, err := g.cache.GetGroupMembersInfo(ctx, groupID, []string{userID})\n\t\tif err != nil {\n\t\t\treturn 0, nil, err\n\t\t}\n\t\ttotalGroupMembers = append(totalGroupMembers, groupMembers...)\n\t}\n\treturn int64(len(groupIDs)), totalGroupMembers, nil\n}\n\nfunc (g *groupDatabase) PageGetGroupMember(ctx context.Context, groupID string, pagination pagination.Pagination) (total int64, totalGroupMembers []*model.GroupMember, err error) {\n\tgroupMemberIDs, err := g.cache.GetGroupMemberIDs(ctx, groupID)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\tpageIDs := datautil.Paginate(groupMemberIDs, int(pagination.GetPageNumber()), int(pagination.GetShowNumber()))\n\tif len(pageIDs) == 0 {\n\t\treturn int64(len(groupMemberIDs)), nil, nil\n\t}\n\tmembers, err := g.cache.GetGroupMembersInfo(ctx, groupID, pageIDs)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\treturn int64(len(groupMemberIDs)), members, nil\n}\n\nfunc (g *groupDatabase) SearchGroupMember(ctx context.Context, keyword string, groupID string, pagination pagination.Pagination) (int64, []*model.GroupMember, error) {\n\treturn g.groupMemberDB.SearchMember(ctx, keyword, groupID, pagination)\n}\n\nfunc (g *groupDatabase) HandlerGroupRequest(ctx context.Context, groupID string, userID string, handledMsg string, handleResult int32, member *model.GroupMember) error {\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := g.groupRequestDB.UpdateHandler(ctx, groupID, userID, handledMsg, handleResult); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif member != nil {\n\t\t\tc := g.cache.CloneGroupCache()\n\t\t\tif err := g.groupMemberDB.Create(ctx, []*model.GroupMember{member}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tc = c.DelGroupMembersHash(groupID).\n\t\t\t\tDelGroupMembersInfo(groupID, member.UserID).\n\t\t\t\tDelGroupMemberIDs(groupID).\n\t\t\t\tDelGroupsMemberNum(groupID).\n\t\t\t\tDelJoinedGroupID(member.UserID).\n\t\t\t\tDelGroupRoleLevel(groupID, []int32{member.RoleLevel}).\n\t\t\t\tDelMaxJoinGroupVersion(userID).\n\t\t\t\tDelMaxGroupMemberVersion(groupID)\n\t\t\tif err := c.ChainExecDel(ctx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (g *groupDatabase) DeleteGroupMember(ctx context.Context, groupID string, userIDs []string) error {\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := g.groupMemberDB.Delete(ctx, groupID, userIDs); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc := g.cache.CloneGroupCache()\n\t\treturn c.DelGroupMembersHash(groupID).\n\t\t\tDelGroupMemberIDs(groupID).\n\t\t\tDelGroupsMemberNum(groupID).\n\t\t\tDelJoinedGroupID(userIDs...).\n\t\t\tDelGroupMembersInfo(groupID, userIDs...).\n\t\t\tDelGroupAllRoleLevel(groupID).\n\t\t\tDelMaxGroupMemberVersion(groupID).\n\t\t\tDelMaxJoinGroupVersion(userIDs...).\n\t\t\tChainExecDel(ctx)\n\t})\n}\n\nfunc (g *groupDatabase) MapGroupMemberUserID(ctx context.Context, groupIDs []string) (map[string]*common.GroupSimpleUserID, error) {\n\treturn g.cache.GetGroupMemberHashMap(ctx, groupIDs)\n}\n\nfunc (g *groupDatabase) MapGroupMemberNum(ctx context.Context, groupIDs []string) (m map[string]uint32, err error) {\n\tm = make(map[string]uint32)\n\tfor _, groupID := range groupIDs {\n\t\tnum, err := g.cache.GetGroupMemberNum(ctx, groupID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tm[groupID] = uint32(num)\n\t}\n\treturn m, nil\n}\n\nfunc (g *groupDatabase) TransferGroupOwner(ctx context.Context, groupID string, oldOwnerUserID, newOwnerUserID string, roleLevel int32) error {\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := g.groupMemberDB.UpdateUserRoleLevels(ctx, groupID, oldOwnerUserID, roleLevel, newOwnerUserID, constant.GroupOwner); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc := g.cache.CloneGroupCache()\n\t\treturn c.DelGroupMembersInfo(groupID, oldOwnerUserID, newOwnerUserID).\n\t\t\tDelGroupAllRoleLevel(groupID).\n\t\t\tDelGroupMembersHash(groupID).\n\t\t\tDelMaxGroupMemberVersion(groupID).\n\t\t\tDelGroupMemberIDs(groupID).\n\t\t\tChainExecDel(ctx)\n\t})\n}\n\nfunc (g *groupDatabase) UpdateGroupMember(ctx context.Context, groupID string, userID string, data map[string]any) error {\n\tif len(data) == 0 {\n\t\treturn nil\n\t}\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := g.groupMemberDB.Update(ctx, groupID, userID, data); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc := g.cache.CloneGroupCache()\n\t\tc = c.DelGroupMembersInfo(groupID, userID)\n\t\tif g.groupMemberDB.IsUpdateRoleLevel(data) {\n\t\t\tc = c.DelGroupAllRoleLevel(groupID).DelGroupMemberIDs(groupID)\n\t\t}\n\t\tc = c.DelMaxGroupMemberVersion(groupID)\n\t\treturn c.ChainExecDel(ctx)\n\t})\n}\n\nfunc (g *groupDatabase) UpdateGroupMembers(ctx context.Context, data []*common.BatchUpdateGroupMember) error {\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tc := g.cache.CloneGroupCache()\n\t\tfor _, item := range data {\n\t\t\tif err := g.groupMemberDB.Update(ctx, item.GroupID, item.UserID, item.Map); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif g.groupMemberDB.IsUpdateRoleLevel(item.Map) {\n\t\t\t\tc = c.DelGroupAllRoleLevel(item.GroupID).DelGroupMemberIDs(item.GroupID)\n\t\t\t}\n\t\t\tc = c.DelGroupMembersInfo(item.GroupID, item.UserID).DelMaxGroupMemberVersion(item.GroupID).DelGroupMembersHash(item.GroupID)\n\t\t}\n\t\treturn c.ChainExecDel(ctx)\n\t})\n}\n\nfunc (g *groupDatabase) CreateGroupRequest(ctx context.Context, requests []*model.GroupRequest) error {\n\treturn g.ctxTx.Transaction(ctx, func(ctx context.Context) error {\n\t\tfor _, request := range requests {\n\t\t\tif err := g.groupRequestDB.Delete(ctx, request.GroupID, request.UserID); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn g.groupRequestDB.Create(ctx, requests)\n\t})\n}\n\nfunc (g *groupDatabase) TakeGroupRequest(ctx context.Context, groupID string, userID string) (*model.GroupRequest, error) {\n\treturn g.groupRequestDB.Take(ctx, groupID, userID)\n}\n\nfunc (g *groupDatabase) PageGroupRequestUser(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) {\n\treturn g.groupRequestDB.Page(ctx, userID, groupIDs, handleResults, pagination)\n}\n\nfunc (g *groupDatabase) CountTotal(ctx context.Context, before *time.Time) (count int64, err error) {\n\treturn g.groupDB.CountTotal(ctx, before)\n}\n\nfunc (g *groupDatabase) CountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error) {\n\treturn g.groupDB.CountRangeEverydayTotal(ctx, start, end)\n}\n\nfunc (g *groupDatabase) FindGroupRequests(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupRequest, error) {\n\treturn g.groupRequestDB.FindGroupRequests(ctx, groupID, userIDs)\n}\n\nfunc (g *groupDatabase) DeleteGroupMemberHash(ctx context.Context, groupIDs []string) error {\n\tif len(groupIDs) == 0 {\n\t\treturn nil\n\t}\n\tc := g.cache.CloneGroupCache()\n\tfor _, groupID := range groupIDs {\n\t\tc = c.DelGroupMembersHash(groupID)\n\t}\n\treturn c.ChainExecDel(ctx)\n}\n\nfunc (g *groupDatabase) FindMemberIncrVersion(ctx context.Context, groupID string, version uint, limit int) (*model.VersionLog, error) {\n\treturn g.groupMemberDB.FindMemberIncrVersion(ctx, groupID, version, limit)\n}\n\nfunc (g *groupDatabase) BatchFindMemberIncrVersion(ctx context.Context, groupIDs []string, versions []uint64, limits []int) (map[string]*model.VersionLog, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn nil, errs.Wrap(errs.New(\"groupIDs is nil.\"))\n\t}\n\n\t// convert []uint64 to []uint\n\tvar uintVersions []uint\n\tfor _, version := range versions {\n\t\tuintVersions = append(uintVersions, uint(version))\n\t}\n\n\tversionLogs, err := g.groupMemberDB.BatchFindMemberIncrVersion(ctx, groupIDs, uintVersions, limits)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\n\tgroupMemberIncrVersionsMap := datautil.SliceToMap(versionLogs, func(e *model.VersionLog) string {\n\t\treturn e.DID\n\t})\n\n\treturn groupMemberIncrVersionsMap, nil\n}\n\nfunc (g *groupDatabase) FindJoinIncrVersion(ctx context.Context, userID string, version uint, limit int) (*model.VersionLog, error) {\n\treturn g.groupMemberDB.FindJoinIncrVersion(ctx, userID, version, limit)\n}\n\nfunc (g *groupDatabase) FindMaxGroupMemberVersionCache(ctx context.Context, groupID string) (*model.VersionLog, error) {\n\treturn g.cache.FindMaxGroupMemberVersion(ctx, groupID)\n}\n\nfunc (g *groupDatabase) BatchFindMaxGroupMemberVersionCache(ctx context.Context, groupIDs []string) (map[string]*model.VersionLog, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn nil, errs.Wrap(errs.New(\"groupIDs is nil in Cache.\"))\n\t}\n\tversionLogs, err := g.cache.BatchFindMaxGroupMemberVersion(ctx, groupIDs)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\tmaxGroupMemberVersionsMap := datautil.SliceToMap(versionLogs, func(e *model.VersionLog) string {\n\t\treturn e.DID\n\t})\n\treturn maxGroupMemberVersionsMap, nil\n}\n\nfunc (g *groupDatabase) FindMaxJoinGroupVersionCache(ctx context.Context, userID string) (*model.VersionLog, error) {\n\treturn g.cache.FindMaxJoinGroupVersion(ctx, userID)\n}\n\nfunc (g *groupDatabase) SearchJoinGroup(ctx context.Context, userID string, keyword string, pagination pagination.Pagination) (int64, []*model.Group, error) {\n\tgroupIDs, err := g.cache.GetJoinedGroupIDs(ctx, userID)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\treturn g.groupDB.SearchJoin(ctx, groupIDs, keyword, pagination)\n}\n\nfunc (g *groupDatabase) MemberGroupIncrVersion(ctx context.Context, groupID string, userIDs []string, state int32) error {\n\tif err := g.groupMemberDB.MemberGroupIncrVersion(ctx, groupID, userIDs, state); err != nil {\n\t\treturn err\n\t}\n\treturn g.cache.DelMaxGroupMemberVersion(groupID).ChainExecDel(ctx)\n}\n\nfunc (g *groupDatabase) GetGroupApplicationUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error) {\n\treturn g.groupRequestDB.GetUnhandledCount(ctx, groupIDs, ts)\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/openimsdk/tools/mq\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/redis/go-redis/v9\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/protocol/constant\"\n\tpbmsg \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nconst (\n\tupdateKeyMsg = iota\n\tupdateKeyRevoke\n)\n\n// CommonMsgDatabase defines the interface for message database operations.\ntype CommonMsgDatabase interface {\n\t// RevokeMsg revokes a message in a conversation.\n\tRevokeMsg(ctx context.Context, conversationID string, seq int64, revoke *model.RevokeModel) error\n\t// MarkSingleChatMsgsAsRead marks messages as read for a single chat by sequence numbers.\n\tMarkSingleChatMsgsAsRead(ctx context.Context, userID string, conversationID string, seqs []int64) error\n\t// GetMsgBySeqsRange retrieves messages from MongoDB by a range of sequence numbers.\n\tGetMsgBySeqsRange(ctx context.Context, userID string, conversationID string, begin, end, num, userMaxSeq int64) (minSeq int64, maxSeq int64, seqMsg []*sdkws.MsgData, err error)\n\t// GetMsgBySeqs retrieves messages for large groups from MongoDB by sequence numbers.\n\tGetMsgBySeqs(ctx context.Context, userID string, conversationID string, seqs []int64) (minSeq int64, maxSeq int64, seqMsg []*sdkws.MsgData, err error)\n\n\tGetMessagesBySeqWithBounds(ctx context.Context, userID string, conversationID string, seqs []int64, pullOrder sdkws.PullOrder) (bool, int64, []*sdkws.MsgData, error)\n\t// DeleteUserMsgsBySeqs allows a user to delete messages based on sequence numbers.\n\tDeleteUserMsgsBySeqs(ctx context.Context, userID string, conversationID string, seqs []int64) error\n\t// DeleteMsgsPhysicalBySeqs physically deletes messages by emptying them based on sequence numbers.\n\tDeleteMsgsPhysicalBySeqs(ctx context.Context, conversationID string, seqs []int64) error\n\t//SetMaxSeq(ctx context.Context, conversationID string, maxSeq int64) error\n\tGetMaxSeqs(ctx context.Context, conversationIDs []string) (map[string]int64, error)\n\tGetMaxSeq(ctx context.Context, conversationID string) (int64, error)\n\tSetMinSeqs(ctx context.Context, seqs map[string]int64) error\n\tSetMinSeq(ctx context.Context, conversationID string, seq int64) error\n\n\tSetUserConversationsMinSeqs(ctx context.Context, userID string, seqs map[string]int64) (err error)\n\tSetHasReadSeq(ctx context.Context, userID string, conversationID string, hasReadSeq int64) error\n\tGetHasReadSeqs(ctx context.Context, userID string, conversationIDs []string) (map[string]int64, error)\n\tGetHasReadSeq(ctx context.Context, userID string, conversationID string) (int64, error)\n\tUserSetHasReadSeqs(ctx context.Context, userID string, hasReadSeqs map[string]int64) error\n\n\tGetMaxSeqsWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error)\n\tGetMaxSeqWithTime(ctx context.Context, conversationID string) (database.SeqTime, error)\n\tGetCacheMaxSeqWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error)\n\n\tSetSendMsgStatus(ctx context.Context, id string, status int32) error\n\tGetSendMsgStatus(ctx context.Context, id string) (int32, error)\n\tSearchMessage(ctx context.Context, req *pbmsg.SearchMessageReq) (total int64, msgData []*pbmsg.SearchedMsgData, err error)\n\tFindOneByDocIDs(ctx context.Context, docIDs []string, seqs map[string]int64) (map[string]*sdkws.MsgData, error)\n\n\t// to mq\n\tMsgToMQ(ctx context.Context, key string, msg2mq *sdkws.MsgData) error\n\n\tRangeUserSendCount(ctx context.Context, start time.Time, end time.Time, group bool, ase bool, pageNumber int32, showNumber int32) (msgCount int64, userCount int64, users []*model.UserCount, dateCount map[string]int64, err error)\n\tRangeGroupSendCount(ctx context.Context, start time.Time, end time.Time, ase bool, pageNumber int32, showNumber int32) (msgCount int64, userCount int64, groups []*model.GroupCount, dateCount map[string]int64, err error)\n\n\tGetRandBeforeMsg(ctx context.Context, ts int64, limit int) ([]*model.MsgDocModel, error)\n\n\tSetUserConversationsMaxSeq(ctx context.Context, conversationID string, userID string, seq int64) error\n\tSetUserConversationsMinSeq(ctx context.Context, conversationID string, userID string, seq int64) error\n\n\tDeleteDoc(ctx context.Context, docID string) error\n\n\tGetLastMessageSeqByTime(ctx context.Context, conversationID string, time int64) (int64, error)\n\n\tGetLastMessage(ctx context.Context, conversationIDS []string, userID string) (map[string]*sdkws.MsgData, error)\n}\n\nfunc NewCommonMsgDatabase(msgDocModel database.Msg, msg cache.MsgCache, seqUser cache.SeqUser, seqConversation cache.SeqConversationCache, producer mq.Producer) CommonMsgDatabase {\n\treturn &commonMsgDatabase{\n\t\tmsgDocDatabase:  msgDocModel,\n\t\tmsgCache:        msg,\n\t\tseqUser:         seqUser,\n\t\tseqConversation: seqConversation,\n\t\tproducer:        producer,\n\t}\n}\n\ntype commonMsgDatabase struct {\n\tmsgDocDatabase  database.Msg\n\tmsgTable        model.MsgDocModel\n\tmsgCache        cache.MsgCache\n\tseqConversation cache.SeqConversationCache\n\tseqUser         cache.SeqUser\n\tproducer        mq.Producer\n}\n\nfunc (db *commonMsgDatabase) MsgToMQ(ctx context.Context, key string, msg2mq *sdkws.MsgData) error {\n\tdata, err := proto.Marshal(msg2mq)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn db.producer.SendMessage(ctx, key, data)\n}\n\nfunc (db *commonMsgDatabase) batchInsertBlock(ctx context.Context, conversationID string, fields []any, key int8, firstSeq int64) error {\n\tif len(fields) == 0 {\n\t\treturn nil\n\t}\n\tnum := db.msgTable.GetSingleGocMsgNum()\n\t// num = 100\n\tfor i, field := range fields { // Check the type of the field\n\t\tvar ok bool\n\t\tswitch key {\n\t\tcase updateKeyMsg:\n\t\t\tvar msg *model.MsgDataModel\n\t\t\tmsg, ok = field.(*model.MsgDataModel)\n\t\t\tif msg != nil && msg.Seq != firstSeq+int64(i) {\n\t\t\t\treturn errs.ErrInternalServer.WrapMsg(\"seq is invalid\")\n\t\t\t}\n\t\tcase updateKeyRevoke:\n\t\t\t_, ok = field.(*model.RevokeModel)\n\t\tdefault:\n\t\t\treturn errs.ErrInternalServer.WrapMsg(\"key is invalid\")\n\t\t}\n\t\tif !ok {\n\t\t\treturn errs.ErrInternalServer.WrapMsg(\"field type is invalid\")\n\t\t}\n\t}\n\t// Returns true if the document exists in the database, false if the document does not exist in the database\n\tupdateMsgModel := func(seq int64, i int) (bool, error) {\n\t\tvar (\n\t\t\tres *mongo.UpdateResult\n\t\t\terr error\n\t\t)\n\t\tdocID := db.msgTable.GetDocID(conversationID, seq)\n\t\tindex := db.msgTable.GetMsgIndex(seq)\n\t\tfield := fields[i]\n\t\tswitch key {\n\t\tcase updateKeyMsg:\n\t\t\tres, err = db.msgDocDatabase.UpdateMsg(ctx, docID, index, \"msg\", field)\n\t\tcase updateKeyRevoke:\n\t\t\tres, err = db.msgDocDatabase.UpdateMsg(ctx, docID, index, \"revoke\", field)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\treturn res.MatchedCount > 0, nil\n\t}\n\ttryUpdate := true\n\tfor i := 0; i < len(fields); i++ {\n\t\tseq := firstSeq + int64(i) // Current sequence number\n\t\tif tryUpdate {\n\t\t\tmatched, err := updateMsgModel(seq, i)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif matched {\n\t\t\t\tcontinue // The current data has been updated, skip the current data\n\t\t\t}\n\t\t}\n\t\tdoc := model.MsgDocModel{\n\t\t\tDocID: db.msgTable.GetDocID(conversationID, seq),\n\t\t\tMsg:   make([]*model.MsgInfoModel, num),\n\t\t}\n\t\tvar insert int // Inserted data number\n\t\tfor j := i; j < len(fields); j++ {\n\t\t\tseq = firstSeq + int64(j)\n\t\t\tif db.msgTable.GetDocID(conversationID, seq) != doc.DocID {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tinsert++\n\t\t\tswitch key {\n\t\t\tcase updateKeyMsg:\n\t\t\t\tdoc.Msg[db.msgTable.GetMsgIndex(seq)] = &model.MsgInfoModel{\n\t\t\t\t\tMsg: fields[j].(*model.MsgDataModel),\n\t\t\t\t}\n\t\t\tcase updateKeyRevoke:\n\t\t\t\tdoc.Msg[db.msgTable.GetMsgIndex(seq)] = &model.MsgInfoModel{\n\t\t\t\t\tRevoke: fields[j].(*model.RevokeModel),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor i, msgInfo := range doc.Msg {\n\t\t\tif msgInfo == nil {\n\t\t\t\tmsgInfo = &model.MsgInfoModel{}\n\t\t\t\tdoc.Msg[i] = msgInfo\n\t\t\t}\n\t\t\tif msgInfo.DelList == nil {\n\t\t\t\tdoc.Msg[i].DelList = []string{}\n\t\t\t}\n\t\t}\n\t\tif err := db.msgDocDatabase.Create(ctx, &doc); err != nil {\n\t\t\tif mongo.IsDuplicateKeyError(err) {\n\t\t\t\ti--              // already inserted\n\t\t\t\ttryUpdate = true // next block use update mode\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\ttryUpdate = false // The current block is inserted successfully, and the next block is inserted preferentially\n\t\ti += insert - 1   // Skip the inserted data\n\t}\n\n\treturn nil\n}\n\nfunc (db *commonMsgDatabase) RevokeMsg(ctx context.Context, conversationID string, seq int64, revoke *model.RevokeModel) error {\n\tif err := db.batchInsertBlock(ctx, conversationID, []any{revoke}, updateKeyRevoke, seq); err != nil {\n\t\treturn err\n\t}\n\treturn db.msgCache.DelMessageBySeqs(ctx, conversationID, []int64{seq})\n}\n\nfunc (db *commonMsgDatabase) MarkSingleChatMsgsAsRead(ctx context.Context, userID string, conversationID string, totalSeqs []int64) error {\n\tfor docID, seqs := range db.msgTable.GetDocIDSeqsMap(conversationID, totalSeqs) {\n\t\tvar indexes []int64\n\t\tfor _, seq := range seqs {\n\t\t\tindexes = append(indexes, db.msgTable.GetMsgIndex(seq))\n\t\t}\n\t\tlog.ZDebug(ctx, \"MarkSingleChatMsgsAsRead\", \"userID\", userID, \"docID\", docID, \"indexes\", indexes)\n\t\tif err := db.msgDocDatabase.MarkSingleChatMsgsAsRead(ctx, userID, docID, indexes); err != nil {\n\t\t\tlog.ZError(ctx, \"MarkSingleChatMsgsAsRead\", err, \"userID\", userID, \"docID\", docID, \"indexes\", indexes)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn db.msgCache.DelMessageBySeqs(ctx, conversationID, totalSeqs)\n}\n\nfunc (db *commonMsgDatabase) getMsgBySeqs(ctx context.Context, userID, conversationID string, seqs []int64) (totalMsgs []*sdkws.MsgData, err error) {\n\treturn db.GetMessageBySeqs(ctx, conversationID, userID, seqs)\n}\n\nfunc (db *commonMsgDatabase) handlerDBMsg(ctx context.Context, cache map[int64][]*model.MsgInfoModel, userID, conversationID string, msg *model.MsgInfoModel) {\n\tif msg == nil || msg.Msg == nil {\n\t\treturn\n\t}\n\tif msg.IsRead {\n\t\tmsg.Msg.IsRead = true\n\t}\n\tif msg.Msg.ContentType != constant.Quote {\n\t\treturn\n\t}\n\tif msg.Msg.Content == \"\" {\n\t\treturn\n\t}\n\ttype MsgData struct {\n\t\tSendID           string                 `json:\"sendID\"`\n\t\tRecvID           string                 `json:\"recvID\"`\n\t\tGroupID          string                 `json:\"groupID\"`\n\t\tClientMsgID      string                 `json:\"clientMsgID\"`\n\t\tServerMsgID      string                 `json:\"serverMsgID\"`\n\t\tSenderPlatformID int32                  `json:\"senderPlatformID\"`\n\t\tSenderNickname   string                 `json:\"senderNickname\"`\n\t\tSenderFaceURL    string                 `json:\"senderFaceURL\"`\n\t\tSessionType      int32                  `json:\"sessionType\"`\n\t\tMsgFrom          int32                  `json:\"msgFrom\"`\n\t\tContentType      int32                  `json:\"contentType\"`\n\t\tContent          string                 `json:\"content\"`\n\t\tSeq              int64                  `json:\"seq\"`\n\t\tSendTime         int64                  `json:\"sendTime\"`\n\t\tCreateTime       int64                  `json:\"createTime\"`\n\t\tStatus           int32                  `json:\"status\"`\n\t\tIsRead           bool                   `json:\"isRead\"`\n\t\tOptions          map[string]bool        `json:\"options,omitempty\"`\n\t\tOfflinePushInfo  *sdkws.OfflinePushInfo `json:\"offlinePushInfo\"`\n\t\tAtUserIDList     []string               `json:\"atUserIDList\"`\n\t\tAttachedInfo     string                 `json:\"attachedInfo\"`\n\t\tEx               string                 `json:\"ex\"`\n\t\tKeyVersion       int32                  `json:\"keyVersion\"`\n\t\tDstUserIDs       []string               `json:\"dstUserIDs\"`\n\t}\n\tvar quoteMsg struct {\n\t\tText              string          `json:\"text,omitempty\"`\n\t\tQuoteMessage      *MsgData        `json:\"quoteMessage,omitempty\"`\n\t\tMessageEntityList json.RawMessage `json:\"messageEntityList,omitempty\"`\n\t}\n\tif err := json.Unmarshal([]byte(msg.Msg.Content), &quoteMsg); err != nil {\n\t\tlog.ZError(ctx, \"json.Unmarshal\", err)\n\t\treturn\n\t}\n\tif quoteMsg.QuoteMessage == nil {\n\t\treturn\n\t}\n\tif quoteMsg.QuoteMessage.Content == \"e30=\" {\n\t\tquoteMsg.QuoteMessage.Content = \"{}\"\n\t\tdata, err := json.Marshal(&quoteMsg)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tmsg.Msg.Content = string(data)\n\t}\n\tif quoteMsg.QuoteMessage.Seq <= 0 && quoteMsg.QuoteMessage.ContentType == constant.MsgRevokeNotification {\n\t\treturn\n\t}\n\tvar msgs []*model.MsgInfoModel\n\tif v, ok := cache[quoteMsg.QuoteMessage.Seq]; ok {\n\t\tmsgs = v\n\t} else {\n\t\tif quoteMsg.QuoteMessage.Seq > 0 {\n\t\t\tms, err := db.msgDocDatabase.GetMsgBySeqIndexIn1Doc(ctx, userID, db.msgTable.GetDocID(conversationID, quoteMsg.QuoteMessage.Seq), []int64{quoteMsg.QuoteMessage.Seq})\n\t\t\tif err != nil {\n\t\t\t\tlog.ZError(ctx, \"GetMsgBySeqIndexIn1Doc\", err, \"conversationID\", conversationID, \"seq\", quoteMsg.QuoteMessage.Seq)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmsgs = ms\n\t\t\tcache[quoteMsg.QuoteMessage.Seq] = ms\n\t\t}\n\t}\n\tif len(msgs) != 0 && msgs[0].Msg.ContentType != constant.MsgRevokeNotification {\n\t\treturn\n\t}\n\tquoteMsg.QuoteMessage.ContentType = constant.MsgRevokeNotification\n\tif len(msgs) > 0 {\n\t\tquoteMsg.QuoteMessage.Content = msgs[0].Msg.Content\n\t} else {\n\t\tquoteMsg.QuoteMessage.Content = \"{}\"\n\t}\n\tdata, err := json.Marshal(&quoteMsg)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"json.Marshal\", err)\n\t\treturn\n\t}\n\tmsg.Msg.Content = string(data)\n}\n\nfunc (db *commonMsgDatabase) findMsgInfoBySeq(ctx context.Context, userID, docID string, conversationID string, seqs []int64) (totalMsgs []*model.MsgInfoModel, err error) {\n\tmsgs, err := db.msgDocDatabase.GetMsgBySeqIndexIn1Doc(ctx, userID, docID, seqs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttempCache := make(map[int64][]*model.MsgInfoModel)\n\tfor _, msg := range msgs {\n\t\tdb.handlerDBMsg(ctx, tempCache, userID, conversationID, msg)\n\t}\n\treturn msgs, err\n}\n\n// GetMsgBySeqsRange In the context of group chat, we have the following parameters:\n//\n// \"maxSeq\" of a conversation: It represents the maximum value of messages in the group conversation.\n// \"minSeq\" of a conversation (default: 1): It represents the minimum value of messages in the group conversation.\n//\n// For a user's perspective regarding the group conversation, we have the following parameters:\n//\n// \"userMaxSeq\": It represents the user's upper limit for message retrieval in the group. If not set (default: 0),\n// it means the upper limit is the same as the conversation's \"maxSeq\".\n// \"userMinSeq\": It represents the user's starting point for message retrieval in the group. If not set (default: 0),\n// it means the starting point is the same as the conversation's \"minSeq\".\n//\n// The scenarios for these parameters are as follows:\n//\n// For users who have been kicked out of the group, \"userMaxSeq\" can be set as the maximum value they had before\n// being kicked out. This limits their ability to retrieve messages up to a certain point.\n// For new users joining the group, if they don't need to receive old messages,\n// \"userMinSeq\" can be set as the same value as the conversation's \"maxSeq\" at the moment they join the group.\n// This ensures that their message retrieval starts from the point they joined.\nfunc (db *commonMsgDatabase) GetMsgBySeqsRange(ctx context.Context, userID string, conversationID string, begin, end, num, userMaxSeq int64) (int64, int64, []*sdkws.MsgData, error) {\n\tuserMinSeq, err := db.seqUser.GetUserMinSeq(ctx, conversationID, userID)\n\tif err != nil && !errors.Is(err, redis.Nil) {\n\t\treturn 0, 0, nil, err\n\t}\n\tminSeq, err := db.seqConversation.GetMinSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\tif userMinSeq > minSeq {\n\t\tminSeq = userMinSeq\n\t}\n\t// \"minSeq\" represents the startSeq value that the user can retrieve.\n\tif minSeq > end {\n\t\tlog.ZWarn(ctx, \"minSeq > end\", errs.New(\"minSeq>end\"), \"minSeq\", minSeq, \"end\", end)\n\t\treturn 0, 0, nil, nil\n\t}\n\tmaxSeq, err := db.seqConversation.GetMaxSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\tlog.ZDebug(ctx, \"GetMsgBySeqsRange\", \"userMinSeq\", userMinSeq, \"conMinSeq\", minSeq, \"conMaxSeq\", maxSeq, \"userMaxSeq\", userMaxSeq)\n\tif userMaxSeq != 0 {\n\t\tif userMaxSeq < maxSeq {\n\t\t\tmaxSeq = userMaxSeq\n\t\t}\n\t}\n\t// \"maxSeq\" represents the endSeq value that the user can retrieve.\n\n\tif begin < minSeq {\n\t\tbegin = minSeq\n\t}\n\tif end > maxSeq {\n\t\tend = maxSeq\n\t}\n\t// \"begin\" and \"end\" represent the actual startSeq and endSeq values that the user can retrieve.\n\tif end < begin {\n\t\treturn 0, 0, nil, errs.ErrArgs.WrapMsg(\"seq end < begin\")\n\t}\n\tvar seqs []int64\n\tif end-begin+1 <= num {\n\t\tfor i := begin; i <= end; i++ {\n\t\t\tseqs = append(seqs, i)\n\t\t}\n\t} else {\n\t\tfor i := end - num + 1; i <= end; i++ {\n\t\t\tseqs = append(seqs, i)\n\t\t}\n\t}\n\tsuccessMsgs, err := db.GetMessageBySeqs(ctx, conversationID, userID, seqs)\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\treturn minSeq, maxSeq, successMsgs, nil\n}\n\nfunc (db *commonMsgDatabase) GetMsgBySeqs(ctx context.Context, userID string, conversationID string, seqs []int64) (int64, int64, []*sdkws.MsgData, error) {\n\tuserMinSeq, err := db.seqUser.GetUserMinSeq(ctx, conversationID, userID)\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\tminSeq, err := db.seqConversation.GetMinSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\tmaxSeq, err := db.seqConversation.GetMaxSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\tuserMaxSeq, err := db.seqUser.GetUserMaxSeq(ctx, conversationID, userID)\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\tif userMinSeq > minSeq {\n\t\tminSeq = userMinSeq\n\t}\n\tif userMaxSeq > 0 && userMaxSeq < maxSeq {\n\t\tmaxSeq = userMaxSeq\n\t}\n\tnewSeqs := make([]int64, 0, len(seqs))\n\tfor _, seq := range seqs {\n\t\tif seq <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif seq >= minSeq && seq <= maxSeq {\n\t\t\tnewSeqs = append(newSeqs, seq)\n\t\t}\n\t}\n\tsuccessMsgs, err := db.GetMessageBySeqs(ctx, conversationID, userID, newSeqs)\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\treturn minSeq, maxSeq, successMsgs, nil\n}\n\nfunc (db *commonMsgDatabase) GetMessagesBySeqWithBounds(ctx context.Context, userID string, conversationID string, seqs []int64, pullOrder sdkws.PullOrder) (bool, int64, []*sdkws.MsgData, error) {\n\tvar endSeq int64\n\tvar isEnd bool\n\tuserMinSeq, err := db.seqUser.GetUserMinSeq(ctx, conversationID, userID)\n\tif err != nil {\n\t\treturn false, 0, nil, err\n\t}\n\tminSeq, err := db.seqConversation.GetMinSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn false, 0, nil, err\n\t}\n\tmaxSeq, err := db.seqConversation.GetMaxSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn false, 0, nil, err\n\t}\n\tuserMaxSeq, err := db.seqUser.GetUserMaxSeq(ctx, conversationID, userID)\n\tif err != nil {\n\t\treturn false, 0, nil, err\n\t}\n\tif userMinSeq > minSeq {\n\t\tminSeq = userMinSeq\n\t}\n\tif userMaxSeq > 0 && userMaxSeq < maxSeq {\n\t\tmaxSeq = userMaxSeq\n\t}\n\tnewSeqs := make([]int64, 0, len(seqs))\n\tfor _, seq := range seqs {\n\t\tif seq <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\t// The normal range and can fetch messages\n\t\tif seq >= minSeq && seq <= maxSeq {\n\t\t\tnewSeqs = append(newSeqs, seq)\n\t\t\tcontinue\n\t\t}\n\t\t// If the requested seq is smaller than the minimum seq and the pull order is descending (pulling older messages)\n\t\tif seq < minSeq && pullOrder == sdkws.PullOrder_PullOrderDesc {\n\t\t\tisEnd = true\n\t\t\tendSeq = minSeq\n\t\t}\n\t\t// If the requested seq is larger than the maximum seq and the pull order is ascending (pulling newer messages)\n\t\tif seq > maxSeq && pullOrder == sdkws.PullOrder_PullOrderAsc {\n\t\t\tisEnd = true\n\t\t\tendSeq = maxSeq\n\t\t}\n\t}\n\tif len(newSeqs) == 0 {\n\t\treturn isEnd, endSeq, nil, nil\n\t}\n\tsuccessMsgs, err := db.GetMessageBySeqs(ctx, conversationID, userID, newSeqs)\n\tif err != nil {\n\t\treturn false, 0, nil, err\n\t}\n\treturn isEnd, endSeq, successMsgs, nil\n}\n\nfunc (db *commonMsgDatabase) DeleteMsgsPhysicalBySeqs(ctx context.Context, conversationID string, allSeqs []int64) error {\n\tfor docID, seqs := range db.msgTable.GetDocIDSeqsMap(conversationID, allSeqs) {\n\t\tvar indexes []int\n\t\tfor _, seq := range seqs {\n\t\t\tindexes = append(indexes, int(db.msgTable.GetMsgIndex(seq)))\n\t\t}\n\t\tif err := db.msgDocDatabase.DeleteMsgsInOneDocByIndex(ctx, docID, indexes); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn db.msgCache.DelMessageBySeqs(ctx, conversationID, allSeqs)\n}\n\nfunc (db *commonMsgDatabase) DeleteUserMsgsBySeqs(ctx context.Context, userID string, conversationID string, seqs []int64) error {\n\tfor docID, seqs := range db.msgTable.GetDocIDSeqsMap(conversationID, seqs) {\n\t\tfor _, seq := range seqs {\n\t\t\tif _, err := db.msgDocDatabase.PushUnique(ctx, docID, db.msgTable.GetMsgIndex(seq), \"del_list\", []string{userID}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn db.msgCache.DelMessageBySeqs(ctx, conversationID, seqs)\n}\n\nfunc (db *commonMsgDatabase) GetMaxSeqs(ctx context.Context, conversationIDs []string) (map[string]int64, error) {\n\treturn db.seqConversation.GetMaxSeqs(ctx, conversationIDs)\n}\n\nfunc (db *commonMsgDatabase) GetMaxSeq(ctx context.Context, conversationID string) (int64, error) {\n\treturn db.seqConversation.GetMaxSeq(ctx, conversationID)\n}\n\nfunc (db *commonMsgDatabase) SetMinSeqs(ctx context.Context, seqs map[string]int64) error {\n\treturn db.seqConversation.SetMinSeqs(ctx, seqs)\n}\n\nfunc (db *commonMsgDatabase) SetUserConversationsMinSeqs(ctx context.Context, userID string, seqs map[string]int64) error {\n\treturn db.seqUser.SetUserMinSeqs(ctx, userID, seqs)\n}\n\nfunc (db *commonMsgDatabase) SetUserConversationsMaxSeq(ctx context.Context, conversationID string, userID string, seq int64) error {\n\treturn db.seqUser.SetUserMaxSeq(ctx, conversationID, userID, seq)\n}\n\nfunc (db *commonMsgDatabase) SetUserConversationsMinSeq(ctx context.Context, conversationID string, userID string, seq int64) error {\n\treturn db.seqUser.SetUserMinSeq(ctx, conversationID, userID, seq)\n}\n\nfunc (db *commonMsgDatabase) UserSetHasReadSeqs(ctx context.Context, userID string, hasReadSeqs map[string]int64) error {\n\treturn db.seqUser.SetUserReadSeqs(ctx, userID, hasReadSeqs)\n}\n\nfunc (db *commonMsgDatabase) SetHasReadSeq(ctx context.Context, userID string, conversationID string, hasReadSeq int64) error {\n\treturn db.seqUser.SetUserReadSeq(ctx, conversationID, userID, hasReadSeq)\n}\n\nfunc (db *commonMsgDatabase) GetHasReadSeqs(ctx context.Context, userID string, conversationIDs []string) (map[string]int64, error) {\n\treturn db.seqUser.GetUserReadSeqs(ctx, userID, conversationIDs)\n}\n\nfunc (db *commonMsgDatabase) GetHasReadSeq(ctx context.Context, userID string, conversationID string) (int64, error) {\n\treturn db.seqUser.GetUserReadSeq(ctx, conversationID, userID)\n}\n\nfunc (db *commonMsgDatabase) SetSendMsgStatus(ctx context.Context, id string, status int32) error {\n\treturn db.msgCache.SetSendMsgStatus(ctx, id, status)\n}\n\nfunc (db *commonMsgDatabase) GetSendMsgStatus(ctx context.Context, id string) (int32, error) {\n\treturn db.msgCache.GetSendMsgStatus(ctx, id)\n}\n\nfunc (db *commonMsgDatabase) GetConversationMinMaxSeqInMongoAndCache(ctx context.Context, conversationID string) (minSeqMongo, maxSeqMongo, minSeqCache, maxSeqCache int64, err error) {\n\tminSeqMongo, maxSeqMongo, err = db.GetMinMaxSeqMongo(ctx, conversationID)\n\tif err != nil {\n\t\treturn\n\t}\n\tminSeqCache, err = db.seqConversation.GetMinSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn\n\t}\n\tmaxSeqCache, err = db.seqConversation.GetMaxSeq(ctx, conversationID)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn\n}\n\nfunc (db *commonMsgDatabase) GetMongoMaxAndMinSeq(ctx context.Context, conversationID string) (minSeqMongo, maxSeqMongo int64, err error) {\n\treturn db.GetMinMaxSeqMongo(ctx, conversationID)\n}\n\nfunc (db *commonMsgDatabase) GetMinMaxSeqMongo(ctx context.Context, conversationID string) (minSeqMongo, maxSeqMongo int64, err error) {\n\toldestMsgMongo, err := db.msgDocDatabase.GetOldestMsg(ctx, conversationID)\n\tif err != nil {\n\t\treturn\n\t}\n\tminSeqMongo = oldestMsgMongo.Msg.Seq\n\tnewestMsgMongo, err := db.msgDocDatabase.GetNewestMsg(ctx, conversationID)\n\tif err != nil {\n\t\treturn\n\t}\n\tmaxSeqMongo = newestMsgMongo.Msg.Seq\n\treturn\n}\n\nfunc (db *commonMsgDatabase) RangeUserSendCount(ctx context.Context, start time.Time, end time.Time, group bool, ase bool, pageNumber int32, showNumber int32) (msgCount int64, userCount int64, users []*model.UserCount, dateCount map[string]int64, err error) {\n\treturn db.msgDocDatabase.RangeUserSendCount(ctx, start, end, group, ase, pageNumber, showNumber)\n}\n\nfunc (db *commonMsgDatabase) RangeGroupSendCount(ctx context.Context, start time.Time, end time.Time, ase bool, pageNumber int32, showNumber int32) (msgCount int64, userCount int64, groups []*model.GroupCount, dateCount map[string]int64, err error) {\n\treturn db.msgDocDatabase.RangeGroupSendCount(ctx, start, end, ase, pageNumber, showNumber)\n}\n\nfunc (db *commonMsgDatabase) SearchMessage(ctx context.Context, req *pbmsg.SearchMessageReq) (total int64, msgData []*pbmsg.SearchedMsgData, err error) {\n\tvar totalMsgs []*pbmsg.SearchedMsgData\n\ttotal, msgs, err := db.msgDocDatabase.SearchMessage(ctx, req)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\tfor _, msg := range msgs {\n\t\tif msg.IsRead {\n\t\t\tmsg.Msg.IsRead = true\n\t\t}\n\t\tsearchedMsgData := &pbmsg.SearchedMsgData{MsgData: convert.MsgDB2Pb(msg.Msg)}\n\n\t\tif msg.Revoke != nil {\n\t\t\tsearchedMsgData.IsRevoked = true\n\t\t}\n\n\t\ttotalMsgs = append(totalMsgs, searchedMsgData)\n\t}\n\treturn total, totalMsgs, nil\n}\n\nfunc (db *commonMsgDatabase) FindOneByDocIDs(ctx context.Context, conversationIDs []string, seqs map[string]int64) (map[string]*sdkws.MsgData, error) {\n\ttotalMsgs := make(map[string]*sdkws.MsgData)\n\tfor _, conversationID := range conversationIDs {\n\t\tseq, ok := seqs[conversationID]\n\t\tif !ok {\n\t\t\tlog.ZWarn(ctx, \"seq not found for conversationID\", errs.New(\"seq not found for conversation\"), \"conversationID\", conversationID)\n\t\t\tcontinue\n\t\t}\n\t\tdocID := db.msgTable.GetDocID(conversationID, seq)\n\t\tmsgs, err := db.msgDocDatabase.FindOneByDocID(ctx, docID)\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"FindOneByDocID failed\", err, \"conversationID\", conversationID, \"docID\", docID, \"seq\", seq)\n\t\t\tcontinue\n\t\t}\n\n\t\tindex := db.msgTable.GetMsgIndex(seq)\n\t\ttotalMsgs[conversationID] = convert.MsgDB2Pb(msgs.Msg[index].Msg)\n\t}\n\treturn totalMsgs, nil\n}\n\nfunc (db *commonMsgDatabase) GetRandBeforeMsg(ctx context.Context, ts int64, limit int) ([]*model.MsgDocModel, error) {\n\treturn db.msgDocDatabase.GetRandBeforeMsg(ctx, ts, limit)\n}\n\nfunc (db *commonMsgDatabase) SetMinSeq(ctx context.Context, conversationID string, seq int64) error {\n\tdbSeq, err := db.seqConversation.GetMinSeq(ctx, conversationID)\n\tif err != nil {\n\t\tif errors.Is(errs.Unwrap(err), redis.Nil) {\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\tif dbSeq >= seq {\n\t\treturn nil\n\t}\n\treturn db.seqConversation.SetMinSeq(ctx, conversationID, seq)\n}\n\nfunc (db *commonMsgDatabase) GetCacheMaxSeqWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error) {\n\treturn db.seqConversation.GetCacheMaxSeqWithTime(ctx, conversationIDs)\n}\n\nfunc (db *commonMsgDatabase) GetMaxSeqWithTime(ctx context.Context, conversationID string) (database.SeqTime, error) {\n\treturn db.seqConversation.GetMaxSeqWithTime(ctx, conversationID)\n}\n\nfunc (db *commonMsgDatabase) GetMaxSeqsWithTime(ctx context.Context, conversationIDs []string) (map[string]database.SeqTime, error) {\n\t// todo: only the time in the redis cache will be taken, not the message time\n\treturn db.seqConversation.GetMaxSeqsWithTime(ctx, conversationIDs)\n}\n\nfunc (db *commonMsgDatabase) DeleteDoc(ctx context.Context, docID string) error {\n\tindex := strings.LastIndex(docID, \":\")\n\tif index <= 0 {\n\t\treturn errs.ErrInternalServer.WrapMsg(\"docID is invalid\", \"docID\", docID)\n\t}\n\tdocIndex, err := strconv.Atoi(docID[index+1:])\n\tif err != nil {\n\t\treturn errs.WrapMsg(err, \"strconv.Atoi\", \"docID\", docID)\n\t}\n\tconversationID := docID[:index]\n\tseqs := make([]int64, db.msgTable.GetSingleGocMsgNum())\n\tminSeq := db.msgTable.GetMinSeq(docIndex)\n\tfor i := range seqs {\n\t\tseqs[i] = minSeq + int64(i)\n\t}\n\tif err := db.msgDocDatabase.DeleteDoc(ctx, docID); err != nil {\n\t\treturn err\n\t}\n\treturn db.msgCache.DelMessageBySeqs(ctx, conversationID, seqs)\n}\n\nfunc (db *commonMsgDatabase) GetLastMessageSeqByTime(ctx context.Context, conversationID string, time int64) (int64, error) {\n\treturn db.msgDocDatabase.GetLastMessageSeqByTime(ctx, conversationID, time)\n}\n\nfunc (db *commonMsgDatabase) handlerDeleteAndRevoked(ctx context.Context, userID string, msgs []*model.MsgInfoModel) {\n\tfor i := range msgs {\n\t\tmsg := msgs[i]\n\t\tif msg == nil || msg.Msg == nil {\n\t\t\tcontinue\n\t\t}\n\t\tmsg.Msg.IsRead = msg.IsRead\n\t\tif datautil.Contain(userID, msg.DelList...) {\n\t\t\tmsg.Msg.Content = \"\"\n\t\t\tmsg.Msg.Status = constant.MsgDeleted\n\t\t}\n\t\tif msg.Revoke == nil {\n\t\t\tcontinue\n\t\t}\n\t\tmsg.Msg.ContentType = constant.MsgRevokeNotification\n\t\trevokeContent := sdkws.MessageRevokedContent{\n\t\t\tRevokerID:                   msg.Revoke.UserID,\n\t\t\tRevokerRole:                 msg.Revoke.Role,\n\t\t\tClientMsgID:                 msg.Msg.ClientMsgID,\n\t\t\tRevokerNickname:             msg.Revoke.Nickname,\n\t\t\tRevokeTime:                  msg.Revoke.Time,\n\t\t\tSourceMessageSendTime:       msg.Msg.SendTime,\n\t\t\tSourceMessageSendID:         msg.Msg.SendID,\n\t\t\tSourceMessageSenderNickname: msg.Msg.SenderNickname,\n\t\t\tSessionType:                 msg.Msg.SessionType,\n\t\t\tSeq:                         msg.Msg.Seq,\n\t\t\tEx:                          msg.Msg.Ex,\n\t\t}\n\t\tdata, err := jsonutil.JsonMarshal(&revokeContent)\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"handlerDeleteAndRevoked JsonMarshal MessageRevokedContent\", err, \"msg\", msg)\n\t\t\tcontinue\n\t\t}\n\t\telem := sdkws.NotificationElem{\n\t\t\tDetail: string(data),\n\t\t}\n\t\tcontent, err := jsonutil.JsonMarshal(&elem)\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"handlerDeleteAndRevoked JsonMarshal NotificationElem\", err, \"msg\", msg)\n\t\t\tcontinue\n\t\t}\n\t\tmsg.Msg.Content = string(content)\n\t}\n}\n\nfunc (db *commonMsgDatabase) handlerQuote(ctx context.Context, userID, conversationID string, msgs []*model.MsgInfoModel) {\n\ttemp := make(map[int64][]*model.MsgInfoModel)\n\tfor i := range msgs {\n\t\tdb.handlerDBMsg(ctx, temp, userID, conversationID, msgs[i])\n\t}\n}\n\nfunc (db *commonMsgDatabase) GetMessageBySeqs(ctx context.Context, conversationID string, userID string, seqs []int64) ([]*sdkws.MsgData, error) {\n\tmsgs, err := db.msgCache.GetMessageBySeqs(ctx, conversationID, seqs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdb.handlerDeleteAndRevoked(ctx, userID, msgs)\n\tdb.handlerQuote(ctx, userID, conversationID, msgs)\n\tseqMsgs := make(map[int64]*model.MsgInfoModel)\n\tfor i, msg := range msgs {\n\t\tif msg.Msg == nil {\n\t\t\tcontinue\n\t\t}\n\t\tseqMsgs[msg.Msg.Seq] = msgs[i]\n\t}\n\tres := make([]*sdkws.MsgData, 0, len(seqs))\n\tfor _, seq := range seqs {\n\t\tif v, ok := seqMsgs[seq]; ok {\n\t\t\tres = append(res, convert.MsgDB2Pb(v.Msg))\n\t\t} else {\n\t\t\tres = append(res, &sdkws.MsgData{Seq: seq, Status: constant.MsgStatusHasDeleted})\n\t\t}\n\t}\n\treturn res, nil\n}\n\nfunc (db *commonMsgDatabase) GetLastMessage(ctx context.Context, conversationIDs []string, userID string) (map[string]*sdkws.MsgData, error) {\n\tres := make(map[string]*sdkws.MsgData)\n\tfor _, conversationID := range conversationIDs {\n\t\tif _, ok := res[conversationID]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tmsg, err := db.msgDocDatabase.GetLastMessage(ctx, conversationID)\n\t\tif err != nil {\n\t\t\tif errs.Unwrap(err) == mongo.ErrNoDocuments {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\ttmp := []*model.MsgInfoModel{msg}\n\t\tdb.handlerDeleteAndRevoked(ctx, userID, tmp)\n\t\tdb.handlerQuote(ctx, userID, conversationID, tmp)\n\t\tres[conversationID] = convert.MsgDB2Pb(msg.Msg)\n\t}\n\treturn res, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/msg_transfer.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/mq\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\tpbmsg \"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n)\n\ntype MsgTransferDatabase interface {\n\t// BatchInsertChat2DB inserts a batch of messages into the database for a specific conversation.\n\tBatchInsertChat2DB(ctx context.Context, conversationID string, msgs []*sdkws.MsgData, currentMaxSeq int64) error\n\t// DeleteMessagesFromCache deletes message caches from Redis by sequence numbers.\n\tDeleteMessagesFromCache(ctx context.Context, conversationID string, seqs []int64) error\n\n\t// BatchInsertChat2Cache increments the sequence number and then batch inserts messages into the cache.\n\tBatchInsertChat2Cache(ctx context.Context, conversationID string, msgs []*sdkws.MsgData) (seq int64, isNewConversation bool, userHasReadMap map[string]int64, err error)\n\n\tSetHasReadSeqs(ctx context.Context, conversationID string, userSeqMap map[string]int64) error\n\n\tSetHasReadSeqToDB(ctx context.Context, conversationID string, userSeqMap map[string]int64) error\n\n\t// to mq\n\tMsgToPushMQ(ctx context.Context, key, conversationID string, msg2mq *sdkws.MsgData) error\n\tMsgToMongoMQ(ctx context.Context, key, conversationID string, msgs []*sdkws.MsgData, lastSeq int64) error\n}\n\nfunc NewMsgTransferDatabase(msgDocModel database.Msg, msg cache.MsgCache, seqUser cache.SeqUser, seqConversation cache.SeqConversationCache, mongoProducer, pushProducer mq.Producer) (MsgTransferDatabase, error) {\n\t//conf, err := kafka.BuildProducerConfig(*kafkaConf.Build())\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\t//producerToMongo, err := kafka.NewKafkaProducerV2(conf, kafkaConf.Address, kafkaConf.ToMongoTopic)\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\t//producerToPush, err := kafka.NewKafkaProducerV2(conf, kafkaConf.Address, kafkaConf.ToPushTopic)\n\t//if err != nil {\n\t//\treturn nil, err\n\t//}\n\treturn &msgTransferDatabase{\n\t\tmsgDocDatabase:  msgDocModel,\n\t\tmsgCache:        msg,\n\t\tseqUser:         seqUser,\n\t\tseqConversation: seqConversation,\n\t\tproducerToMongo: mongoProducer,\n\t\tproducerToPush:  pushProducer,\n\t}, nil\n}\n\ntype msgTransferDatabase struct {\n\tmsgDocDatabase  database.Msg\n\tmsgTable        model.MsgDocModel\n\tmsgCache        cache.MsgCache\n\tseqConversation cache.SeqConversationCache\n\tseqUser         cache.SeqUser\n\tproducerToMongo mq.Producer\n\tproducerToPush  mq.Producer\n}\n\nfunc (db *msgTransferDatabase) BatchInsertChat2DB(ctx context.Context, conversationID string, msgList []*sdkws.MsgData, currentMaxSeq int64) error {\n\tif len(msgList) == 0 {\n\t\treturn errs.ErrArgs.WrapMsg(\"msgList is empty\")\n\t}\n\tmsgs := make([]any, len(msgList))\n\tseqs := make([]int64, len(msgList))\n\tfor i, msg := range msgList {\n\t\tif msg == nil {\n\t\t\tcontinue\n\t\t}\n\t\tseqs[i] = msg.Seq\n\t\tif msg.Status == constant.MsgStatusSending {\n\t\t\tmsg.Status = constant.MsgStatusSendSuccess\n\t\t}\n\t\tmsgs[i] = convert.MsgPb2DB(msg)\n\t}\n\tif err := db.BatchInsertBlock(ctx, conversationID, msgs, updateKeyMsg, msgList[0].Seq); err != nil {\n\t\treturn err\n\t}\n\t//return db.msgCache.DelMessageBySeqs(ctx, conversationID, seqs)\n\treturn nil\n}\n\nfunc (db *msgTransferDatabase) BatchInsertBlock(ctx context.Context, conversationID string, fields []any, key int8, firstSeq int64) error {\n\tif len(fields) == 0 {\n\t\treturn nil\n\t}\n\tnum := db.msgTable.GetSingleGocMsgNum()\n\t// num = 100\n\tfor i, field := range fields { // Check the type of the field\n\t\tvar ok bool\n\t\tswitch key {\n\t\tcase updateKeyMsg:\n\t\t\tvar msg *model.MsgDataModel\n\t\t\tmsg, ok = field.(*model.MsgDataModel)\n\t\t\tif msg != nil && msg.Seq != firstSeq+int64(i) {\n\t\t\t\treturn errs.ErrInternalServer.WrapMsg(\"seq is invalid\")\n\t\t\t}\n\t\tcase updateKeyRevoke:\n\t\t\t_, ok = field.(*model.RevokeModel)\n\t\tdefault:\n\t\t\treturn errs.ErrInternalServer.WrapMsg(\"key is invalid\")\n\t\t}\n\t\tif !ok {\n\t\t\treturn errs.ErrInternalServer.WrapMsg(\"field type is invalid\")\n\t\t}\n\t}\n\t// Returns true if the document exists in the database, false if the document does not exist in the database\n\tupdateMsgModel := func(seq int64, i int) (bool, error) {\n\t\tvar (\n\t\t\tres *mongo.UpdateResult\n\t\t\terr error\n\t\t)\n\t\tdocID := db.msgTable.GetDocID(conversationID, seq)\n\t\tindex := db.msgTable.GetMsgIndex(seq)\n\t\tfield := fields[i]\n\t\tswitch key {\n\t\tcase updateKeyMsg:\n\t\t\tres, err = db.msgDocDatabase.UpdateMsg(ctx, docID, index, \"msg\", field)\n\t\tcase updateKeyRevoke:\n\t\t\tres, err = db.msgDocDatabase.UpdateMsg(ctx, docID, index, \"revoke\", field)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\treturn res.MatchedCount > 0, nil\n\t}\n\ttryUpdate := true\n\tfor i := 0; i < len(fields); i++ {\n\t\tseq := firstSeq + int64(i) // Current sequence number\n\t\tif tryUpdate {\n\t\t\tmatched, err := updateMsgModel(seq, i)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif matched {\n\t\t\t\tcontinue // The current data has been updated, skip the current data\n\t\t\t}\n\t\t}\n\t\tdoc := model.MsgDocModel{\n\t\t\tDocID: db.msgTable.GetDocID(conversationID, seq),\n\t\t\tMsg:   make([]*model.MsgInfoModel, num),\n\t\t}\n\t\tvar insert int // Inserted data number\n\t\tfor j := i; j < len(fields); j++ {\n\t\t\tseq = firstSeq + int64(j)\n\t\t\tif db.msgTable.GetDocID(conversationID, seq) != doc.DocID {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tinsert++\n\t\t\tswitch key {\n\t\t\tcase updateKeyMsg:\n\t\t\t\tdoc.Msg[db.msgTable.GetMsgIndex(seq)] = &model.MsgInfoModel{\n\t\t\t\t\tMsg: fields[j].(*model.MsgDataModel),\n\t\t\t\t}\n\t\t\tcase updateKeyRevoke:\n\t\t\t\tdoc.Msg[db.msgTable.GetMsgIndex(seq)] = &model.MsgInfoModel{\n\t\t\t\t\tRevoke: fields[j].(*model.RevokeModel),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor i, msgInfo := range doc.Msg {\n\t\t\tif msgInfo == nil {\n\t\t\t\tmsgInfo = &model.MsgInfoModel{}\n\t\t\t\tdoc.Msg[i] = msgInfo\n\t\t\t}\n\t\t\tif msgInfo.DelList == nil {\n\t\t\t\tdoc.Msg[i].DelList = []string{}\n\t\t\t}\n\t\t}\n\t\tif err := db.msgDocDatabase.Create(ctx, &doc); err != nil {\n\t\t\tif mongo.IsDuplicateKeyError(err) {\n\t\t\t\ti--              // already inserted\n\t\t\t\ttryUpdate = true // next block use update mode\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\ttryUpdate = false // The current block is inserted successfully, and the next block is inserted preferentially\n\t\ti += insert - 1   // Skip the inserted data\n\t}\n\treturn nil\n}\n\nfunc (db *msgTransferDatabase) DeleteMessagesFromCache(ctx context.Context, conversationID string, seqs []int64) error {\n\treturn db.msgCache.DelMessageBySeqs(ctx, conversationID, seqs)\n}\n\nfunc (db *msgTransferDatabase) BatchInsertChat2Cache(ctx context.Context, conversationID string, msgs []*sdkws.MsgData) (seq int64, isNew bool, userHasReadMap map[string]int64, err error) {\n\tlenList := len(msgs)\n\tif int64(lenList) > db.msgTable.GetSingleGocMsgNum() {\n\t\treturn 0, false, nil, errs.New(\"message count exceeds limit\", \"limit\", db.msgTable.GetSingleGocMsgNum()).Wrap()\n\t}\n\tif lenList < 1 {\n\t\treturn 0, false, nil, errs.New(\"no messages to insert\", \"minCount\", 1).Wrap()\n\t}\n\tcurrentMaxSeq, err := db.seqConversation.Malloc(ctx, conversationID, int64(len(msgs)))\n\tif err != nil {\n\t\tlog.ZError(ctx, \"storage.seq.Malloc\", err)\n\t\treturn 0, false, nil, err\n\t}\n\tisNew = currentMaxSeq == 0\n\tlastMaxSeq := currentMaxSeq\n\tuserSeqMap := make(map[string]int64)\n\tseqs := make([]int64, 0, lenList)\n\tfor _, m := range msgs {\n\t\tcurrentMaxSeq++\n\t\tm.Seq = currentMaxSeq\n\t\tuserSeqMap[m.SendID] = m.Seq\n\t\tseqs = append(seqs, m.Seq)\n\t}\n\tmsgToDB := func(msg *sdkws.MsgData) *model.MsgInfoModel {\n\t\treturn &model.MsgInfoModel{\n\t\t\tMsg: convert.MsgPb2DB(msg),\n\t\t}\n\t}\n\tif err := db.msgCache.SetMessageBySeqs(ctx, conversationID, datautil.Slice(msgs, msgToDB)); err != nil {\n\t\treturn 0, false, nil, err\n\t}\n\treturn lastMaxSeq, isNew, userSeqMap, nil\n}\n\nfunc (db *msgTransferDatabase) SetHasReadSeqs(ctx context.Context, conversationID string, userSeqMap map[string]int64) error {\n\tfor userID, seq := range userSeqMap {\n\t\tif err := db.seqUser.SetUserReadSeq(ctx, conversationID, userID, seq); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (db *msgTransferDatabase) SetHasReadSeqToDB(ctx context.Context, conversationID string, userSeqMap map[string]int64) error {\n\tfor userID, seq := range userSeqMap {\n\t\tif err := db.seqUser.SetUserReadSeqToDB(ctx, conversationID, userID, seq); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (db *msgTransferDatabase) MsgToPushMQ(ctx context.Context, key, conversationID string, msg2mq *sdkws.MsgData) error {\n\tdata, err := proto.Marshal(&pbmsg.PushMsgDataToMQ{MsgData: msg2mq, ConversationID: conversationID})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := db.producerToPush.SendMessage(ctx, key, data); err != nil {\n\t\tlog.ZError(ctx, \"MsgToPushMQ\", err, \"key\", key, \"conversationID\", conversationID)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (db *msgTransferDatabase) MsgToMongoMQ(ctx context.Context, key, conversationID string, messages []*sdkws.MsgData, lastSeq int64) error {\n\tif len(messages) > 0 {\n\t\tdata, err := proto.Marshal(&pbmsg.MsgDataToMongoByMQ{LastSeq: lastSeq, ConversationID: conversationID, MsgData: messages})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := db.producerToMongo.SendMessage(ctx, key, data); err != nil {\n\t\t\tlog.ZError(ctx, \"MsgToMongoMQ\", err, \"key\", key, \"conversationID\", conversationID, \"lastSeq\", lastSeq)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/push.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/protocol/push\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mq\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\ntype PushDatabase interface {\n\tDelFcmToken(ctx context.Context, userID string, platformID int) error\n\tMsgToOfflinePushMQ(ctx context.Context, key string, userIDs []string, msg2mq *sdkws.MsgData) error\n}\n\ntype pushDataBase struct {\n\tcache                 cache.ThirdCache\n\tproducerToOfflinePush mq.Producer\n}\n\nfunc NewPushDatabase(cache cache.ThirdCache, offlinePushProducer mq.Producer) PushDatabase {\n\treturn &pushDataBase{\n\t\tcache:                 cache,\n\t\tproducerToOfflinePush: offlinePushProducer,\n\t}\n}\n\nfunc (p *pushDataBase) DelFcmToken(ctx context.Context, userID string, platformID int) error {\n\treturn p.cache.DelFcmToken(ctx, userID, platformID)\n}\n\nfunc (p *pushDataBase) MsgToOfflinePushMQ(ctx context.Context, key string, userIDs []string, msg2mq *sdkws.MsgData) error {\n\tdata, err := proto.Marshal(&push.PushMsgReq{MsgData: msg2mq, UserIDs: userIDs})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := p.producerToOfflinePush.SendMessage(ctx, key, data); err != nil {\n\t\tlog.ZError(ctx, \"message is push to offlinePush topic\", err, \"key\", key, \"userIDs\", userIDs, \"msg\", msg2mq.String())\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/s3.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tredisCache \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/tools/s3\"\n\t\"github.com/openimsdk/tools/s3/cont\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype S3Database interface {\n\tPartLimit() (*s3.PartLimit, error)\n\tPartSize(ctx context.Context, size int64) (int64, error)\n\tAuthSign(ctx context.Context, uploadID string, partNumbers []int) (*s3.AuthSignResult, error)\n\tInitiateMultipartUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int, contentType string) (*cont.InitiateUploadResult, error)\n\tCompleteMultipartUpload(ctx context.Context, uploadID string, parts []string) (*cont.UploadResult, error)\n\tAccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (time.Time, string, error)\n\tSetObject(ctx context.Context, info *model.Object) error\n\tStatObject(ctx context.Context, name string) (*s3.ObjectInfo, error)\n\tFormData(ctx context.Context, name string, size int64, contentType string, duration time.Duration) (*s3.FormData, error)\n\tFindExpirationObject(ctx context.Context, engine string, expiration time.Time, needDelType []string, count int64) ([]*model.Object, error)\n\tDeleteSpecifiedData(ctx context.Context, engine string, name []string) error\n\tDelS3Key(ctx context.Context, engine string, keys ...string) error\n\tGetKeyCount(ctx context.Context, engine string, key string) (int64, error)\n}\n\nfunc NewS3Database(rdb redis.UniversalClient, s3 s3.Interface, obj database.ObjectInfo) S3Database {\n\treturn &s3Database{\n\t\ts3:      cont.New(redisCache.NewS3Cache(rdb, s3), s3),\n\t\tcache:   redisCache.NewObjectCacheRedis(rdb, obj),\n\t\ts3cache: redisCache.NewS3Cache(rdb, s3),\n\t\tdb:      obj,\n\t}\n}\n\ntype s3Database struct {\n\ts3      *cont.Controller\n\tcache   cache.ObjectCache\n\ts3cache cont.S3Cache\n\tdb      database.ObjectInfo\n}\n\nfunc (s *s3Database) PartSize(ctx context.Context, size int64) (int64, error) {\n\treturn s.s3.PartSize(ctx, size)\n}\n\nfunc (s *s3Database) PartLimit() (*s3.PartLimit, error) {\n\treturn s.s3.PartLimit()\n}\n\nfunc (s *s3Database) AuthSign(ctx context.Context, uploadID string, partNumbers []int) (*s3.AuthSignResult, error) {\n\treturn s.s3.AuthSign(ctx, uploadID, partNumbers)\n}\n\nfunc (s *s3Database) InitiateMultipartUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int, contentType string) (*cont.InitiateUploadResult, error) {\n\treturn s.s3.InitiateUploadContentType(ctx, hash, size, expire, maxParts, contentType)\n}\n\nfunc (s *s3Database) CompleteMultipartUpload(ctx context.Context, uploadID string, parts []string) (*cont.UploadResult, error) {\n\treturn s.s3.CompleteUpload(ctx, uploadID, parts)\n}\n\nfunc (s *s3Database) SetObject(ctx context.Context, info *model.Object) error {\n\tinfo.Engine = s.s3.Engine()\n\tif err := s.db.SetObject(ctx, info); err != nil {\n\t\treturn err\n\t}\n\treturn s.cache.DelObjectName(info.Engine, info.Name).ChainExecDel(ctx)\n}\n\nfunc (s *s3Database) AccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (time.Time, string, error) {\n\tobj, err := s.cache.GetName(ctx, s.s3.Engine(), name)\n\tif err != nil {\n\t\treturn time.Time{}, \"\", err\n\t}\n\tif opt == nil {\n\t\topt = &s3.AccessURLOption{}\n\t}\n\tif opt.ContentType == \"\" {\n\t\topt.ContentType = obj.ContentType\n\t}\n\tif opt.Filename == \"\" {\n\t\topt.Filename = filepath.Base(obj.Name)\n\t}\n\texpireTime := time.Now().Add(expire)\n\trawURL, err := s.s3.AccessURL(ctx, obj.Key, expire, opt)\n\tif err != nil {\n\t\treturn time.Time{}, \"\", err\n\t}\n\treturn expireTime, rawURL, nil\n}\n\nfunc (s *s3Database) StatObject(ctx context.Context, name string) (*s3.ObjectInfo, error) {\n\treturn s.s3.StatObject(ctx, name)\n}\n\nfunc (s *s3Database) FormData(ctx context.Context, name string, size int64, contentType string, duration time.Duration) (*s3.FormData, error) {\n\treturn s.s3.FormData(ctx, name, size, contentType, duration)\n}\n\nfunc (s *s3Database) FindExpirationObject(ctx context.Context, engine string, expiration time.Time, needDelType []string, count int64) ([]*model.Object, error) {\n\treturn s.db.FindExpirationObject(ctx, engine, expiration, needDelType, count)\n}\n\nfunc (s *s3Database) GetKeyCount(ctx context.Context, engine string, key string) (int64, error) {\n\treturn s.db.GetKeyCount(ctx, engine, key)\n}\n\nfunc (s *s3Database) DeleteSpecifiedData(ctx context.Context, engine string, name []string) error {\n\treturn s.db.Delete(ctx, engine, name)\n}\n\nfunc (s *s3Database) DelS3Key(ctx context.Context, engine string, keys ...string) error {\n\treturn s.s3cache.DelS3Key(ctx, engine, keys...)\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/third.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\ntype ThirdDatabase interface {\n\tFcmUpdateToken(ctx context.Context, account string, platformID int, fcmToken string, expireTime int64) error\n\tSetAppBadge(ctx context.Context, userID string, value int) error\n\t// about log for debug\n\tUploadLogs(ctx context.Context, logs []*model.Log) error\n\tDeleteLogs(ctx context.Context, logID []string, userID string) error\n\tSearchLogs(ctx context.Context, keyword string, start time.Time, end time.Time, pagination pagination.Pagination) (int64, []*model.Log, error)\n\tGetLogs(ctx context.Context, LogIDs []string, userID string) ([]*model.Log, error)\n}\n\ntype thirdDatabase struct {\n\tcache cache.ThirdCache\n\tlogdb database.Log\n}\n\n// DeleteLogs implements ThirdDatabase.\nfunc (t *thirdDatabase) DeleteLogs(ctx context.Context, logID []string, userID string) error {\n\treturn t.logdb.Delete(ctx, logID, userID)\n}\n\n// GetLogs implements ThirdDatabase.\nfunc (t *thirdDatabase) GetLogs(ctx context.Context, LogIDs []string, userID string) ([]*model.Log, error) {\n\treturn t.logdb.Get(ctx, LogIDs, userID)\n}\n\n// SearchLogs implements ThirdDatabase.\nfunc (t *thirdDatabase) SearchLogs(ctx context.Context, keyword string, start time.Time, end time.Time, pagination pagination.Pagination) (int64, []*model.Log, error) {\n\treturn t.logdb.Search(ctx, keyword, start, end, pagination)\n}\n\n// UploadLogs implements ThirdDatabase.\nfunc (t *thirdDatabase) UploadLogs(ctx context.Context, logs []*model.Log) error {\n\treturn t.logdb.Create(ctx, logs)\n}\n\nfunc NewThirdDatabase(cache cache.ThirdCache, logdb database.Log) ThirdDatabase {\n\treturn &thirdDatabase{cache: cache, logdb: logdb}\n}\n\nfunc (t *thirdDatabase) FcmUpdateToken(ctx context.Context, account string, platformID int, fcmToken string, expireTime int64) error {\n\treturn t.cache.SetFcmToken(ctx, account, platformID, fcmToken, expireTime)\n}\n\nfunc (t *thirdDatabase) SetAppBadge(ctx context.Context, userID string, value int) error {\n\treturn t.cache.SetUserBadgeUnreadCountSum(ctx, userID, value)\n}\n"
  },
  {
    "path": "pkg/common/storage/controller/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage controller\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/db/tx\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache\"\n)\n\ntype UserDatabase interface {\n\t// FindWithError Get the information of the specified user. If the userID is not found, it will also return an error\n\tFindWithError(ctx context.Context, userIDs []string) (users []*model.User, err error)\n\t// Find Get the information of the specified user If the userID is not found, no error will be returned\n\tFind(ctx context.Context, userIDs []string) (users []*model.User, err error)\n\t// Find userInfo By Nickname\n\tFindByNickname(ctx context.Context, nickname string) (users []*model.User, err error)\n\t// FindNotification find system account by level\n\tFindNotification(ctx context.Context, level int64) (users []*model.User, err error)\n\t// FindSystemAccount find all system account\n\tFindSystemAccount(ctx context.Context) (users []*model.User, err error)\n\t// Create Insert multiple external guarantees that the userID is not repeated and does not exist in the storage\n\tCreate(ctx context.Context, users []*model.User) (err error)\n\t// UpdateByMap update (zero value) external guarantee userID exists\n\tUpdateByMap(ctx context.Context, userID string, args map[string]any) (err error)\n\t// FindUser\n\tPageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error)\n\t// FindUser with keyword\n\tPageFindUserWithKeyword(ctx context.Context, level1 int64, level2 int64, userID string, nickName string, pagination pagination.Pagination) (count int64, users []*model.User, err error)\n\t// Page If not found, no error is returned\n\tPage(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error)\n\t// IsExist true as long as one exists\n\tIsExist(ctx context.Context, userIDs []string) (exist bool, err error)\n\t// GetAllUserID Get all user IDs\n\tGetAllUserID(ctx context.Context, pagination pagination.Pagination) (int64, []string, error)\n\t// Get user by userID\n\tGetUserByID(ctx context.Context, userID string) (user *model.User, err error)\n\t// InitOnce Inside the function, first query whether it exists in the storage, if it exists, do nothing; if it does not exist, insert it\n\tInitOnce(ctx context.Context, users []*model.User) (err error)\n\t// CountTotal Get the total number of users\n\tCountTotal(ctx context.Context, before *time.Time) (int64, error)\n\t// CountRangeEverydayTotal Get the user increment in the range\n\tCountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error)\n\n\tSortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error)\n\n\t// CRUD user command\n\tAddUserCommand(ctx context.Context, userID string, Type int32, UUID string, value string, ex string) error\n\tDeleteUserCommand(ctx context.Context, userID string, Type int32, UUID string) error\n\tUpdateUserCommand(ctx context.Context, userID string, Type int32, UUID string, val map[string]any) error\n\tGetUserCommands(ctx context.Context, userID string, Type int32) ([]*user.CommandInfoResp, error)\n\tGetAllUserCommands(ctx context.Context, userID string) ([]*user.AllCommandInfoResp, error)\n}\n\ntype userDatabase struct {\n\ttx     tx.Tx\n\tuserDB database.User\n\tcache  cache.UserCache\n}\n\nfunc NewUserDatabase(userDB database.User, cache cache.UserCache, tx tx.Tx) UserDatabase {\n\treturn &userDatabase{userDB: userDB, cache: cache, tx: tx}\n}\n\nfunc (u *userDatabase) InitOnce(ctx context.Context, users []*model.User) error {\n\t// Extract user IDs from the given user models.\n\tuserIDs := datautil.Slice(users, func(e *model.User) string {\n\t\treturn e.UserID\n\t})\n\n\t// Find existing users in the database.\n\texistingUsers, err := u.userDB.Find(ctx, userIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Determine which users are missing from the database.\n\tvar (\n\t\tmissing, update []*model.User\n\t)\n\texistMap := datautil.SliceToMap(existingUsers, func(e *model.User) string {\n\t\treturn e.UserID\n\t})\n\torgMap := datautil.SliceToMap(users, func(e *model.User) string { return e.UserID })\n\tfor k, u1 := range orgMap {\n\t\tif u2, ok := existMap[k]; !ok {\n\t\t\tmissing = append(missing, u1)\n\t\t} else if u1.Nickname != u2.Nickname {\n\t\t\tupdate = append(update, u1)\n\t\t}\n\t}\n\n\t// Create records for missing users.\n\tif len(missing) > 0 {\n\t\tif err := u.userDB.Create(ctx, missing); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(update) > 0 {\n\t\tfor i := range update {\n\t\t\tif err := u.userDB.UpdateByMap(ctx, update[i].UserID, map[string]any{\"nickname\": update[i].Nickname}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// FindWithError Get the information of the specified user and return an error if the userID is not found.\nfunc (u *userDatabase) FindWithError(ctx context.Context, userIDs []string) (users []*model.User, err error) {\n\tuserIDs = datautil.Distinct(userIDs)\n\n\t// TODO: Add logic to identify which user IDs are distinct and which user IDs were not found.\n\n\tusers, err = u.cache.GetUsersInfo(ctx, userIDs)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif len(users) != len(userIDs) {\n\t\terr = errs.ErrRecordNotFound.WrapMsg(\"userID not found\")\n\t}\n\treturn\n}\n\n// Find Get the information of the specified user. If the userID is not found, no error will be returned.\nfunc (u *userDatabase) Find(ctx context.Context, userIDs []string) (users []*model.User, err error) {\n\treturn u.cache.GetUsersInfo(ctx, userIDs)\n}\n\nfunc (u *userDatabase) FindByNickname(ctx context.Context, nickname string) (users []*model.User, err error) {\n\treturn u.userDB.TakeByNickname(ctx, nickname)\n}\n\nfunc (u *userDatabase) FindNotification(ctx context.Context, level int64) (users []*model.User, err error) {\n\treturn u.userDB.TakeNotification(ctx, level)\n}\n\nfunc (u *userDatabase) FindSystemAccount(ctx context.Context) (users []*model.User, err error) {\n\treturn u.userDB.TakeGTEAppManagerLevel(ctx, constant.AppNotificationAdmin)\n}\n\n// Create Insert multiple external guarantees that the userID is not repeated and does not exist in the storage.\nfunc (u *userDatabase) Create(ctx context.Context, users []*model.User) (err error) {\n\treturn u.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err = u.userDB.Create(ctx, users); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn u.cache.DelUsersInfo(datautil.Slice(users, func(e *model.User) string {\n\t\t\treturn e.UserID\n\t\t})...).ChainExecDel(ctx)\n\t})\n}\n\n// UpdateByMap update (zero value) externally guarantees that userID exists.\nfunc (u *userDatabase) UpdateByMap(ctx context.Context, userID string, args map[string]any) (err error) {\n\treturn u.tx.Transaction(ctx, func(ctx context.Context) error {\n\t\tif err := u.userDB.UpdateByMap(ctx, userID, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn u.cache.DelUsersInfo(userID).ChainExecDel(ctx)\n\t})\n}\n\n// Page Gets, returns no error if not found.\nfunc (u *userDatabase) Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error) {\n\treturn u.userDB.Page(ctx, pagination)\n}\n\nfunc (u *userDatabase) PageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error) {\n\treturn u.userDB.PageFindUser(ctx, level1, level2, pagination)\n}\n\nfunc (u *userDatabase) PageFindUserWithKeyword(ctx context.Context, level1 int64, level2 int64, userID, nickName string, pagination pagination.Pagination) (count int64, users []*model.User, err error) {\n\treturn u.userDB.PageFindUserWithKeyword(ctx, level1, level2, userID, nickName, pagination)\n}\n\n// IsExist Does userIDs exist? As long as there is one, it will be true.\nfunc (u *userDatabase) IsExist(ctx context.Context, userIDs []string) (exist bool, err error) {\n\tusers, err := u.userDB.Find(ctx, userIDs)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif len(users) > 0 {\n\t\treturn true, nil\n\t}\n\treturn false, nil\n}\n\n// GetAllUserID Get all user IDs.\nfunc (u *userDatabase) GetAllUserID(ctx context.Context, pagination pagination.Pagination) (total int64, userIDs []string, err error) {\n\treturn u.userDB.GetAllUserID(ctx, pagination)\n}\n\nfunc (u *userDatabase) GetUserByID(ctx context.Context, userID string) (user *model.User, err error) {\n\treturn u.cache.GetUserInfo(ctx, userID)\n}\n\n// CountTotal Get the total number of users.\nfunc (u *userDatabase) CountTotal(ctx context.Context, before *time.Time) (count int64, err error) {\n\treturn u.userDB.CountTotal(ctx, before)\n}\n\n// CountRangeEverydayTotal Get the user increment in the range.\nfunc (u *userDatabase) CountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error) {\n\treturn u.userDB.CountRangeEverydayTotal(ctx, start, end)\n}\n\nfunc (u *userDatabase) SortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error) {\n\treturn u.userDB.SortQuery(ctx, userIDName, asc)\n}\n\nfunc (u *userDatabase) AddUserCommand(ctx context.Context, userID string, Type int32, UUID string, value string, ex string) error {\n\treturn u.userDB.AddUserCommand(ctx, userID, Type, UUID, value, ex)\n}\n\nfunc (u *userDatabase) DeleteUserCommand(ctx context.Context, userID string, Type int32, UUID string) error {\n\treturn u.userDB.DeleteUserCommand(ctx, userID, Type, UUID)\n}\n\nfunc (u *userDatabase) UpdateUserCommand(ctx context.Context, userID string, Type int32, UUID string, val map[string]any) error {\n\treturn u.userDB.UpdateUserCommand(ctx, userID, Type, UUID, val)\n}\n\nfunc (u *userDatabase) GetUserCommands(ctx context.Context, userID string, Type int32) ([]*user.CommandInfoResp, error) {\n\tcommands, err := u.userDB.GetUserCommand(ctx, userID, Type)\n\treturn commands, err\n}\n\nfunc (u *userDatabase) GetAllUserCommands(ctx context.Context, userID string) ([]*user.AllCommandInfoResp, error) {\n\tcommands, err := u.userDB.GetAllUserCommand(ctx, userID)\n\treturn commands, err\n}\n"
  },
  {
    "path": "pkg/common/storage/database/black.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\ntype Black interface {\n\tCreate(ctx context.Context, blacks []*model.Black) (err error)\n\tDelete(ctx context.Context, blacks []*model.Black) (err error)\n\tFind(ctx context.Context, blacks []*model.Black) (blackList []*model.Black, err error)\n\tTake(ctx context.Context, ownerUserID, blockUserID string) (black *model.Black, err error)\n\tFindOwnerBlacks(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, blacks []*model.Black, err error)\n\tFindOwnerBlackInfos(ctx context.Context, ownerUserID string, userIDs []string) (blacks []*model.Black, err error)\n\tFindBlackUserIDs(ctx context.Context, ownerUserID string) (blackUserIDs []string, err error)\n}\n\nvar (\n\t_ Black = (*mgoImpl)(nil)\n\t_ Black = (*redisImpl)(nil)\n)\n\ntype mgoImpl struct {\n}\n\nfunc (m *mgoImpl) Create(ctx context.Context, blacks []*model.Black) (err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (m *mgoImpl) Delete(ctx context.Context, blacks []*model.Black) (err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (m *mgoImpl) Find(ctx context.Context, blacks []*model.Black) (blackList []*model.Black, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (m *mgoImpl) Take(ctx context.Context, ownerUserID, blockUserID string) (black *model.Black, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (m *mgoImpl) FindOwnerBlacks(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, blacks []*model.Black, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (m *mgoImpl) FindOwnerBlackInfos(ctx context.Context, ownerUserID string, userIDs []string) (blacks []*model.Black, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (m *mgoImpl) FindBlackUserIDs(ctx context.Context, ownerUserID string) (blackUserIDs []string, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\ntype redisImpl struct {\n}\n\nfunc (r *redisImpl) Create(ctx context.Context, blacks []*model.Black) (err error) {\n\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (r *redisImpl) Delete(ctx context.Context, blacks []*model.Black) (err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (r *redisImpl) Find(ctx context.Context, blacks []*model.Black) (blackList []*model.Black, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (r *redisImpl) Take(ctx context.Context, ownerUserID, blockUserID string) (black *model.Black, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (r *redisImpl) FindOwnerBlacks(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, blacks []*model.Black, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (r *redisImpl) FindOwnerBlackInfos(ctx context.Context, ownerUserID string, userIDs []string) (blacks []*model.Black, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (r *redisImpl) FindBlackUserIDs(ctx context.Context, ownerUserID string) (blackUserIDs []string, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n"
  },
  {
    "path": "pkg/common/storage/database/cache.go",
    "content": "package database\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\ntype Cache interface {\n\tGet(ctx context.Context, key []string) (map[string]string, error)\n\tPrefix(ctx context.Context, prefix string) (map[string]string, error)\n\tSet(ctx context.Context, key string, value string, expireAt time.Duration) error\n\tIncr(ctx context.Context, key string, value int) (int, error)\n\tDel(ctx context.Context, key []string) error\n\tLock(ctx context.Context, key string, duration time.Duration) (string, error)\n\tUnlock(ctx context.Context, key string, value string) error\n}\n"
  },
  {
    "path": "pkg/common/storage/database/client_config.go",
    "content": "package database\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\ntype ClientConfig interface {\n\tSet(ctx context.Context, userID string, config map[string]string) error\n\tGet(ctx context.Context, userID string) (map[string]string, error)\n\tDel(ctx context.Context, userID string, keys []string) error\n\tGetPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\ntype Conversation interface {\n\tCreate(ctx context.Context, conversations []*model.Conversation) (err error)\n\tUpdateByMap(ctx context.Context, userIDs []string, conversationID string, args map[string]any) (rows int64, err error)\n\tUpdateUserConversations(ctx context.Context, userID string, args map[string]any) ([]*model.Conversation, error)\n\tUpdate(ctx context.Context, conversation *model.Conversation) (err error)\n\tFind(ctx context.Context, ownerUserID string, conversationIDs []string) (conversations []*model.Conversation, err error)\n\tFindUserID(ctx context.Context, userIDs []string, conversationIDs []string) ([]string, error)\n\tFindUserIDAllConversationID(ctx context.Context, userID string) ([]string, error)\n\tFindUserIDAllNotNotifyConversationID(ctx context.Context, userID string) ([]string, error)\n\tFindUserIDAllPinnedConversationID(ctx context.Context, userID string) ([]string, error)\n\tTake(ctx context.Context, userID, conversationID string) (conversation *model.Conversation, err error)\n\tFindConversationID(ctx context.Context, userID string, conversationIDs []string) (existConversationID []string, err error)\n\tFindUserIDAllConversations(ctx context.Context, userID string) (conversations []*model.Conversation, err error)\n\tFindRecvMsgUserIDs(ctx context.Context, conversationID string, recvOpts []int) ([]string, error)\n\tGetUserRecvMsgOpt(ctx context.Context, ownerUserID, conversationID string) (opt int, err error)\n\tGetAllConversationIDs(ctx context.Context) ([]string, error)\n\tGetAllConversationIDsNumber(ctx context.Context) (int64, error)\n\tPageConversationIDs(ctx context.Context, pagination pagination.Pagination) (conversationIDs []string, err error)\n\tGetConversationIDsNeedDestruct(ctx context.Context) ([]*model.Conversation, error)\n\tGetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error)\n\tFindConversationUserVersion(ctx context.Context, userID string, version uint, limit int) (*model.VersionLog, error)\n\tFindRandConversation(ctx context.Context, ts int64, limit int) ([]*model.Conversation, error)\n\tDeleteUsersConversations(ctx context.Context, userID string, conversationIDs []string) (err error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage database // import \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model/relation\"\n"
  },
  {
    "path": "pkg/common/storage/database/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\n// Friend defines the operations for managing friends in MongoDB.\ntype Friend interface {\n\t// Create inserts multiple friend records.\n\tCreate(ctx context.Context, friends []*model.Friend) (err error)\n\t// Delete removes specified friends of the owner user.\n\tDelete(ctx context.Context, ownerUserID string, friendUserIDs []string) (err error)\n\t// UpdateByMap updates specific fields of a friend document using a map.\n\tUpdateByMap(ctx context.Context, ownerUserID string, friendUserID string, args map[string]any) (err error)\n\t// UpdateRemark modify remarks.\n\tUpdateRemark(ctx context.Context, ownerUserID, friendUserID, remark string) (err error)\n\t// Take retrieves a single friend document. Returns an error if not found.\n\tTake(ctx context.Context, ownerUserID, friendUserID string) (friend *model.Friend, err error)\n\t// FindUserState finds the friendship status between two users.\n\tFindUserState(ctx context.Context, userID1, userID2 string) (friends []*model.Friend, err error)\n\t// FindFriends retrieves a list of friends for a given owner. Missing friends do not cause an error.\n\tFindFriends(ctx context.Context, ownerUserID string, friendUserIDs []string) (friends []*model.Friend, err error)\n\t// FindReversalFriends finds users who have added the specified user as a friend.\n\tFindReversalFriends(ctx context.Context, friendUserID string, ownerUserIDs []string) (friends []*model.Friend, err error)\n\t// FindOwnerFriends retrieves a paginated list of friends for a given owner.\n\tFindOwnerFriends(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, friends []*model.Friend, err error)\n\t// FindInWhoseFriends finds users who have added the specified user as a friend, with pagination.\n\tFindInWhoseFriends(ctx context.Context, friendUserID string, pagination pagination.Pagination) (total int64, friends []*model.Friend, err error)\n\t// FindFriendUserIDs retrieves a list of friend user IDs for a given owner.\n\tFindFriendUserIDs(ctx context.Context, ownerUserID string) (friendUserIDs []string, err error)\n\t// UpdateFriends update friends' fields\n\tUpdateFriends(ctx context.Context, ownerUserID string, friendUserIDs []string, val map[string]any) (err error)\n\n\tFindIncrVersion(ctx context.Context, ownerUserID string, version uint, limit int) (*model.VersionLog, error)\n\n\tFindFriendUserID(ctx context.Context, friendUserID string) ([]string, error)\n\n\t//SearchFriend(ctx context.Context, ownerUserID, keyword string, pagination pagination.Pagination) (int64, []*model.Friend, error)\n\n\tFindOwnerFriendUserIds(ctx context.Context, ownerUserID string, limit int) ([]string, error)\n\n\tIncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error\n}\n"
  },
  {
    "path": "pkg/common/storage/database/friend_request.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\ntype FriendRequest interface {\n\t// Insert multiple records\n\tCreate(ctx context.Context, friendRequests []*model.FriendRequest) (err error)\n\t// Delete record\n\tDelete(ctx context.Context, fromUserID, toUserID string) (err error)\n\t// Update with zero values\n\tUpdateByMap(ctx context.Context, formUserID string, toUserID string, args map[string]any) (err error)\n\t// Update multiple records (non-zero values)\n\tUpdate(ctx context.Context, friendRequest *model.FriendRequest) (err error)\n\t// Get friend requests sent to a specific user, no error returned if not found\n\tFind(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error)\n\tTake(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error)\n\t// Get list of friend requests received by toUserID\n\tFindToUserID(ctx context.Context, toUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error)\n\t// Get list of friend requests sent by fromUserID\n\tFindFromUserID(ctx context.Context, fromUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error)\n\tFindBothFriendRequests(ctx context.Context, fromUserID, toUserID string) (friends []*model.FriendRequest, err error)\n\tGetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"time\"\n)\n\ntype Group interface {\n\tCreate(ctx context.Context, groups []*model.Group) (err error)\n\tUpdateMap(ctx context.Context, groupID string, args map[string]any) (err error)\n\tUpdateStatus(ctx context.Context, groupID string, status int32) (err error)\n\tFind(ctx context.Context, groupIDs []string) (groups []*model.Group, err error)\n\tTake(ctx context.Context, groupID string) (group *model.Group, err error)\n\tSearch(ctx context.Context, keyword string, pagination pagination.Pagination) (total int64, groups []*model.Group, err error)\n\t// Get Group total quantity\n\tCountTotal(ctx context.Context, before *time.Time) (count int64, err error)\n\t// Get Group total quantity every day\n\tCountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error)\n\n\tFindJoinSortGroupID(ctx context.Context, groupIDs []string) ([]string, error)\n\n\tSearchJoin(ctx context.Context, groupIDs []string, keyword string, pagination pagination.Pagination) (int64, []*model.Group, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/group_member.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\ntype GroupMember interface {\n\tCreate(ctx context.Context, groupMembers []*model.GroupMember) (err error)\n\tDelete(ctx context.Context, groupID string, userIDs []string) (err error)\n\tUpdate(ctx context.Context, groupID string, userID string, data map[string]any) (err error)\n\tUpdateRoleLevel(ctx context.Context, groupID string, userID string, roleLevel int32) error\n\tUpdateUserRoleLevels(ctx context.Context, groupID string, firstUserID string, firstUserRoleLevel int32, secondUserID string, secondUserRoleLevel int32) error\n\tFindMemberUserID(ctx context.Context, groupID string) (userIDs []string, err error)\n\tTake(ctx context.Context, groupID string, userID string) (groupMember *model.GroupMember, err error)\n\tFind(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupMember, error)\n\tFindInGroup(ctx context.Context, userID string, groupIDs []string) ([]*model.GroupMember, error)\n\tTakeOwner(ctx context.Context, groupID string) (groupMember *model.GroupMember, err error)\n\tSearchMember(ctx context.Context, keyword string, groupID string, pagination pagination.Pagination) (total int64, groupList []*model.GroupMember, err error)\n\tFindRoleLevelUserIDs(ctx context.Context, groupID string, roleLevel int32) ([]string, error)\n\tFindUserJoinedGroupID(ctx context.Context, userID string) (groupIDs []string, err error)\n\tTakeGroupMemberNum(ctx context.Context, groupID string) (count int64, err error)\n\tFindUserManagedGroupID(ctx context.Context, userID string) (groupIDs []string, err error)\n\tIsUpdateRoleLevel(data map[string]any) bool\n\tJoinGroupIncrVersion(ctx context.Context, userID string, groupIDs []string, state int32) error\n\tMemberGroupIncrVersion(ctx context.Context, groupID string, userIDs []string, state int32) error\n\tFindMemberIncrVersion(ctx context.Context, groupID string, version uint, limit int) (*model.VersionLog, error)\n\tBatchFindMemberIncrVersion(ctx context.Context, groupIDs []string, versions []uint, limits []int) ([]*model.VersionLog, error)\n\tFindJoinIncrVersion(ctx context.Context, userID string, version uint, limit int) (*model.VersionLog, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/group_request.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\ntype GroupRequest interface {\n\tCreate(ctx context.Context, groupRequests []*model.GroupRequest) (err error)\n\tDelete(ctx context.Context, groupID string, userID string) (err error)\n\tUpdateHandler(ctx context.Context, groupID string, userID string, handledMsg string, handleResult int32) (err error)\n\tTake(ctx context.Context, groupID string, userID string) (groupRequest *model.GroupRequest, err error)\n\tFindGroupRequests(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupRequest, error)\n\tPage(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error)\n\tPageGroup(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error)\n\tGetUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/log.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"time\"\n)\n\ntype Log interface {\n\tCreate(ctx context.Context, log []*model.Log) error\n\tSearch(ctx context.Context, keyword string, start time.Time, end time.Time, pagination pagination.Pagination) (int64, []*model.Log, error)\n\tDelete(ctx context.Context, logID []string, userID string) error\n\tGet(ctx context.Context, logIDs []string, userID string) ([]*model.Log, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/black.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewBlackMongo(db *mongo.Database) (database.Black, error) {\n\tcoll := db.Collection(database.BlackName)\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"owner_user_id\", Value: 1},\n\t\t\t{Key: \"block_user_id\", Value: 1},\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &BlackMgo{coll: coll}, nil\n}\n\ntype BlackMgo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (b *BlackMgo) blackFilter(ownerUserID, blockUserID string) bson.M {\n\treturn bson.M{\n\t\t\"owner_user_id\": ownerUserID,\n\t\t\"block_user_id\": blockUserID,\n\t}\n}\n\nfunc (b *BlackMgo) blacksFilter(blacks []*model.Black) bson.M {\n\tif len(blacks) == 0 {\n\t\treturn nil\n\t}\n\tor := make(bson.A, 0, len(blacks))\n\tfor _, black := range blacks {\n\t\tor = append(or, b.blackFilter(black.OwnerUserID, black.BlockUserID))\n\t}\n\treturn bson.M{\"$or\": or}\n}\n\nfunc (b *BlackMgo) Create(ctx context.Context, blacks []*model.Black) (err error) {\n\treturn mongoutil.InsertMany(ctx, b.coll, blacks)\n}\n\nfunc (b *BlackMgo) Delete(ctx context.Context, blacks []*model.Black) (err error) {\n\tif len(blacks) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.DeleteMany(ctx, b.coll, b.blacksFilter(blacks))\n}\n\nfunc (b *BlackMgo) UpdateByMap(ctx context.Context, ownerUserID, blockUserID string, args map[string]any) (err error) {\n\tif len(args) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.UpdateOne(ctx, b.coll, b.blackFilter(ownerUserID, blockUserID), bson.M{\"$set\": args}, false)\n}\n\nfunc (b *BlackMgo) Find(ctx context.Context, blacks []*model.Black) (blackList []*model.Black, err error) {\n\treturn mongoutil.Find[*model.Black](ctx, b.coll, b.blacksFilter(blacks))\n}\n\nfunc (b *BlackMgo) Take(ctx context.Context, ownerUserID, blockUserID string) (black *model.Black, err error) {\n\treturn mongoutil.FindOne[*model.Black](ctx, b.coll, b.blackFilter(ownerUserID, blockUserID))\n}\n\nfunc (b *BlackMgo) FindOwnerBlacks(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (total int64, blacks []*model.Black, err error) {\n\treturn mongoutil.FindPage[*model.Black](ctx, b.coll, bson.M{\"owner_user_id\": ownerUserID}, pagination)\n}\n\nfunc (b *BlackMgo) FindOwnerBlackInfos(ctx context.Context, ownerUserID string, userIDs []string) (blacks []*model.Black, err error) {\n\tif len(userIDs) == 0 {\n\t\treturn mongoutil.Find[*model.Black](ctx, b.coll, bson.M{\"owner_user_id\": ownerUserID})\n\t}\n\treturn mongoutil.Find[*model.Black](ctx, b.coll, bson.M{\"owner_user_id\": ownerUserID, \"block_user_id\": bson.M{\"$in\": userIDs}})\n}\n\nfunc (b *BlackMgo) FindBlackUserIDs(ctx context.Context, ownerUserID string) (blackUserIDs []string, err error) {\n\treturn mongoutil.Find[string](ctx, b.coll, bson.M{\"owner_user_id\": ownerUserID}, options.Find().SetProjection(bson.M{\"_id\": 0, \"block_user_id\": 1}))\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/cache.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewCacheMgo(db *mongo.Database) (*CacheMgo, error) {\n\tcoll := db.Collection(database.CacheName)\n\t_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"key\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index().SetUnique(true),\n\t\t},\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"expire_at\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index().SetExpireAfterSeconds(0),\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn &CacheMgo{coll: coll}, nil\n}\n\ntype CacheMgo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (x *CacheMgo) findToMap(res []model.Cache, now time.Time) map[string]string {\n\tkv := make(map[string]string)\n\tfor _, re := range res {\n\t\tif re.ExpireAt != nil && re.ExpireAt.Before(now) {\n\t\t\tcontinue\n\t\t}\n\t\tkv[re.Key] = re.Value\n\t}\n\treturn kv\n\n}\n\nfunc (x *CacheMgo) Get(ctx context.Context, key []string) (map[string]string, error) {\n\tif len(key) == 0 {\n\t\treturn nil, nil\n\t}\n\tnow := time.Now()\n\tres, err := mongoutil.Find[model.Cache](ctx, x.coll, bson.M{\n\t\t\"key\": bson.M{\"$in\": key},\n\t\t\"$or\": []bson.M{\n\t\t\t{\"expire_at\": bson.M{\"$gt\": now}},\n\t\t\t{\"expire_at\": nil},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn x.findToMap(res, now), nil\n}\n\nfunc (x *CacheMgo) Prefix(ctx context.Context, prefix string) (map[string]string, error) {\n\tnow := time.Now()\n\tres, err := mongoutil.Find[model.Cache](ctx, x.coll, bson.M{\n\t\t\"key\": bson.M{\"$regex\": \"^\" + prefix},\n\t\t\"$or\": []bson.M{\n\t\t\t{\"expire_at\": bson.M{\"$gt\": now}},\n\t\t\t{\"expire_at\": nil},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn x.findToMap(res, now), nil\n}\n\nfunc (x *CacheMgo) Set(ctx context.Context, key string, value string, expireAt time.Duration) error {\n\tcv := &model.Cache{\n\t\tKey:   key,\n\t\tValue: value,\n\t}\n\tif expireAt > 0 {\n\t\tnow := time.Now().Add(expireAt)\n\t\tcv.ExpireAt = &now\n\t}\n\topt := options.Update().SetUpsert(true)\n\treturn mongoutil.UpdateOne(ctx, x.coll, bson.M{\"key\": key}, bson.M{\"$set\": cv}, false, opt)\n}\n\nfunc (x *CacheMgo) Incr(ctx context.Context, key string, value int) (int, error) {\n\tpipeline := mongo.Pipeline{\n\t\t{\n\t\t\t{\"$set\", bson.M{\n\t\t\t\t\"value\": bson.M{\n\t\t\t\t\t\"$toString\": bson.M{\n\t\t\t\t\t\t\"$add\": bson.A{\n\t\t\t\t\t\t\tbson.M{\"$toInt\": \"$value\"},\n\t\t\t\t\t\t\tvalue,\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\topt := options.FindOneAndUpdate().SetReturnDocument(options.After)\n\tres, err := mongoutil.FindOneAndUpdate[model.Cache](ctx, x.coll, bson.M{\"key\": key}, pipeline, opt)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn strconv.Atoi(res.Value)\n}\n\nfunc (x *CacheMgo) Del(ctx context.Context, key []string) error {\n\tif len(key) == 0 {\n\t\treturn nil\n\t}\n\t_, err := x.coll.DeleteMany(ctx, bson.M{\"key\": bson.M{\"$in\": key}})\n\treturn errs.Wrap(err)\n}\n\nfunc (x *CacheMgo) lockKey(key string) string {\n\treturn \"LOCK_\" + key\n}\n\nfunc (x *CacheMgo) Lock(ctx context.Context, key string, duration time.Duration) (string, error) {\n\ttmp, err := uuid.NewUUID()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif duration <= 0 || duration > time.Minute*10 {\n\t\tduration = time.Minute * 10\n\t}\n\tcv := &model.Cache{\n\t\tKey:      x.lockKey(key),\n\t\tValue:    tmp.String(),\n\t\tExpireAt: nil,\n\t}\n\tctx, cancel := context.WithTimeout(ctx, time.Second*30)\n\tdefer cancel()\n\twait := func() error {\n\t\ttimeout := time.NewTimer(time.Millisecond * 100)\n\t\tdefer timeout.Stop()\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tcase <-timeout.C:\n\t\t\treturn nil\n\t\t}\n\t}\n\tfor {\n\t\tif err := mongoutil.DeleteOne(ctx, x.coll, bson.M{\"key\": key, \"expire_at\": bson.M{\"$lt\": time.Now()}}); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\texpireAt := time.Now().Add(duration)\n\t\tcv.ExpireAt = &expireAt\n\t\tif err := mongoutil.InsertMany[*model.Cache](ctx, x.coll, []*model.Cache{cv}); err != nil {\n\t\t\tif mongo.IsDuplicateKeyError(err) {\n\t\t\t\tif err := wait(); err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn cv.Value, nil\n\t}\n}\n\nfunc (x *CacheMgo) Unlock(ctx context.Context, key string, value string) error {\n\treturn mongoutil.DeleteOne(ctx, x.coll, bson.M{\"key\": x.lockKey(key), \"value\": value})\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/cache_test.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc TestName1111(t *testing.T) {\n\tcoll := Mongodb().Collection(\"temp\")\n\n\t//updatePipeline := mongo.Pipeline{\n\t//\t{\n\t//\t\t{\"$set\", bson.M{\n\t//\t\t\t\"age\": bson.M{\n\t//\t\t\t\t\"$toString\": bson.M{\n\t//\t\t\t\t\t\"$add\": bson.A{\n\t//\t\t\t\t\t\tbson.M{\"$toInt\": \"$age\"},\n\t//\t\t\t\t\t\t1,\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\n\tpipeline := mongo.Pipeline{\n\t\t{\n\t\t\t{\"$set\", bson.M{\n\t\t\t\t\"value\": bson.M{\n\t\t\t\t\t\"$toString\": bson.M{\n\t\t\t\t\t\t\"$add\": bson.A{\n\t\t\t\t\t\t\tbson.M{\"$toInt\": \"$value\"},\n\t\t\t\t\t\t\t1,\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\n\topt := options.FindOneAndUpdate().SetUpsert(false).SetReturnDocument(options.After)\n\tres, err := mongoutil.FindOneAndUpdate[model.Cache](context.Background(), coll, bson.M{\"key\": \"123456\"}, pipeline, opt)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(res)\n}\n\nfunc TestName33333(t *testing.T) {\n\tc, err := NewCacheMgo(Mongodb())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif err := c.Set(context.Background(), \"123456\", \"123456\", time.Hour); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := c.Set(context.Background(), \"123666\", \"123666\", time.Hour); err != nil {\n\t\tpanic(err)\n\t}\n\n\tres1, err := c.Get(context.Background(), []string{\"123456\"})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(res1)\n\n\tres2, err := c.Prefix(context.Background(), \"123\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(res2)\n}\n\nfunc TestName1111aa(t *testing.T) {\n\n\tc, err := NewCacheMgo(Mongodb())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar count int\n\n\tkey := \"123456\"\n\n\tdoFunc := func() {\n\t\tvalue, err := c.Lock(context.Background(), key, time.Second*30)\n\t\tif err != nil {\n\t\t\tt.Log(\"Lock error\", err)\n\t\t\treturn\n\t\t}\n\t\ttmp := count\n\t\ttmp++\n\t\tcount = tmp\n\t\tt.Log(\"count\", tmp)\n\t\tif err := c.Unlock(context.Background(), key, value); err != nil {\n\t\t\tt.Log(\"Unlock error\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif _, err := c.Lock(context.Background(), key, time.Second*10); err != nil {\n\t\tt.Log(err)\n\t\treturn\n\t}\n\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < 32; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tdoFunc()\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\n}\n\nfunc TestName111111a(t *testing.T) {\n\tarr := strings.SplitN(\"1:testkakskdask:1111\", \":\", 2)\n\tt.Log(arr)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/client_config.go",
    "content": "// Copyright © 2023 OpenIM open source community. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nfunc NewClientConfig(db *mongo.Database) (database.ClientConfig, error) {\n\tcoll := db.Collection(\"config\")\n\t_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"key\", Value: 1},\n\t\t\t\t{Key: \"user_id\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index().SetUnique(true),\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn &ClientConfig{\n\t\tcoll: coll,\n\t}, nil\n}\n\ntype ClientConfig struct {\n\tcoll *mongo.Collection\n}\n\nfunc (x *ClientConfig) Set(ctx context.Context, userID string, config map[string]string) error {\n\tif len(config) == 0 {\n\t\treturn nil\n\t}\n\tfor key, value := range config {\n\t\tfilter := bson.M{\"key\": key, \"user_id\": userID}\n\t\tupdate := bson.M{\n\t\t\t\"value\": value,\n\t\t}\n\t\terr := mongoutil.UpdateOne(ctx, x.coll, filter, bson.M{\"$set\": update}, false, options.Update().SetUpsert(true))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *ClientConfig) Get(ctx context.Context, userID string) (map[string]string, error) {\n\tcs, err := mongoutil.Find[*model.ClientConfig](ctx, x.coll, bson.M{\"user_id\": userID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcm := make(map[string]string)\n\tfor _, config := range cs {\n\t\tcm[config.Key] = config.Value\n\t}\n\treturn cm, nil\n}\n\nfunc (x *ClientConfig) Del(ctx context.Context, userID string, keys []string) error {\n\tif len(keys) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.DeleteMany(ctx, x.coll, bson.M{\"key\": bson.M{\"$in\": keys}, \"user_id\": userID})\n}\n\nfunc (x *ClientConfig) GetPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) {\n\tfilter := bson.M{}\n\tif userID != \"\" {\n\t\tfilter[\"user_id\"] = userID\n\t}\n\tif key != \"\" {\n\t\tfilter[\"key\"] = key\n\t}\n\treturn mongoutil.FindPage[*model.ClientConfig](ctx, x.coll, filter, pagination)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nfunc NewConversationMongo(db *mongo.Database) (*ConversationMgo, error) {\n\tcoll := db.Collection(database.ConversationName)\n\t_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"owner_user_id\", Value: 1},\n\t\t\t\t{Key: \"conversation_id\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index().SetUnique(true),\n\t\t},\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"user_id\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index(),\n\t\t},\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"conversation_id\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index().SetUnique(true),\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\tversion, err := NewVersionLog(db.Collection(database.ConversationVersionName))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &ConversationMgo{version: version, coll: coll}, nil\n}\n\ntype ConversationMgo struct {\n\tversion database.VersionLog\n\tcoll    *mongo.Collection\n}\n\nfunc (c *ConversationMgo) Create(ctx context.Context, conversations []*model.Conversation) (err error) {\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.InsertMany(ctx, c.coll, conversations)\n\t}, func() error {\n\t\tuserConversation := make(map[string][]string)\n\t\tfor _, conversation := range conversations {\n\t\t\tuserConversation[conversation.OwnerUserID] = append(userConversation[conversation.OwnerUserID], conversation.ConversationID)\n\t\t}\n\t\tfor userID, conversationIDs := range userConversation {\n\t\t\tif err := c.version.IncrVersion(ctx, userID, conversationIDs, model.VersionStateInsert); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (c *ConversationMgo) UpdateByMap(ctx context.Context, userIDs []string, conversationID string, args map[string]any) (int64, error) {\n\tif len(args) == 0 || len(userIDs) == 0 {\n\t\treturn 0, nil\n\t}\n\tfilter := bson.M{\n\t\t\"conversation_id\": conversationID,\n\t\t\"owner_user_id\":   bson.M{\"$in\": userIDs},\n\t}\n\tvar rows int64\n\terr := mongoutil.IncrVersion(func() error {\n\t\tres, err := mongoutil.UpdateMany(ctx, c.coll, filter, bson.M{\"$set\": args})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trows = res.ModifiedCount\n\t\treturn nil\n\t}, func() error {\n\t\tfor _, userID := range userIDs {\n\t\t\tif err := c.version.IncrVersion(ctx, userID, []string{conversationID}, model.VersionStateUpdate); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn rows, nil\n}\n\nfunc (c *ConversationMgo) UpdateUserConversations(ctx context.Context, userID string, args map[string]any) ([]*model.Conversation, error) {\n\tif len(args) == 0 {\n\t\treturn nil, nil\n\t}\n\tfilter := bson.M{\n\t\t\"user_id\": userID,\n\t}\n\n\tconversations, err := mongoutil.Find[*model.Conversation](ctx, c.coll, filter, options.Find().SetProjection(bson.M{\"_id\": 0, \"owner_user_id\": 1, \"conversation_id\": 1}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = mongoutil.IncrVersion(func() error {\n\t\t_, err := mongoutil.UpdateMany(ctx, c.coll, filter, bson.M{\"$set\": args})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}, func() error {\n\t\tfor _, conversation := range conversations {\n\t\t\tif err := c.version.IncrVersion(ctx, conversation.OwnerUserID, []string{conversation.ConversationID}, model.VersionStateUpdate); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn conversations, nil\n}\n\nfunc (c *ConversationMgo) Update(ctx context.Context, conversation *model.Conversation) (err error) {\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.UpdateOne(ctx, c.coll, bson.M{\"owner_user_id\": conversation.OwnerUserID, \"conversation_id\": conversation.ConversationID}, bson.M{\"$set\": conversation}, true)\n\t}, func() error {\n\t\treturn c.version.IncrVersion(ctx, conversation.OwnerUserID, []string{conversation.ConversationID}, model.VersionStateUpdate)\n\t})\n}\n\nfunc (c *ConversationMgo) Find(ctx context.Context, ownerUserID string, conversationIDs []string) (conversations []*model.Conversation, err error) {\n\treturn mongoutil.Find[*model.Conversation](ctx, c.coll, bson.M{\"owner_user_id\": ownerUserID, \"conversation_id\": bson.M{\"$in\": conversationIDs}})\n}\n\nfunc (c *ConversationMgo) FindUserID(ctx context.Context, userIDs []string, conversationIDs []string) ([]string, error) {\n\treturn mongoutil.Find[string](\n\t\tctx,\n\t\tc.coll,\n\t\tbson.M{\"owner_user_id\": bson.M{\"$in\": userIDs}, \"conversation_id\": bson.M{\"$in\": conversationIDs}},\n\t\toptions.Find().SetProjection(bson.M{\"_id\": 0, \"owner_user_id\": 1}),\n\t)\n}\nfunc (c *ConversationMgo) FindUserIDAllConversationID(ctx context.Context, userID string) ([]string, error) {\n\treturn mongoutil.Find[string](ctx, c.coll, bson.M{\"owner_user_id\": userID}, options.Find().SetProjection(bson.M{\"_id\": 0, \"conversation_id\": 1}))\n}\n\nfunc (c *ConversationMgo) FindUserIDAllNotNotifyConversationID(ctx context.Context, userID string) ([]string, error) {\n\treturn mongoutil.Find[string](ctx, c.coll, bson.M{\n\t\t\"owner_user_id\": userID,\n\t\t\"recv_msg_opt\":  constant.ReceiveNotNotifyMessage,\n\t}, options.Find().SetProjection(bson.M{\"_id\": 0, \"conversation_id\": 1}))\n}\n\nfunc (c *ConversationMgo) FindUserIDAllPinnedConversationID(ctx context.Context, userID string) ([]string, error) {\n\treturn mongoutil.Find[string](ctx, c.coll, bson.M{\n\t\t\"owner_user_id\": userID,\n\t\t\"is_pinned\":     true,\n\t}, options.Find().SetProjection(bson.M{\"_id\": 0, \"conversation_id\": 1}))\n}\n\nfunc (c *ConversationMgo) Take(ctx context.Context, userID, conversationID string) (conversation *model.Conversation, err error) {\n\treturn mongoutil.FindOne[*model.Conversation](ctx, c.coll, bson.M{\"owner_user_id\": userID, \"conversation_id\": conversationID})\n}\n\nfunc (c *ConversationMgo) FindConversationID(ctx context.Context, userID string, conversationIDs []string) (existConversationID []string, err error) {\n\treturn mongoutil.Find[string](ctx, c.coll, bson.M{\"owner_user_id\": userID, \"conversation_id\": bson.M{\"$in\": conversationIDs}}, options.Find().SetProjection(bson.M{\"_id\": 0, \"conversation_id\": 1}))\n}\n\nfunc (c *ConversationMgo) FindUserIDAllConversations(ctx context.Context, userID string) (conversations []*model.Conversation, err error) {\n\treturn mongoutil.Find[*model.Conversation](ctx, c.coll, bson.M{\"owner_user_id\": userID})\n}\n\nfunc (c *ConversationMgo) FindRecvMsgUserIDs(ctx context.Context, conversationID string, recvOpts []int) ([]string, error) {\n\tvar filter any\n\tif len(recvOpts) == 0 {\n\t\tfilter = bson.M{\"conversation_id\": conversationID}\n\t} else {\n\t\tfilter = bson.M{\"conversation_id\": conversationID, \"recv_msg_opt\": bson.M{\"$in\": recvOpts}}\n\t}\n\treturn mongoutil.Find[string](ctx, c.coll, filter, options.Find().SetProjection(bson.M{\"_id\": 0, \"owner_user_id\": 1}))\n}\n\nfunc (c *ConversationMgo) GetUserRecvMsgOpt(ctx context.Context, ownerUserID, conversationID string) (opt int, err error) {\n\treturn mongoutil.FindOne[int](ctx, c.coll, bson.M{\"owner_user_id\": ownerUserID, \"conversation_id\": conversationID}, options.FindOne().SetProjection(bson.M{\"recv_msg_opt\": 1}))\n}\n\nfunc (c *ConversationMgo) GetAllConversationIDs(ctx context.Context) ([]string, error) {\n\treturn mongoutil.Aggregate[string](ctx, c.coll, []bson.M{\n\t\t{\"$group\": bson.M{\"_id\": \"$conversation_id\"}},\n\t\t{\"$project\": bson.M{\"_id\": 0, \"conversation_id\": \"$_id\"}},\n\t})\n}\n\nfunc (c *ConversationMgo) GetAllConversationIDsNumber(ctx context.Context) (int64, error) {\n\tcounts, err := mongoutil.Aggregate[int64](ctx, c.coll, []bson.M{\n\t\t{\"$group\": bson.M{\"_id\": \"$conversation_id\"}},\n\t\t{\"$group\": bson.M{\"_id\": nil, \"count\": bson.M{\"$sum\": 1}}},\n\t\t{\"$project\": bson.M{\"_id\": 0}},\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif len(counts) == 0 {\n\t\treturn 0, nil\n\t}\n\treturn counts[0], nil\n}\n\nfunc (c *ConversationMgo) PageConversationIDs(ctx context.Context, pagination pagination.Pagination) (conversationIDs []string, err error) {\n\treturn mongoutil.FindPageOnly[string](ctx, c.coll, bson.M{}, pagination, options.Find().SetProjection(bson.M{\"conversation_id\": 1}))\n}\n\nfunc (c *ConversationMgo) GetConversationIDsNeedDestruct(ctx context.Context) ([]*model.Conversation, error) {\n\t// \"is_msg_destruct = 1 && msg_destruct_time != 0 && (UNIX_TIMESTAMP(NOW()) > (msg_destruct_time + UNIX_TIMESTAMP(latest_msg_destruct_time)) || latest_msg_destruct_time is NULL)\"\n\treturn mongoutil.Find[*model.Conversation](ctx, c.coll, bson.M{\n\t\t\"is_msg_destruct\":   1,\n\t\t\"msg_destruct_time\": bson.M{\"$ne\": 0},\n\t\t\"$or\": []bson.M{\n\t\t\t{\n\t\t\t\t\"$expr\": bson.M{\n\t\t\t\t\t\"$gt\": []any{\n\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\tbson.M{\"$add\": []any{\"$msg_destruct_time\", \"$latest_msg_destruct_time\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"latest_msg_destruct_time\": nil,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc (c *ConversationMgo) GetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error) {\n\treturn mongoutil.Find[string](\n\t\tctx,\n\t\tc.coll,\n\t\tbson.M{\"conversation_id\": conversationID, \"recv_msg_opt\": bson.M{\"$ne\": constant.ReceiveMessage}},\n\t\toptions.Find().SetProjection(bson.M{\"_id\": 0, \"owner_user_id\": 1}),\n\t)\n}\n\nfunc (c *ConversationMgo) FindConversationUserVersion(ctx context.Context, userID string, version uint, limit int) (*model.VersionLog, error) {\n\treturn c.version.FindChangeLog(ctx, userID, version, limit)\n}\n\nfunc (c *ConversationMgo) FindRandConversation(ctx context.Context, ts int64, limit int) ([]*model.Conversation, error) {\n\tpipeline := []bson.M{\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"is_msg_destruct\":   true,\n\t\t\t\t\"msg_destruct_time\": bson.M{\"$ne\": 0},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"next_msg_destruct_timestamp\": bson.M{\n\t\t\t\t\t\"$add\": []any{\n\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\"$toLong\": \"$latest_msg_destruct_time\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\"$multiply\": []any{\n\t\t\t\t\t\t\t\t\"$msg_destruct_time\",\n\t\t\t\t\t\t\t\t1000, // convert to milliseconds\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\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"next_msg_destruct_timestamp\": bson.M{\"$lt\": ts},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$sample\": bson.M{\n\t\t\t\t\"size\": limit,\n\t\t\t},\n\t\t},\n\t}\n\treturn mongoutil.Aggregate[*model.Conversation](ctx, c.coll, pipeline)\n}\n\nfunc (c *ConversationMgo) DeleteUsersConversations(ctx context.Context, userID string, conversationIDs []string) (err error) {\n\tif len(conversationIDs) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.IncrVersion(func() error {\n\t\terr := mongoutil.DeleteMany(ctx, c.coll, bson.M{\"owner_user_id\": userID, \"conversation_id\": bson.M{\"$in\": conversationIDs}})\n\t\treturn err\n\t}, func() error {\n\t\tfor _, conversationID := range conversationIDs {\n\t\t\tif err := c.version.IncrVersion(ctx, userID, []string{conversationID}, model.VersionStateDelete); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage mgo // import \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"time\"\n\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\n// FriendMgo implements Friend using MongoDB as the storage backend.\ntype FriendMgo struct {\n\tcoll  *mongo.Collection\n\towner database.VersionLog\n}\n\n// NewFriendMongo creates a new instance of FriendMgo with the provided MongoDB database.\nfunc NewFriendMongo(db *mongo.Database) (database.Friend, error) {\n\tcoll := db.Collection(database.FriendName)\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"owner_user_id\", Value: 1},\n\t\t\t{Key: \"friend_user_id\", Value: 1},\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\towner, err := NewVersionLog(db.Collection(database.FriendVersionName))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &FriendMgo{coll: coll, owner: owner}, nil\n}\n\nfunc (f *FriendMgo) friendSort() any {\n\treturn bson.D{{\"is_pinned\", -1}, {\"_id\", 1}}\n}\n\n// Create inserts multiple friend records.\nfunc (f *FriendMgo) Create(ctx context.Context, friends []*model.Friend) error {\n\tfor i, friend := range friends {\n\t\tif friend.ID.IsZero() {\n\t\t\tfriends[i].ID = primitive.NewObjectID()\n\t\t}\n\t\tif friend.CreateTime.IsZero() {\n\t\t\tfriends[i].CreateTime = time.Now()\n\t\t}\n\t}\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.InsertMany(ctx, f.coll, friends)\n\t}, func() error {\n\t\tmp := make(map[string][]string)\n\t\tfor _, friend := range friends {\n\t\t\tmp[friend.OwnerUserID] = append(mp[friend.OwnerUserID], friend.FriendUserID)\n\t\t}\n\t\tfor ownerUserID, friendUserIDs := range mp {\n\t\t\tif err := f.owner.IncrVersion(ctx, ownerUserID, friendUserIDs, model.VersionStateInsert); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// Delete removes specified friends of the owner user.\nfunc (f *FriendMgo) Delete(ctx context.Context, ownerUserID string, friendUserIDs []string) error {\n\tfilter := bson.M{\n\t\t\"owner_user_id\":  ownerUserID,\n\t\t\"friend_user_id\": bson.M{\"$in\": friendUserIDs},\n\t}\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.DeleteOne(ctx, f.coll, filter)\n\t}, func() error {\n\t\treturn f.owner.IncrVersion(ctx, ownerUserID, friendUserIDs, model.VersionStateDelete)\n\t})\n}\n\n// UpdateByMap updates specific fields of a friend document using a map.\nfunc (f *FriendMgo) UpdateByMap(ctx context.Context, ownerUserID string, friendUserID string, args map[string]any) error {\n\tif len(args) == 0 {\n\t\treturn nil\n\t}\n\tfilter := bson.M{\n\t\t\"owner_user_id\":  ownerUserID,\n\t\t\"friend_user_id\": friendUserID,\n\t}\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.UpdateOne(ctx, f.coll, filter, bson.M{\"$set\": args}, true)\n\t}, func() error {\n\t\tvar friendUserIDs []string\n\t\tif f.IsUpdateIsPinned(args) {\n\t\t\tfriendUserIDs = []string{model.VersionSortChangeID, friendUserID}\n\t\t} else {\n\t\t\tfriendUserIDs = []string{friendUserID}\n\t\t}\n\t\treturn f.owner.IncrVersion(ctx, ownerUserID, friendUserIDs, model.VersionStateUpdate)\n\t})\n}\n\n// UpdateRemark updates the remark for a specific friend.\nfunc (f *FriendMgo) UpdateRemark(ctx context.Context, ownerUserID, friendUserID, remark string) error {\n\treturn f.UpdateByMap(ctx, ownerUserID, friendUserID, map[string]any{\"remark\": remark})\n}\n\nfunc (f *FriendMgo) fillTime(friends ...*model.Friend) {\n\tfor i, friend := range friends {\n\t\tif friend.CreateTime.IsZero() {\n\t\t\tfriends[i].CreateTime = friend.ID.Timestamp()\n\t\t}\n\t}\n}\n\nfunc (f *FriendMgo) findOne(ctx context.Context, filter any) (*model.Friend, error) {\n\tfriend, err := mongoutil.FindOne[*model.Friend](ctx, f.coll, filter)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tf.fillTime(friend)\n\treturn friend, nil\n}\n\nfunc (f *FriendMgo) find(ctx context.Context, filter any) ([]*model.Friend, error) {\n\tfriends, err := mongoutil.Find[*model.Friend](ctx, f.coll, filter)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tf.fillTime(friends...)\n\treturn friends, nil\n}\n\nfunc (f *FriendMgo) findPage(ctx context.Context, filter any, pagination pagination.Pagination, opts ...*options.FindOptions) (int64, []*model.Friend, error) {\n\treturn mongoutil.FindPage[*model.Friend](ctx, f.coll, filter, pagination, opts...)\n}\n\n// Take retrieves a single friend document. Returns an error if not found.\nfunc (f *FriendMgo) Take(ctx context.Context, ownerUserID, friendUserID string) (*model.Friend, error) {\n\tfilter := bson.M{\n\t\t\"owner_user_id\":  ownerUserID,\n\t\t\"friend_user_id\": friendUserID,\n\t}\n\treturn f.findOne(ctx, filter)\n}\n\n// FindUserState finds the friendship status between two users.\nfunc (f *FriendMgo) FindUserState(ctx context.Context, userID1, userID2 string) ([]*model.Friend, error) {\n\tfilter := bson.M{\n\t\t\"$or\": []bson.M{\n\t\t\t{\"owner_user_id\": userID1, \"friend_user_id\": userID2},\n\t\t\t{\"owner_user_id\": userID2, \"friend_user_id\": userID1},\n\t\t},\n\t}\n\treturn f.find(ctx, filter)\n}\n\n// FindFriends retrieves a list of friends for a given owner. Missing friends do not cause an error.\nfunc (f *FriendMgo) FindFriends(ctx context.Context, ownerUserID string, friendUserIDs []string) ([]*model.Friend, error) {\n\tfilter := bson.M{\n\t\t\"owner_user_id\":  ownerUserID,\n\t\t\"friend_user_id\": bson.M{\"$in\": friendUserIDs},\n\t}\n\treturn f.find(ctx, filter)\n}\n\n// FindReversalFriends finds users who have added the specified user as a friend.\nfunc (f *FriendMgo) FindReversalFriends(ctx context.Context, friendUserID string, ownerUserIDs []string) ([]*model.Friend, error) {\n\tfilter := bson.M{\n\t\t\"owner_user_id\":  bson.M{\"$in\": ownerUserIDs},\n\t\t\"friend_user_id\": friendUserID,\n\t}\n\treturn f.find(ctx, filter)\n}\n\n// FindOwnerFriends retrieves a paginated list of friends for a given owner.\nfunc (f *FriendMgo) FindOwnerFriends(ctx context.Context, ownerUserID string, pagination pagination.Pagination) (int64, []*model.Friend, error) {\n\tfilter := bson.M{\"owner_user_id\": ownerUserID}\n\topt := options.Find().SetSort(f.friendSort())\n\treturn f.findPage(ctx, filter, pagination, opt)\n}\n\nfunc (f *FriendMgo) FindOwnerFriendUserIds(ctx context.Context, ownerUserID string, limit int) ([]string, error) {\n\tfilter := bson.M{\"owner_user_id\": ownerUserID}\n\topt := options.Find().SetProjection(bson.M{\"_id\": 0, \"friend_user_id\": 1}).SetSort(f.friendSort()).SetLimit(int64(limit))\n\treturn mongoutil.Find[string](ctx, f.coll, filter, opt)\n}\n\n// FindInWhoseFriends finds users who have added the specified user as a friend, with pagination.\nfunc (f *FriendMgo) FindInWhoseFriends(ctx context.Context, friendUserID string, pagination pagination.Pagination) (int64, []*model.Friend, error) {\n\tfilter := bson.M{\"friend_user_id\": friendUserID}\n\topt := options.Find().SetSort(f.friendSort())\n\treturn f.findPage(ctx, filter, pagination, opt)\n}\n\n// FindFriendUserIDs retrieves a list of friend user IDs for a given owner.\nfunc (f *FriendMgo) FindFriendUserIDs(ctx context.Context, ownerUserID string) ([]string, error) {\n\tfilter := bson.M{\"owner_user_id\": ownerUserID}\n\treturn mongoutil.Find[string](ctx, f.coll, filter, options.Find().SetProjection(bson.M{\"_id\": 0, \"friend_user_id\": 1}).SetSort(f.friendSort()))\n}\n\nfunc (f *FriendMgo) UpdateFriends(ctx context.Context, ownerUserID string, friendUserIDs []string, val map[string]any) error {\n\t// Ensure there are IDs to update\n\tif len(friendUserIDs) == 0 || len(val) == 0 {\n\t\treturn nil // Or return an error if you expect there to always be IDs\n\t}\n\n\t// Create a filter to match documents with the specified ownerUserID and any of the friendUserIDs\n\tfilter := bson.M{\n\t\t\"owner_user_id\":  ownerUserID,\n\t\t\"friend_user_id\": bson.M{\"$in\": friendUserIDs},\n\t}\n\n\t// Create an update document\n\tupdate := bson.M{\"$set\": val}\n\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.Ignore(mongoutil.UpdateMany(ctx, f.coll, filter, update))\n\t}, func() error {\n\t\tvar userIDs []string\n\t\tif f.IsUpdateIsPinned(val) {\n\t\t\tuserIDs = append([]string{model.VersionSortChangeID}, friendUserIDs...)\n\t\t} else {\n\t\t\tuserIDs = friendUserIDs\n\t\t}\n\t\treturn f.owner.IncrVersion(ctx, ownerUserID, userIDs, model.VersionStateUpdate)\n\t})\n}\n\nfunc (f *FriendMgo) FindIncrVersion(ctx context.Context, ownerUserID string, version uint, limit int) (*model.VersionLog, error) {\n\treturn f.owner.FindChangeLog(ctx, ownerUserID, version, limit)\n}\n\nfunc (f *FriendMgo) FindFriendUserID(ctx context.Context, friendUserID string) ([]string, error) {\n\tfilter := bson.M{\n\t\t\"friend_user_id\": friendUserID,\n\t}\n\treturn mongoutil.Find[string](ctx, f.coll, filter, options.Find().SetProjection(bson.M{\"_id\": 0, \"owner_user_id\": 1}).SetSort(f.friendSort()))\n}\n\nfunc (f *FriendMgo) IncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error {\n\treturn f.owner.IncrVersion(ctx, ownerUserID, friendUserIDs, state)\n}\n\nfunc (f *FriendMgo) IsUpdateIsPinned(data map[string]any) bool {\n\tif data == nil {\n\t\treturn false\n\t}\n\t_, ok := data[\"is_pinned\"]\n\treturn ok\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/friend_request.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\nfunc NewFriendRequestMongo(db *mongo.Database) (database.FriendRequest, error) {\n\tcoll := db.Collection(database.FriendRequestName)\n\t_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"from_user_id\", Value: 1},\n\t\t\t\t{Key: \"to_user_id\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index().SetUnique(true),\n\t\t},\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"create_time\", Value: -1},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &FriendRequestMgo{coll: coll}, nil\n}\n\ntype FriendRequestMgo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (f *FriendRequestMgo) sort() any {\n\treturn bson.D{{Key: \"create_time\", Value: -1}}\n}\n\nfunc (f *FriendRequestMgo) FindToUserID(ctx context.Context, toUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) {\n\tfilter := bson.M{\"to_user_id\": toUserID}\n\tif len(handleResults) > 0 {\n\t\tfilter[\"handle_result\"] = bson.M{\"$in\": handleResults}\n\t}\n\treturn mongoutil.FindPage[*model.FriendRequest](ctx, f.coll, filter, pagination, options.Find().SetSort(f.sort()))\n}\n\nfunc (f *FriendRequestMgo) FindFromUserID(ctx context.Context, fromUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) {\n\tfilter := bson.M{\"from_user_id\": fromUserID}\n\tif len(handleResults) > 0 {\n\t\tfilter[\"handle_result\"] = bson.M{\"$in\": handleResults}\n\t}\n\treturn mongoutil.FindPage[*model.FriendRequest](ctx, f.coll, filter, pagination, options.Find().SetSort(f.sort()))\n}\n\nfunc (f *FriendRequestMgo) FindBothFriendRequests(ctx context.Context, fromUserID, toUserID string) (friends []*model.FriendRequest, err error) {\n\tfilter := bson.M{\"$or\": []bson.M{\n\t\t{\"from_user_id\": fromUserID, \"to_user_id\": toUserID},\n\t\t{\"from_user_id\": toUserID, \"to_user_id\": fromUserID},\n\t}}\n\treturn mongoutil.Find[*model.FriendRequest](ctx, f.coll, filter)\n}\n\nfunc (f *FriendRequestMgo) Create(ctx context.Context, friendRequests []*model.FriendRequest) error {\n\treturn mongoutil.InsertMany(ctx, f.coll, friendRequests)\n}\n\nfunc (f *FriendRequestMgo) Delete(ctx context.Context, fromUserID, toUserID string) (err error) {\n\treturn mongoutil.DeleteOne(ctx, f.coll, bson.M{\"from_user_id\": fromUserID, \"to_user_id\": toUserID})\n}\n\nfunc (f *FriendRequestMgo) UpdateByMap(ctx context.Context, formUserID, toUserID string, args map[string]any) (err error) {\n\tif len(args) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.UpdateOne(ctx, f.coll, bson.M{\"from_user_id\": formUserID, \"to_user_id\": toUserID}, bson.M{\"$set\": args}, true)\n}\n\nfunc (f *FriendRequestMgo) Update(ctx context.Context, friendRequest *model.FriendRequest) (err error) {\n\tupdater := bson.M{}\n\tif friendRequest.HandleResult != 0 {\n\t\tupdater[\"handle_result\"] = friendRequest.HandleResult\n\t}\n\tif friendRequest.ReqMsg != \"\" {\n\t\tupdater[\"req_msg\"] = friendRequest.ReqMsg\n\t}\n\tif friendRequest.HandlerUserID != \"\" {\n\t\tupdater[\"handler_user_id\"] = friendRequest.HandlerUserID\n\t}\n\tif friendRequest.HandleMsg != \"\" {\n\t\tupdater[\"handle_msg\"] = friendRequest.HandleMsg\n\t}\n\tif !friendRequest.HandleTime.IsZero() {\n\t\tupdater[\"handle_time\"] = friendRequest.HandleTime\n\t}\n\tif friendRequest.Ex != \"\" {\n\t\tupdater[\"ex\"] = friendRequest.Ex\n\t}\n\tif len(updater) == 0 {\n\t\treturn nil\n\t}\n\tfilter := bson.M{\"from_user_id\": friendRequest.FromUserID, \"to_user_id\": friendRequest.ToUserID}\n\treturn mongoutil.UpdateOne(ctx, f.coll, filter, bson.M{\"$set\": updater}, true)\n}\n\nfunc (f *FriendRequestMgo) Find(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error) {\n\treturn mongoutil.FindOne[*model.FriendRequest](ctx, f.coll, bson.M{\"from_user_id\": fromUserID, \"to_user_id\": toUserID})\n}\n\nfunc (f *FriendRequestMgo) Take(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error) {\n\treturn f.Find(ctx, fromUserID, toUserID)\n}\n\nfunc (f *FriendRequestMgo) GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) {\n\tfilter := bson.M{\"to_user_id\": userID, \"handle_result\": 0}\n\tif ts != 0 {\n\t\tfilter[\"create_time\"] = bson.M{\"$gt\": time.UnixMilli(ts)}\n\t}\n\treturn mongoutil.Count(ctx, f.coll, filter)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"time\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewGroupMongo(db *mongo.Database) (database.Group, error) {\n\tcoll := db.Collection(database.GroupName)\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"group_id\", Value: 1},\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn &GroupMgo{coll: coll}, nil\n}\n\ntype GroupMgo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (g *GroupMgo) sortGroup() any {\n\treturn bson.D{{\"group_name\", 1}, {\"create_time\", 1}}\n}\n\nfunc (g *GroupMgo) Create(ctx context.Context, groups []*model.Group) (err error) {\n\treturn mongoutil.InsertMany(ctx, g.coll, groups)\n}\n\nfunc (g *GroupMgo) UpdateStatus(ctx context.Context, groupID string, status int32) (err error) {\n\treturn g.UpdateMap(ctx, groupID, map[string]any{\"status\": status})\n}\n\nfunc (g *GroupMgo) UpdateMap(ctx context.Context, groupID string, args map[string]any) (err error) {\n\tif len(args) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.UpdateOne(ctx, g.coll, bson.M{\"group_id\": groupID}, bson.M{\"$set\": args}, true)\n}\n\nfunc (g *GroupMgo) Find(ctx context.Context, groupIDs []string) (groups []*model.Group, err error) {\n\treturn mongoutil.Find[*model.Group](ctx, g.coll, bson.M{\"group_id\": bson.M{\"$in\": groupIDs}})\n}\n\nfunc (g *GroupMgo) Take(ctx context.Context, groupID string) (group *model.Group, err error) {\n\treturn mongoutil.FindOne[*model.Group](ctx, g.coll, bson.M{\"group_id\": groupID})\n}\n\nfunc (g *GroupMgo) Search(ctx context.Context, keyword string, pagination pagination.Pagination) (total int64, groups []*model.Group, err error) {\n\t// Define the sorting options\n\topts := options.Find().SetSort(bson.D{{Key: \"create_time\", Value: -1}})\n\n\t// Perform the search with pagination and sorting\n\treturn mongoutil.FindPage[*model.Group](ctx, g.coll, bson.M{\n\t\t\"group_name\": bson.M{\"$regex\": keyword},\n\t\t\"status\":     bson.M{\"$ne\": constant.GroupStatusDismissed},\n\t}, pagination, opts)\n}\n\nfunc (g *GroupMgo) CountTotal(ctx context.Context, before *time.Time) (count int64, err error) {\n\tif before == nil {\n\t\treturn mongoutil.Count(ctx, g.coll, bson.M{})\n\t}\n\treturn mongoutil.Count(ctx, g.coll, bson.M{\"create_time\": bson.M{\"$lt\": before}})\n}\n\nfunc (g *GroupMgo) CountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error) {\n\tpipeline := bson.A{\n\t\tbson.M{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"create_time\": bson.M{\n\t\t\t\t\t\"$gte\": start,\n\t\t\t\t\t\"$lt\":  end,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": bson.M{\n\t\t\t\t\t\"$dateToString\": bson.M{\n\t\t\t\t\t\t\"format\": \"%Y-%m-%d\",\n\t\t\t\t\t\t\"date\":   \"$create_time\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\ttype Item struct {\n\t\tDate  string `bson:\"_id\"`\n\t\tCount int64  `bson:\"count\"`\n\t}\n\titems, err := mongoutil.Aggregate[Item](ctx, g.coll, pipeline)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make(map[string]int64, len(items))\n\tfor _, item := range items {\n\t\tres[item.Date] = item.Count\n\t}\n\treturn res, nil\n}\n\nfunc (g *GroupMgo) FindJoinSortGroupID(ctx context.Context, groupIDs []string) ([]string, error) {\n\tif len(groupIDs) < 2 {\n\t\treturn groupIDs, nil\n\t}\n\tfilter := bson.M{\n\t\t\"group_id\": bson.M{\"$in\": groupIDs},\n\t\t\"status\":   bson.M{\"$ne\": constant.GroupStatusDismissed},\n\t}\n\topt := options.Find().SetSort(g.sortGroup()).SetProjection(bson.M{\"_id\": 0, \"group_id\": 1})\n\treturn mongoutil.Find[string](ctx, g.coll, filter, opt)\n}\n\nfunc (g *GroupMgo) SearchJoin(ctx context.Context, groupIDs []string, keyword string, pagination pagination.Pagination) (int64, []*model.Group, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn 0, nil, nil\n\t}\n\tfilter := bson.M{\n\t\t\"group_id\": bson.M{\"$in\": groupIDs},\n\t\t\"status\":   bson.M{\"$ne\": constant.GroupStatusDismissed},\n\t}\n\tif keyword != \"\" {\n\t\tfilter[\"group_name\"] = bson.M{\"$regex\": keyword}\n\t}\n\t// Define the sorting options\n\topts := options.Find().SetSort(g.sortGroup())\n\t// Perform the search with pagination and sorting\n\treturn mongoutil.FindPage[*model.Group](ctx, g.coll, filter, pagination, opts)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/group_member.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/log\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewGroupMember(db *mongo.Database) (database.GroupMember, error) {\n\tcoll := db.Collection(database.GroupMemberName)\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"group_id\", Value: 1},\n\t\t\t{Key: \"user_id\", Value: 1},\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\tmember, err := NewVersionLog(db.Collection(database.GroupMemberVersionName))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjoin, err := NewVersionLog(db.Collection(database.GroupJoinVersionName))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &GroupMemberMgo{coll: coll, member: member, join: join}, nil\n}\n\ntype GroupMemberMgo struct {\n\tcoll   *mongo.Collection\n\tmember database.VersionLog\n\tjoin   database.VersionLog\n}\n\nfunc (g *GroupMemberMgo) memberSort() any {\n\treturn bson.D{{Key: \"role_level\", Value: -1}, {Key: \"create_time\", Value: 1}}\n}\n\nfunc (g *GroupMemberMgo) Create(ctx context.Context, groupMembers []*model.GroupMember) (err error) {\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.InsertMany(ctx, g.coll, groupMembers)\n\t}, func() error {\n\t\tgms := make(map[string][]string)\n\t\tfor _, member := range groupMembers {\n\t\t\tgms[member.GroupID] = append(gms[member.GroupID], member.UserID)\n\t\t}\n\t\tfor groupID, userIDs := range gms {\n\t\t\tif err := g.member.IncrVersion(ctx, groupID, userIDs, model.VersionStateInsert); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}, func() error {\n\t\tgms := make(map[string][]string)\n\t\tfor _, member := range groupMembers {\n\t\t\tgms[member.UserID] = append(gms[member.UserID], member.GroupID)\n\t\t}\n\t\tfor userID, groupIDs := range gms {\n\t\t\tif err := g.join.IncrVersion(ctx, userID, groupIDs, model.VersionStateInsert); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (g *GroupMemberMgo) Delete(ctx context.Context, groupID string, userIDs []string) (err error) {\n\tfilter := bson.M{\"group_id\": groupID}\n\tif len(userIDs) > 0 {\n\t\tfilter[\"user_id\"] = bson.M{\"$in\": userIDs}\n\t}\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.DeleteMany(ctx, g.coll, filter)\n\t}, func() error {\n\t\tif len(userIDs) == 0 {\n\t\t\treturn g.member.Delete(ctx, groupID)\n\t\t} else {\n\t\t\treturn g.member.IncrVersion(ctx, groupID, userIDs, model.VersionStateDelete)\n\t\t}\n\t}, func() error {\n\t\tfor _, userID := range userIDs {\n\t\t\tif err := g.join.IncrVersion(ctx, userID, []string{groupID}, model.VersionStateDelete); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (g *GroupMemberMgo) UpdateRoleLevel(ctx context.Context, groupID string, userID string, roleLevel int32) error {\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.UpdateOne(ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": userID},\n\t\t\tbson.M{\"$set\": bson.M{\"role_level\": roleLevel}}, true)\n\t}, func() error {\n\t\treturn g.member.IncrVersion(ctx, groupID, []string{model.VersionSortChangeID, userID}, model.VersionStateUpdate)\n\t})\n}\nfunc (g *GroupMemberMgo) UpdateUserRoleLevels(ctx context.Context, groupID string, firstUserID string, firstUserRoleLevel int32, secondUserID string, secondUserRoleLevel int32) error {\n\treturn mongoutil.IncrVersion(func() error {\n\t\tif err := mongoutil.UpdateOne(ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": firstUserID},\n\t\t\tbson.M{\"$set\": bson.M{\"role_level\": firstUserRoleLevel}}, true); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := mongoutil.UpdateOne(ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": secondUserID},\n\t\t\tbson.M{\"$set\": bson.M{\"role_level\": secondUserRoleLevel}}, true); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}, func() error {\n\t\treturn g.member.IncrVersion(ctx, groupID, []string{model.VersionSortChangeID, firstUserID, secondUserID}, model.VersionStateUpdate)\n\t})\n}\n\nfunc (g *GroupMemberMgo) Update(ctx context.Context, groupID string, userID string, data map[string]any) (err error) {\n\tif len(data) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.IncrVersion(func() error {\n\t\treturn mongoutil.UpdateOne(ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": userID}, bson.M{\"$set\": data}, true)\n\t}, func() error {\n\t\tvar userIDs []string\n\t\tif g.IsUpdateRoleLevel(data) {\n\t\t\tuserIDs = []string{model.VersionSortChangeID, userID}\n\t\t} else {\n\t\t\tuserIDs = []string{userID}\n\t\t}\n\t\treturn g.member.IncrVersion(ctx, groupID, userIDs, model.VersionStateUpdate)\n\t})\n}\n\nfunc (g *GroupMemberMgo) FindMemberUserID(ctx context.Context, groupID string) (userIDs []string, err error) {\n\treturn mongoutil.Find[string](ctx, g.coll, bson.M{\"group_id\": groupID}, options.Find().SetProjection(bson.M{\"_id\": 0, \"user_id\": 1}).SetSort(g.memberSort()))\n}\n\nfunc (g *GroupMemberMgo) Find(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupMember, error) {\n\tfilter := bson.M{\"group_id\": groupID}\n\tif len(userIDs) > 0 {\n\t\tfilter[\"user_id\"] = bson.M{\"$in\": userIDs}\n\t}\n\treturn mongoutil.Find[*model.GroupMember](ctx, g.coll, filter)\n}\n\nfunc (g *GroupMemberMgo) FindInGroup(ctx context.Context, userID string, groupIDs []string) ([]*model.GroupMember, error) {\n\tfilter := bson.M{\"user_id\": userID}\n\tif len(groupIDs) > 0 {\n\t\tfilter[\"group_id\"] = bson.M{\"$in\": groupIDs}\n\t}\n\treturn mongoutil.Find[*model.GroupMember](ctx, g.coll, filter)\n}\n\nfunc (g *GroupMemberMgo) Take(ctx context.Context, groupID string, userID string) (groupMember *model.GroupMember, err error) {\n\treturn mongoutil.FindOne[*model.GroupMember](ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": userID})\n}\n\nfunc (g *GroupMemberMgo) TakeOwner(ctx context.Context, groupID string) (groupMember *model.GroupMember, err error) {\n\treturn mongoutil.FindOne[*model.GroupMember](ctx, g.coll, bson.M{\"group_id\": groupID, \"role_level\": constant.GroupOwner})\n}\n\nfunc (g *GroupMemberMgo) FindRoleLevelUserIDs(ctx context.Context, groupID string, roleLevel int32) ([]string, error) {\n\treturn mongoutil.Find[string](ctx, g.coll, bson.M{\"group_id\": groupID, \"role_level\": roleLevel}, options.Find().SetProjection(bson.M{\"_id\": 0, \"user_id\": 1}))\n}\n\nfunc (g *GroupMemberMgo) SearchMember(ctx context.Context, keyword string, groupID string, pagination pagination.Pagination) (int64, []*model.GroupMember, error) {\n\tfilter := bson.M{\"group_id\": groupID, \"nickname\": bson.M{\"$regex\": keyword}}\n\treturn mongoutil.FindPage[*model.GroupMember](ctx, g.coll, filter, pagination, options.Find().SetSort(g.memberSort()))\n}\n\nfunc (g *GroupMemberMgo) FindUserJoinedGroupID(ctx context.Context, userID string) (groupIDs []string, err error) {\n\treturn mongoutil.Find[string](ctx, g.coll, bson.M{\"user_id\": userID}, options.Find().SetProjection(bson.M{\"_id\": 0, \"group_id\": 1}).SetSort(g.memberSort()))\n}\n\nfunc (g *GroupMemberMgo) TakeGroupMemberNum(ctx context.Context, groupID string) (count int64, err error) {\n\treturn mongoutil.Count(ctx, g.coll, bson.M{\"group_id\": groupID})\n}\n\nfunc (g *GroupMemberMgo) FindUserManagedGroupID(ctx context.Context, userID string) (groupIDs []string, err error) {\n\tfilter := bson.M{\n\t\t\"user_id\": userID,\n\t\t\"role_level\": bson.M{\n\t\t\t\"$in\": []int{constant.GroupOwner, constant.GroupAdmin},\n\t\t},\n\t}\n\treturn mongoutil.Find[string](ctx, g.coll, filter, options.Find().SetProjection(bson.M{\"_id\": 0, \"group_id\": 1}))\n}\n\nfunc (g *GroupMemberMgo) IsUpdateRoleLevel(data map[string]any) bool {\n\tif len(data) == 0 {\n\t\treturn false\n\t}\n\t_, ok := data[\"role_level\"]\n\treturn ok\n}\n\nfunc (g *GroupMemberMgo) JoinGroupIncrVersion(ctx context.Context, userID string, groupIDs []string, state int32) error {\n\treturn g.join.IncrVersion(ctx, userID, groupIDs, state)\n}\n\nfunc (g *GroupMemberMgo) MemberGroupIncrVersion(ctx context.Context, groupID string, userIDs []string, state int32) error {\n\treturn g.member.IncrVersion(ctx, groupID, userIDs, state)\n}\n\nfunc (g *GroupMemberMgo) FindMemberIncrVersion(ctx context.Context, groupID string, version uint, limit int) (*model.VersionLog, error) {\n\tlog.ZDebug(ctx, \"find member incr version\", \"groupID\", groupID, \"version\", version)\n\treturn g.member.FindChangeLog(ctx, groupID, version, limit)\n}\n\nfunc (g *GroupMemberMgo) BatchFindMemberIncrVersion(ctx context.Context, groupIDs []string, versions []uint, limits []int) ([]*model.VersionLog, error) {\n\tlog.ZDebug(ctx, \"Batch find member incr version\", \"groupIDs\", groupIDs, \"versions\", versions)\n\treturn g.member.BatchFindChangeLog(ctx, groupIDs, versions, limits)\n}\n\nfunc (g *GroupMemberMgo) FindJoinIncrVersion(ctx context.Context, userID string, version uint, limit int) (*model.VersionLog, error) {\n\tlog.ZDebug(ctx, \"find join incr version\", \"userID\", userID, \"version\", version)\n\treturn g.join.FindChangeLog(ctx, userID, version, limit)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/group_request.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nfunc NewGroupRequestMgo(db *mongo.Database) (database.GroupRequest, error) {\n\tcoll := db.Collection(database.GroupRequestName)\n\t_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"group_id\", Value: 1},\n\t\t\t\t{Key: \"user_id\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index().SetUnique(true),\n\t\t},\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"req_time\", Value: -1},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn &GroupRequestMgo{coll: coll}, nil\n}\n\ntype GroupRequestMgo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (g *GroupRequestMgo) Create(ctx context.Context, groupRequests []*model.GroupRequest) (err error) {\n\treturn mongoutil.InsertMany(ctx, g.coll, groupRequests)\n}\n\nfunc (g *GroupRequestMgo) Delete(ctx context.Context, groupID string, userID string) (err error) {\n\treturn mongoutil.DeleteOne(ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": userID})\n}\n\nfunc (g *GroupRequestMgo) UpdateHandler(ctx context.Context, groupID string, userID string, handledMsg string, handleResult int32) (err error) {\n\treturn mongoutil.UpdateOne(ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": userID}, bson.M{\"$set\": bson.M{\"handle_msg\": handledMsg, \"handle_result\": handleResult}}, true)\n}\n\nfunc (g *GroupRequestMgo) Take(ctx context.Context, groupID string, userID string) (groupRequest *model.GroupRequest, err error) {\n\treturn mongoutil.FindOne[*model.GroupRequest](ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": userID})\n}\n\nfunc (g *GroupRequestMgo) FindGroupRequests(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupRequest, error) {\n\treturn mongoutil.Find[*model.GroupRequest](ctx, g.coll, bson.M{\"group_id\": groupID, \"user_id\": bson.M{\"$in\": userIDs}})\n}\n\nfunc (g *GroupRequestMgo) sort() any {\n\treturn bson.D{{Key: \"req_time\", Value: -1}}\n}\n\nfunc (g *GroupRequestMgo) Page(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) {\n\tfilter := bson.M{\"user_id\": userID}\n\tif len(groupIDs) > 0 {\n\t\tfilter[\"group_id\"] = bson.M{\"$in\": datautil.Distinct(groupIDs)}\n\t}\n\tif len(handleResults) > 0 {\n\t\tfilter[\"handle_result\"] = bson.M{\"$in\": handleResults}\n\t}\n\treturn mongoutil.FindPage[*model.GroupRequest](ctx, g.coll, filter, pagination, options.Find().SetSort(g.sort()))\n}\n\nfunc (g *GroupRequestMgo) PageGroup(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) {\n\tif len(groupIDs) == 0 {\n\t\treturn 0, nil, nil\n\t}\n\tfilter := bson.M{\"group_id\": bson.M{\"$in\": groupIDs}}\n\tif len(handleResults) > 0 {\n\t\tfilter[\"handle_result\"] = bson.M{\"$in\": handleResults}\n\t}\n\treturn mongoutil.FindPage[*model.GroupRequest](ctx, g.coll, filter, pagination, options.Find().SetSort(g.sort()))\n}\n\nfunc (g *GroupRequestMgo) GetUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn 0, nil\n\t}\n\tfilter := bson.M{\"group_id\": bson.M{\"$in\": groupIDs}, \"handle_result\": 0}\n\tif ts != 0 {\n\t\tfilter[\"req_time\"] = bson.M{\"$gt\": time.UnixMilli(ts)}\n\t}\n\treturn mongoutil.Count(ctx, g.coll, filter)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/helpers.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"github.com/openimsdk/tools/errs\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n)\n\nfunc IsNotFound(err error) bool {\n\treturn errs.Unwrap(err) == mongo.ErrNoDocuments\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/log.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"time\"\n\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewLogMongo(db *mongo.Database) (database.Log, error) {\n\tcoll := db.Collection(database.LogName)\n\t_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"log_id\", Value: 1},\n\t\t\t},\n\t\t\tOptions: options.Index().SetUnique(true),\n\t\t},\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"user_id\", Value: 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tKeys: bson.D{\n\t\t\t\t{Key: \"create_time\", Value: -1},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &LogMgo{coll: coll}, nil\n}\n\ntype LogMgo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (l *LogMgo) Create(ctx context.Context, log []*model.Log) error {\n\treturn mongoutil.InsertMany(ctx, l.coll, log)\n}\n\nfunc (l *LogMgo) Search(ctx context.Context, keyword string, start time.Time, end time.Time, pagination pagination.Pagination) (int64, []*model.Log, error) {\n\tfilter := bson.M{\"create_time\": bson.M{\"$gte\": start, \"$lte\": end}}\n\tif keyword != \"\" {\n\t\tfilter[\"user_id\"] = bson.M{\"$regex\": keyword}\n\t}\n\treturn mongoutil.FindPage[*model.Log](ctx, l.coll, filter, pagination, options.Find().SetSort(bson.M{\"create_time\": -1}))\n}\n\nfunc (l *LogMgo) Delete(ctx context.Context, logID []string, userID string) error {\n\tif userID == \"\" {\n\t\treturn mongoutil.DeleteMany(ctx, l.coll, bson.M{\"log_id\": bson.M{\"$in\": logID}})\n\t}\n\treturn mongoutil.DeleteMany(ctx, l.coll, bson.M{\"log_id\": bson.M{\"$in\": logID}, \"user_id\": userID})\n}\n\nfunc (l *LogMgo) Get(ctx context.Context, logIDs []string, userID string) ([]*model.Log, error) {\n\tif userID == \"\" {\n\t\treturn mongoutil.Find[*model.Log](ctx, l.coll, bson.M{\"log_id\": bson.M{\"$in\": logIDs}})\n\t}\n\treturn mongoutil.Find[*model.Log](ctx, l.coll, bson.M{\"log_id\": bson.M{\"$in\": logIDs}, \"user_id\": userID})\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/msg.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n)\n\nfunc NewMsgMongo(db *mongo.Database) (database.Msg, error) {\n\tcoll := db.Collection(new(model.MsgDocModel).TableName())\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"doc_id\", Value: 1},\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn &MsgMgo{coll: coll}, nil\n}\n\ntype MsgMgo struct {\n\tcoll  *mongo.Collection\n\tmodel model.MsgDocModel\n}\n\nfunc (m *MsgMgo) Create(ctx context.Context, msg *model.MsgDocModel) error {\n\treturn mongoutil.InsertMany(ctx, m.coll, []*model.MsgDocModel{msg})\n}\n\nfunc (m *MsgMgo) UpdateMsg(ctx context.Context, docID string, index int64, key string, value any) (*mongo.UpdateResult, error) {\n\tvar field string\n\tif key == \"\" {\n\t\tfield = fmt.Sprintf(\"msgs.%d\", index)\n\t} else {\n\t\tfield = fmt.Sprintf(\"msgs.%d.%s\", index, key)\n\t}\n\tfilter := bson.M{\"doc_id\": docID}\n\tupdate := bson.M{\"$set\": bson.M{field: value}}\n\treturn mongoutil.UpdateOneResult(ctx, m.coll, filter, update)\n}\n\nfunc (m *MsgMgo) PushUnique(ctx context.Context, docID string, index int64, key string, value any) (*mongo.UpdateResult, error) {\n\tvar field string\n\tif key == \"\" {\n\t\tfield = fmt.Sprintf(\"msgs.%d\", index)\n\t} else {\n\t\tfield = fmt.Sprintf(\"msgs.%d.%s\", index, key)\n\t}\n\tfilter := bson.M{\"doc_id\": docID}\n\tupdate := bson.M{\n\t\t\"$addToSet\": bson.M{\n\t\t\tfield: bson.M{\"$each\": value},\n\t\t},\n\t}\n\treturn mongoutil.UpdateOneResult(ctx, m.coll, filter, update)\n}\n\nfunc (m *MsgMgo) FindOneByDocID(ctx context.Context, docID string) (*model.MsgDocModel, error) {\n\treturn mongoutil.FindOne[*model.MsgDocModel](ctx, m.coll, bson.M{\"doc_id\": docID})\n}\n\nfunc (m *MsgMgo) GetMsgBySeqIndexIn1Doc(ctx context.Context, userID, docID string, seqs []int64) ([]*model.MsgInfoModel, error) {\n\tmsgs, err := m.getMsgBySeqIndexIn1Doc(ctx, userID, docID, seqs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(msgs) == len(seqs) {\n\t\treturn msgs, nil\n\t}\n\ttmp := make(map[int64]*model.MsgInfoModel)\n\tfor i, val := range msgs {\n\t\ttmp[val.Msg.Seq] = msgs[i]\n\t}\n\tres := make([]*model.MsgInfoModel, 0, len(seqs))\n\tfor _, seq := range seqs {\n\t\tif val, ok := tmp[seq]; ok {\n\t\t\tres = append(res, val)\n\t\t} else {\n\t\t\tres = append(res, &model.MsgInfoModel{Msg: &model.MsgDataModel{Seq: seq}})\n\t\t}\n\t}\n\treturn res, nil\n}\n\nfunc (m *MsgMgo) getMsgBySeqIndexIn1Doc(ctx context.Context, userID, docID string, seqs []int64) ([]*model.MsgInfoModel, error) {\n\tindexes := make([]int64, 0, len(seqs))\n\tfor _, seq := range seqs {\n\t\tindexes = append(indexes, m.model.GetMsgIndex(seq))\n\t}\n\tpipeline := mongo.Pipeline{\n\t\tbson.D{{Key: \"$match\", Value: bson.D{\n\t\t\t{Key: \"doc_id\", Value: docID},\n\t\t}}},\n\t\tbson.D{{Key: \"$project\", Value: bson.D{\n\t\t\t{Key: \"_id\", Value: 0},\n\t\t\t{Key: \"doc_id\", Value: 1},\n\t\t\t{Key: \"msgs\", Value: bson.D{\n\t\t\t\t{Key: \"$map\", Value: bson.D{\n\t\t\t\t\t{Key: \"input\", Value: indexes},\n\t\t\t\t\t{Key: \"as\", Value: \"index\"},\n\t\t\t\t\t{Key: \"in\", Value: bson.D{\n\t\t\t\t\t\t{Key: \"$arrayElemAt\", Value: bson.A{\"$msgs\", \"$$index\"}},\n\t\t\t\t\t}},\n\t\t\t\t}},\n\t\t\t}},\n\t\t}}},\n\t}\n\tmsgDocModel, err := mongoutil.Aggregate[*model.MsgDocModel](ctx, m.coll, pipeline)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(msgDocModel) == 0 {\n\t\treturn nil, errs.Wrap(mongo.ErrNoDocuments)\n\t}\n\tmsgs := make([]*model.MsgInfoModel, 0, len(msgDocModel[0].Msg))\n\tfor i := range msgDocModel[0].Msg {\n\t\tmsg := msgDocModel[0].Msg[i]\n\t\tif msg == nil || msg.Msg == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif datautil.Contain(userID, msg.DelList...) {\n\t\t\tmsg.Msg.Content = \"\"\n\t\t\tmsg.Msg.Status = constant.MsgDeleted\n\t\t}\n\t\tif msg.Revoke != nil {\n\t\t\trevokeContent := sdkws.MessageRevokedContent{\n\t\t\t\tRevokerID:                   msg.Revoke.UserID,\n\t\t\t\tRevokerRole:                 msg.Revoke.Role,\n\t\t\t\tClientMsgID:                 msg.Msg.ClientMsgID,\n\t\t\t\tRevokerNickname:             msg.Revoke.Nickname,\n\t\t\t\tRevokeTime:                  msg.Revoke.Time,\n\t\t\t\tSourceMessageSendTime:       msg.Msg.SendTime,\n\t\t\t\tSourceMessageSendID:         msg.Msg.SendID,\n\t\t\t\tSourceMessageSenderNickname: msg.Msg.SenderNickname,\n\t\t\t\tSessionType:                 msg.Msg.SessionType,\n\t\t\t\tSeq:                         msg.Msg.Seq,\n\t\t\t\tEx:                          msg.Msg.Ex,\n\t\t\t}\n\t\t\tdata, err := jsonutil.JsonMarshal(&revokeContent)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errs.WrapMsg(err, fmt.Sprintf(\"docID is %s, seqs is %v\", docID, seqs))\n\t\t\t}\n\t\t\telem := sdkws.NotificationElem{\n\t\t\t\tDetail: string(data),\n\t\t\t}\n\t\t\tcontent, err := jsonutil.JsonMarshal(&elem)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errs.WrapMsg(err, fmt.Sprintf(\"docID is %s, seqs is %v\", docID, seqs))\n\t\t\t}\n\t\t\tmsg.Msg.ContentType = constant.MsgRevokeNotification\n\t\t\tmsg.Msg.Content = string(content)\n\t\t}\n\t\tmsgs = append(msgs, msg)\n\t}\n\treturn msgs, nil\n}\n\nfunc (m *MsgMgo) GetNewestMsg(ctx context.Context, conversationID string) (*model.MsgInfoModel, error) {\n\tfor skip := int64(0); ; skip++ {\n\t\tmsgDocModel, err := m.GetMsgDocModelByIndex(ctx, conversationID, skip, -1)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := len(msgDocModel.Msg) - 1; i >= 0; i-- {\n\t\t\tif msgDocModel.Msg[i].Msg != nil {\n\t\t\t\treturn msgDocModel.Msg[i], nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (m *MsgMgo) GetOldestMsg(ctx context.Context, conversationID string) (*model.MsgInfoModel, error) {\n\tfor skip := int64(0); ; skip++ {\n\t\tmsgDocModel, err := m.GetMsgDocModelByIndex(ctx, conversationID, skip, 1)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i, v := range msgDocModel.Msg {\n\t\t\tif v.Msg != nil {\n\t\t\t\treturn msgDocModel.Msg[i], nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (m *MsgMgo) GetMsgDocModelByIndex(ctx context.Context, conversationID string, index, sort int64) (*model.MsgDocModel, error) {\n\tif sort != 1 && sort != -1 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"mongo sort must be 1 or -1\")\n\t}\n\topt := options.Find().SetSkip(index).SetSort(bson.M{\"_id\": sort}).SetLimit(1)\n\tfilter := bson.M{\"doc_id\": primitive.Regex{Pattern: fmt.Sprintf(\"^%s:\", conversationID)}}\n\tmsgs, err := mongoutil.Find[*model.MsgDocModel](ctx, m.coll, filter, opt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(msgs) > 0 {\n\t\treturn msgs[0], nil\n\t}\n\treturn nil, errs.Wrap(model.ErrMsgListNotExist)\n}\n\nfunc (m *MsgMgo) DeleteMsgsInOneDocByIndex(ctx context.Context, docID string, indexes []int) error {\n\tupdate := bson.M{\n\t\t\"$set\": bson.M{},\n\t}\n\tfor _, index := range indexes {\n\t\tupdate[\"$set\"].(bson.M)[fmt.Sprintf(\"msgs.%d\", index)] = bson.M{\n\t\t\t\"msg\": nil,\n\t\t}\n\t}\n\t_, err := mongoutil.UpdateMany(ctx, m.coll, bson.M{\"doc_id\": docID}, update)\n\treturn err\n}\n\nfunc (m *MsgMgo) MarkSingleChatMsgsAsRead(ctx context.Context, userID string, docID string, indexes []int64) error {\n\tvar updates []mongo.WriteModel\n\tfor _, index := range indexes {\n\t\tfilter := bson.M{\n\t\t\t\"doc_id\": docID,\n\t\t\tfmt.Sprintf(\"msgs.%d.msg.send_id\", index): bson.M{\n\t\t\t\t\"$ne\": userID,\n\t\t\t},\n\t\t}\n\t\tupdate := bson.M{\n\t\t\t\"$set\": bson.M{\n\t\t\t\tfmt.Sprintf(\"msgs.%d.is_read\", index): true,\n\t\t\t},\n\t\t}\n\t\tupdateModel := mongo.NewUpdateManyModel().\n\t\t\tSetFilter(filter).\n\t\t\tSetUpdate(update)\n\t\tupdates = append(updates, updateModel)\n\t}\n\tif _, err := m.coll.BulkWrite(ctx, updates); err != nil {\n\t\treturn errs.WrapMsg(err, fmt.Sprintf(\"docID is %s, indexes is %v\", docID, indexes))\n\t}\n\treturn nil\n}\n\ntype searchMessageIndex struct {\n\tID    primitive.ObjectID `bson:\"_id\"`\n\tIndex []int64            `bson:\"index\"`\n}\n\nfunc (m *MsgMgo) searchMessageIndex(ctx context.Context, filter any, nextID primitive.ObjectID, limit int) ([]searchMessageIndex, error) {\n\tvar pipeline bson.A\n\tif !nextID.IsZero() {\n\t\tpipeline = append(pipeline, bson.M{\"$match\": bson.M{\"_id\": bson.M{\"$gt\": nextID}}})\n\t}\n\tcoarseFilter := bson.M{\n\t\t\"$or\": bson.A{\n\t\t\tbson.M{\n\t\t\t\t\"doc_id\": primitive.Regex{Pattern: \"^sg_\"},\n\t\t\t},\n\t\t\tbson.M{\n\t\t\t\t\"doc_id\": primitive.Regex{Pattern: \"^si_\"},\n\t\t\t},\n\t\t},\n\t}\n\tpipeline = append(pipeline,\n\t\tbson.M{\"$sort\": bson.M{\"_id\": 1}},\n\t\tbson.M{\"$match\": coarseFilter},\n\t\tbson.M{\"$match\": filter},\n\t\tbson.M{\"$limit\": limit},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\": 1,\n\t\t\t\t\"msgs\": bson.M{\n\t\t\t\t\t\"$map\": bson.M{\n\t\t\t\t\t\t\"input\": \"$msgs\",\n\t\t\t\t\t\t\"as\":    \"msg\",\n\t\t\t\t\t\t\"in\": bson.M{\n\t\t\t\t\t\t\t\"$mergeObjects\": bson.A{\n\t\t\t\t\t\t\t\t\"$$msg\",\n\t\t\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\t\t\"_search_temp_index\": bson.M{\n\t\t\t\t\t\t\t\t\t\t\"$indexOfArray\": bson.A{\n\t\t\t\t\t\t\t\t\t\t\t\"$msgs\", \"$$msg\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\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\tbson.M{\"$unwind\": \"$msgs\"},\n\t\tbson.M{\"$match\": filter},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":                     1,\n\t\t\t\t\"msgs._search_temp_index\": 1,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\":   \"$_id\",\n\t\t\t\t\"index\": bson.M{\"$push\": \"$msgs._search_temp_index\"},\n\t\t\t},\n\t\t},\n\t\tbson.M{\"$sort\": bson.M{\"_id\": 1}},\n\t)\n\treturn mongoutil.Aggregate[searchMessageIndex](ctx, m.coll, pipeline)\n}\n\nfunc (m *MsgMgo) searchMessage(ctx context.Context, req *msg.SearchMessageReq) (int64, []searchMessageIndex, error) {\n\tfilter := bson.M{\n\t\t\"msgs.msg\": bson.M{\n\t\t\t\"$exists\": true,\n\t\t\t\"$type\":   \"object\",\n\t\t},\n\t}\n\tif req.RecvID != \"\" {\n\t\tfilter[\"$or\"] = bson.A{\n\t\t\tbson.M{\"msgs.msg.recv_id\": req.RecvID},\n\t\t\tbson.M{\"msgs.msg.group_id\": req.RecvID},\n\t\t}\n\t}\n\tif req.SendID != \"\" {\n\t\tfilter[\"msgs.msg.send_id\"] = req.SendID\n\t}\n\tif req.ContentType != 0 {\n\t\tfilter[\"msgs.msg.content_type\"] = req.ContentType\n\t}\n\tif req.SessionType != 0 {\n\t\tfilter[\"msgs.msg.session_type\"] = req.SessionType\n\t}\n\tif req.SendTime != \"\" {\n\t\tsendTime, err := time.Parse(time.DateOnly, req.SendTime)\n\t\tif err != nil {\n\t\t\treturn 0, nil, errs.ErrArgs.WrapMsg(\"invalid sendTime\", \"req\", req.SendTime, \"format\", time.DateOnly, \"cause\", err.Error())\n\t\t}\n\t\tfilter[\"$and\"] = bson.A{\n\t\t\tbson.M{\"msgs.msg.send_time\": bson.M{\n\t\t\t\t\"$gte\": sendTime.UnixMilli(),\n\t\t\t}},\n\t\t\tbson.M{\n\t\t\t\t\"msgs.msg.send_time\": bson.M{\n\t\t\t\t\t\"$lt\": sendTime.Add(time.Hour * 24).UnixMilli(),\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\tvar (\n\t\tnextID    primitive.ObjectID\n\t\tcount     int\n\t\tdataRange []searchMessageIndex\n\t\tskip      = int((req.Pagination.GetPageNumber() - 1) * req.Pagination.GetShowNumber())\n\t)\n\t_, _ = dataRange, skip\n\tconst maxDoc = 50\n\tdata := make([]searchMessageIndex, 0, req.Pagination.GetShowNumber())\n\tpush := cap(data)\n\tfor i := 0; ; i++ {\n\t\tres, err := m.searchMessageIndex(ctx, filter, nextID, maxDoc)\n\t\tif err != nil {\n\t\t\treturn 0, nil, err\n\t\t}\n\t\tif len(res) > 0 {\n\t\t\tnextID = res[len(res)-1].ID\n\t\t}\n\t\tfor _, r := range res {\n\t\t\tvar dataIndex []int64\n\t\t\tfor _, index := range r.Index {\n\t\t\t\tif push > 0 && count >= skip {\n\t\t\t\t\tdataIndex = append(dataIndex, index)\n\t\t\t\t\tpush--\n\t\t\t\t}\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif len(dataIndex) > 0 {\n\t\t\t\tdata = append(data, searchMessageIndex{\n\t\t\t\t\tID:    r.ID,\n\t\t\t\t\tIndex: dataIndex,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tif push <= 0 {\n\t\t\tpush--\n\t\t}\n\t\tif len(res) < maxDoc || push < -10 {\n\t\t\treturn int64(count), data, nil\n\t\t}\n\t}\n}\n\nfunc (m *MsgMgo) SearchMessage(ctx context.Context, req *msg.SearchMessageReq) (int64, []*model.MsgInfoModel, error) {\n\tcount, data, err := m.searchMessage(ctx, req)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\tvar msgs []*model.MsgInfoModel\n\tif len(data) > 0 {\n\t\tvar n int\n\t\tfor _, d := range data {\n\t\t\tn += len(d.Index)\n\t\t}\n\t\tmsgs = make([]*model.MsgInfoModel, 0, n)\n\t}\n\tfor _, val := range data {\n\t\tres, err := mongoutil.FindOne[*model.MsgDocModel](ctx, m.coll, bson.M{\"_id\": val.ID})\n\t\tif err != nil {\n\t\t\treturn 0, nil, err\n\t\t}\n\t\tfor _, i := range val.Index {\n\t\t\tif i >= int64(len(res.Msg)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmsgs = append(msgs, res.Msg[i])\n\t\t}\n\t}\n\treturn count, msgs, nil\n}\n\nfunc (m *MsgMgo) RangeUserSendCount(ctx context.Context, start time.Time, end time.Time, group bool, ase bool, pageNumber int32, showNumber int32) (msgCount int64, userCount int64, users []*model.UserCount, dateCount map[string]int64, err error) {\n\tvar sort int\n\tif ase {\n\t\tsort = 1\n\t} else {\n\t\tsort = -1\n\t}\n\ttype Result struct {\n\t\tMsgCount  int64 `bson:\"msg_count\"`\n\t\tUserCount int64 `bson:\"user_count\"`\n\t\tUsers     []struct {\n\t\t\tUserID string `bson:\"_id\"`\n\t\t\tCount  int64  `bson:\"count\"`\n\t\t} `bson:\"users\"`\n\t\tDates []struct {\n\t\t\tDate  string `bson:\"_id\"`\n\t\t\tCount int64  `bson:\"count\"`\n\t\t} `bson:\"dates\"`\n\t}\n\tor := bson.A{\n\t\tbson.M{\n\t\t\t\"doc_id\": bson.M{\n\t\t\t\t\"$regex\":   \"^si_\",\n\t\t\t\t\"$options\": \"i\",\n\t\t\t},\n\t\t},\n\t}\n\tif group {\n\t\tor = append(or,\n\t\t\tbson.M{\n\t\t\t\t\"doc_id\": bson.M{\n\t\t\t\t\t\"$regex\":   \"^g_\",\n\t\t\t\t\t\"$options\": \"i\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tbson.M{\n\t\t\t\t\"doc_id\": bson.M{\n\t\t\t\t\t\"$regex\":   \"^sg_\",\n\t\t\t\t\t\"$options\": \"i\",\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\t}\n\tpipeline := bson.A{\n\t\tbson.M{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"$and\": bson.A{\n\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\"msgs.msg.send_time\": bson.M{\n\t\t\t\t\t\t\t\"$gte\": start.UnixMilli(),\n\t\t\t\t\t\t\t\"$lt\":  end.UnixMilli(),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\"$or\": or,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"msgs\": bson.M{\n\t\t\t\t\t\"$filter\": bson.M{\n\t\t\t\t\t\t\"input\": \"$msgs\",\n\t\t\t\t\t\t\"as\":    \"item\",\n\t\t\t\t\t\t\"cond\": bson.M{\n\t\t\t\t\t\t\t\"$and\": bson.A{\n\t\t\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\t\t\"$gte\": bson.A{\n\t\t\t\t\t\t\t\t\t\t\"$$item.msg.send_time\", start.UnixMilli(),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\t\t\"$lt\": bson.A{\n\t\t\t\t\t\t\t\t\t\t\"$$item.msg.send_time\", end.UnixMilli(),\n\t\t\t\t\t\t\t\t\t},\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\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\": 0,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"result\": bson.M{\n\t\t\t\t\t\"$map\": bson.M{\n\t\t\t\t\t\t\"input\": \"$msgs\",\n\t\t\t\t\t\t\"as\":    \"item\",\n\t\t\t\t\t\t\"in\": bson.M{\n\t\t\t\t\t\t\t\"user_id\": \"$$item.msg.send_id\",\n\t\t\t\t\t\t\t\"send_date\": bson.M{\n\t\t\t\t\t\t\t\t\"$dateToString\": bson.M{\n\t\t\t\t\t\t\t\t\t\"format\": \"%Y-%m-%d\",\n\t\t\t\t\t\t\t\t\t\"date\": bson.M{\n\t\t\t\t\t\t\t\t\t\t\"$toDate\": \"$$item.msg.send_time\", // Millisecond timestamp\n\t\t\t\t\t\t\t\t\t},\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\tbson.M{\n\t\t\t\"$unwind\": \"$result\",\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": \"$result.send_date\",\n\t\t\t\t\"count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t\t\"original\": bson.M{\n\t\t\t\t\t\"$push\": \"$$ROOT\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"dates\": \"$$ROOT\",\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":            0,\n\t\t\t\t\"count\":          0,\n\t\t\t\t\"dates.original\": 0,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": nil,\n\t\t\t\t\"count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t\t\"dates\": bson.M{\n\t\t\t\t\t\"$push\": \"$dates\",\n\t\t\t\t},\n\t\t\t\t\"original\": bson.M{\n\t\t\t\t\t\"$push\": \"$original\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$unwind\": \"$original\",\n\t\t},\n\t\tbson.M{\n\t\t\t\"$unwind\": \"$original\",\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": \"$original.result.user_id\",\n\t\t\t\t\"count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t\t\"original\": bson.M{\n\t\t\t\t\t\"$push\": \"$dates\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"dates\": bson.M{\n\t\t\t\t\t\"$arrayElemAt\": bson.A{\"$original\", 0},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"original\": 0,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$sort\": bson.M{\n\t\t\t\t\"count\": sort,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": nil,\n\t\t\t\t\"user_count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t\t\"users\": bson.M{\n\t\t\t\t\t\"$push\": \"$$ROOT\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"dates\": bson.M{\n\t\t\t\t\t\"$arrayElemAt\": bson.A{\"$users\", 0},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"dates\": \"$dates.dates\",\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":         0,\n\t\t\t\t\"users.dates\": 0,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"msg_count\": bson.M{\n\t\t\t\t\t\"$sum\": \"$users.count\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"users\": bson.M{\n\t\t\t\t\t\"$slice\": bson.A{\"$users\", pageNumber - 1, showNumber},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tresult, err := mongoutil.Aggregate[*Result](ctx, m.coll, pipeline, options.Aggregate().SetAllowDiskUse(true))\n\tif err != nil {\n\t\treturn 0, 0, nil, nil, err\n\t}\n\tif len(result) == 0 {\n\t\treturn 0, 0, nil, nil, errs.Wrap(err)\n\t}\n\tusers = make([]*model.UserCount, len(result[0].Users))\n\tfor i, r := range result[0].Users {\n\t\tusers[i] = &model.UserCount{\n\t\t\tUserID: r.UserID,\n\t\t\tCount:  r.Count,\n\t\t}\n\t}\n\tdateCount = make(map[string]int64)\n\tfor _, r := range result[0].Dates {\n\t\tdateCount[r.Date] = r.Count\n\t}\n\treturn result[0].MsgCount, result[0].UserCount, users, dateCount, nil\n}\n\nfunc (m *MsgMgo) RangeGroupSendCount(ctx context.Context, start time.Time, end time.Time, ase bool, pageNumber int32, showNumber int32) (msgCount int64, userCount int64, groups []*model.GroupCount, dateCount map[string]int64, err error) {\n\tvar sort int\n\tif ase {\n\t\tsort = 1\n\t} else {\n\t\tsort = -1\n\t}\n\ttype Result struct {\n\t\tMsgCount  int64 `bson:\"msg_count\"`\n\t\tUserCount int64 `bson:\"user_count\"`\n\t\tGroups    []struct {\n\t\t\tGroupID string `bson:\"_id\"`\n\t\t\tCount   int64  `bson:\"count\"`\n\t\t} `bson:\"groups\"`\n\t\tDates []struct {\n\t\t\tDate  string `bson:\"_id\"`\n\t\t\tCount int64  `bson:\"count\"`\n\t\t} `bson:\"dates\"`\n\t}\n\tpipeline := bson.A{\n\t\tbson.M{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"$and\": bson.A{\n\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\"msgs.msg.send_time\": bson.M{\n\t\t\t\t\t\t\t\"$gte\": start.UnixMilli(),\n\t\t\t\t\t\t\t\"$lt\":  end.UnixMilli(),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\"$or\": bson.A{\n\t\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\t\"doc_id\": bson.M{\n\t\t\t\t\t\t\t\t\t\"$regex\":   \"^g_\",\n\t\t\t\t\t\t\t\t\t\"$options\": \"i\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\t\"doc_id\": bson.M{\n\t\t\t\t\t\t\t\t\t\"$regex\":   \"^sg_\",\n\t\t\t\t\t\t\t\t\t\"$options\": \"i\",\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\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"msgs\": bson.M{\n\t\t\t\t\t\"$filter\": bson.M{\n\t\t\t\t\t\t\"input\": \"$msgs\",\n\t\t\t\t\t\t\"as\":    \"item\",\n\t\t\t\t\t\t\"cond\": bson.M{\n\t\t\t\t\t\t\t\"$and\": bson.A{\n\t\t\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\t\t\"$gte\": bson.A{\n\t\t\t\t\t\t\t\t\t\t\"$$item.msg.send_time\", start.UnixMilli(),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\t\t\"$lt\": bson.A{\n\t\t\t\t\t\t\t\t\t\t\"$$item.msg.send_time\", end.UnixMilli(),\n\t\t\t\t\t\t\t\t\t},\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\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\": 0,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"result\": bson.M{\n\t\t\t\t\t\"$map\": bson.M{\n\t\t\t\t\t\t\"input\": \"$msgs\",\n\t\t\t\t\t\t\"as\":    \"item\",\n\t\t\t\t\t\t\"in\": bson.M{\n\t\t\t\t\t\t\t\"group_id\": \"$$item.msg.group_id\",\n\t\t\t\t\t\t\t\"send_date\": bson.M{\n\t\t\t\t\t\t\t\t\"$dateToString\": bson.M{\n\t\t\t\t\t\t\t\t\t\"format\": \"%Y-%m-%d\",\n\t\t\t\t\t\t\t\t\t\"date\": bson.M{\n\t\t\t\t\t\t\t\t\t\t\"$toDate\": \"$$item.msg.send_time\", // Millisecond timestamp\n\t\t\t\t\t\t\t\t\t},\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\tbson.M{\n\t\t\t\"$unwind\": \"$result\",\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": \"$result.send_date\",\n\t\t\t\t\"count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t\t\"original\": bson.M{\n\t\t\t\t\t\"$push\": \"$$ROOT\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"dates\": \"$$ROOT\",\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":            0,\n\t\t\t\t\"count\":          0,\n\t\t\t\t\"dates.original\": 0,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": nil,\n\t\t\t\t\"count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t\t\"dates\": bson.M{\n\t\t\t\t\t\"$push\": \"$dates\",\n\t\t\t\t},\n\t\t\t\t\"original\": bson.M{\n\t\t\t\t\t\"$push\": \"$original\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$unwind\": \"$original\",\n\t\t},\n\t\tbson.M{\n\t\t\t\"$unwind\": \"$original\",\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": \"$original.result.group_id\",\n\t\t\t\t\"count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t\t\"original\": bson.M{\n\t\t\t\t\t\"$push\": \"$dates\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"dates\": bson.M{\n\t\t\t\t\t\"$arrayElemAt\": bson.A{\"$original\", 0},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"original\": 0,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$sort\": bson.M{\n\t\t\t\t\"count\": sort,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": nil,\n\t\t\t\t\"user_count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t\t\"groups\": bson.M{\n\t\t\t\t\t\"$push\": \"$$ROOT\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"dates\": bson.M{\n\t\t\t\t\t\"$arrayElemAt\": bson.A{\"$groups\", 0},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"dates\": \"$dates.dates\",\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":          0,\n\t\t\t\t\"groups.dates\": 0,\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"msg_count\": bson.M{\n\t\t\t\t\t\"$sum\": \"$groups.count\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"groups\": bson.M{\n\t\t\t\t\t\"$slice\": bson.A{\"$groups\", pageNumber - 1, showNumber},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tresult, err := mongoutil.Aggregate[*Result](ctx, m.coll, pipeline, options.Aggregate().SetAllowDiskUse(true))\n\tif err != nil {\n\t\treturn 0, 0, nil, nil, err\n\t}\n\tif len(result) == 0 {\n\t\treturn 0, 0, nil, nil, errs.Wrap(err)\n\t}\n\tgroups = make([]*model.GroupCount, len(result[0].Groups))\n\tfor i, r := range result[0].Groups {\n\t\tgroups[i] = &model.GroupCount{\n\t\t\tGroupID: r.GroupID,\n\t\t\tCount:   r.Count,\n\t\t}\n\t}\n\tdateCount = make(map[string]int64)\n\tfor _, r := range result[0].Dates {\n\t\tdateCount[r.Date] = r.Count\n\t}\n\treturn result[0].MsgCount, result[0].UserCount, groups, dateCount, nil\n}\n\nfunc (m *MsgMgo) GetRandBeforeMsg(ctx context.Context, ts int64, limit int) ([]*model.MsgDocModel, error) {\n\treturn mongoutil.Aggregate[*model.MsgDocModel](ctx, m.coll, []bson.M{\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"msgs\": bson.M{\n\t\t\t\t\t\"$not\": bson.M{\n\t\t\t\t\t\t\"$elemMatch\": bson.M{\n\t\t\t\t\t\t\t\"msg.send_time\": bson.M{\n\t\t\t\t\t\t\t\t\"$gt\": ts,\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\t{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":                0,\n\t\t\t\t\"doc_id\":             1,\n\t\t\t\t\"msgs.msg.send_time\": 1,\n\t\t\t\t\"msgs.msg.seq\":       1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$sample\": bson.M{\n\t\t\t\t\"size\": limit,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc (m *MsgMgo) DeleteDoc(ctx context.Context, docID string) error {\n\treturn mongoutil.DeleteOne(ctx, m.coll, bson.M{\"doc_id\": docID})\n}\n\nfunc (m *MsgMgo) GetLastMessageSeqByTime(ctx context.Context, conversationID string, time int64) (int64, error) {\n\tpipeline := []bson.M{\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"doc_id\": bson.M{\n\t\t\t\t\t\"$regex\": fmt.Sprintf(\"^%s\", conversationID),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"msgs.msg.send_time\": bson.M{\n\t\t\t\t\t\"$lte\": time,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$sort\": bson.M{\n\t\t\t\t\"_id\": -1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$limit\": 1,\n\t\t},\n\t\t{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":                0,\n\t\t\t\t\"doc_id\":             1,\n\t\t\t\t\"msgs.msg.send_time\": 1,\n\t\t\t\t\"msgs.msg.seq\":       1,\n\t\t\t},\n\t\t},\n\t}\n\tres, err := mongoutil.Aggregate[*model.MsgDocModel](ctx, m.coll, pipeline)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif len(res) == 0 {\n\t\treturn 0, nil\n\t}\n\tvar seq int64\n\tfor _, v := range res[0].Msg {\n\t\tif v.Msg == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif v.Msg.SendTime <= time {\n\t\t\tseq = v.Msg.Seq\n\t\t}\n\t}\n\treturn seq, nil\n}\n\nfunc (m *MsgMgo) GetLastMessage(ctx context.Context, conversationID string) (*model.MsgInfoModel, error) {\n\tpipeline := []bson.M{\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"doc_id\": bson.M{\n\t\t\t\t\t\"$regex\": fmt.Sprintf(\"^%s\", conversationID),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"msgs.msg.status\": bson.M{\n\t\t\t\t\t\"$lt\": constant.MsgStatusHasDeleted,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$sort\": bson.M{\n\t\t\t\t\"_id\": -1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$limit\": 1,\n\t\t},\n\t\t{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":    0,\n\t\t\t\t\"doc_id\": 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$unwind\": \"$msgs\",\n\t\t},\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"msgs.msg.status\": bson.M{\n\t\t\t\t\t\"$lt\": constant.MsgStatusHasDeleted,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$sort\": bson.M{\n\t\t\t\t\"msgs.msg.seq\": -1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$limit\": 1,\n\t\t},\n\t}\n\ttype Result struct {\n\t\tMsgs *model.MsgInfoModel `bson:\"msgs\"`\n\t}\n\tres, err := mongoutil.Aggregate[*Result](ctx, m.coll, pipeline)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(res) == 0 {\n\t\treturn nil, errs.Wrap(mongo.ErrNoDocuments)\n\t}\n\treturn res[0].Msgs, nil\n}\n\nfunc (m *MsgMgo) onlyFindDocIndex(ctx context.Context, docID string, indexes []int64) ([]*model.MsgInfoModel, error) {\n\tif len(indexes) == 0 {\n\t\treturn nil, nil\n\t}\n\tpipeline := mongo.Pipeline{\n\t\tbson.D{{Key: \"$match\", Value: bson.D{\n\t\t\t{Key: \"doc_id\", Value: docID},\n\t\t}}},\n\t\tbson.D{{Key: \"$project\", Value: bson.D{\n\t\t\t{Key: \"_id\", Value: 0},\n\t\t\t{Key: \"doc_id\", Value: 1},\n\t\t\t{Key: \"msgs\", Value: bson.D{\n\t\t\t\t{Key: \"$map\", Value: bson.D{\n\t\t\t\t\t{Key: \"input\", Value: indexes},\n\t\t\t\t\t{Key: \"as\", Value: \"index\"},\n\t\t\t\t\t{Key: \"in\", Value: bson.D{\n\t\t\t\t\t\t{Key: \"$arrayElemAt\", Value: bson.A{\"$msgs\", \"$$index\"}},\n\t\t\t\t\t}},\n\t\t\t\t}},\n\t\t\t}},\n\t\t}}},\n\t}\n\tmsgDocModel, err := mongoutil.Aggregate[*model.MsgDocModel](ctx, m.coll, pipeline)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(msgDocModel) == 0 {\n\t\treturn nil, nil\n\t}\n\treturn msgDocModel[0].Msg, nil\n}\n\n//func (m *MsgMgo) FindSeqs(ctx context.Context, conversationID string, seqs []int64) ([]*model.MsgInfoModel, error) {\n//\tif len(seqs) == 0 {\n//\t\treturn nil, nil\n//\t}\n//\tresult := make([]*model.MsgInfoModel, 0, len(seqs))\n//\tfor docID, seqs := range m.model.GetDocIDSeqsMap(conversationID, seqs) {\n//\t\tres, err := m.onlyFindDocIndex(ctx, docID, datautil.Slice(seqs, m.model.GetMsgIndex))\n//\t\tif err != nil {\n//\t\t\treturn nil, err\n//\t\t}\n//\t\tfor i, re := range res {\n//\t\t\tif re == nil || re.Msg == nil {\n//\t\t\t\tcontinue\n//\t\t\t}\n//\t\t\tresult = append(result, res[i])\n//\t\t}\n//\t}\n//\treturn result, nil\n//}\n\nfunc (m *MsgMgo) findBeforeDocSendTime(ctx context.Context, docID string, limit int64) (int64, int64, error) {\n\tif limit == 0 {\n\t\treturn 0, 0, nil\n\t}\n\tpipeline := []bson.M{\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"doc_id\": docID,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t\"_id\":    0,\n\t\t\t\t\"doc_id\": 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$unwind\": \"$msgs\",\n\t\t},\n\t\t{\n\t\t\t\"$project\": bson.M{\n\t\t\t\t//\"_id\":                0,\n\t\t\t\t//\"doc_id\":             0,\n\t\t\t\t\"msgs.msg.send_time\": 1,\n\t\t\t\t\"msgs.msg.seq\":       1,\n\t\t\t},\n\t\t},\n\t}\n\tif limit > 0 {\n\t\tpipeline = append(pipeline, bson.M{\"$limit\": limit})\n\t}\n\ttype Result struct {\n\t\tMsgs *model.MsgInfoModel `bson:\"msgs\"`\n\t}\n\tres, err := mongoutil.Aggregate[Result](ctx, m.coll, pipeline)\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\tfor i := len(res) - 1; i >= 0; i-- {\n\t\tv := res[i]\n\t\tif v.Msgs != nil && v.Msgs.Msg != nil && v.Msgs.Msg.SendTime > 0 {\n\t\t\treturn v.Msgs.Msg.Seq, v.Msgs.Msg.SendTime, nil\n\t\t}\n\t}\n\treturn 0, 0, nil\n}\n\nfunc (m *MsgMgo) findBeforeSendTime(ctx context.Context, conversationID string, seq int64) (int64, int64, error) {\n\tfirst := true\n\tfor i := m.model.GetDocIndex(seq); i >= 0; i-- {\n\t\tlimit := int64(-1)\n\t\tif first {\n\t\t\tfirst = false\n\t\t\tlimit = m.model.GetLimitForSingleDoc(seq)\n\t\t}\n\t\tdocID := m.model.BuildDocIDByIndex(conversationID, i)\n\t\tmsgSeq, msgSendTime, err := m.findBeforeDocSendTime(ctx, docID, limit)\n\t\tif err != nil {\n\t\t\treturn 0, 0, err\n\t\t}\n\t\tif msgSendTime > 0 {\n\t\t\treturn msgSeq, msgSendTime, nil\n\t\t}\n\t}\n\treturn 0, 0, nil\n}\n\nfunc (m *MsgMgo) FindSeqs(ctx context.Context, conversationID string, seqs []int64) ([]*model.MsgInfoModel, error) {\n\tif len(seqs) == 0 {\n\t\treturn nil, nil\n\t}\n\tvar abnormalSeq []int64\n\tresult := make([]*model.MsgInfoModel, 0, len(seqs))\n\tfor docID, docSeqs := range m.model.GetDocIDSeqsMap(conversationID, seqs) {\n\t\tres, err := m.onlyFindDocIndex(ctx, docID, datautil.Slice(docSeqs, m.model.GetMsgIndex))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(res) == 0 {\n\t\t\tabnormalSeq = append(abnormalSeq, docSeqs...)\n\t\t\tcontinue\n\t\t}\n\t\tfor i, re := range res {\n\t\t\tif re == nil || re.Msg == nil || re.Msg.SendTime == 0 {\n\t\t\t\tabnormalSeq = append(abnormalSeq, docSeqs[i])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresult = append(result, res[i])\n\t\t}\n\t}\n\tif len(abnormalSeq) > 0 {\n\t\tdatautil.Sort(abnormalSeq, false)\n\t\tsendTime := make(map[int64]int64)\n\t\tvar (\n\t\t\tlastSeq      int64\n\t\t\tlastSendTime int64\n\t\t)\n\t\tfor _, seq := range abnormalSeq {\n\t\t\tif lastSendTime > 0 && lastSeq <= seq {\n\t\t\t\tsendTime[seq] = lastSendTime\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmsgSeq, msgSendTime, err := m.findBeforeSendTime(ctx, conversationID, seq)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif msgSendTime <= 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tsendTime[seq] = msgSendTime\n\t\t\tlastSeq = msgSeq\n\t\t\tlastSendTime = msgSendTime\n\t\t}\n\t\tfor _, seq := range abnormalSeq {\n\t\t\tresult = append(result, &model.MsgInfoModel{\n\t\t\t\tMsg: &model.MsgDataModel{\n\t\t\t\t\tSeq:      seq,\n\t\t\t\t\tStatus:   constant.MsgStatusHasDeleted,\n\t\t\t\t\tSendTime: sendTime[seq],\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/msg_test.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"math\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc TestName1(t *testing.T) {\n\t//ctx, cancel := context.WithTimeout(context.Background(), time.Second*300)\n\t//defer cancel()\n\t//cli := Result(mongo.Connect(ctx, options.Client().ApplyURI(\"mongodb://openIM:openIM123@172.16.8.66:37017/openim_v3?maxPoolSize=100\").SetConnectTimeout(5*time.Second)))\n\t//\n\t//v := &MsgMgo{\n\t//\tcoll: cli.Database(\"openim_v3\").Collection(\"msg3\"),\n\t//}\n\t//\n\t//req := &msg.SearchMessageReq{\n\t//\t//RecvID: \"3187706596\",\n\t//\t//SendID:      \"7009965934\",\n\t//\tContentType: 101,\n\t//\t//SendTime:    \"2024-05-06\",\n\t//\t//SessionType: 3,\n\t//\tPagination: &sdkws.RequestPagination{\n\t//\t\tPageNumber: 1,\n\t//\t\tShowNumber: 10,\n\t//\t},\n\t//}\n\t//total, res, err := v.SearchMessage(ctx, req)\n\t//if err != nil {\n\t//\tpanic(err)\n\t//}\n\t//\n\t//for i, re := range res {\n\t//\tt.Logf(\"%d => %d | %+v\", i+1, re.Msg.Seq, re.Msg.Content)\n\t//}\n\t//\n\t//t.Log(total)\n\t//\n\t//msg, err := NewMsgMongo(cli.Database(\"openim_v3\"))\n\t//if err != nil {\n\t//\tpanic(err)\n\t//}\n\t//res, err := msg.GetBeforeMsg(ctx, time.Now().UnixMilli(), []string{\"1:0\"}, 1000)\n\t//if err != nil {\n\t//\tpanic(err)\n\t//}\n\t//t.Log(len(res))\n}\n\nfunc TestName10(t *testing.T) {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\n\tdefer cancel()\n\tcli := Result(mongo.Connect(ctx, options.Client().ApplyURI(\"mongodb://openIM:openIM123@172.16.8.48:37017/openim_v3?maxPoolSize=100\").SetConnectTimeout(5*time.Second)))\n\n\tv := &MsgMgo{\n\t\tcoll: cli.Database(\"openim_v3\").Collection(\"msg3\"),\n\t}\n\topt := options.Find().SetLimit(1000)\n\n\tres, err := mongoutil.Find[model.MsgDocModel](ctx, v.coll, bson.M{}, opt)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tctx = context.Background()\n\tfor i := 0; i < 100000; i++ {\n\t\tfor j := range res {\n\t\t\tres[j].DocID = strconv.FormatUint(rand.Uint64(), 10) + \":0\"\n\t\t}\n\t\tif err := mongoutil.InsertMany(ctx, v.coll, res); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tt.Log(\"====>\", time.Now(), i)\n\t}\n\n}\n\nfunc TestName3(t *testing.T) {\n\tt.Log(uint64(math.MaxUint64))\n\tt.Log(int64(math.MaxInt64))\n\n\tt.Log(int64(math.MinInt64))\n}\n\nfunc TestName4(t *testing.T) {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*300)\n\tdefer cancel()\n\tcli := Result(mongo.Connect(ctx, options.Client().ApplyURI(\"mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100\").SetConnectTimeout(5*time.Second)))\n\n\tmsg, err := NewMsgMongo(cli.Database(\"openim_v3\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tts := time.Now().Add(-time.Hour * 24 * 5).UnixMilli()\n\tt.Log(ts)\n\tres, err := msg.GetLastMessageSeqByTime(ctx, \"sg_1523453548\", ts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(res)\n}\n\nfunc TestName5(t *testing.T) {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*300)\n\tdefer cancel()\n\tcli := Result(mongo.Connect(ctx, options.Client().ApplyURI(\"mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100\").SetConnectTimeout(5*time.Second)))\n\n\ttmp, err := NewMsgMongo(cli.Database(\"openim_v3\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tmsg := tmp.(*MsgMgo)\n\tts := time.Now().Add(-time.Hour * 24 * 5).UnixMilli()\n\tt.Log(ts)\n\tvar seqs []int64\n\tfor i := 1; i < 256; i++ {\n\t\tseqs = append(seqs, int64(i))\n\t}\n\tres, err := msg.FindSeqs(ctx, \"si_4924054191_9511766539\", seqs)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(res)\n}\n\n//func TestName6(t *testing.T) {\n//\tctx, cancel := context.WithTimeout(context.Background(), time.Second*300)\n//\tdefer cancel()\n//\tcli := Result(mongo.Connect(ctx, options.Client().ApplyURI(\"mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100\").SetConnectTimeout(5*time.Second)))\n//\n//\ttmp, err := NewMsgMongo(cli.Database(\"openim_v3\"))\n//\tif err != nil {\n//\t\tpanic(err)\n//\t}\n//\tmsg := tmp.(*MsgMgo)\n//\tseq, sendTime, err := msg.findBeforeSendTime(ctx, \"si_4924054191_9511766539\", 1144)\n//\tif err != nil {\n//\t\tpanic(err)\n//\t}\n//\tt.Log(seq, sendTime)\n//}\n\nfunc TestSearchMessage(t *testing.T) {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*300)\n\tdefer cancel()\n\tcli := Result(mongo.Connect(ctx, options.Client().ApplyURI(\"mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100\").SetConnectTimeout(5*time.Second)))\n\n\tmsgMongo, err := NewMsgMongo(cli.Database(\"openim_v3\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tts := time.Now().Add(-time.Hour * 24 * 5).UnixMilli()\n\tt.Log(ts)\n\treq := &msg.SearchMessageReq{\n\t\t//SendID: \"yjz\",\n\t\t//RecvID: \"aibot\",\n\t\tPagination: &sdkws.RequestPagination{\n\t\t\tPageNumber: 1,\n\t\t\tShowNumber: 20,\n\t\t},\n\t}\n\tcount, resp, err := msgMongo.SearchMessage(ctx, req)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tt.Log(resp, count)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/object.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewS3Mongo(db *mongo.Database) (database.ObjectInfo, error) {\n\tcoll := db.Collection(database.ObjectName)\n\n\t// Create index for name\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"name\", Value: 1},\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\n\t// Create index for create_time\n\t_, err = coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"create_time\", Value: 1},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\n\t// Create index for key\n\t_, err = coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"key\", Value: 1},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\n\treturn &S3Mongo{coll: coll}, nil\n}\n\ntype S3Mongo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (o *S3Mongo) SetObject(ctx context.Context, obj *model.Object) error {\n\tfilter := bson.M{\"name\": obj.Name, \"engine\": obj.Engine}\n\tupdate := bson.M{\n\t\t\"name\":         obj.Name,\n\t\t\"engine\":       obj.Engine,\n\t\t\"key\":          obj.Key,\n\t\t\"size\":         obj.Size,\n\t\t\"content_type\": obj.ContentType,\n\t\t\"group\":        obj.Group,\n\t\t\"create_time\":  obj.CreateTime,\n\t}\n\treturn mongoutil.UpdateOne(ctx, o.coll, filter, bson.M{\"$set\": update}, false, options.Update().SetUpsert(true))\n}\n\nfunc (o *S3Mongo) Take(ctx context.Context, engine string, name string) (*model.Object, error) {\n\tif engine == \"\" {\n\t\treturn mongoutil.FindOne[*model.Object](ctx, o.coll, bson.M{\"name\": name})\n\t}\n\treturn mongoutil.FindOne[*model.Object](ctx, o.coll, bson.M{\"name\": name, \"engine\": engine})\n}\n\nfunc (o *S3Mongo) Delete(ctx context.Context, engine string, name []string) error {\n\tif len(name) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.DeleteOne(ctx, o.coll, bson.M{\"engine\": engine, \"name\": bson.M{\"$in\": name}})\n}\n\nfunc (o *S3Mongo) FindExpirationObject(ctx context.Context, engine string, expiration time.Time, needDelType []string, count int64) ([]*model.Object, error) {\n\topt := options.Find()\n\tif count > 0 {\n\t\topt.SetLimit(count)\n\t}\n\treturn mongoutil.Find[*model.Object](ctx, o.coll, bson.M{\n\t\t\"engine\":      engine,\n\t\t\"create_time\": bson.M{\"$lt\": expiration},\n\t\t\"group\":       bson.M{\"$in\": needDelType},\n\t}, opt)\n}\n\nfunc (o *S3Mongo) GetKeyCount(ctx context.Context, engine string, key string) (int64, error) {\n\treturn mongoutil.Count(ctx, o.coll, bson.M{\"engine\": engine, \"key\": key})\n}\n\nfunc (o *S3Mongo) GetEngineCount(ctx context.Context, engine string) (int64, error) {\n\treturn mongoutil.Count(ctx, o.coll, bson.M{\"engine\": engine})\n}\n\nfunc (o *S3Mongo) GetEngineInfo(ctx context.Context, engine string, limit int, skip int) ([]*model.Object, error) {\n\treturn mongoutil.Find[*model.Object](ctx, o.coll, bson.M{\"engine\": engine}, options.Find().SetLimit(int64(limit)).SetSkip(int64(skip)))\n}\n\nfunc (o *S3Mongo) UpdateEngine(ctx context.Context, oldEngine, oldName string, newEngine string) error {\n\treturn mongoutil.UpdateOne(ctx, o.coll, bson.M{\"engine\": oldEngine, \"name\": oldName}, bson.M{\"$set\": bson.M{\"engine\": newEngine}}, false)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/seq_conversation.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewSeqConversationMongo(db *mongo.Database) (database.SeqConversation, error) {\n\tcoll := db.Collection(database.SeqConversationName)\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"conversation_id\", Value: 1},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &seqConversationMongo{coll: coll}, nil\n}\n\ntype seqConversationMongo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (s *seqConversationMongo) setSeq(ctx context.Context, conversationID string, seq int64, field string) error {\n\tfilter := map[string]any{\n\t\t\"conversation_id\": conversationID,\n\t}\n\tinsert := bson.M{\n\t\t\"conversation_id\": conversationID,\n\t\t\"min_seq\":         0,\n\t\t\"max_seq\":         0,\n\t}\n\tdelete(insert, field)\n\tupdate := map[string]any{\n\t\t\"$set\": bson.M{\n\t\t\tfield: seq,\n\t\t},\n\t\t\"$setOnInsert\": insert,\n\t}\n\topt := options.Update().SetUpsert(true)\n\treturn mongoutil.UpdateOne(ctx, s.coll, filter, update, false, opt)\n}\n\nfunc (s *seqConversationMongo) Malloc(ctx context.Context, conversationID string, size int64) (int64, error) {\n\tif size < 0 {\n\t\treturn 0, errors.New(\"size must be greater than 0\")\n\t}\n\tif size == 0 {\n\t\treturn s.GetMaxSeq(ctx, conversationID)\n\t}\n\tfilter := map[string]any{\"conversation_id\": conversationID}\n\tupdate := map[string]any{\n\t\t\"$inc\":         map[string]any{\"max_seq\": size},\n\t\t\"$setOnInsert\": map[string]any{\"min_seq\": int64(0)},\n\t}\n\topt := options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After).SetProjection(map[string]any{\"_id\": 0, \"max_seq\": 1})\n\tlastSeq, err := mongoutil.FindOneAndUpdate[int64](ctx, s.coll, filter, update, opt)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn lastSeq - size, nil\n}\n\nfunc (s *seqConversationMongo) SetMaxSeq(ctx context.Context, conversationID string, seq int64) error {\n\treturn s.setSeq(ctx, conversationID, seq, \"max_seq\")\n}\n\nfunc (s *seqConversationMongo) GetMaxSeq(ctx context.Context, conversationID string) (int64, error) {\n\tseq, err := mongoutil.FindOne[int64](ctx, s.coll, bson.M{\"conversation_id\": conversationID}, options.FindOne().SetProjection(map[string]any{\"_id\": 0, \"max_seq\": 1}))\n\tif err == nil {\n\t\treturn seq, nil\n\t} else if IsNotFound(err) {\n\t\treturn 0, nil\n\t} else {\n\t\treturn 0, err\n\t}\n}\n\nfunc (s *seqConversationMongo) GetMinSeq(ctx context.Context, conversationID string) (int64, error) {\n\tseq, err := mongoutil.FindOne[int64](ctx, s.coll, bson.M{\"conversation_id\": conversationID}, options.FindOne().SetProjection(map[string]any{\"_id\": 0, \"min_seq\": 1}))\n\tif err == nil {\n\t\treturn seq, nil\n\t} else if IsNotFound(err) {\n\t\treturn 0, nil\n\t} else {\n\t\treturn 0, err\n\t}\n}\n\nfunc (s *seqConversationMongo) SetMinSeq(ctx context.Context, conversationID string, seq int64) error {\n\treturn s.setSeq(ctx, conversationID, seq, \"min_seq\")\n}\n\nfunc (s *seqConversationMongo) GetConversation(ctx context.Context, conversationID string) (*model.SeqConversation, error) {\n\treturn mongoutil.FindOne[*model.SeqConversation](ctx, s.coll, bson.M{\"conversation_id\": conversationID})\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/seq_conversation_test.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc Result[V any](val V, err error) V {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn val\n}\n\nfunc Mongodb() *mongo.Database {\n\treturn Result(\n\t\tmongo.Connect(context.Background(),\n\t\t\toptions.Client().\n\t\t\t\tApplyURI(\"mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100\").\n\t\t\t\tSetConnectTimeout(5*time.Second)),\n\t).Database(\"openim_v3\")\n}\n\nfunc TestUserSeq(t *testing.T) {\n\tuSeq := Result(NewSeqUserMongo(Mongodb())).(*seqUserMongo)\n\tt.Log(uSeq.SetUserMinSeq(context.Background(), \"1000\", \"2000\", 4))\n}\n\nfunc TestConversationSeq(t *testing.T) {\n\tcSeq := Result(NewSeqConversationMongo(Mongodb())).(*seqConversationMongo)\n\tt.Log(cSeq.SetMaxSeq(context.Background(), \"2000\", 10))\n\tt.Log(cSeq.Malloc(context.Background(), \"2000\", 10))\n\tt.Log(cSeq.GetMaxSeq(context.Background(), \"2000\"))\n}\n\nfunc TestUserGetUserReadSeqs(t *testing.T) {\n\tuSeq := Result(NewSeqUserMongo(Mongodb())).(*seqUserMongo)\n\tt.Log(uSeq.GetUserReadSeqs(context.Background(), \"2110910952\", []string{\"sg_345762580\", \"2000\", \"3000\"}))\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/seq_user.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewSeqUserMongo(db *mongo.Database) (database.SeqUser, error) {\n\tcoll := db.Collection(database.SeqUserName)\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"user_id\", Value: 1},\n\t\t\t{Key: \"conversation_id\", Value: 1},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &seqUserMongo{coll: coll}, nil\n}\n\ntype seqUserMongo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (s *seqUserMongo) setSeq(ctx context.Context, conversationID string, userID string, seq int64, field string) error {\n\tfilter := map[string]any{\n\t\t\"user_id\":         userID,\n\t\t\"conversation_id\": conversationID,\n\t}\n\tinsert := bson.M{\n\t\t\"user_id\":         userID,\n\t\t\"conversation_id\": conversationID,\n\t\t\"min_seq\":         0,\n\t\t\"max_seq\":         0,\n\t\t\"read_seq\":        0,\n\t}\n\tdelete(insert, field)\n\tupdate := map[string]any{\n\t\t\"$set\": bson.M{\n\t\t\tfield: seq,\n\t\t},\n\t\t\"$setOnInsert\": insert,\n\t}\n\topt := options.Update().SetUpsert(true)\n\treturn mongoutil.UpdateOne(ctx, s.coll, filter, update, false, opt)\n}\n\nfunc (s *seqUserMongo) getSeq(ctx context.Context, conversationID string, userID string, failed string) (int64, error) {\n\tfilter := map[string]any{\n\t\t\"user_id\":         userID,\n\t\t\"conversation_id\": conversationID,\n\t}\n\topt := options.FindOne().SetProjection(bson.M{\"_id\": 0, failed: 1})\n\tseq, err := mongoutil.FindOne[int64](ctx, s.coll, filter, opt)\n\tif err == nil {\n\t\treturn seq, nil\n\t} else if errors.Is(err, mongo.ErrNoDocuments) {\n\t\treturn 0, nil\n\t} else {\n\t\treturn 0, err\n\t}\n}\n\nfunc (s *seqUserMongo) GetUserMaxSeq(ctx context.Context, conversationID string, userID string) (int64, error) {\n\treturn s.getSeq(ctx, conversationID, userID, \"max_seq\")\n}\n\nfunc (s *seqUserMongo) SetUserMaxSeq(ctx context.Context, conversationID string, userID string, seq int64) error {\n\treturn s.setSeq(ctx, conversationID, userID, seq, \"max_seq\")\n}\n\nfunc (s *seqUserMongo) GetUserMinSeq(ctx context.Context, conversationID string, userID string) (int64, error) {\n\treturn s.getSeq(ctx, conversationID, userID, \"min_seq\")\n}\n\nfunc (s *seqUserMongo) SetUserMinSeq(ctx context.Context, conversationID string, userID string, seq int64) error {\n\treturn s.setSeq(ctx, conversationID, userID, seq, \"min_seq\")\n}\n\nfunc (s *seqUserMongo) GetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error) {\n\treturn s.getSeq(ctx, conversationID, userID, \"read_seq\")\n}\n\nfunc (s *seqUserMongo) notFoundSet0(seq map[string]int64, conversationIDs []string) {\n\tfor _, conversationID := range conversationIDs {\n\t\tif _, ok := seq[conversationID]; !ok {\n\t\t\tseq[conversationID] = 0\n\t\t}\n\t}\n}\n\nfunc (s *seqUserMongo) GetUserReadSeqs(ctx context.Context, userID string, conversationID []string) (map[string]int64, error) {\n\tif len(conversationID) == 0 {\n\t\treturn map[string]int64{}, nil\n\t}\n\tfilter := bson.M{\"user_id\": userID, \"conversation_id\": bson.M{\"$in\": conversationID}}\n\topt := options.Find().SetProjection(bson.M{\"_id\": 0, \"conversation_id\": 1, \"read_seq\": 1})\n\tseqs, err := mongoutil.Find[*model.SeqUser](ctx, s.coll, filter, opt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make(map[string]int64)\n\tfor _, seq := range seqs {\n\t\tres[seq.ConversationID] = seq.ReadSeq\n\t}\n\ts.notFoundSet0(res, conversationID)\n\treturn res, nil\n}\n\nfunc (s *seqUserMongo) SetUserReadSeq(ctx context.Context, conversationID string, userID string, seq int64) error {\n\tdbSeq, err := s.GetUserReadSeq(ctx, conversationID, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif dbSeq > seq {\n\t\treturn nil\n\t}\n\treturn s.setSeq(ctx, conversationID, userID, seq, \"read_seq\")\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage mgo\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\n\t\"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewUserMongo(db *mongo.Database) (database.User, error) {\n\tcoll := db.Collection(database.UserName)\n\t_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{\n\t\tKeys: bson.D{\n\t\t\t{Key: \"user_id\", Value: 1},\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn &UserMgo{coll: coll}, nil\n}\n\ntype UserMgo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (u *UserMgo) Create(ctx context.Context, users []*model.User) error {\n\treturn mongoutil.InsertMany(ctx, u.coll, users)\n}\n\nfunc (u *UserMgo) UpdateByMap(ctx context.Context, userID string, args map[string]any) (err error) {\n\tif len(args) == 0 {\n\t\treturn nil\n\t}\n\treturn mongoutil.UpdateOne(ctx, u.coll, bson.M{\"user_id\": userID}, bson.M{\"$set\": args}, true)\n}\n\nfunc (u *UserMgo) Find(ctx context.Context, userIDs []string) (users []*model.User, err error) {\n\treturn mongoutil.Find[*model.User](ctx, u.coll, bson.M{\"user_id\": bson.M{\"$in\": userIDs}})\n}\n\nfunc (u *UserMgo) Take(ctx context.Context, userID string) (user *model.User, err error) {\n\treturn mongoutil.FindOne[*model.User](ctx, u.coll, bson.M{\"user_id\": userID})\n}\n\nfunc (u *UserMgo) TakeNotification(ctx context.Context, level int64) (user []*model.User, err error) {\n\treturn mongoutil.Find[*model.User](ctx, u.coll, bson.M{\"app_manger_level\": level})\n}\n\nfunc (u *UserMgo) TakeGTEAppManagerLevel(ctx context.Context, level int64) (user []*model.User, err error) {\n\treturn mongoutil.Find[*model.User](ctx, u.coll, bson.M{\"app_manger_level\": bson.M{\"$gte\": level}})\n}\n\nfunc (u *UserMgo) TakeByNickname(ctx context.Context, nickname string) (user []*model.User, err error) {\n\treturn mongoutil.Find[*model.User](ctx, u.coll, bson.M{\"nickname\": nickname})\n}\n\nfunc (u *UserMgo) Page(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error) {\n\treturn mongoutil.FindPage[*model.User](ctx, u.coll, bson.M{}, pagination)\n}\n\nfunc (u *UserMgo) PageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error) {\n\tquery := bson.M{\n\t\t\"$or\": []bson.M{\n\t\t\t{\"app_manger_level\": level1},\n\t\t\t{\"app_manger_level\": level2},\n\t\t},\n\t}\n\n\treturn mongoutil.FindPage[*model.User](ctx, u.coll, query, pagination)\n}\n\nfunc (u *UserMgo) PageFindUserWithKeyword(\n\tctx context.Context,\n\tlevel1 int64,\n\tlevel2 int64,\n\tuserID string,\n\tnickName string,\n\tpagination pagination.Pagination,\n) (count int64, users []*model.User, err error) {\n\t// Initialize the base query with level conditions\n\tquery := bson.M{\n\t\t\"$and\": []bson.M{\n\t\t\t{\"app_manger_level\": bson.M{\"$in\": []int64{level1, level2}}},\n\t\t},\n\t}\n\n\t// Add userID and userName conditions to the query if they are provided\n\tif userID != \"\" || nickName != \"\" {\n\t\tuserConditions := []bson.M{}\n\t\tif userID != \"\" {\n\t\t\t// Use regex for userID\n\t\t\tregexPattern := primitive.Regex{Pattern: userID, Options: \"i\"} // 'i' for case-insensitive matching\n\t\t\tuserConditions = append(userConditions, bson.M{\"user_id\": regexPattern})\n\t\t}\n\t\tif nickName != \"\" {\n\t\t\t// Use regex for userName\n\t\t\tregexPattern := primitive.Regex{Pattern: nickName, Options: \"i\"} // 'i' for case-insensitive matching\n\t\t\tuserConditions = append(userConditions, bson.M{\"nickname\": regexPattern})\n\t\t}\n\t\tquery[\"$and\"] = append(query[\"$and\"].([]bson.M), bson.M{\"$or\": userConditions})\n\t}\n\n\t// Perform the paginated search\n\treturn mongoutil.FindPage[*model.User](ctx, u.coll, query, pagination)\n}\n\nfunc (u *UserMgo) GetAllUserID(ctx context.Context, pagination pagination.Pagination) (int64, []string, error) {\n\treturn mongoutil.FindPage[string](ctx, u.coll, bson.M{}, pagination, options.Find().SetProjection(bson.M{\"_id\": 0, \"user_id\": 1}))\n}\n\nfunc (u *UserMgo) Exist(ctx context.Context, userID string) (exist bool, err error) {\n\treturn mongoutil.Exist(ctx, u.coll, bson.M{\"user_id\": userID})\n}\n\nfunc (u *UserMgo) GetUserGlobalRecvMsgOpt(ctx context.Context, userID string) (opt int, err error) {\n\treturn mongoutil.FindOne[int](ctx, u.coll, bson.M{\"user_id\": userID}, options.FindOne().SetProjection(bson.M{\"_id\": 0, \"global_recv_msg_opt\": 1}))\n}\n\nfunc (u *UserMgo) CountTotal(ctx context.Context, before *time.Time) (count int64, err error) {\n\tif before == nil {\n\t\treturn mongoutil.Count(ctx, u.coll, bson.M{})\n\t}\n\treturn mongoutil.Count(ctx, u.coll, bson.M{\"create_time\": bson.M{\"$lt\": before}})\n}\n\nfunc (u *UserMgo) AddUserCommand(ctx context.Context, userID string, Type int32, UUID string, value string, ex string) error {\n\tcollection := u.coll.Database().Collection(\"userCommands\")\n\n\t// Create a new document instead of updating an existing one\n\tdoc := bson.M{\n\t\t\"userID\":     userID,\n\t\t\"type\":       Type,\n\t\t\"uuid\":       UUID,\n\t\t\"createTime\": time.Now().Unix(), // assuming you want the creation time in Unix timestamp\n\t\t\"value\":      value,\n\t\t\"ex\":         ex,\n\t}\n\n\t_, err := collection.InsertOne(ctx, doc)\n\treturn errs.Wrap(err)\n}\n\nfunc (u *UserMgo) DeleteUserCommand(ctx context.Context, userID string, Type int32, UUID string) error {\n\tcollection := u.coll.Database().Collection(\"userCommands\")\n\n\tfilter := bson.M{\"userID\": userID, \"type\": Type, \"uuid\": UUID}\n\n\tresult, err := collection.DeleteOne(ctx, filter)\n\t// when err is not nil, result might be nil\n\tif err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\tif result.DeletedCount == 0 {\n\t\t// No records found to update\n\t\treturn errs.Wrap(errs.ErrRecordNotFound)\n\t}\n\treturn errs.Wrap(err)\n}\nfunc (u *UserMgo) UpdateUserCommand(ctx context.Context, userID string, Type int32, UUID string, val map[string]any) error {\n\tif len(val) == 0 {\n\t\treturn nil\n\t}\n\n\tcollection := u.coll.Database().Collection(\"userCommands\")\n\n\tfilter := bson.M{\"userID\": userID, \"type\": Type, \"uuid\": UUID}\n\tupdate := bson.M{\"$set\": val}\n\n\tresult, err := collection.UpdateOne(ctx, filter, update)\n\tif err != nil {\n\t\treturn errs.Wrap(err)\n\t}\n\n\tif result.MatchedCount == 0 {\n\t\t// No records found to update\n\t\treturn errs.Wrap(errs.ErrRecordNotFound)\n\t}\n\n\treturn nil\n}\n\nfunc (u *UserMgo) GetUserCommand(ctx context.Context, userID string, Type int32) ([]*user.CommandInfoResp, error) {\n\tcollection := u.coll.Database().Collection(\"userCommands\")\n\tfilter := bson.M{\"userID\": userID, \"type\": Type}\n\n\tcursor, err := collection.Find(ctx, filter)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer cursor.Close(ctx)\n\n\t// Initialize commands as a slice of pointers\n\tcommands := []*user.CommandInfoResp{}\n\n\tfor cursor.Next(ctx) {\n\t\tvar document struct {\n\t\t\tType       int32  `bson:\"type\"`\n\t\t\tUUID       string `bson:\"uuid\"`\n\t\t\tValue      string `bson:\"value\"`\n\t\t\tCreateTime int64  `bson:\"createTime\"`\n\t\t\tEx         string `bson:\"ex\"`\n\t\t}\n\n\t\tif err := cursor.Decode(&document); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcommandInfo := &user.CommandInfoResp{\n\t\t\tType:       document.Type,\n\t\t\tUuid:       document.UUID,\n\t\t\tValue:      document.Value,\n\t\t\tCreateTime: document.CreateTime,\n\t\t\tEx:         document.Ex,\n\t\t}\n\n\t\tcommands = append(commands, commandInfo)\n\t}\n\n\tif err := cursor.Err(); err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\n\treturn commands, nil\n}\nfunc (u *UserMgo) GetAllUserCommand(ctx context.Context, userID string) ([]*user.AllCommandInfoResp, error) {\n\tcollection := u.coll.Database().Collection(\"userCommands\")\n\tfilter := bson.M{\"userID\": userID}\n\n\tcursor, err := collection.Find(ctx, filter)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\tdefer cursor.Close(ctx)\n\n\t// Initialize commands as a slice of pointers\n\tcommands := []*user.AllCommandInfoResp{}\n\n\tfor cursor.Next(ctx) {\n\t\tvar document struct {\n\t\t\tType       int32  `bson:\"type\"`\n\t\t\tUUID       string `bson:\"uuid\"`\n\t\t\tValue      string `bson:\"value\"`\n\t\t\tCreateTime int64  `bson:\"createTime\"`\n\t\t\tEx         string `bson:\"ex\"`\n\t\t}\n\n\t\tif err := cursor.Decode(&document); err != nil {\n\t\t\treturn nil, errs.Wrap(err)\n\t\t}\n\n\t\tcommandInfo := &user.AllCommandInfoResp{\n\t\t\tType:       document.Type,\n\t\t\tUuid:       document.UUID,\n\t\t\tValue:      document.Value,\n\t\t\tCreateTime: document.CreateTime,\n\t\t\tEx:         document.Ex,\n\t\t}\n\n\t\tcommands = append(commands, commandInfo)\n\t}\n\n\tif err := cursor.Err(); err != nil {\n\t\treturn nil, errs.Wrap(err)\n\t}\n\treturn commands, nil\n}\nfunc (u *UserMgo) CountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error) {\n\tpipeline := bson.A{\n\t\tbson.M{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"create_time\": bson.M{\n\t\t\t\t\t\"$gte\": start,\n\t\t\t\t\t\"$lt\":  end,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tbson.M{\n\t\t\t\"$group\": bson.M{\n\t\t\t\t\"_id\": bson.M{\n\t\t\t\t\t\"$dateToString\": bson.M{\n\t\t\t\t\t\t\"format\": \"%Y-%m-%d\",\n\t\t\t\t\t\t\"date\":   \"$create_time\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"count\": bson.M{\n\t\t\t\t\t\"$sum\": 1,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\ttype Item struct {\n\t\tDate  string `bson:\"_id\"`\n\t\tCount int64  `bson:\"count\"`\n\t}\n\titems, err := mongoutil.Aggregate[Item](ctx, u.coll, pipeline)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make(map[string]int64, len(items))\n\tfor _, item := range items {\n\t\tres[item.Date] = item.Count\n\t}\n\treturn res, nil\n}\n\nfunc (u *UserMgo) SortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error) {\n\tif len(userIDName) == 0 {\n\t\treturn nil, nil\n\t}\n\tuserIDs := make([]string, 0, len(userIDName))\n\tattached := make(map[string]string)\n\tfor userID, name := range userIDName {\n\t\tuserIDs = append(userIDs, userID)\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tattached[userID] = name\n\t}\n\tvar sortValue int\n\tif asc {\n\t\tsortValue = 1\n\t} else {\n\t\tsortValue = -1\n\t}\n\tif len(attached) == 0 {\n\t\tfilter := bson.M{\"user_id\": bson.M{\"$in\": userIDs}}\n\t\topt := options.Find().SetSort(bson.M{\"nickname\": sortValue})\n\t\treturn mongoutil.Find[*model.User](ctx, u.coll, filter, opt)\n\t}\n\tpipeline := []bson.M{\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"user_id\": bson.M{\"$in\": userIDs},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"_query_sort_name\": bson.M{\n\t\t\t\t\t\"$arrayElemAt\": []any{\n\t\t\t\t\t\tbson.M{\n\t\t\t\t\t\t\t\"$filter\": bson.M{\n\t\t\t\t\t\t\t\t\"input\": bson.M{\n\t\t\t\t\t\t\t\t\t\"$objectToArray\": attached,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"as\": \"item\",\n\t\t\t\t\t\t\t\t\"cond\": bson.M{\n\t\t\t\t\t\t\t\t\t\"$eq\": []any{\"$$item.k\", \"$user_id\"},\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\t0,\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\"$addFields\": bson.M{\n\t\t\t\t\"_query_sort_name\": bson.M{\n\t\t\t\t\t\"$ifNull\": []any{\"$_query_sort_name.v\", \"$nickname\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$sort\": bson.M{\n\t\t\t\t\"_query_sort_name\": sortValue,\n\t\t\t},\n\t\t},\n\t}\n\treturn mongoutil.Aggregate[*model.User](ctx, u.coll, pipeline)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/version_log.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/versionctx\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nfunc NewVersionLog(coll *mongo.Collection) (database.VersionLog, error) {\n\tlm := &VersionLogMgo{coll: coll}\n\tif err := lm.initIndex(context.Background()); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"init version log index failed\", \"coll\", coll.Name())\n\t}\n\treturn lm, nil\n}\n\ntype VersionLogMgo struct {\n\tcoll *mongo.Collection\n}\n\nfunc (l *VersionLogMgo) initIndex(ctx context.Context) error {\n\t_, err := l.coll.Indexes().CreateOne(ctx, mongo.IndexModel{\n\t\tKeys: bson.M{\n\t\t\t\"d_id\": 1,\n\t\t},\n\t\tOptions: options.Index().SetUnique(true),\n\t})\n\n\treturn err\n}\n\nfunc (l *VersionLogMgo) IncrVersion(ctx context.Context, dId string, eIds []string, state int32) error {\n\t_, err := l.IncrVersionResult(ctx, dId, eIds, state)\n\treturn err\n}\n\nfunc (l *VersionLogMgo) IncrVersionResult(ctx context.Context, dId string, eIds []string, state int32) (*model.VersionLog, error) {\n\tvl, err := l.incrVersionResult(ctx, dId, eIds, state)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tversionctx.GetVersionLog(ctx).Append(versionctx.Collection{\n\t\tName: l.coll.Name(),\n\t\tDoc:  vl,\n\t})\n\treturn vl, nil\n}\n\nfunc (l *VersionLogMgo) incrVersionResult(ctx context.Context, dId string, eIds []string, state int32) (*model.VersionLog, error) {\n\tif len(eIds) == 0 {\n\t\treturn nil, errs.ErrArgs.WrapMsg(\"elem id is empty\", \"dId\", dId)\n\t}\n\tnow := time.Now()\n\tif res, err := l.writeLogBatch2(ctx, dId, eIds, state, now); err == nil {\n\t\treturn res, nil\n\t} else if !errors.Is(err, mongo.ErrNoDocuments) {\n\t\treturn nil, err\n\t}\n\tif res, err := l.initDoc(ctx, dId, eIds, state, now); err == nil {\n\t\treturn res, nil\n\t} else if !mongo.IsDuplicateKeyError(err) {\n\t\treturn nil, err\n\t}\n\treturn l.writeLogBatch2(ctx, dId, eIds, state, now)\n}\n\nfunc (l *VersionLogMgo) initDoc(ctx context.Context, dId string, eIds []string, state int32, now time.Time) (*model.VersionLog, error) {\n\twl := model.VersionLogTable{\n\t\tID:         primitive.NewObjectID(),\n\t\tDID:        dId,\n\t\tLogs:       make([]model.VersionLogElem, 0, len(eIds)),\n\t\tVersion:    database.FirstVersion,\n\t\tDeleted:    database.DefaultDeleteVersion,\n\t\tLastUpdate: now,\n\t}\n\tfor _, eId := range eIds {\n\t\twl.Logs = append(wl.Logs, model.VersionLogElem{\n\t\t\tEID:        eId,\n\t\t\tState:      state,\n\t\t\tVersion:    database.FirstVersion,\n\t\t\tLastUpdate: now,\n\t\t})\n\t}\n\tif _, err := l.coll.InsertOne(ctx, &wl); err != nil {\n\t\treturn nil, err\n\t}\n\treturn wl.VersionLog(), nil\n}\n\nfunc (l *VersionLogMgo) writeLogBatch2(ctx context.Context, dId string, eIds []string, state int32, now time.Time) (*model.VersionLog, error) {\n\tif eIds == nil {\n\t\teIds = []string{}\n\t}\n\tfilter := bson.M{\n\t\t\"d_id\": dId,\n\t}\n\telems := make([]bson.M, 0, len(eIds))\n\tfor _, eId := range eIds {\n\t\telems = append(elems, bson.M{\n\t\t\t\"e_id\":        eId,\n\t\t\t\"version\":     \"$version\",\n\t\t\t\"state\":       state,\n\t\t\t\"last_update\": now,\n\t\t})\n\t}\n\tpipeline := []bson.M{\n\t\t{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"delete_e_ids\": eIds,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$set\": bson.M{\n\t\t\t\t\"version\":     bson.M{\"$add\": []any{\"$version\", 1}},\n\t\t\t\t\"last_update\": now,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$set\": bson.M{\n\t\t\t\t\"logs\": bson.M{\n\t\t\t\t\t\"$filter\": bson.M{\n\t\t\t\t\t\t\"input\": \"$logs\",\n\t\t\t\t\t\t\"as\":    \"log\",\n\t\t\t\t\t\t\"cond\": bson.M{\n\t\t\t\t\t\t\t\"$not\": bson.M{\n\t\t\t\t\t\t\t\t\"$in\": []any{\"$$log.e_id\", \"$delete_e_ids\"},\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\t{\n\t\t\t\"$set\": bson.M{\n\t\t\t\t\"logs\": bson.M{\n\t\t\t\t\t\"$concatArrays\": []any{\n\t\t\t\t\t\t\"$logs\",\n\t\t\t\t\t\telems,\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\"$unset\": \"delete_e_ids\",\n\t\t},\n\t}\n\tprojection := bson.M{\n\t\t\"logs\": 0,\n\t}\n\topt := options.FindOneAndUpdate().SetUpsert(false).SetReturnDocument(options.After).SetProjection(projection)\n\tres, err := mongoutil.FindOneAndUpdate[*model.VersionLog](ctx, l.coll, filter, pipeline, opt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres.Logs = make([]model.VersionLogElem, 0, len(eIds))\n\tfor _, id := range eIds {\n\t\tres.Logs = append(res.Logs, model.VersionLogElem{\n\t\t\tEID:        id,\n\t\t\tState:      state,\n\t\t\tVersion:    res.Version,\n\t\t\tLastUpdate: res.LastUpdate,\n\t\t})\n\t}\n\treturn res, nil\n}\n\nfunc (l *VersionLogMgo) findDoc(ctx context.Context, dId string) (*model.VersionLog, error) {\n\tvl, err := mongoutil.FindOne[*model.VersionLogTable](ctx, l.coll, bson.M{\"d_id\": dId}, options.FindOne().SetProjection(bson.M{\"logs\": 0}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn vl.VersionLog(), nil\n}\n\nfunc (l *VersionLogMgo) FindChangeLog(ctx context.Context, dId string, version uint, limit int) (*model.VersionLog, error) {\n\tif wl, err := l.findChangeLog(ctx, dId, version, limit); err == nil {\n\t\treturn wl, nil\n\t} else if !errors.Is(err, mongo.ErrNoDocuments) {\n\t\treturn nil, err\n\t}\n\tlog.ZDebug(ctx, \"init doc\", \"dId\", dId)\n\tif res, err := l.initDoc(ctx, dId, nil, 0, time.Now()); err == nil {\n\t\tlog.ZDebug(ctx, \"init doc success\", \"dId\", dId)\n\t\treturn res, nil\n\t} else if mongo.IsDuplicateKeyError(err) {\n\t\treturn l.findChangeLog(ctx, dId, version, limit)\n\t} else {\n\t\treturn nil, err\n\t}\n}\n\nfunc (l *VersionLogMgo) BatchFindChangeLog(ctx context.Context, dIds []string, versions []uint, limits []int) (vLogs []*model.VersionLog, err error) {\n\tfor i := 0; i < len(dIds); i++ {\n\t\tif vLog, err := l.findChangeLog(ctx, dIds[i], versions[i], limits[i]); err == nil {\n\t\t\tvLogs = append(vLogs, vLog)\n\t\t} else if !errors.Is(err, mongo.ErrNoDocuments) {\n\t\t\tlog.ZError(ctx, \"findChangeLog error:\", errs.Wrap(err))\n\t\t}\n\t\tlog.ZDebug(ctx, \"init doc\", \"dId\", dIds[i])\n\t\tif res, err := l.initDoc(ctx, dIds[i], nil, 0, time.Now()); err == nil {\n\t\t\tlog.ZDebug(ctx, \"init doc success\", \"dId\", dIds[i])\n\t\t\tvLogs = append(vLogs, res)\n\t\t} else if mongo.IsDuplicateKeyError(err) {\n\t\t\tl.findChangeLog(ctx, dIds[i], versions[i], limits[i])\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"init doc error:\", errs.Wrap(err))\n\t\t}\n\t}\n\treturn vLogs, errs.Wrap(err)\n}\n\nfunc (l *VersionLogMgo) findChangeLog(ctx context.Context, dId string, version uint, limit int) (*model.VersionLog, error) {\n\tif version == 0 && limit == 0 {\n\t\treturn l.findDoc(ctx, dId)\n\t}\n\tpipeline := []bson.M{\n\t\t{\n\t\t\t\"$match\": bson.M{\n\t\t\t\t\"d_id\": dId,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"logs\": bson.M{\n\t\t\t\t\t\"$cond\": bson.M{\n\t\t\t\t\t\t\"if\": bson.M{\n\t\t\t\t\t\t\t\"$or\": []bson.M{\n\t\t\t\t\t\t\t\t{\"$lt\": []any{\"$version\", version}},\n\t\t\t\t\t\t\t\t{\"$gte\": []any{\"$deleted\", version}},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"then\": []any{},\n\t\t\t\t\t\t\"else\": \"$logs\",\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\"$addFields\": bson.M{\n\t\t\t\t\"logs\": bson.M{\n\t\t\t\t\t\"$filter\": bson.M{\n\t\t\t\t\t\t\"input\": \"$logs\",\n\t\t\t\t\t\t\"as\":    \"l\",\n\t\t\t\t\t\t\"cond\": bson.M{\n\t\t\t\t\t\t\t\"$gt\": []any{\"$$l.version\", version},\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\t{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"log_len\": bson.M{\"$size\": \"$logs\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"$addFields\": bson.M{\n\t\t\t\t\"logs\": bson.M{\n\t\t\t\t\t\"$cond\": bson.M{\n\t\t\t\t\t\t\"if\": bson.M{\n\t\t\t\t\t\t\t\"$gt\": []any{\"$log_len\", limit},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"then\": []any{},\n\t\t\t\t\t\t\"else\": \"$logs\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tif limit <= 0 {\n\t\tpipeline = pipeline[:len(pipeline)-1]\n\t}\n\tvl, err := mongoutil.Aggregate[*model.VersionLog](ctx, l.coll, pipeline)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(vl) == 0 {\n\t\treturn nil, mongo.ErrNoDocuments\n\t}\n\treturn vl[0], nil\n}\n\nfunc (l *VersionLogMgo) DeleteAfterUnchangedLog(ctx context.Context, deadline time.Time) error {\n\treturn mongoutil.DeleteMany(ctx, l.coll, bson.M{\n\t\t\"last_update\": bson.M{\n\t\t\t\"$lt\": deadline,\n\t\t},\n\t})\n}\n\nfunc (l *VersionLogMgo) Delete(ctx context.Context, dId string) error {\n\treturn mongoutil.DeleteOne(ctx, l.coll, bson.M{\"d_id\": dId})\n}\n"
  },
  {
    "path": "pkg/common/storage/database/mgo/version_test.go",
    "content": "package mgo\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n\t\"testing\"\n\t\"time\"\n)\n\n//func Result[V any](val V, err error) V {\n//\tif err != nil {\n//\t\tpanic(err)\n//\t}\n//\treturn val\n//}\n\nfunc Check(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TestName(t *testing.T) {\n\tcli := Result(mongo.Connect(context.Background(), options.Client().ApplyURI(\"mongodb://openIM:openIM123@172.16.8.48:37017/openim_v3?maxPoolSize=100\").SetConnectTimeout(5*time.Second)))\n\tcoll := cli.Database(\"openim_v3\").Collection(\"version_test\")\n\ttmp, err := NewVersionLog(coll)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvl := tmp.(*VersionLogMgo)\n\tres, err := vl.incrVersionResult(context.Background(), \"100\", []string{\"1000\", \"1001\", \"1003\"}, model.VersionStateInsert)\n\tif err != nil {\n\t\tt.Log(err)\n\t\treturn\n\t}\n\tt.Logf(\"%+v\", res)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n)\n\ntype Msg interface {\n\tCreate(ctx context.Context, model *model.MsgDocModel) error\n\tUpdateMsg(ctx context.Context, docID string, index int64, key string, value any) (*mongo.UpdateResult, error)\n\tPushUnique(ctx context.Context, docID string, index int64, key string, value any) (*mongo.UpdateResult, error)\n\tFindOneByDocID(ctx context.Context, docID string) (*model.MsgDocModel, error)\n\tGetMsgBySeqIndexIn1Doc(ctx context.Context, userID, docID string, seqs []int64) ([]*model.MsgInfoModel, error)\n\tGetNewestMsg(ctx context.Context, conversationID string) (*model.MsgInfoModel, error)\n\tGetOldestMsg(ctx context.Context, conversationID string) (*model.MsgInfoModel, error)\n\tDeleteMsgsInOneDocByIndex(ctx context.Context, docID string, indexes []int) error\n\tMarkSingleChatMsgsAsRead(ctx context.Context, userID string, docID string, indexes []int64) error\n\tSearchMessage(ctx context.Context, req *msg.SearchMessageReq) (int64, []*model.MsgInfoModel, error)\n\tRangeUserSendCount(ctx context.Context, start time.Time, end time.Time, group bool, ase bool, pageNumber int32, showNumber int32) (msgCount int64, userCount int64, users []*model.UserCount, dateCount map[string]int64, err error)\n\tRangeGroupSendCount(ctx context.Context, start time.Time, end time.Time, ase bool, pageNumber int32, showNumber int32) (msgCount int64, userCount int64, groups []*model.GroupCount, dateCount map[string]int64, err error)\n\tDeleteDoc(ctx context.Context, docID string) error\n\tGetRandBeforeMsg(ctx context.Context, ts int64, limit int) ([]*model.MsgDocModel, error)\n\tGetLastMessageSeqByTime(ctx context.Context, conversationID string, time int64) (int64, error)\n\tGetLastMessage(ctx context.Context, conversationID string) (*model.MsgInfoModel, error)\n\tFindSeqs(ctx context.Context, conversationID string, seqs []int64) ([]*model.MsgInfoModel, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/name.go",
    "content": "package database\n\nconst (\n\tBlackName               = \"black\"\n\tConversationName        = \"conversation\"\n\tFriendName              = \"friend\"\n\tFriendVersionName       = \"friend_version\"\n\tFriendRequestName       = \"friend_request\"\n\tGroupName               = \"group\"\n\tGroupMemberName         = \"group_member\"\n\tGroupMemberVersionName  = \"group_member_version\"\n\tGroupJoinVersionName    = \"group_join_version\"\n\tConversationVersionName = \"conversation_version\"\n\tGroupRequestName        = \"group_request\"\n\tLogName                 = \"log\"\n\tObjectName              = \"s3\"\n\tUserName                = \"user\"\n\tSeqConversationName     = \"seq\"\n\tSeqUserName             = \"seq_user\"\n\tStreamMsgName           = \"stream_msg\"\n\tCacheName               = \"cache\"\n)\n"
  },
  {
    "path": "pkg/common/storage/database/object.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n)\n\ntype ObjectInfo interface {\n\tSetObject(ctx context.Context, obj *model.Object) error\n\tTake(ctx context.Context, engine string, name string) (*model.Object, error)\n\tDelete(ctx context.Context, engine string, name []string) error\n\tFindExpirationObject(ctx context.Context, engine string, expiration time.Time, needDelType []string, count int64) ([]*model.Object, error)\n\tGetKeyCount(ctx context.Context, engine string, key string) (int64, error)\n\n\tGetEngineCount(ctx context.Context, engine string) (int64, error)\n\tGetEngineInfo(ctx context.Context, engine string, limit int, skip int) ([]*model.Object, error)\n\tUpdateEngine(ctx context.Context, oldEngine, oldName string, newEngine string) error\n}\n"
  },
  {
    "path": "pkg/common/storage/database/seq.go",
    "content": "package database\n\nimport \"context\"\n\ntype SeqTime struct {\n\tSeq  int64\n\tTime int64\n}\n\ntype SeqConversation interface {\n\tMalloc(ctx context.Context, conversationID string, size int64) (int64, error)\n\tGetMaxSeq(ctx context.Context, conversationID string) (int64, error)\n\tSetMaxSeq(ctx context.Context, conversationID string, seq int64) error\n\tGetMinSeq(ctx context.Context, conversationID string) (int64, error)\n\tSetMinSeq(ctx context.Context, conversationID string, seq int64) error\n}\n"
  },
  {
    "path": "pkg/common/storage/database/seq_user.go",
    "content": "package database\n\nimport \"context\"\n\ntype SeqUser interface {\n\tGetUserMaxSeq(ctx context.Context, conversationID string, userID string) (int64, error)\n\tSetUserMaxSeq(ctx context.Context, conversationID string, userID string, seq int64) error\n\tGetUserMinSeq(ctx context.Context, conversationID string, userID string) (int64, error)\n\tSetUserMinSeq(ctx context.Context, conversationID string, userID string, seq int64) error\n\tGetUserReadSeq(ctx context.Context, conversationID string, userID string) (int64, error)\n\tSetUserReadSeq(ctx context.Context, conversationID string, userID string, seq int64) error\n\tGetUserReadSeqs(ctx context.Context, userID string, conversationID []string) (map[string]int64, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage database\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/db/pagination\"\n)\n\ntype User interface {\n\tCreate(ctx context.Context, users []*model.User) (err error)\n\tUpdateByMap(ctx context.Context, userID string, args map[string]any) (err error)\n\tFind(ctx context.Context, userIDs []string) (users []*model.User, err error)\n\tTake(ctx context.Context, userID string) (user *model.User, err error)\n\tTakeNotification(ctx context.Context, level int64) (user []*model.User, err error)\n\tTakeGTEAppManagerLevel(ctx context.Context, level int64) (user []*model.User, err error)\n\tTakeByNickname(ctx context.Context, nickname string) (user []*model.User, err error)\n\tPage(ctx context.Context, pagination pagination.Pagination) (count int64, users []*model.User, err error)\n\tPageFindUser(ctx context.Context, level1 int64, level2 int64, pagination pagination.Pagination) (count int64, users []*model.User, err error)\n\tPageFindUserWithKeyword(ctx context.Context, level1 int64, level2 int64, userID, nickName string, pagination pagination.Pagination) (count int64, users []*model.User, err error)\n\tExist(ctx context.Context, userID string) (exist bool, err error)\n\tGetAllUserID(ctx context.Context, pagination pagination.Pagination) (count int64, userIDs []string, err error)\n\tGetUserGlobalRecvMsgOpt(ctx context.Context, userID string) (opt int, err error)\n\t// Get user total quantity\n\tCountTotal(ctx context.Context, before *time.Time) (count int64, err error)\n\t// Get user total quantity every day\n\tCountRangeEverydayTotal(ctx context.Context, start time.Time, end time.Time) (map[string]int64, error)\n\n\tSortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error)\n\n\t// CRUD user command\n\tAddUserCommand(ctx context.Context, userID string, Type int32, UUID string, value string, ex string) error\n\tDeleteUserCommand(ctx context.Context, userID string, Type int32, UUID string) error\n\tUpdateUserCommand(ctx context.Context, userID string, Type int32, UUID string, val map[string]any) error\n\tGetUserCommand(ctx context.Context, userID string, Type int32) ([]*user.CommandInfoResp, error)\n\tGetAllUserCommand(ctx context.Context, userID string) ([]*user.AllCommandInfoResp, error)\n}\n"
  },
  {
    "path": "pkg/common/storage/database/version_log.go",
    "content": "package database\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n)\n\nconst (\n\tFirstVersion         = 1\n\tDefaultDeleteVersion = 0\n)\n\ntype VersionLog interface {\n\tIncrVersion(ctx context.Context, dId string, eIds []string, state int32) error\n\tFindChangeLog(ctx context.Context, dId string, version uint, limit int) (*model.VersionLog, error)\n\tBatchFindChangeLog(ctx context.Context, dIds []string, versions []uint, limits []int) ([]*model.VersionLog, error)\n\tDeleteAfterUnchangedLog(ctx context.Context, deadline time.Time) error\n\tDelete(ctx context.Context, dId string) error\n}\n"
  },
  {
    "path": "pkg/common/storage/kafka/config.go",
    "content": "// Copyright © 2024 OpenIM open source community. All rights reserved.\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\npackage kafka\n\ntype TLSConfig struct {\n\tEnableTLS          bool   `yaml:\"enableTLS\"`\n\tCACrt              string `yaml:\"caCrt\"`\n\tClientCrt          string `yaml:\"clientCrt\"`\n\tClientKey          string `yaml:\"clientKey\"`\n\tClientKeyPwd       string `yaml:\"clientKeyPwd\"`\n\tInsecureSkipVerify bool   `yaml:\"insecureSkipVerify\"`\n}\n\ntype Config struct {\n\tUsername     string    `yaml:\"username\"`\n\tPassword     string    `yaml:\"password\"`\n\tProducerAck  string    `yaml:\"producerAck\"`\n\tCompressType string    `yaml:\"compressType\"`\n\tAddr         []string  `yaml:\"addr\"`\n\tTLS          TLSConfig `yaml:\"tls\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/kafka/consumer_group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage kafka\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/IBM/sarama\"\n\t\"github.com/openimsdk/tools/log\"\n)\n\ntype MConsumerGroup struct {\n\tsarama.ConsumerGroup\n\tgroupID string\n\ttopics  []string\n}\n\nfunc NewMConsumerGroup(conf *Config, groupID string, topics []string, autoCommitEnable bool) (*MConsumerGroup, error) {\n\tconfig, err := BuildConsumerGroupConfig(conf, sarama.OffsetNewest, autoCommitEnable)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgroup, err := NewConsumerGroup(config, conf.Addr, groupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &MConsumerGroup{\n\t\tConsumerGroup: group,\n\t\tgroupID:       groupID,\n\t\ttopics:        topics,\n\t}, nil\n}\n\nfunc (mc *MConsumerGroup) GetContextFromMsg(cMsg *sarama.ConsumerMessage) context.Context {\n\treturn GetContextWithMQHeader(cMsg.Headers)\n}\n\nfunc (mc *MConsumerGroup) RegisterHandleAndConsumer(ctx context.Context, handler sarama.ConsumerGroupHandler) {\n\tfor {\n\t\terr := mc.ConsumerGroup.Consume(ctx, mc.topics, handler)\n\t\tif errors.Is(err, sarama.ErrClosedConsumerGroup) {\n\t\t\treturn\n\t\t}\n\t\tif errors.Is(err, context.Canceled) {\n\t\t\treturn\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"consume err\", err, \"topic\", mc.topics, \"groupID\", mc.groupID)\n\t\t}\n\t}\n}\n\nfunc (mc *MConsumerGroup) Close() error {\n\treturn mc.ConsumerGroup.Close()\n}\n"
  },
  {
    "path": "pkg/common/storage/kafka/producer.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage kafka\n\nimport (\n\t\"context\"\n\t\"github.com/IBM/sarama\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// Producer represents a Kafka producer.\ntype Producer struct {\n\taddr     []string\n\ttopic    string\n\tconfig   *sarama.Config\n\tproducer sarama.SyncProducer\n}\n\nfunc NewKafkaProducer(config *sarama.Config, addr []string, topic string) (*Producer, error) {\n\tproducer, err := NewProducer(config, addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Producer{\n\t\taddr:     addr,\n\t\ttopic:    topic,\n\t\tconfig:   config,\n\t\tproducer: producer,\n\t}, nil\n}\n\n// SendMessage sends a message to the Kafka topic configured in the Producer.\nfunc (p *Producer) SendMessage(ctx context.Context, key string, msg proto.Message) (int32, int64, error) {\n\t// Marshal the protobuf message\n\tbMsg, err := proto.Marshal(msg)\n\tif err != nil {\n\t\treturn 0, 0, errs.WrapMsg(err, \"kafka proto Marshal err\")\n\t}\n\tif len(bMsg) == 0 {\n\t\treturn 0, 0, errs.WrapMsg(errEmptyMsg, \"kafka proto Marshal err\")\n\t}\n\n\t// Prepare Kafka message\n\tkMsg := &sarama.ProducerMessage{\n\t\tTopic: p.topic,\n\t\tKey:   sarama.StringEncoder(key),\n\t\tValue: sarama.ByteEncoder(bMsg),\n\t}\n\n\t// Validate message key and value\n\tif kMsg.Key.Length() == 0 || kMsg.Value.Length() == 0 {\n\t\treturn 0, 0, errs.Wrap(errEmptyMsg)\n\t}\n\n\t// Attach context metadata as headers\n\theader, err := GetMQHeaderWithContext(ctx)\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\tkMsg.Headers = header\n\n\t// Send the message\n\tpartition, offset, err := p.producer.SendMessage(kMsg)\n\tif err != nil {\n\t\treturn 0, 0, errs.WrapMsg(err, \"p.producer.SendMessage error\")\n\t}\n\n\treturn partition, offset, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/kafka/sarama.go",
    "content": "package kafka\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\n\t\"github.com/IBM/sarama\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nfunc BuildConsumerGroupConfig(conf *Config, initial int64, autoCommitEnable bool) (*sarama.Config, error) {\n\tkfk := sarama.NewConfig()\n\tkfk.Version = sarama.V2_0_0_0\n\tkfk.Consumer.Offsets.Initial = initial\n\tkfk.Consumer.Offsets.AutoCommit.Enable = autoCommitEnable\n\tkfk.Consumer.Return.Errors = false\n\tif conf.Username != \"\" || conf.Password != \"\" {\n\t\tkfk.Net.SASL.Enable = true\n\t\tkfk.Net.SASL.User = conf.Username\n\t\tkfk.Net.SASL.Password = conf.Password\n\t}\n\tif conf.TLS.EnableTLS {\n\t\ttls, err := newTLSConfig(conf.TLS.ClientCrt, conf.TLS.ClientKey, conf.TLS.CACrt, []byte(conf.TLS.ClientKeyPwd), conf.TLS.InsecureSkipVerify)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tkfk.Net.TLS.Config = tls\n\t\tkfk.Net.TLS.Enable = true\n\t}\n\treturn kfk, nil\n}\n\nfunc NewConsumerGroup(conf *sarama.Config, addr []string, groupID string) (sarama.ConsumerGroup, error) {\n\tcg, err := sarama.NewConsumerGroup(addr, groupID, conf)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"NewConsumerGroup failed\", \"addr\", addr, \"groupID\", groupID, \"conf\", *conf)\n\t}\n\treturn cg, nil\n}\n\nfunc BuildProducerConfig(conf Config) (*sarama.Config, error) {\n\tkfk := sarama.NewConfig()\n\tkfk.Producer.Return.Successes = true\n\tkfk.Producer.Return.Errors = true\n\tkfk.Producer.Partitioner = sarama.NewHashPartitioner\n\tif conf.Username != \"\" || conf.Password != \"\" {\n\t\tkfk.Net.SASL.Enable = true\n\t\tkfk.Net.SASL.User = conf.Username\n\t\tkfk.Net.SASL.Password = conf.Password\n\t}\n\tswitch strings.ToLower(conf.ProducerAck) {\n\tcase \"no_response\":\n\t\tkfk.Producer.RequiredAcks = sarama.NoResponse\n\tcase \"wait_for_local\":\n\t\tkfk.Producer.RequiredAcks = sarama.WaitForLocal\n\tcase \"wait_for_all\":\n\t\tkfk.Producer.RequiredAcks = sarama.WaitForAll\n\tdefault:\n\t\tkfk.Producer.RequiredAcks = sarama.WaitForAll\n\t}\n\tif conf.CompressType == \"\" {\n\t\tkfk.Producer.Compression = sarama.CompressionNone\n\t} else {\n\t\tif err := kfk.Producer.Compression.UnmarshalText(bytes.ToLower([]byte(conf.CompressType))); err != nil {\n\t\t\treturn nil, errs.WrapMsg(err, \"UnmarshalText failed\", \"compressType\", conf.CompressType)\n\t\t}\n\t}\n\tif conf.TLS.EnableTLS {\n\t\ttls, err := newTLSConfig(conf.TLS.ClientCrt, conf.TLS.ClientKey, conf.TLS.CACrt, []byte(conf.TLS.ClientKeyPwd), conf.TLS.InsecureSkipVerify)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tkfk.Net.TLS.Config = tls\n\t\tkfk.Net.TLS.Enable = true\n\t}\n\treturn kfk, nil\n}\n\nfunc NewProducer(conf *sarama.Config, addr []string) (sarama.SyncProducer, error) {\n\tproducer, err := sarama.NewSyncProducer(addr, conf)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"NewSyncProducer failed\", \"addr\", addr, \"conf\", *conf)\n\t}\n\treturn producer, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/kafka/tls.go",
    "content": "// Copyright © 2024 OpenIM open source community. All rights reserved.\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\npackage kafka\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"os\"\n\n\t\"github.com/openimsdk/tools/errs\"\n)\n\n// decryptPEM decrypts a PEM block using a password.\nfunc decryptPEM(data []byte, passphrase []byte) ([]byte, error) {\n\tif len(passphrase) == 0 {\n\t\treturn data, nil\n\t}\n\tb, _ := pem.Decode(data)\n\td, err := x509.DecryptPEMBlock(b, passphrase)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"DecryptPEMBlock failed\")\n\t}\n\treturn pem.EncodeToMemory(&pem.Block{\n\t\tType:  b.Type,\n\t\tBytes: d,\n\t}), nil\n}\n\nfunc readEncryptablePEMBlock(path string, pwd []byte) ([]byte, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"ReadFile failed\", \"path\", path)\n\t}\n\treturn decryptPEM(data, pwd)\n}\n\n// newTLSConfig setup the TLS config from general config file.\nfunc newTLSConfig(clientCertFile, clientKeyFile, caCertFile string, keyPwd []byte, insecureSkipVerify bool) (*tls.Config, error) {\n\tvar tlsConfig tls.Config\n\tif clientCertFile != \"\" && clientKeyFile != \"\" {\n\t\tcertPEMBlock, err := os.ReadFile(clientCertFile)\n\t\tif err != nil {\n\t\t\treturn nil, errs.WrapMsg(err, \"ReadFile failed\", \"clientCertFile\", clientCertFile)\n\t\t}\n\t\tkeyPEMBlock, err := readEncryptablePEMBlock(clientKeyFile, keyPwd)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)\n\t\tif err != nil {\n\t\t\treturn nil, errs.WrapMsg(err, \"X509KeyPair failed\")\n\t\t}\n\t\ttlsConfig.Certificates = []tls.Certificate{cert}\n\t}\n\n\tif caCertFile != \"\" {\n\t\tcaCert, err := os.ReadFile(caCertFile)\n\t\tif err != nil {\n\t\t\treturn nil, errs.WrapMsg(err, \"ReadFile failed\", \"caCertFile\", caCertFile)\n\t\t}\n\t\tcaCertPool := x509.NewCertPool()\n\t\tif ok := caCertPool.AppendCertsFromPEM(caCert); !ok {\n\t\t\treturn nil, errs.New(\"AppendCertsFromPEM failed\")\n\t\t}\n\t\ttlsConfig.RootCAs = caCertPool\n\t}\n\ttlsConfig.InsecureSkipVerify = insecureSkipVerify\n\treturn &tlsConfig, nil\n}\n"
  },
  {
    "path": "pkg/common/storage/kafka/util.go",
    "content": "package kafka\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"github.com/IBM/sarama\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/mcontext\"\n)\n\nvar errEmptyMsg = errors.New(\"kafka binary msg is empty\")\n\n// GetMQHeaderWithContext extracts message queue headers from the context.\nfunc GetMQHeaderWithContext(ctx context.Context) ([]sarama.RecordHeader, error) {\n\toperationID, opUserID, platform, connID, err := mcontext.GetCtxInfos(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn []sarama.RecordHeader{\n\t\t{Key: []byte(constant.OperationID), Value: []byte(operationID)},\n\t\t{Key: []byte(constant.OpUserID), Value: []byte(opUserID)},\n\t\t{Key: []byte(constant.OpUserPlatform), Value: []byte(platform)},\n\t\t{Key: []byte(constant.ConnID), Value: []byte(connID)},\n\t}, nil\n}\n\n// GetContextWithMQHeader creates a context from message queue headers.\nfunc GetContextWithMQHeader(header []*sarama.RecordHeader) context.Context {\n\tvar values []string\n\tfor _, recordHeader := range header {\n\t\tvalues = append(values, string(recordHeader.Value))\n\t}\n\treturn mcontext.WithMustInfoCtx(values) // Attach extracted values to context\n}\n"
  },
  {
    "path": "pkg/common/storage/kafka/verify.go",
    "content": "// Copyright © 2024 OpenIM open source community. All rights reserved.\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\npackage kafka\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/IBM/sarama\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nfunc CheckTopics(ctx context.Context, conf *Config, topics []string) error {\n\tkfk, err := BuildConsumerGroupConfig(conf, sarama.OffsetNewest, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcli, err := sarama.NewClient(conf.Addr, kfk)\n\tif err != nil {\n\t\treturn errs.WrapMsg(err, \"NewClient failed\", \"config: \", fmt.Sprintf(\"%+v\", conf))\n\t}\n\tdefer cli.Close()\n\n\texistingTopics, err := cli.Topics()\n\tif err != nil {\n\t\treturn errs.WrapMsg(err, \"Failed to list topics\")\n\t}\n\n\texistingTopicsMap := make(map[string]bool)\n\tfor _, t := range existingTopics {\n\t\texistingTopicsMap[t] = true\n\t}\n\n\tfor _, topic := range topics {\n\t\tif !existingTopicsMap[topic] {\n\t\t\treturn errs.New(\"topic not exist\", \"topic\", topic).Wrap()\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc CheckHealth(ctx context.Context, conf *Config) error {\n\tkfk, err := BuildConsumerGroupConfig(conf, sarama.OffsetNewest, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcli, err := sarama.NewClient(conf.Addr, kfk)\n\tif err != nil {\n\t\treturn errs.WrapMsg(err, \"NewClient failed\", \"config: \", fmt.Sprintf(\"%+v\", conf))\n\t}\n\tdefer cli.Close()\n\n\t// Get broker list\n\tbrokers := cli.Brokers()\n\tif len(brokers) == 0 {\n\t\treturn errs.New(\"no brokers found\").Wrap()\n\t}\n\n\t// Check if all brokers are reachable\n\tfor _, broker := range brokers {\n\t\tif err := broker.Open(kfk); err != nil {\n\t\t\treturn errs.WrapMsg(err, \"failed to open broker\", \"broker\", broker.Addr())\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/storage/model/application.go",
    "content": "package model\n\nimport (\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"time\"\n)\n\ntype Application struct {\n\tID         primitive.ObjectID `bson:\"_id\"`\n\tPlatform   string             `bson:\"platform\"`\n\tHot        bool               `bson:\"hot\"`\n\tVersion    string             `bson:\"version\"`\n\tUrl        string             `bson:\"url\"`\n\tText       string             `bson:\"text\"`\n\tForce      bool               `bson:\"force\"`\n\tLatest     bool               `bson:\"latest\"`\n\tCreateTime time.Time          `bson:\"create_time\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/black.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype Black struct {\n\tOwnerUserID    string    `bson:\"owner_user_id\"`\n\tBlockUserID    string    `bson:\"block_user_id\"`\n\tCreateTime     time.Time `bson:\"create_time\"`\n\tAddSource      int32     `bson:\"add_source\"`\n\tOperatorUserID string    `bson:\"operator_user_id\"`\n\tEx             string    `bson:\"ex\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/cache.go",
    "content": "package model\n\nimport \"time\"\n\ntype Cache struct {\n\tKey      string     `bson:\"key\"`\n\tValue    string     `bson:\"value\"`\n\tExpireAt *time.Time `bson:\"expire_at\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/client_config.go",
    "content": "package model\n\ntype ClientConfig struct {\n\tKey    string `bson:\"key\"`\n\tUserID string `bson:\"user_id\"`\n\tValue  string `bson:\"value\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype Conversation struct {\n\tOwnerUserID           string    `bson:\"owner_user_id\"`\n\tConversationID        string    `bson:\"conversation_id\"`\n\tConversationType      int32     `bson:\"conversation_type\"`\n\tUserID                string    `bson:\"user_id\"`\n\tGroupID               string    `bson:\"group_id\"`\n\tRecvMsgOpt            int32     `bson:\"recv_msg_opt\"`\n\tIsPinned              bool      `bson:\"is_pinned\"`\n\tIsPrivateChat         bool      `bson:\"is_private_chat\"`\n\tBurnDuration          int32     `bson:\"burn_duration\"`\n\tGroupAtType           int32     `bson:\"group_at_type\"`\n\tAttachedInfo          string    `bson:\"attached_info\"`\n\tEx                    string    `bson:\"ex\"`\n\tMaxSeq                int64     `bson:\"max_seq\"`\n\tMinSeq                int64     `bson:\"min_seq\"`\n\tCreateTime            time.Time `bson:\"create_time\"`\n\tIsMsgDestruct         bool      `bson:\"is_msg_destruct\"`\n\tMsgDestructTime       int64     `bson:\"msg_destruct_time\"`\n\tLatestMsgDestructTime time.Time `bson:\"latest_msg_destruct_time\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage model // import \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model/relation\"\n"
  },
  {
    "path": "pkg/common/storage/model/friend.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"time\"\n)\n\n// Friend represents the data structure for a friend relationship in MongoDB.\ntype Friend struct {\n\tID             primitive.ObjectID `bson:\"_id\"`\n\tOwnerUserID    string             `bson:\"owner_user_id\"`\n\tFriendUserID   string             `bson:\"friend_user_id\"`\n\tRemark         string             `bson:\"remark\"`\n\tCreateTime     time.Time          `bson:\"create_time\"`\n\tAddSource      int32              `bson:\"add_source\"`\n\tOperatorUserID string             `bson:\"operator_user_id\"`\n\tEx             string             `bson:\"ex\"`\n\tIsPinned       bool               `bson:\"is_pinned\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/friend_request.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype FriendRequest struct {\n\tFromUserID    string    `bson:\"from_user_id\"`\n\tToUserID      string    `bson:\"to_user_id\"`\n\tHandleResult  int32     `bson:\"handle_result\"`\n\tReqMsg        string    `bson:\"req_msg\"`\n\tCreateTime    time.Time `bson:\"create_time\"`\n\tHandlerUserID string    `bson:\"handler_user_id\"`\n\tHandleMsg     string    `bson:\"handle_msg\"`\n\tHandleTime    time.Time `bson:\"handle_time\"`\n\tEx            string    `bson:\"ex\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/group.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype Group struct {\n\tGroupID                string    `bson:\"group_id\"`\n\tGroupName              string    `bson:\"group_name\"`\n\tNotification           string    `bson:\"notification\"`\n\tIntroduction           string    `bson:\"introduction\"`\n\tFaceURL                string    `bson:\"face_url\"`\n\tCreateTime             time.Time `bson:\"create_time\"`\n\tEx                     string    `bson:\"ex\"`\n\tStatus                 int32     `bson:\"status\"`\n\tCreatorUserID          string    `bson:\"creator_user_id\"`\n\tGroupType              int32     `bson:\"group_type\"`\n\tNeedVerification       int32     `bson:\"need_verification\"`\n\tLookMemberInfo         int32     `bson:\"look_member_info\"`\n\tApplyMemberFriend      int32     `bson:\"apply_member_friend\"`\n\tNotificationUpdateTime time.Time `bson:\"notification_update_time\"`\n\tNotificationUserID     string    `bson:\"notification_user_id\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/group_member.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype GroupMember struct {\n\tGroupID        string    `bson:\"group_id\"`\n\tUserID         string    `bson:\"user_id\"`\n\tNickname       string    `bson:\"nickname\"`\n\tFaceURL        string    `bson:\"face_url\"`\n\tRoleLevel      int32     `bson:\"role_level\"`\n\tJoinTime       time.Time `bson:\"join_time\"`\n\tJoinSource     int32     `bson:\"join_source\"`\n\tInviterUserID  string    `bson:\"inviter_user_id\"`\n\tOperatorUserID string    `bson:\"operator_user_id\"`\n\tMuteEndTime    time.Time `bson:\"mute_end_time\"`\n\tEx             string    `bson:\"ex\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/group_request.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype GroupRequest struct {\n\tUserID        string    `bson:\"user_id\"`\n\tGroupID       string    `bson:\"group_id\"`\n\tHandleResult  int32     `bson:\"handle_result\"`\n\tReqMsg        string    `bson:\"req_msg\"`\n\tHandledMsg    string    `bson:\"handled_msg\"`\n\tReqTime       time.Time `bson:\"req_time\"`\n\tHandleUserID  string    `bson:\"handle_user_id\"`\n\tHandledTime   time.Time `bson:\"handled_time\"`\n\tJoinSource    int32     `bson:\"join_source\"`\n\tInviterUserID string    `bson:\"inviter_user_id\"`\n\tEx            string    `bson:\"ex\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/log.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype Log struct {\n\tLogID        string    `bson:\"log_id\"`\n\tPlatform     string    `bson:\"platform\"`\n\tUserID       string    `bson:\"user_id\"`\n\tCreateTime   time.Time `bson:\"create_time\"`\n\tUrl          string    `bson:\"url\"`\n\tFileName     string    `bson:\"file_name\"`\n\tSystemType   string    `bson:\"system_type\"`\n\tAppFramework string    `bson:\"app_framework\"`\n\tVersion      string    `bson:\"version\"`\n\tEx           string    `bson:\"ex\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\nconst (\n\tsingleGocMsgNum     = 100\n\tsingleGocMsgNum5000 = 5000\n\tMsgTableName        = \"msg\"\n\tOldestList          = 0\n\tNewestList          = -1\n)\n\nvar ErrMsgListNotExist = errs.New(\"user not have msg in mongoDB\")\n\ntype MsgDocModel struct {\n\tDocID string          `bson:\"doc_id\"`\n\tMsg   []*MsgInfoModel `bson:\"msgs\"`\n}\n\ntype RevokeModel struct {\n\tRole     int32  `bson:\"role\"`\n\tUserID   string `bson:\"user_id\"`\n\tNickname string `bson:\"nickname\"`\n\tTime     int64  `bson:\"time\"`\n}\n\ntype OfflinePushModel struct {\n\tTitle         string `bson:\"title\"`\n\tDesc          string `bson:\"desc\"`\n\tEx            string `bson:\"ex\"`\n\tIOSPushSound  string `bson:\"ios_push_sound\"`\n\tIOSBadgeCount bool   `bson:\"ios_badge_count\"`\n}\n\ntype MsgDataModel struct {\n\tSendID           string            `bson:\"send_id\"`\n\tRecvID           string            `bson:\"recv_id\"`\n\tGroupID          string            `bson:\"group_id\"`\n\tClientMsgID      string            `bson:\"client_msg_id\"`\n\tServerMsgID      string            `bson:\"server_msg_id\"`\n\tSenderPlatformID int32             `bson:\"sender_platform_id\"`\n\tSenderNickname   string            `bson:\"sender_nickname\"`\n\tSenderFaceURL    string            `bson:\"sender_face_url\"`\n\tSessionType      int32             `bson:\"session_type\"`\n\tMsgFrom          int32             `bson:\"msg_from\"`\n\tContentType      int32             `bson:\"content_type\"`\n\tContent          string            `bson:\"content\"`\n\tSeq              int64             `bson:\"seq\"`\n\tSendTime         int64             `bson:\"send_time\"`\n\tCreateTime       int64             `bson:\"create_time\"`\n\tStatus           int32             `bson:\"status\"`\n\tIsRead           bool              `bson:\"is_read\"`\n\tOptions          map[string]bool   `bson:\"options\"`\n\tOfflinePush      *OfflinePushModel `bson:\"offline_push\"`\n\tAtUserIDList     []string          `bson:\"at_user_id_list\"`\n\tAttachedInfo     string            `bson:\"attached_info\"`\n\tEx               string            `bson:\"ex\"`\n}\n\ntype MsgInfoModel struct {\n\tMsg     *MsgDataModel `bson:\"msg\"`\n\tRevoke  *RevokeModel  `bson:\"revoke\"`\n\tDelList []string      `bson:\"del_list\"`\n\tIsRead  bool          `bson:\"is_read\"`\n}\n\ntype UserCount struct {\n\tUserID string `bson:\"user_id\"`\n\tCount  int64  `bson:\"count\"`\n}\n\ntype GroupCount struct {\n\tGroupID string `bson:\"group_id\"`\n\tCount   int64  `bson:\"count\"`\n}\n\nfunc (*MsgDocModel) TableName() string {\n\treturn MsgTableName\n}\n\nfunc (*MsgDocModel) GetSingleGocMsgNum() int64 {\n\treturn singleGocMsgNum\n}\n\nfunc (*MsgDocModel) GetSingleGocMsgNum5000() int64 {\n\treturn singleGocMsgNum5000\n}\n\nfunc (m *MsgDocModel) IsFull() bool {\n\treturn m.Msg[len(m.Msg)-1].Msg != nil\n}\n\nfunc (m *MsgDocModel) GetDocIndex(seq int64) int64 {\n\treturn (seq - 1) / singleGocMsgNum\n}\n\nfunc (m *MsgDocModel) GetDocID(conversationID string, seq int64) string {\n\tseqSuffix := (seq - 1) / singleGocMsgNum\n\treturn m.indexGen(conversationID, seqSuffix)\n}\n\nfunc (m *MsgDocModel) GetDocIDSeqsMap(conversationID string, seqs []int64) map[string][]int64 {\n\tt := make(map[string][]int64)\n\tfor _, seq := range seqs {\n\t\tdocID := m.GetDocID(conversationID, seq)\n\t\tt[docID] = append(t[docID], seq)\n\t}\n\n\treturn t\n}\n\nfunc (*MsgDocModel) GetMsgIndex(seq int64) int64 {\n\treturn (seq - 1) % singleGocMsgNum\n}\n\nfunc (*MsgDocModel) GetLimitForSingleDoc(seq int64) int64 {\n\treturn seq % singleGocMsgNum\n}\n\nfunc (*MsgDocModel) indexGen(conversationID string, seqSuffix int64) string {\n\treturn conversationID + \":\" + strconv.FormatInt(seqSuffix, 10)\n}\n\nfunc (*MsgDocModel) BuildDocIDByIndex(conversationID string, index int64) string {\n\treturn conversationID + \":\" + strconv.FormatInt(index, 10)\n}\n\nfunc (*MsgDocModel) GenExceptionMessageBySeqs(seqs []int64) (exceptionMsg []*sdkws.MsgData) {\n\tfor _, v := range seqs {\n\t\tmsgModel := new(sdkws.MsgData)\n\t\tmsgModel.Seq = v\n\t\texceptionMsg = append(exceptionMsg, msgModel)\n\t}\n\treturn exceptionMsg\n}\n\nfunc (*MsgDocModel) GetMinSeq(index int) int64 {\n\treturn int64(index*singleGocMsgNum) + 1\n}\n"
  },
  {
    "path": "pkg/common/storage/model/object.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype Object struct {\n\tName        string    `bson:\"name\"`\n\tUserID      string    `bson:\"user_id\"`\n\tHash        string    `bson:\"hash\"`\n\tEngine      string    `bson:\"engine\"`\n\tKey         string    `bson:\"key\"`\n\tSize        int64     `bson:\"size\"`\n\tContentType string    `bson:\"content_type\"`\n\tGroup       string    `bson:\"group\"`\n\tCreateTime  time.Time `bson:\"create_time\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/seq.go",
    "content": "package model\n\ntype SeqConversation struct {\n\tConversationID string `bson:\"conversation_id\"`\n\tMaxSeq         int64  `bson:\"max_seq\"`\n\tMinSeq         int64  `bson:\"min_seq\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/seq_user.go",
    "content": "package model\n\ntype SeqUser struct {\n\tUserID         string `bson:\"user_id\"`\n\tConversationID string `bson:\"conversation_id\"`\n\tMinSeq         int64  `bson:\"min_seq\"`\n\tMaxSeq         int64  `bson:\"max_seq\"`\n\tReadSeq        int64  `bson:\"read_seq\"`\n}\n"
  },
  {
    "path": "pkg/common/storage/model/subscribe.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\n// SubscribeUserTableName collection constant.\nconst (\n\tSubscribeUserTableName = \"subscribe_user\"\n)\n\n// SubscribeUser collection structure.\ntype SubscribeUser struct {\n\tUserID     string   `bson:\"user_id\"      json:\"userID\"`\n\tUserIDList []string `bson:\"user_id_list\" json:\"userIDList\"`\n}\n\nfunc (SubscribeUser) TableName() string {\n\treturn SubscribeUserTableName\n}\n"
  },
  {
    "path": "pkg/common/storage/model/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage model\n\nimport (\n\t\"time\"\n)\n\ntype User struct {\n\tUserID           string    `bson:\"user_id\"`\n\tNickname         string    `bson:\"nickname\"`\n\tFaceURL          string    `bson:\"face_url\"`\n\tEx               string    `bson:\"ex\"`\n\tAppMangerLevel   int32     `bson:\"app_manger_level\"`\n\tGlobalRecvMsgOpt int32     `bson:\"global_recv_msg_opt\"`\n\tCreateTime       time.Time `bson:\"create_time\"`\n}\n\nfunc (u *User) GetNickname() string {\n\treturn u.Nickname\n}\n\nfunc (u *User) GetFaceURL() string {\n\treturn u.FaceURL\n}\n\nfunc (u *User) GetUserID() string {\n\treturn u.UserID\n}\n\nfunc (u *User) GetEx() string {\n\treturn u.Ex\n}\n"
  },
  {
    "path": "pkg/common/storage/model/version_log.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"go.mongodb.org/mongo-driver/bson/primitive\"\n\t\"time\"\n)\n\nconst (\n\tVersionStateInsert = iota + 1\n\tVersionStateDelete\n\tVersionStateUpdate\n)\n\nconst (\n\tVersionGroupChangeID = \"\"\n\tVersionSortChangeID  = \"____S_O_R_T_I_D____\"\n)\n\ntype VersionLogElem struct {\n\tEID        string    `bson:\"e_id\"`\n\tState      int32     `bson:\"state\"`\n\tVersion    uint      `bson:\"version\"`\n\tLastUpdate time.Time `bson:\"last_update\"`\n}\n\ntype VersionLogTable struct {\n\tID         primitive.ObjectID `bson:\"_id\"`\n\tDID        string             `bson:\"d_id\"`\n\tLogs       []VersionLogElem   `bson:\"logs\"`\n\tVersion    uint               `bson:\"version\"`\n\tDeleted    uint               `bson:\"deleted\"`\n\tLastUpdate time.Time          `bson:\"last_update\"`\n}\n\nfunc (v *VersionLogTable) VersionLog() *VersionLog {\n\treturn &VersionLog{\n\t\tID:         v.ID,\n\t\tDID:        v.DID,\n\t\tLogs:       v.Logs,\n\t\tVersion:    v.Version,\n\t\tDeleted:    v.Deleted,\n\t\tLastUpdate: v.LastUpdate,\n\t\tLogLen:     len(v.Logs),\n\t}\n}\n\ntype VersionLog struct {\n\tID         primitive.ObjectID `bson:\"_id\"`\n\tDID        string             `bson:\"d_id\"`\n\tLogs       []VersionLogElem   `bson:\"logs\"`\n\tVersion    uint               `bson:\"version\"`\n\tDeleted    uint               `bson:\"deleted\"`\n\tLastUpdate time.Time          `bson:\"last_update\"`\n\tLogLen     int                `bson:\"log_len\"`\n}\n\nfunc (v *VersionLog) DeleteAndChangeIDs() (insertIds, deleteIds, updateIds []string) {\n\tfor _, l := range v.Logs {\n\t\tswitch l.State {\n\t\tcase VersionStateInsert:\n\t\t\tinsertIds = append(insertIds, l.EID)\n\t\tcase VersionStateDelete:\n\t\t\tdeleteIds = append(deleteIds, l.EID)\n\t\tcase VersionStateUpdate:\n\t\t\tupdateIds = append(updateIds, l.EID)\n\t\tdefault:\n\t\t\tlog.ZError(context.Background(), \"invalid version status found\", errors.New(\"dirty database data\"), \"objID\", v.ID.Hex(), \"did\", v.DID, \"elem\", l)\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "pkg/common/storage/versionctx/rpc.go",
    "content": "package versionctx\n\nimport (\n\t\"context\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc EnableVersionCtx() grpc.ServerOption {\n\treturn grpc.ChainUnaryInterceptor(enableVersionCtxInterceptor)\n}\n\nfunc enableVersionCtxInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {\n\treturn handler(WithVersionLog(ctx), req)\n}\n"
  },
  {
    "path": "pkg/common/storage/versionctx/version.go",
    "content": "package versionctx\n\nimport (\n\t\"context\"\n\ttablerelation \"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model\"\n\t\"sync\"\n)\n\ntype Collection struct {\n\tName string\n\tDoc  *tablerelation.VersionLog\n}\n\ntype versionKey struct{}\n\nfunc WithVersionLog(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, versionKey{}, &VersionLog{})\n}\n\nfunc GetVersionLog(ctx context.Context) *VersionLog {\n\tif v, ok := ctx.Value(versionKey{}).(*VersionLog); ok {\n\t\treturn v\n\t}\n\treturn nil\n}\n\ntype VersionLog struct {\n\tlock sync.Mutex\n\tdata []Collection\n}\n\nfunc (v *VersionLog) Append(data ...Collection) {\n\tif v == nil || len(data) == 0 {\n\t\treturn\n\t}\n\tv.lock.Lock()\n\tdefer v.lock.Unlock()\n\tv.data = append(v.data, data...)\n}\n\nfunc (v *VersionLog) Get() []Collection {\n\tif v == nil {\n\t\treturn nil\n\t}\n\tv.lock.Lock()\n\tdefer v.lock.Unlock()\n\treturn v.data\n}\n"
  },
  {
    "path": "pkg/common/webhook/condition.go",
    "content": "package webhook\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n)\n\nfunc WithCondition(ctx context.Context, before *config.BeforeConfig, callback func(context.Context) error) error {\n\tif !before.Enable {\n\t\treturn nil\n\t}\n\treturn callback(ctx)\n}\n"
  },
  {
    "path": "pkg/common/webhook/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage webhook // import \"github.com/openimsdk/open-im-server/v3/pkg/common/webhook\"\n"
  },
  {
    "path": "pkg/common/webhook/http_client.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage webhook\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/openimsdk/tools/mq/memamq\"\n\t\"github.com/openimsdk/tools/utils/httputil\"\n)\n\ntype Client struct {\n\tclient *httputil.HTTPClient\n\turl    string\n\tqueue  *memamq.MemoryQueue\n}\n\nconst (\n\twebhookWorkerCount = 2\n\twebhookBufferSize  = 100\n\n\tKey = \"key\"\n)\n\nfunc NewWebhookClient(url string, options ...*memamq.MemoryQueue) *Client {\n\tvar queue *memamq.MemoryQueue\n\tif len(options) > 0 && options[0] != nil {\n\t\tqueue = options[0]\n\t} else {\n\t\tqueue = memamq.NewMemoryQueue(webhookWorkerCount, webhookBufferSize)\n\t}\n\n\thttp.DefaultTransport.(*http.Transport).MaxConnsPerHost = 100 // Enhance the default number of max connections per host\n\n\treturn &Client{\n\t\tclient: httputil.NewHTTPClient(httputil.NewClientConfig()),\n\t\turl:    url,\n\t\tqueue:  queue,\n\t}\n}\n\nfunc (c *Client) SyncPost(ctx context.Context, command string, req callbackstruct.CallbackReq, resp callbackstruct.CallbackResp, before *config.BeforeConfig) error {\n\treturn c.post(ctx, command, req, resp, before.Timeout)\n}\n\nfunc (c *Client) AsyncPost(ctx context.Context, command string, req callbackstruct.CallbackReq, resp callbackstruct.CallbackResp, after *config.AfterConfig) {\n\tif after.Enable {\n\t\tc.queue.Push(func() { c.post(ctx, command, req, resp, after.Timeout) })\n\t}\n}\n\nfunc (c *Client) AsyncPostWithQuery(ctx context.Context, command string, req callbackstruct.CallbackReq, resp callbackstruct.CallbackResp, after *config.AfterConfig, queryParams map[string]string) {\n\tif after.Enable {\n\t\tc.queue.Push(func() { c.postWithQuery(ctx, command, req, resp, after.Timeout, queryParams) })\n\t}\n}\n\nfunc (c *Client) post(ctx context.Context, command string, input interface{}, output callbackstruct.CallbackResp, timeout int) error {\n\tctx = mcontext.WithMustInfoCtx([]string{mcontext.GetOperationID(ctx), mcontext.GetOpUserID(ctx), mcontext.GetOpUserPlatform(ctx), mcontext.GetConnID(ctx)})\n\tfullURL := c.url + \"/\" + command\n\tlog.ZInfo(ctx, \"webhook\", \"url\", fullURL, \"input\", input, \"config\", timeout)\n\toperationID, _ := ctx.Value(constant.OperationID).(string)\n\tb, err := c.client.Post(ctx, fullURL, map[string]string{constant.OperationID: operationID}, input, timeout)\n\tif err != nil {\n\t\treturn servererrs.ErrNetwork.WrapMsg(err.Error(), \"post url\", fullURL)\n\t}\n\tif err = json.Unmarshal(b, output); err != nil {\n\t\treturn servererrs.ErrData.WithDetail(err.Error() + \" response format error\")\n\t}\n\tif err := output.Parse(); err != nil {\n\t\treturn err\n\t}\n\tlog.ZInfo(ctx, \"webhook success\", \"url\", fullURL, \"input\", input, \"response\", string(b))\n\treturn nil\n}\n\nfunc (c *Client) postWithQuery(ctx context.Context, command string, input interface{}, output callbackstruct.CallbackResp, timeout int, queryParams map[string]string) error {\n\tctx = mcontext.WithMustInfoCtx([]string{mcontext.GetOperationID(ctx), mcontext.GetOpUserID(ctx), mcontext.GetOpUserPlatform(ctx), mcontext.GetConnID(ctx)})\n\tfullURL := c.url + \"/\" + command\n\n\tparsedURL, err := url.Parse(fullURL)\n\tif err != nil {\n\t\treturn servererrs.ErrNetwork.WrapMsg(err.Error(), \"failed to parse URL\", fullURL)\n\t}\n\n\tquery := parsedURL.Query()\n\n\toperationID, _ := ctx.Value(constant.OperationID).(string)\n\n\tfor key, value := range queryParams {\n\t\tquery.Set(key, value)\n\t}\n\n\tparsedURL.RawQuery = query.Encode()\n\n\tfullURL = parsedURL.String()\n\tlog.ZInfo(ctx, \"webhook\", \"url\", fullURL, \"input\", input, \"config\", timeout)\n\n\tb, err := c.client.Post(ctx, fullURL, map[string]string{constant.OperationID: operationID}, input, timeout)\n\tif err != nil {\n\t\treturn servererrs.ErrNetwork.WrapMsg(err.Error(), \"post url\", fullURL)\n\t}\n\n\tif err = json.Unmarshal(b, output); err != nil {\n\t\treturn servererrs.ErrData.WithDetail(err.Error() + \" response format error\")\n\t}\n\tif err := output.Parse(); err != nil {\n\t\treturn err\n\t}\n\n\tlog.ZInfo(ctx, \"webhook success\", \"url\", fullURL, \"input\", input, \"response\", string(b))\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/common/webhook/http_client_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage webhook\n"
  },
  {
    "path": "pkg/dbbuild/builder.go",
    "content": "package dbbuild\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype Builder interface {\n\tMongo(ctx context.Context) (*mongoutil.Client, error)\n\tRedis(ctx context.Context) (redis.UniversalClient, error)\n}\n\nfunc NewBuilder(mongoConf *config.Mongo, redisConf *config.Redis) Builder {\n\tif config.Standalone() {\n\t\tglobalStandalone.setConfig(mongoConf, redisConf)\n\t\treturn globalStandalone\n\t}\n\treturn &microservices{\n\t\tmongo: mongoConf,\n\t\tredis: redisConf,\n\t}\n}\n"
  },
  {
    "path": "pkg/dbbuild/microservices.go",
    "content": "package dbbuild\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/redisutil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype microservices struct {\n\tmongo *config.Mongo\n\tredis *config.Redis\n}\n\nfunc (x *microservices) Mongo(ctx context.Context) (*mongoutil.Client, error) {\n\treturn mongoutil.NewMongoDB(ctx, x.mongo.Build())\n}\n\nfunc (x *microservices) Redis(ctx context.Context) (redis.UniversalClient, error) {\n\tif x.redis.Disable {\n\t\treturn nil, nil\n\t}\n\treturn redisutil.NewRedisClient(ctx, x.redis.Build())\n}\n"
  },
  {
    "path": "pkg/dbbuild/standalone.go",
    "content": "package dbbuild\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/redisutil\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tstandaloneMongo = \"mongo\"\n\tstandaloneRedis = \"redis\"\n)\n\nvar globalStandalone = &standalone{}\n\ntype standaloneConn[C any] struct {\n\tConn C\n\tErr  error\n}\n\nfunc (x *standaloneConn[C]) result() (C, error) {\n\treturn x.Conn, x.Err\n}\n\ntype standalone struct {\n\tlock  sync.Mutex\n\tmongo *config.Mongo\n\tredis *config.Redis\n\tconn  map[string]any\n}\n\nfunc (x *standalone) setConfig(mongoConf *config.Mongo, redisConf *config.Redis) {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tx.mongo = mongoConf\n\tx.redis = redisConf\n}\n\nfunc (x *standalone) Mongo(ctx context.Context) (*mongoutil.Client, error) {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tif x.conn == nil {\n\t\tx.conn = make(map[string]any)\n\t}\n\tv, ok := x.conn[standaloneMongo]\n\tif !ok {\n\t\tvar val standaloneConn[*mongoutil.Client]\n\t\tval.Conn, val.Err = mongoutil.NewMongoDB(ctx, x.mongo.Build())\n\t\tv = &val\n\t\tx.conn[standaloneMongo] = v\n\t}\n\treturn v.(*standaloneConn[*mongoutil.Client]).result()\n}\n\nfunc (x *standalone) Redis(ctx context.Context) (redis.UniversalClient, error) {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tif x.redis.Disable {\n\t\treturn nil, nil\n\t}\n\tif x.conn == nil {\n\t\tx.conn = make(map[string]any)\n\t}\n\tv, ok := x.conn[standaloneRedis]\n\tif !ok {\n\t\tvar val standaloneConn[redis.UniversalClient]\n\t\tval.Conn, val.Err = redisutil.NewRedisClient(ctx, x.redis.Build())\n\t\tv = &val\n\t\tx.conn[standaloneRedis] = v\n\t}\n\treturn v.(*standaloneConn[redis.UniversalClient]).result()\n}\n"
  },
  {
    "path": "pkg/localcache/cache.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage localcache\n\nimport (\n\t\"context\"\n\t\"hash/fnv\"\n\t\"unsafe\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache/link\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache/lru\"\n)\n\ntype Cache[V any] interface {\n\tGet(ctx context.Context, key string, fetch func(ctx context.Context) (V, error)) (V, error)\n\tGetLink(ctx context.Context, key string, fetch func(ctx context.Context) (V, error), link ...string) (V, error)\n\tDel(ctx context.Context, key ...string)\n\tDelLocal(ctx context.Context, key ...string)\n\tStop()\n}\n\nfunc LRUStringHash(key string) uint64 {\n\th := fnv.New64a()\n\th.Write(*(*[]byte)(unsafe.Pointer(&key)))\n\treturn h.Sum64()\n}\n\nfunc New[V any](opts ...Option) Cache[V] {\n\topt := defaultOption()\n\tfor _, o := range opts {\n\t\to(opt)\n\t}\n\n\tc := cache[V]{opt: opt}\n\tif opt.localSlotNum > 0 && opt.localSlotSize > 0 {\n\t\tcreateSimpleLRU := func() lru.LRU[string, V] {\n\t\t\tif opt.expirationEvict {\n\t\t\t\treturn lru.NewExpirationLRU[string, V](opt.localSlotSize, opt.localSuccessTTL, opt.localFailedTTL, opt.target, c.onEvict)\n\t\t\t} else {\n\t\t\t\treturn lru.NewLazyLRU[string, V](opt.localSlotSize, opt.localSuccessTTL, opt.localFailedTTL, opt.target, c.onEvict)\n\t\t\t}\n\t\t}\n\t\tif opt.localSlotNum == 1 {\n\t\t\tc.local = createSimpleLRU()\n\t\t} else {\n\t\t\tc.local = lru.NewSlotLRU[string, V](opt.localSlotNum, LRUStringHash, createSimpleLRU)\n\t\t}\n\t\tif opt.linkSlotNum > 0 {\n\t\t\tc.link = link.New(opt.linkSlotNum)\n\t\t}\n\t}\n\treturn &c\n}\n\ntype cache[V any] struct {\n\topt   *option\n\tlink  link.Link\n\tlocal lru.LRU[string, V]\n}\n\nfunc (c *cache[V]) onEvict(key string, value V) {\n\tif c.link != nil {\n\t\t// Do not delete other keys while the underlying LRU still holds its lock;\n\t\t// defer linked deletions to avoid re-entering the same slot and deadlocking.\n\t\tif lks := c.link.Del(key); len(lks) > 0 {\n\t\t\tgo c.delLinked(key, lks)\n\t\t}\n\t}\n}\n\nfunc (c *cache[V]) delLinked(src string, keys map[string]struct{}) {\n\tfor k := range keys {\n\t\tif src != k {\n\t\t\tc.local.Del(k)\n\t\t}\n\t}\n}\n\nfunc (c *cache[V]) del(key ...string) {\n\tif c.local == nil {\n\t\treturn\n\t}\n\tfor _, k := range key {\n\t\tc.local.Del(k)\n\t\tif c.link != nil {\n\t\t\tlks := c.link.Del(k)\n\t\t\tfor k := range lks {\n\t\t\t\tc.local.Del(k)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *cache[V]) Get(ctx context.Context, key string, fetch func(ctx context.Context) (V, error)) (V, error) {\n\treturn c.GetLink(ctx, key, fetch)\n}\n\nfunc (c *cache[V]) GetLink(ctx context.Context, key string, fetch func(ctx context.Context) (V, error), link ...string) (V, error) {\n\tif c.local != nil {\n\t\treturn c.local.Get(key, func() (V, error) {\n\t\t\tif len(link) > 0 && c.link != nil {\n\t\t\t\tc.link.Link(key, link...)\n\t\t\t}\n\t\t\treturn fetch(ctx)\n\t\t})\n\t} else {\n\t\treturn fetch(ctx)\n\t}\n}\n\nfunc (c *cache[V]) Del(ctx context.Context, key ...string) {\n\tfor _, fn := range c.opt.delFn {\n\t\tfn(ctx, key...)\n\t}\n\tc.del(key...)\n}\n\nfunc (c *cache[V]) DelLocal(ctx context.Context, key ...string) {\n\tc.del(key...)\n}\n\nfunc (c *cache[V]) Stop() {\n\tc.local.Stop()\n}\n"
  },
  {
    "path": "pkg/localcache/cache_test.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage localcache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache/lru\"\n)\n\nfunc TestName(t *testing.T) {\n\tc := New[string](WithExpirationEvict())\n\t//c := New[string]()\n\tctx := context.Background()\n\n\tconst (\n\t\tnum  = 10000\n\t\ttNum = 10000\n\t\tkNum = 100000\n\t\tpNum = 100\n\t)\n\n\tgetKey := func(v uint64) string {\n\t\treturn fmt.Sprintf(\"key_%d\", v%kNum)\n\t}\n\n\tstart := time.Now()\n\tt.Log(\"start\", start)\n\n\tvar (\n\t\tget atomic.Int64\n\t\tdel atomic.Int64\n\t)\n\n\tincrGet := func() {\n\t\tif v := get.Add(1); v%pNum == 0 {\n\t\t\t//t.Log(\"#get count\", v/pNum)\n\t\t}\n\t}\n\tincrDel := func() {\n\t\tif v := del.Add(1); v%pNum == 0 {\n\t\t\t//t.Log(\"@del count\", v/pNum)\n\t\t}\n\t}\n\n\tvar wg sync.WaitGroup\n\n\tfor i := 0; i < tNum; i++ {\n\t\twg.Add(2)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor i := 0; i < num; i++ {\n\t\t\t\tc.Get(ctx, getKey(rand.Uint64()), func(ctx context.Context) (string, error) {\n\t\t\t\t\treturn fmt.Sprintf(\"index_%d\", i), nil\n\t\t\t\t})\n\t\t\t\tincrGet()\n\t\t\t}\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\ttime.Sleep(time.Second / 10)\n\t\t\tfor i := 0; i < num; i++ {\n\t\t\t\tc.Del(ctx, getKey(rand.Uint64()))\n\t\t\t\tincrDel()\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\tend := time.Now()\n\tt.Log(\"end\", end)\n\tt.Log(\"time\", end.Sub(start))\n\tt.Log(\"get\", get.Load())\n\tt.Log(\"del\", del.Load())\n\t// 137.35s\n}\n\n// Test deadlock scenario when eviction callback deletes a linked key that hashes to the same slot.\nfunc TestCacheEvictDeadlock(t *testing.T) {\n\tctx := context.Background()\n\tc := New[string](WithLocalSlotNum(1), WithLocalSlotSize(1), WithLazy())\n\n\tif _, err := c.GetLink(ctx, \"k1\", func(ctx context.Context) (string, error) {\n\t\treturn \"v1\", nil\n\t}, \"k2\"); err != nil {\n\t\tt.Fatalf(\"seed cache failed: %v\", err)\n\t}\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\t\t_, _ = c.GetLink(ctx, \"k2\", func(ctx context.Context) (string, error) {\n\t\t\treturn \"v2\", nil\n\t\t}, \"k1\")\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\t// expected to finish quickly; current implementation deadlocks here.\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"GetLink deadlocked during eviction of linked key\")\n\t}\n}\n\nfunc TestExpirationLRUGetBatch(t *testing.T) {\n\tl := lru.NewExpirationLRU[string, string](2, time.Minute, time.Second*5, EmptyTarget{}, nil)\n\n\tkeys := []string{\"a\", \"b\"}\n\tvalues, err := l.GetBatch(keys, func(keys []string) (map[string]string, error) {\n\t\tres := make(map[string]string)\n\t\tfor _, k := range keys {\n\t\t\tres[k] = k + \"_v\"\n\t\t}\n\t\treturn res, nil\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif len(values) != len(keys) {\n\t\tt.Fatalf(\"expected %d values, got %d\", len(keys), len(values))\n\t}\n\tfor _, k := range keys {\n\t\tif v, ok := values[k]; !ok || v != k+\"_v\" {\n\t\t\tt.Fatalf(\"unexpected value for %s: %q, ok=%v\", k, v, ok)\n\t\t}\n\t}\n\n\t// second batch should hit cache\n\tvalues, err = l.GetBatch(keys, func(keys []string) (map[string]string, error) {\n\t\tt.Fatalf(\"should not fetch on cache hit\")\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error on cache hit: %v\", err)\n\t}\n\tfor _, k := range keys {\n\t\tif v, ok := values[k]; !ok || v != k+\"_v\" {\n\t\t\tt.Fatalf(\"unexpected cached value for %s: %q, ok=%v\", k, v, ok)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/localcache/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage localcache // import \"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n"
  },
  {
    "path": "pkg/localcache/init.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage localcache\n\nimport (\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n)\n\nvar (\n\tonce      sync.Once\n\tsubscribe map[string][]string\n)\n\nfunc InitLocalCache(localCache *config.LocalCache) {\n\tonce.Do(func() {\n\t\tlist := []struct {\n\t\t\tLocal config.CacheConfig\n\t\t\tKeys  []string\n\t\t}{\n\t\t\t{\n\t\t\t\tLocal: localCache.User,\n\t\t\t\tKeys:  []string{cachekey.UserInfoKey, cachekey.UserGlobalRecvMsgOptKey},\n\t\t\t},\n\t\t\t{\n\t\t\t\tLocal: localCache.Group,\n\t\t\t\tKeys:  []string{cachekey.GroupMemberIDsKey, cachekey.GroupInfoKey, cachekey.GroupMemberInfoKey},\n\t\t\t},\n\t\t\t{\n\t\t\t\tLocal: localCache.Friend,\n\t\t\t\tKeys:  []string{cachekey.FriendIDsKey, cachekey.BlackIDsKey},\n\t\t\t},\n\t\t\t{\n\t\t\t\tLocal: localCache.Conversation,\n\t\t\t\tKeys:  []string{cachekey.ConversationKey, cachekey.ConversationIDsKey, cachekey.ConversationNotReceiveMessageUserIDsKey},\n\t\t\t},\n\t\t}\n\t\tsubscribe = make(map[string][]string)\n\t\tfor _, v := range list {\n\t\t\tif v.Local.Enable() {\n\t\t\t\tsubscribe[v.Local.Topic] = v.Keys\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc GetPublishKeysByTopic(topics []string, keys []string) map[string][]string {\n\tkeysByTopic := make(map[string][]string)\n\tfor _, topic := range topics {\n\t\tkeysByTopic[topic] = []string{}\n\t}\n\n\tfor _, key := range keys {\n\t\tfor _, topic := range topics {\n\t\t\tprefixes, ok := subscribe[topic]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, prefix := range prefixes {\n\t\t\t\tif strings.HasPrefix(key, prefix) {\n\t\t\t\t\tkeysByTopic[topic] = append(keysByTopic[topic], key)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn keysByTopic\n}\n"
  },
  {
    "path": "pkg/localcache/link/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage link // import \"github.com/openimsdk/open-im-server/v3/pkg/localcache/link\"\n"
  },
  {
    "path": "pkg/localcache/link/link.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage link\n\nimport (\n\t\"hash/fnv\"\n\t\"sync\"\n\t\"unsafe\"\n)\n\ntype Link interface {\n\tLink(key string, link ...string)\n\tDel(key string) map[string]struct{}\n}\n\nfunc newLinkKey() *linkKey {\n\treturn &linkKey{\n\t\tdata: make(map[string]map[string]struct{}),\n\t}\n}\n\ntype linkKey struct {\n\tlock sync.Mutex\n\tdata map[string]map[string]struct{}\n}\n\nfunc (x *linkKey) link(key string, link ...string) {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tv, ok := x.data[key]\n\tif !ok {\n\t\tv = make(map[string]struct{})\n\t\tx.data[key] = v\n\t}\n\tfor _, k := range link {\n\t\tv[k] = struct{}{}\n\t}\n}\n\nfunc (x *linkKey) del(key string) map[string]struct{} {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tks, ok := x.data[key]\n\tif !ok {\n\t\treturn nil\n\t}\n\tdelete(x.data, key)\n\treturn ks\n}\n\nfunc New(n int) Link {\n\tif n <= 0 {\n\t\tpanic(\"must be greater than 0\")\n\t}\n\tslots := make([]*linkKey, n)\n\tfor i := 0; i < len(slots); i++ {\n\t\tslots[i] = newLinkKey()\n\t}\n\treturn &slot{\n\t\tn:     uint64(n),\n\t\tslots: slots,\n\t}\n}\n\ntype slot struct {\n\tn     uint64\n\tslots []*linkKey\n}\n\nfunc (x *slot) index(s string) uint64 {\n\th := fnv.New64a()\n\t_, _ = h.Write(*(*[]byte)(unsafe.Pointer(&s)))\n\treturn h.Sum64() % x.n\n}\n\nfunc (x *slot) Link(key string, link ...string) {\n\tif len(link) == 0 {\n\t\treturn\n\t}\n\tmk := key\n\tlks := make([]string, len(link))\n\tfor i, k := range link {\n\t\tlks[i] = k\n\t}\n\tx.slots[x.index(mk)].link(mk, lks...)\n\tfor _, lk := range lks {\n\t\tx.slots[x.index(lk)].link(lk, mk)\n\t}\n}\n\nfunc (x *slot) Del(key string) map[string]struct{} {\n\treturn x.delKey(key)\n}\n\nfunc (x *slot) delKey(k string) map[string]struct{} {\n\tdel := make(map[string]struct{})\n\tstack := []string{k}\n\tfor len(stack) > 0 {\n\t\tcurr := stack[len(stack)-1]\n\t\tstack = stack[:len(stack)-1]\n\t\tif _, ok := del[curr]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tdel[curr] = struct{}{}\n\t\tchildKeys := x.slots[x.index(curr)].del(curr)\n\t\tfor ck := range childKeys {\n\t\t\tstack = append(stack, ck)\n\t\t}\n\t}\n\treturn del\n}\n"
  },
  {
    "path": "pkg/localcache/link/link_test.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage link\n\nimport (\n\t\"testing\"\n)\n\nfunc TestName(t *testing.T) {\n\n\tv := New(1)\n\n\t//v.Link(\"a:1\", \"b:1\", \"c:1\", \"d:1\")\n\tv.Link(\"a:1\", \"b:1\", \"c:1\")\n\tv.Link(\"z:1\", \"b:1\")\n\n\t//v.DelKey(\"a:1\")\n\tv.Del(\"z:1\")\n\n\tt.Log(v)\n\n}\n"
  },
  {
    "path": "pkg/localcache/lru/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage lru // import \"github.com/openimsdk/open-im-server/v3/pkg/localcache/lru\"\n"
  },
  {
    "path": "pkg/localcache/lru/lru.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage lru\n\nimport \"github.com/hashicorp/golang-lru/v2/simplelru\"\n\ntype EvictCallback[K comparable, V any] simplelru.EvictCallback[K, V]\n\ntype LRU[K comparable, V any] interface {\n\tGet(key K, fetch func() (V, error)) (V, error)\n\tSet(key K, value V)\n\tSetHas(key K, value V) bool\n\tGetBatch(keys []K, fetch func(keys []K) (map[K]V, error)) (map[K]V, error)\n\tDel(key K) bool\n\tStop()\n}\n\ntype Target interface {\n\tIncrGetHit()\n\tIncrGetSuccess()\n\tIncrGetFailed()\n\n\tIncrDelHit()\n\tIncrDelNotFound()\n}\n"
  },
  {
    "path": "pkg/localcache/lru/lru_expiration.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage lru\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/golang-lru/v2/expirable\"\n)\n\nfunc NewExpirationLRU[K comparable, V any](size int, successTTL, failedTTL time.Duration, target Target, onEvict EvictCallback[K, V]) LRU[K, V] {\n\tvar cb expirable.EvictCallback[K, *expirationLruItem[V]]\n\tif onEvict != nil {\n\t\tcb = func(key K, value *expirationLruItem[V]) {\n\t\t\tonEvict(key, value.value)\n\t\t}\n\t}\n\tcore := expirable.NewLRU[K, *expirationLruItem[V]](size, cb, successTTL)\n\treturn &ExpirationLRU[K, V]{\n\t\tcore:       core,\n\t\tsuccessTTL: successTTL,\n\t\tfailedTTL:  failedTTL,\n\t\ttarget:     target,\n\t}\n}\n\ntype expirationLruItem[V any] struct {\n\tlock  sync.RWMutex\n\terr   error\n\tvalue V\n}\n\ntype ExpirationLRU[K comparable, V any] struct {\n\tlock       sync.Mutex\n\tcore       *expirable.LRU[K, *expirationLruItem[V]]\n\tsuccessTTL time.Duration\n\tfailedTTL  time.Duration\n\ttarget     Target\n}\n\nfunc (x *ExpirationLRU[K, V]) GetBatch(keys []K, fetch func(keys []K) (map[K]V, error)) (map[K]V, error) {\n\tvar (\n\t\terr     error\n\t\tresults = make(map[K]V)\n\t\tmisses  = make([]K, 0, len(keys))\n\t)\n\n\tfor _, key := range keys {\n\t\tx.lock.Lock()\n\t\tv, ok := x.core.Get(key)\n\t\tx.lock.Unlock()\n\t\tif ok {\n\t\t\tx.target.IncrGetHit()\n\t\t\tv.lock.RLock()\n\t\t\tresults[key] = v.value\n\t\t\tif v.err != nil && err == nil {\n\t\t\t\terr = v.err\n\t\t\t}\n\t\t\tv.lock.RUnlock()\n\t\t\tcontinue\n\t\t}\n\t\tmisses = append(misses, key)\n\t}\n\n\tif len(misses) == 0 {\n\t\treturn results, err\n\t}\n\n\tfetchValues, fetchErr := fetch(misses)\n\tif fetchErr != nil && err == nil {\n\t\terr = fetchErr\n\t}\n\n\tfor key, val := range fetchValues {\n\t\tresults[key] = val\n\t\tif fetchErr != nil {\n\t\t\tx.target.IncrGetFailed()\n\t\t\tcontinue\n\t\t}\n\t\tx.target.IncrGetSuccess()\n\t\titem := &expirationLruItem[V]{value: val}\n\t\tx.lock.Lock()\n\t\tx.core.Add(key, item)\n\t\tx.lock.Unlock()\n\t}\n\n\t// any keys not returned from fetch remain absent (no cache write)\n\treturn results, err\n}\n\nfunc (x *ExpirationLRU[K, V]) Get(key K, fetch func() (V, error)) (V, error) {\n\tx.lock.Lock()\n\tv, ok := x.core.Get(key)\n\tif ok {\n\t\tx.lock.Unlock()\n\t\tx.target.IncrGetSuccess()\n\t\tv.lock.RLock()\n\t\tdefer v.lock.RUnlock()\n\t\treturn v.value, v.err\n\t} else {\n\t\tv = &expirationLruItem[V]{}\n\t\tx.core.Add(key, v)\n\t\tv.lock.Lock()\n\t\tx.lock.Unlock()\n\t\tdefer v.lock.Unlock()\n\t\tv.value, v.err = fetch()\n\t\tif v.err == nil {\n\t\t\tx.target.IncrGetSuccess()\n\t\t} else {\n\t\t\tx.target.IncrGetFailed()\n\t\t\tx.core.Remove(key)\n\t\t}\n\t\treturn v.value, v.err\n\t}\n}\n\nfunc (x *ExpirationLRU[K, V]) Del(key K) bool {\n\tx.lock.Lock()\n\tok := x.core.Remove(key)\n\tx.lock.Unlock()\n\tif ok {\n\t\tx.target.IncrDelHit()\n\t} else {\n\t\tx.target.IncrDelNotFound()\n\t}\n\treturn ok\n}\n\nfunc (x *ExpirationLRU[K, V]) SetHas(key K, value V) bool {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tif x.core.Contains(key) {\n\t\tx.core.Add(key, &expirationLruItem[V]{value: value})\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (x *ExpirationLRU[K, V]) Set(key K, value V) {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tx.core.Add(key, &expirationLruItem[V]{value: value})\n}\n\nfunc (x *ExpirationLRU[K, V]) Stop() {\n}\n"
  },
  {
    "path": "pkg/localcache/lru/lru_lazy.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage lru\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/golang-lru/v2/simplelru\"\n)\n\ntype lazyLruItem[V any] struct {\n\tlock    sync.Mutex\n\texpires int64\n\terr     error\n\tvalue   V\n}\n\nfunc NewLazyLRU[K comparable, V any](size int, successTTL, failedTTL time.Duration, target Target, onEvict EvictCallback[K, V]) *LazyLRU[K, V] {\n\tvar cb simplelru.EvictCallback[K, *lazyLruItem[V]]\n\tif onEvict != nil {\n\t\tcb = func(key K, value *lazyLruItem[V]) {\n\t\t\tonEvict(key, value.value)\n\t\t}\n\t}\n\tcore, err := simplelru.NewLRU[K, *lazyLruItem[V]](size, cb)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn &LazyLRU[K, V]{\n\t\tcore:       core,\n\t\tsuccessTTL: successTTL,\n\t\tfailedTTL:  failedTTL,\n\t\ttarget:     target,\n\t}\n}\n\ntype LazyLRU[K comparable, V any] struct {\n\tlock       sync.Mutex\n\tcore       *simplelru.LRU[K, *lazyLruItem[V]]\n\tsuccessTTL time.Duration\n\tfailedTTL  time.Duration\n\ttarget     Target\n}\n\nfunc (x *LazyLRU[K, V]) Get(key K, fetch func() (V, error)) (V, error) {\n\tx.lock.Lock()\n\tv, ok := x.core.Get(key)\n\tif ok {\n\t\tx.lock.Unlock()\n\t\tv.lock.Lock()\n\t\texpires, value, err := v.expires, v.value, v.err\n\t\tif expires != 0 && expires > time.Now().UnixMilli() {\n\t\t\tv.lock.Unlock()\n\t\t\tx.target.IncrGetHit()\n\t\t\treturn value, err\n\t\t}\n\t} else {\n\t\tv = &lazyLruItem[V]{}\n\t\tx.core.Add(key, v)\n\t\tv.lock.Lock()\n\t\tx.lock.Unlock()\n\t}\n\tdefer v.lock.Unlock()\n\tif v.expires > time.Now().UnixMilli() {\n\t\treturn v.value, v.err\n\t}\n\tv.value, v.err = fetch()\n\tif v.err == nil {\n\t\tv.expires = time.Now().Add(x.successTTL).UnixMilli()\n\t\tx.target.IncrGetSuccess()\n\t} else {\n\t\tv.expires = time.Now().Add(x.failedTTL).UnixMilli()\n\t\tx.target.IncrGetFailed()\n\t}\n\treturn v.value, v.err\n}\n\nfunc (x *LazyLRU[K, V]) GetBatch(keys []K, fetch func(keys []K) (map[K]V, error)) (map[K]V, error) {\n\tvar (\n\t\terr  error\n\t\tonce sync.Once\n\t)\n\n\tres := make(map[K]V)\n\tqueries := make([]K, 0, len(keys))\n\n\tfor _, key := range keys {\n\t\tx.lock.Lock()\n\t\tv, ok := x.core.Get(key)\n\t\tx.lock.Unlock()\n\t\tif ok {\n\t\t\tv.lock.Lock()\n\t\t\texpires, value, err1 := v.expires, v.value, v.err\n\t\t\tv.lock.Unlock()\n\t\t\tif expires != 0 && expires > time.Now().UnixMilli() {\n\t\t\t\tx.target.IncrGetHit()\n\t\t\t\tres[key] = value\n\t\t\t\tif err1 != nil {\n\t\t\t\t\tonce.Do(func() {\n\t\t\t\t\t\terr = err1\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tqueries = append(queries, key)\n\t}\n\n\tif len(queries) == 0 {\n\t\treturn res, err\n\t}\n\n\tvalues, fetchErr := fetch(queries)\n\tif fetchErr != nil {\n\t\tonce.Do(func() {\n\t\t\terr = fetchErr\n\t\t})\n\t}\n\n\tfor key, val := range values {\n\t\tv := &lazyLruItem[V]{}\n\t\tv.value = val\n\n\t\tif err == nil {\n\t\t\tv.expires = time.Now().Add(x.successTTL).UnixMilli()\n\t\t\tx.target.IncrGetSuccess()\n\t\t} else {\n\t\t\tv.expires = time.Now().Add(x.failedTTL).UnixMilli()\n\t\t\tx.target.IncrGetFailed()\n\t\t}\n\n\t\tx.lock.Lock()\n\t\tx.core.Add(key, v)\n\t\tx.lock.Unlock()\n\t\tres[key] = val\n\t}\n\n\treturn res, err\n}\n\n//func (x *LazyLRU[K, V]) Has(key K) bool {\n//\tx.lock.Lock()\n//\tdefer x.lock.Unlock()\n//\treturn x.core.Contains(key)\n//}\n\nfunc (x *LazyLRU[K, V]) Set(key K, value V) {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tx.core.Add(key, &lazyLruItem[V]{value: value, expires: time.Now().Add(x.successTTL).UnixMilli()})\n}\n\nfunc (x *LazyLRU[K, V]) SetHas(key K, value V) bool {\n\tx.lock.Lock()\n\tdefer x.lock.Unlock()\n\tif x.core.Contains(key) {\n\t\tx.core.Add(key, &lazyLruItem[V]{value: value, expires: time.Now().Add(x.successTTL).UnixMilli()})\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (x *LazyLRU[K, V]) Del(key K) bool {\n\tx.lock.Lock()\n\tok := x.core.Remove(key)\n\tx.lock.Unlock()\n\tif ok {\n\t\tx.target.IncrDelHit()\n\t} else {\n\t\tx.target.IncrDelNotFound()\n\t}\n\treturn ok\n}\n\nfunc (x *LazyLRU[K, V]) Stop() {\n\n}\n"
  },
  {
    "path": "pkg/localcache/lru/lru_lazy_test.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage lru\n\nimport (\n\t\"fmt\"\n\t\"hash/fnv\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\t\"unsafe\"\n)\n\ntype cacheTarget struct {\n\tgetHit      int64\n\tgetSuccess  int64\n\tgetFailed   int64\n\tdelHit      int64\n\tdelNotFound int64\n}\n\nfunc (r *cacheTarget) IncrGetHit() {\n\tatomic.AddInt64(&r.getHit, 1)\n}\n\nfunc (r *cacheTarget) IncrGetSuccess() {\n\tatomic.AddInt64(&r.getSuccess, 1)\n}\n\nfunc (r *cacheTarget) IncrGetFailed() {\n\tatomic.AddInt64(&r.getFailed, 1)\n}\n\nfunc (r *cacheTarget) IncrDelHit() {\n\tatomic.AddInt64(&r.delHit, 1)\n}\n\nfunc (r *cacheTarget) IncrDelNotFound() {\n\tatomic.AddInt64(&r.delNotFound, 1)\n}\n\nfunc (r *cacheTarget) String() string {\n\treturn fmt.Sprintf(\"getHit: %d, getSuccess: %d, getFailed: %d, delHit: %d, delNotFound: %d\", r.getHit, r.getSuccess, r.getFailed, r.delHit, r.delNotFound)\n}\n\nfunc TestName(t *testing.T) {\n\ttarget := &cacheTarget{}\n\tl := NewSlotLRU[string, string](100, func(k string) uint64 {\n\t\th := fnv.New64a()\n\t\th.Write(*(*[]byte)(unsafe.Pointer(&k)))\n\t\treturn h.Sum64()\n\t}, func() LRU[string, string] {\n\t\treturn NewExpirationLRU[string, string](100, time.Second*60, time.Second, target, nil)\n\t})\n\t//l := NewInertiaLRU[string, string](1000, time.Second*20, time.Second*5, target)\n\n\tfn := func(key string, n int, fetch func() (string, error)) {\n\t\tfor i := 0; i < n; i++ {\n\t\t\t//v, err := l.Get(key, fetch)\n\t\t\t//if err == nil {\n\t\t\t//\tt.Log(\"key\", key, \"value\", v)\n\t\t\t//} else {\n\t\t\t//\tt.Error(\"key\", key, err)\n\t\t\t//}\n\t\t\tv, err := l.Get(key, fetch)\n\t\t\t//time.Sleep(time.Second / 100)\n\t\t\tfunc(v ...any) {}(v, err)\n\t\t}\n\t}\n\n\ttmp := make(map[string]struct{})\n\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < 10000; i++ {\n\t\twg.Add(1)\n\t\tkey := fmt.Sprintf(\"key_%d\", i%200)\n\t\ttmp[key] = struct{}{}\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\t//t.Log(key)\n\t\t\tfn(key, 10000, func() (string, error) {\n\n\t\t\t\treturn \"value_\" + key, nil\n\t\t\t})\n\t\t}()\n\n\t\t//wg.Add(1)\n\t\t//go func() {\n\t\t//\tdefer wg.Done()\n\t\t//\tfor i := 0; i < 10; i++ {\n\t\t//\t\tl.Del(key)\n\t\t//\t\ttime.Sleep(time.Second / 3)\n\t\t//\t}\n\t\t//}()\n\t}\n\twg.Wait()\n\tt.Log(len(tmp))\n\tt.Log(target.String())\n\n}\n"
  },
  {
    "path": "pkg/localcache/lru/lru_slot.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage lru\n\nfunc NewSlotLRU[K comparable, V any](slotNum int, hash func(K) uint64, create func() LRU[K, V]) LRU[K, V] {\n\tx := &slotLRU[K, V]{\n\t\tn:     uint64(slotNum),\n\t\tslots: make([]LRU[K, V], slotNum),\n\t\thash:  hash,\n\t}\n\tfor i := 0; i < slotNum; i++ {\n\t\tx.slots[i] = create()\n\t}\n\treturn x\n}\n\ntype slotLRU[K comparable, V any] struct {\n\tn     uint64\n\tslots []LRU[K, V]\n\thash  func(k K) uint64\n}\n\nfunc (x *slotLRU[K, V]) GetBatch(keys []K, fetch func(keys []K) (map[K]V, error)) (map[K]V, error) {\n\tvar (\n\t\tslotKeys = make(map[uint64][]K)\n\t\tvs       = make(map[K]V)\n\t)\n\n\tfor _, k := range keys {\n\t\tindex := x.getIndex(k)\n\t\tslotKeys[index] = append(slotKeys[index], k)\n\t}\n\n\tfor k, v := range slotKeys {\n\t\tbatches, err := x.slots[k].GetBatch(v, fetch)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor key, value := range batches {\n\t\t\tvs[key] = value\n\t\t}\n\t}\n\treturn vs, nil\n}\n\nfunc (x *slotLRU[K, V]) getIndex(k K) uint64 {\n\treturn x.hash(k) % x.n\n}\n\nfunc (x *slotLRU[K, V]) Get(key K, fetch func() (V, error)) (V, error) {\n\treturn x.slots[x.getIndex(key)].Get(key, fetch)\n}\n\nfunc (x *slotLRU[K, V]) Set(key K, value V) {\n\tx.slots[x.getIndex(key)].Set(key, value)\n}\n\nfunc (x *slotLRU[K, V]) SetHas(key K, value V) bool {\n\treturn x.slots[x.getIndex(key)].SetHas(key, value)\n}\n\nfunc (x *slotLRU[K, V]) Del(key K) bool {\n\treturn x.slots[x.getIndex(key)].Del(key)\n}\n\nfunc (x *slotLRU[K, V]) Stop() {\n\tfor _, slot := range x.slots {\n\t\tslot.Stop()\n\t}\n}\n"
  },
  {
    "path": "pkg/localcache/option.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage localcache\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache/lru\"\n)\n\nfunc defaultOption() *option {\n\treturn &option{\n\t\tlocalSlotNum:    500,\n\t\tlocalSlotSize:   20000,\n\t\tlinkSlotNum:     500,\n\t\texpirationEvict: false,\n\t\tlocalSuccessTTL: time.Minute,\n\t\tlocalFailedTTL:  time.Second * 5,\n\t\tdelFn:           make([]func(ctx context.Context, key ...string), 0, 2),\n\t\ttarget:          EmptyTarget{},\n\t}\n}\n\ntype option struct {\n\tlocalSlotNum  int\n\tlocalSlotSize int\n\tlinkSlotNum   int\n\t// expirationEvict: true means that the cache will be actively cleared when the timer expires,\n\t// false means that the cache will be lazily deleted.\n\texpirationEvict bool\n\tlocalSuccessTTL time.Duration\n\tlocalFailedTTL  time.Duration\n\tdelFn           []func(ctx context.Context, key ...string)\n\ttarget          lru.Target\n}\n\ntype Option func(o *option)\n\nfunc WithExpirationEvict() Option {\n\treturn func(o *option) {\n\t\to.expirationEvict = true\n\t}\n}\n\nfunc WithLazy() Option {\n\treturn func(o *option) {\n\t\to.expirationEvict = false\n\t}\n}\n\nfunc WithLocalDisable() Option {\n\treturn WithLinkSlotNum(0)\n}\n\nfunc WithLinkDisable() Option {\n\treturn WithLinkSlotNum(0)\n}\n\nfunc WithLinkSlotNum(linkSlotNum int) Option {\n\treturn func(o *option) {\n\t\to.linkSlotNum = linkSlotNum\n\t}\n}\n\nfunc WithLocalSlotNum(localSlotNum int) Option {\n\treturn func(o *option) {\n\t\to.localSlotNum = localSlotNum\n\t}\n}\n\nfunc WithLocalSlotSize(localSlotSize int) Option {\n\treturn func(o *option) {\n\t\to.localSlotSize = localSlotSize\n\t}\n}\n\nfunc WithLocalSuccessTTL(localSuccessTTL time.Duration) Option {\n\tif localSuccessTTL < 0 {\n\t\tpanic(\"localSuccessTTL should be greater than 0\")\n\t}\n\treturn func(o *option) {\n\t\to.localSuccessTTL = localSuccessTTL\n\t}\n}\n\nfunc WithLocalFailedTTL(localFailedTTL time.Duration) Option {\n\tif localFailedTTL < 0 {\n\t\tpanic(\"localFailedTTL should be greater than 0\")\n\t}\n\treturn func(o *option) {\n\t\to.localFailedTTL = localFailedTTL\n\t}\n}\n\nfunc WithTarget(target lru.Target) Option {\n\tif target == nil {\n\t\tpanic(\"target should not be nil\")\n\t}\n\treturn func(o *option) {\n\t\to.target = target\n\t}\n}\n\nfunc WithDeleteKeyBefore(fn func(ctx context.Context, key ...string)) Option {\n\tif fn == nil {\n\t\tpanic(\"fn should not be nil\")\n\t}\n\treturn func(o *option) {\n\t\to.delFn = append(o.delFn, fn)\n\t}\n}\n\ntype EmptyTarget struct{}\n\nfunc (e EmptyTarget) IncrGetHit() {}\n\nfunc (e EmptyTarget) IncrGetSuccess() {}\n\nfunc (e EmptyTarget) IncrGetFailed() {}\n\nfunc (e EmptyTarget) IncrDelHit() {}\n\nfunc (e EmptyTarget) IncrDelNotFound() {}\n"
  },
  {
    "path": "pkg/localcache/tool.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage localcache\n\nfunc AnyValue[V any](v any, err error) (V, error) {\n\tif err != nil {\n\t\tvar zero V\n\t\treturn zero, err\n\t}\n\treturn v.(V), nil\n}\n"
  },
  {
    "path": "pkg/mqbuild/builder.go",
    "content": "package mqbuild\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/mq\"\n\t\"github.com/openimsdk/tools/mq/kafka\"\n\t\"github.com/openimsdk/tools/mq/simmq\"\n)\n\ntype Builder interface {\n\tGetTopicProducer(ctx context.Context, topic string) (mq.Producer, error)\n\tGetTopicConsumer(ctx context.Context, topic string) (mq.Consumer, error)\n}\n\nfunc NewBuilder(kafka *config.Kafka) Builder {\n\tif config.Standalone() {\n\t\treturn standaloneBuilder{}\n\t}\n\treturn &kafkaBuilder{\n\t\taddr:   kafka.Address,\n\t\tconfig: kafka.Build(),\n\t\ttopicGroupID: map[string]string{\n\t\t\tkafka.ToRedisTopic:       kafka.ToRedisGroupID,\n\t\t\tkafka.ToMongoTopic:       kafka.ToMongoGroupID,\n\t\t\tkafka.ToPushTopic:        kafka.ToPushGroupID,\n\t\t\tkafka.ToOfflinePushTopic: kafka.ToOfflineGroupID,\n\t\t},\n\t}\n}\n\ntype standaloneBuilder struct{}\n\nfunc (standaloneBuilder) GetTopicProducer(ctx context.Context, topic string) (mq.Producer, error) {\n\treturn simmq.GetTopicProducer(topic), nil\n}\n\nfunc (standaloneBuilder) GetTopicConsumer(ctx context.Context, topic string) (mq.Consumer, error) {\n\treturn simmq.GetTopicConsumer(topic), nil\n}\n\ntype kafkaBuilder struct {\n\taddr         []string\n\tconfig       *kafka.Config\n\ttopicGroupID map[string]string\n}\n\nfunc (x *kafkaBuilder) GetTopicProducer(ctx context.Context, topic string) (mq.Producer, error) {\n\treturn kafka.NewKafkaProducerV2(x.config, x.addr, topic)\n}\n\nfunc (x *kafkaBuilder) GetTopicConsumer(ctx context.Context, topic string) (mq.Consumer, error) {\n\tgroupID, ok := x.topicGroupID[topic]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"topic %s groupID not found\", topic)\n\t}\n\treturn kafka.NewMConsumerGroupV2(ctx, x.config, groupID, []string{topic}, true)\n}\n"
  },
  {
    "path": "pkg/msgprocessor/conversation.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msgprocessor\n\nimport (\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\nfunc IsGroupConversationID(conversationID string) bool {\n\treturn strings.HasPrefix(conversationID, \"g_\") || strings.HasPrefix(conversationID, \"sg_\")\n}\n\nfunc GetNotificationConversationIDByMsg(msg *sdkws.MsgData) string {\n\tswitch msg.SessionType {\n\tcase constant.SingleChatType:\n\t\tl := []string{msg.SendID, msg.RecvID}\n\t\tsort.Strings(l)\n\t\treturn \"n_\" + strings.Join(l, \"_\")\n\tcase constant.WriteGroupChatType:\n\t\treturn \"n_\" + msg.GroupID\n\tcase constant.ReadGroupChatType:\n\t\treturn \"n_\" + msg.GroupID\n\tcase constant.NotificationChatType:\n\t\tl := []string{msg.SendID, msg.RecvID}\n\t\tsort.Strings(l)\n\t\treturn \"n_\" + strings.Join(l, \"_\")\n\t}\n\treturn \"\"\n}\n\nfunc GetChatConversationIDByMsg(msg *sdkws.MsgData) string {\n\tswitch msg.SessionType {\n\tcase constant.SingleChatType:\n\t\tl := []string{msg.SendID, msg.RecvID}\n\t\tsort.Strings(l)\n\t\treturn \"si_\" + strings.Join(l, \"_\")\n\tcase constant.WriteGroupChatType:\n\t\treturn \"g_\" + msg.GroupID\n\tcase constant.ReadGroupChatType:\n\t\treturn \"sg_\" + msg.GroupID\n\tcase constant.NotificationChatType:\n\t\tl := []string{msg.SendID, msg.RecvID}\n\t\tsort.Strings(l)\n\t\treturn \"sn_\" + strings.Join(l, \"_\")\n\t}\n\n\treturn \"\"\n}\n\nfunc GetConversationIDByMsg(msg *sdkws.MsgData) string {\n\toptions := Options(msg.Options)\n\tswitch msg.SessionType {\n\tcase constant.SingleChatType:\n\t\tl := []string{msg.SendID, msg.RecvID}\n\t\tsort.Strings(l)\n\t\tif !options.IsNotNotification() {\n\t\t\treturn \"n_\" + strings.Join(l, \"_\")\n\t\t}\n\t\treturn \"si_\" + strings.Join(l, \"_\") // single chat\n\tcase constant.WriteGroupChatType:\n\t\tif !options.IsNotNotification() {\n\t\t\treturn \"n_\" + msg.GroupID // group chat\n\t\t}\n\t\treturn \"g_\" + msg.GroupID // group chat\n\tcase constant.ReadGroupChatType:\n\t\tif !options.IsNotNotification() {\n\t\t\treturn \"n_\" + msg.GroupID // super group chat\n\t\t}\n\t\treturn \"sg_\" + msg.GroupID // super group chat\n\tcase constant.NotificationChatType:\n\t\tl := []string{msg.SendID, msg.RecvID}\n\t\tsort.Strings(l)\n\t\tif !options.IsNotNotification() {\n\t\t\treturn \"n_\" + strings.Join(l, \"_\")\n\t\t}\n\t\treturn \"sn_\" + strings.Join(l, \"_\")\n\t}\n\treturn \"\"\n}\n\nfunc GetConversationIDBySessionType(sessionType int, ids ...string) string {\n\tsort.Strings(ids)\n\tif len(ids) > 2 || len(ids) < 1 {\n\t\treturn \"\"\n\t}\n\tswitch sessionType {\n\tcase constant.SingleChatType:\n\t\treturn \"si_\" + strings.Join(ids, \"_\") // single chat\n\tcase constant.WriteGroupChatType:\n\t\treturn \"g_\" + ids[0] // group chat\n\tcase constant.ReadGroupChatType:\n\t\treturn \"sg_\" + ids[0] // super group chat\n\tcase constant.NotificationChatType:\n\t\treturn \"sn_\" + strings.Join(ids, \"_\") // server notification chat\n\t}\n\treturn \"\"\n}\n\nfunc IsNotification(conversationID string) bool {\n\treturn strings.HasPrefix(conversationID, \"n_\")\n}\n\nfunc IsNotificationByMsg(msg *sdkws.MsgData) bool {\n\treturn !Options(msg.Options).IsNotNotification()\n}\n\ntype MsgBySeq []*sdkws.MsgData\n\nfunc (s MsgBySeq) Len() int {\n\treturn len(s)\n}\n\nfunc (s MsgBySeq) Less(i, j int) bool {\n\treturn s[i].Seq < s[j].Seq\n}\n\nfunc (s MsgBySeq) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc Pb2String(pb proto.Message) (string, error) {\n\ts, err := proto.Marshal(pb)\n\tif err != nil {\n\t\treturn \"\", errs.Wrap(err)\n\t}\n\treturn string(s), nil\n}\n\nfunc String2Pb(s string, pb proto.Message) error {\n\treturn proto.Unmarshal([]byte(s), pb)\n}\n"
  },
  {
    "path": "pkg/msgprocessor/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage msgprocessor // import \"github.com/openimsdk/open-im-server/v3/pkg/msgprocessor\"\n"
  },
  {
    "path": "pkg/msgprocessor/options.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage msgprocessor\n\nimport \"github.com/openimsdk/protocol/constant\"\n\ntype (\n\tOptions    map[string]bool\n\tOptionsOpt func(Options)\n)\n\nfunc NewOptions(opts ...OptionsOpt) Options {\n\toptions := make(map[string]bool, 11)\n\toptions[constant.IsNotNotification] = false\n\toptions[constant.IsSendMsg] = false\n\toptions[constant.IsHistory] = false\n\toptions[constant.IsPersistent] = false\n\toptions[constant.IsOfflinePush] = false\n\toptions[constant.IsUnreadCount] = false\n\toptions[constant.IsConversationUpdate] = false\n\toptions[constant.IsSenderSync] = true\n\toptions[constant.IsNotPrivate] = false\n\toptions[constant.IsSenderConversationUpdate] = false\n\toptions[constant.IsReactionFromCache] = false\n\tfor _, opt := range opts {\n\t\topt(options)\n\t}\n\n\treturn options\n}\n\nfunc NewMsgOptions() Options {\n\toptions := make(map[string]bool, 11)\n\toptions[constant.IsOfflinePush] = false\n\treturn make(map[string]bool)\n}\n\nfunc WithOptions(options Options, opts ...OptionsOpt) Options {\n\tfor _, opt := range opts {\n\t\topt(options)\n\t}\n\treturn options\n}\n\nfunc WithNotNotification(b bool) OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsNotNotification] = b\n\t}\n}\n\nfunc WithSendMsg(b bool) OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsSendMsg] = b\n\t}\n}\n\nfunc WithHistory(b bool) OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsHistory] = b\n\t}\n}\n\nfunc WithPersistent() OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsPersistent] = true\n\t}\n}\n\nfunc WithOfflinePush(b bool) OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsOfflinePush] = b\n\t}\n}\n\nfunc WithUnreadCount(b bool) OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsUnreadCount] = b\n\t}\n}\n\nfunc WithConversationUpdate() OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsConversationUpdate] = true\n\t}\n}\n\nfunc WithSenderSync() OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsSenderSync] = true\n\t}\n}\n\nfunc WithNotPrivate() OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsNotPrivate] = true\n\t}\n}\n\nfunc WithSenderConversationUpdate() OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsSenderConversationUpdate] = true\n\t}\n}\n\nfunc WithReactionFromCache() OptionsOpt {\n\treturn func(options Options) {\n\t\toptions[constant.IsReactionFromCache] = true\n\t}\n}\n\nfunc (o Options) Is(notification string) bool {\n\tv, ok := o[notification]\n\tif !ok || v {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (o Options) IsNotNotification() bool {\n\treturn o.Is(constant.IsNotNotification)\n}\n\nfunc (o Options) IsSendMsg() bool {\n\treturn o.Is(constant.IsSendMsg)\n}\n\nfunc (o Options) IsHistory() bool {\n\treturn o.Is(constant.IsHistory)\n}\n\nfunc (o Options) IsPersistent() bool {\n\treturn o.Is(constant.IsPersistent)\n}\n\nfunc (o Options) IsOfflinePush() bool {\n\treturn o.Is(constant.IsOfflinePush)\n}\n\nfunc (o Options) IsUnreadCount() bool {\n\treturn o.Is(constant.IsUnreadCount)\n}\n\nfunc (o Options) IsConversationUpdate() bool {\n\treturn o.Is(constant.IsConversationUpdate)\n}\n\nfunc (o Options) IsSenderSync() bool {\n\treturn o.Is(constant.IsSenderSync)\n}\n\nfunc (o Options) IsNotPrivate() bool {\n\treturn o.Is(constant.IsNotPrivate)\n}\n\nfunc (o Options) IsSenderConversationUpdate() bool {\n\treturn o.Is(constant.IsSenderConversationUpdate)\n}\n\nfunc (o Options) IsReactionFromCache() bool {\n\treturn o.Is(constant.IsReactionFromCache)\n}\n"
  },
  {
    "path": "pkg/notification/common_user/common.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage common_user\n\ntype CommonUser interface {\n\tGetNickname() string\n\tGetFaceURL() string\n\tGetUserID() string\n\tGetEx() string\n}\n\ntype CommonGroup interface {\n\tGetNickname() string\n\tGetFaceURL() string\n\tGetGroupID() string\n\tGetEx() string\n}\n"
  },
  {
    "path": "pkg/notification/grouphash/grouphash.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage grouphash\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n)\n\nfunc NewGroupHashFromGroupClient(x group.GroupClient) *GroupHash {\n\treturn &GroupHash{\n\t\tgetGroupAllUserIDs: func(ctx context.Context, groupID string) ([]string, error) {\n\t\t\tresp, err := x.GetGroupMemberUserIDs(ctx, &group.GetGroupMemberUserIDsReq{GroupID: groupID})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn resp.UserIDs, nil\n\t\t},\n\t\tgetGroupMemberInfo: func(ctx context.Context, groupID string, userIDs []string) ([]*sdkws.GroupMemberFullInfo, error) {\n\t\t\tresp, err := x.GetGroupMembersInfo(ctx, &group.GetGroupMembersInfoReq{GroupID: groupID, UserIDs: userIDs})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn resp.Members, nil\n\t\t},\n\t}\n}\n\nfunc NewGroupHashFromGroupServer(x group.GroupServer) *GroupHash {\n\treturn &GroupHash{\n\t\tgetGroupAllUserIDs: func(ctx context.Context, groupID string) ([]string, error) {\n\t\t\tresp, err := x.GetGroupMemberUserIDs(ctx, &group.GetGroupMemberUserIDsReq{GroupID: groupID})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn resp.UserIDs, nil\n\t\t},\n\t\tgetGroupMemberInfo: func(ctx context.Context, groupID string, userIDs []string) ([]*sdkws.GroupMemberFullInfo, error) {\n\t\t\tresp, err := x.GetGroupMembersInfo(ctx, &group.GetGroupMembersInfoReq{GroupID: groupID, UserIDs: userIDs})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn resp.Members, nil\n\t\t},\n\t}\n}\n\ntype GroupHash struct {\n\tgetGroupAllUserIDs func(ctx context.Context, groupID string) ([]string, error)\n\tgetGroupMemberInfo func(ctx context.Context, groupID string, userIDs []string) ([]*sdkws.GroupMemberFullInfo, error)\n}\n\nfunc (gh *GroupHash) GetGroupHash(ctx context.Context, groupID string) (uint64, error) {\n\tuserIDs, err := gh.getGroupAllUserIDs(ctx, groupID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tvar members []*sdkws.GroupMemberFullInfo\n\tif len(userIDs) > 0 {\n\t\tmembers, err = gh.getGroupMemberInfo(ctx, groupID, userIDs)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tdatautil.Sort(userIDs, true)\n\t}\n\tmemberMap := datautil.SliceToMap(members, func(e *sdkws.GroupMemberFullInfo) string {\n\t\treturn e.UserID\n\t})\n\tres := make([]*sdkws.GroupMemberFullInfo, 0, len(members))\n\tfor _, userID := range userIDs {\n\t\tmember, ok := memberMap[userID]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tmember.AppMangerLevel = 0\n\t\tres = append(res, member)\n\t}\n\tdata, err := json.Marshal(res)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tsum := md5.Sum(data)\n\treturn binary.BigEndian.Uint64(sum[:]), nil\n}\n"
  },
  {
    "path": "pkg/notification/msg.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage notification\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mq/memamq\"\n\t\"github.com/openimsdk/tools/utils/idutil\"\n\t\"github.com/openimsdk/tools/utils/jsonutil\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n)\n\nfunc newContentTypeConf(conf *config.Notification) map[int32]config.NotificationConfig {\n\treturn map[int32]config.NotificationConfig{\n\t\t// group\n\t\tconstant.GroupCreatedNotification:                 conf.GroupCreated,\n\t\tconstant.GroupInfoSetNotification:                 conf.GroupInfoSet,\n\t\tconstant.JoinGroupApplicationNotification:         conf.JoinGroupApplication,\n\t\tconstant.MemberQuitNotification:                   conf.MemberQuit,\n\t\tconstant.GroupApplicationAcceptedNotification:     conf.GroupApplicationAccepted,\n\t\tconstant.GroupApplicationRejectedNotification:     conf.GroupApplicationRejected,\n\t\tconstant.GroupOwnerTransferredNotification:        conf.GroupOwnerTransferred,\n\t\tconstant.MemberKickedNotification:                 conf.MemberKicked,\n\t\tconstant.MemberInvitedNotification:                conf.MemberInvited,\n\t\tconstant.MemberEnterNotification:                  conf.MemberEnter,\n\t\tconstant.GroupDismissedNotification:               conf.GroupDismissed,\n\t\tconstant.GroupMutedNotification:                   conf.GroupMuted,\n\t\tconstant.GroupCancelMutedNotification:             conf.GroupCancelMuted,\n\t\tconstant.GroupMemberMutedNotification:             conf.GroupMemberMuted,\n\t\tconstant.GroupMemberCancelMutedNotification:       conf.GroupMemberCancelMuted,\n\t\tconstant.GroupMemberInfoSetNotification:           conf.GroupMemberInfoSet,\n\t\tconstant.GroupMemberSetToAdminNotification:        conf.GroupMemberSetToAdmin,\n\t\tconstant.GroupMemberSetToOrdinaryUserNotification: conf.GroupMemberSetToOrdinary,\n\t\tconstant.GroupInfoSetAnnouncementNotification:     conf.GroupInfoSetAnnouncement,\n\t\tconstant.GroupInfoSetNameNotification:             conf.GroupInfoSetName,\n\t\t// user\n\t\tconstant.UserInfoUpdatedNotification:  conf.UserInfoUpdated,\n\t\tconstant.UserStatusChangeNotification: conf.UserStatusChanged,\n\t\t// friend\n\t\tconstant.FriendApplicationNotification:         conf.FriendApplicationAdded,\n\t\tconstant.FriendApplicationApprovedNotification: conf.FriendApplicationApproved,\n\t\tconstant.FriendApplicationRejectedNotification: conf.FriendApplicationRejected,\n\t\tconstant.FriendAddedNotification:               conf.FriendAdded,\n\t\tconstant.FriendDeletedNotification:             conf.FriendDeleted,\n\t\tconstant.FriendRemarkSetNotification:           conf.FriendRemarkSet,\n\t\tconstant.BlackAddedNotification:                conf.BlackAdded,\n\t\tconstant.BlackDeletedNotification:              conf.BlackDeleted,\n\t\tconstant.FriendInfoUpdatedNotification:         conf.FriendInfoUpdated,\n\t\tconstant.FriendsInfoUpdateNotification:         conf.FriendInfoUpdated, // use the same FriendInfoUpdated\n\t\t// conversation\n\t\tconstant.ConversationChangeNotification:      conf.ConversationChanged,\n\t\tconstant.ConversationUnreadNotification:      conf.ConversationChanged,\n\t\tconstant.ConversationPrivateChatNotification: conf.ConversationSetPrivate,\n\t\t// msg\n\t\tconstant.MsgRevokeNotification:  {IsSendMsg: false, ReliabilityLevel: constant.ReliableNotificationNoMsg},\n\t\tconstant.HasReadReceipt:         {IsSendMsg: false, ReliabilityLevel: constant.ReliableNotificationNoMsg},\n\t\tconstant.DeleteMsgsNotification: {IsSendMsg: false, ReliabilityLevel: constant.ReliableNotificationNoMsg},\n\t}\n}\n\nfunc newSessionTypeConf() map[int32]int32 {\n\treturn map[int32]int32{\n\t\t// group\n\t\tconstant.GroupCreatedNotification:                 constant.ReadGroupChatType,\n\t\tconstant.GroupInfoSetNotification:                 constant.ReadGroupChatType,\n\t\tconstant.JoinGroupApplicationNotification:         constant.SingleChatType,\n\t\tconstant.MemberQuitNotification:                   constant.ReadGroupChatType,\n\t\tconstant.GroupApplicationAcceptedNotification:     constant.SingleChatType,\n\t\tconstant.GroupApplicationRejectedNotification:     constant.SingleChatType,\n\t\tconstant.GroupOwnerTransferredNotification:        constant.ReadGroupChatType,\n\t\tconstant.MemberKickedNotification:                 constant.ReadGroupChatType,\n\t\tconstant.MemberInvitedNotification:                constant.ReadGroupChatType,\n\t\tconstant.MemberEnterNotification:                  constant.ReadGroupChatType,\n\t\tconstant.GroupDismissedNotification:               constant.ReadGroupChatType,\n\t\tconstant.GroupMutedNotification:                   constant.ReadGroupChatType,\n\t\tconstant.GroupCancelMutedNotification:             constant.ReadGroupChatType,\n\t\tconstant.GroupMemberMutedNotification:             constant.ReadGroupChatType,\n\t\tconstant.GroupMemberCancelMutedNotification:       constant.ReadGroupChatType,\n\t\tconstant.GroupMemberInfoSetNotification:           constant.ReadGroupChatType,\n\t\tconstant.GroupMemberSetToAdminNotification:        constant.ReadGroupChatType,\n\t\tconstant.GroupMemberSetToOrdinaryUserNotification: constant.ReadGroupChatType,\n\t\tconstant.GroupInfoSetAnnouncementNotification:     constant.ReadGroupChatType,\n\t\tconstant.GroupInfoSetNameNotification:             constant.ReadGroupChatType,\n\t\t// user\n\t\tconstant.UserInfoUpdatedNotification:  constant.SingleChatType,\n\t\tconstant.UserStatusChangeNotification: constant.SingleChatType,\n\t\t// friend\n\t\tconstant.FriendApplicationNotification:         constant.SingleChatType,\n\t\tconstant.FriendApplicationApprovedNotification: constant.SingleChatType,\n\t\tconstant.FriendApplicationRejectedNotification: constant.SingleChatType,\n\t\tconstant.FriendAddedNotification:               constant.SingleChatType,\n\t\tconstant.FriendDeletedNotification:             constant.SingleChatType,\n\t\tconstant.FriendRemarkSetNotification:           constant.SingleChatType,\n\t\tconstant.BlackAddedNotification:                constant.SingleChatType,\n\t\tconstant.BlackDeletedNotification:              constant.SingleChatType,\n\t\tconstant.FriendInfoUpdatedNotification:         constant.SingleChatType,\n\t\tconstant.FriendsInfoUpdateNotification:         constant.SingleChatType,\n\t\t// conversation\n\t\tconstant.ConversationChangeNotification:      constant.SingleChatType,\n\t\tconstant.ConversationUnreadNotification:      constant.SingleChatType,\n\t\tconstant.ConversationPrivateChatNotification: constant.SingleChatType,\n\t\t// delete\n\t\tconstant.DeleteMsgsNotification: constant.SingleChatType,\n\t}\n}\n\ntype NotificationSender struct {\n\tcontentTypeConf map[int32]config.NotificationConfig\n\tsessionTypeConf map[int32]int32\n\tsendMsg         func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error)\n\tgetUserInfo     func(ctx context.Context, userID string) (*sdkws.UserInfo, error)\n\tqueue           *memamq.MemoryQueue\n}\n\nfunc WithQueue(queue *memamq.MemoryQueue) NotificationSenderOptions {\n\treturn func(s *NotificationSender) {\n\t\ts.queue = queue\n\t}\n}\n\ntype NotificationSenderOptions func(*NotificationSender)\n\nfunc WithLocalSendMsg(sendMsg func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error)) NotificationSenderOptions {\n\treturn func(s *NotificationSender) {\n\t\ts.sendMsg = sendMsg\n\t}\n}\n\nfunc WithRpcClient(sendMsg func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error)) NotificationSenderOptions {\n\treturn func(s *NotificationSender) {\n\t\ts.sendMsg = func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {\n\t\t\treturn sendMsg(ctx, req)\n\t\t}\n\t}\n}\n\nfunc WithUserRpcClient(getUserInfo func(ctx context.Context, userID string) (*sdkws.UserInfo, error)) NotificationSenderOptions {\n\treturn func(s *NotificationSender) {\n\t\ts.getUserInfo = getUserInfo\n\t}\n}\n\nconst (\n\tnotificationWorkerCount = 16\n\tnotificationBufferSize  = 1024 * 1024 * 2\n)\n\nfunc NewNotificationSender(conf *config.Notification, opts ...NotificationSenderOptions) *NotificationSender {\n\tnotificationSender := &NotificationSender{contentTypeConf: newContentTypeConf(conf), sessionTypeConf: newSessionTypeConf()}\n\tfor _, opt := range opts {\n\t\topt(notificationSender)\n\t}\n\tif notificationSender.queue == nil {\n\t\tnotificationSender.queue = memamq.NewMemoryQueue(notificationWorkerCount, notificationBufferSize)\n\t}\n\treturn notificationSender\n}\n\ntype notificationOpt struct {\n\tRpcGetUsername bool\n\tSendMessage    *bool\n}\n\ntype NotificationOptions func(*notificationOpt)\n\nfunc WithRpcGetUserName() NotificationOptions {\n\treturn func(opt *notificationOpt) {\n\t\topt.RpcGetUsername = true\n\t}\n}\nfunc WithSendMessage(sendMessage *bool) NotificationOptions {\n\treturn func(opt *notificationOpt) {\n\t\topt.SendMessage = sendMessage\n\t}\n}\n\nfunc (s *NotificationSender) send(ctx context.Context, sendID, recvID string, contentType, sessionType int32, m proto.Message, opts ...NotificationOptions) {\n\tctx = context.WithoutCancel(ctx)\n\tctx, cancel := context.WithTimeout(ctx, time.Second*time.Duration(5))\n\tdefer cancel()\n\tn := sdkws.NotificationElem{Detail: jsonutil.StructToJsonString(m)}\n\tcontent, err := json.Marshal(&n)\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"json.Marshal failed\", err, \"sendID\", sendID, \"recvID\", recvID, \"contentType\", contentType, \"msg\", jsonutil.StructToJsonString(m))\n\t\treturn\n\t}\n\tnotificationOpt := &notificationOpt{}\n\tfor _, opt := range opts {\n\t\topt(notificationOpt)\n\t}\n\tvar req msg.SendMsgReq\n\tvar msg sdkws.MsgData\n\tvar userInfo *sdkws.UserInfo\n\tif notificationOpt.RpcGetUsername && s.getUserInfo != nil {\n\t\tuserInfo, err = s.getUserInfo(ctx, sendID)\n\t\tif err != nil {\n\t\t\tlog.ZWarn(ctx, \"getUserInfo failed\", err, \"sendID\", sendID)\n\t\t\treturn\n\t\t}\n\t\tmsg.SenderNickname = userInfo.Nickname\n\t\tmsg.SenderFaceURL = userInfo.FaceURL\n\t}\n\tvar offlineInfo sdkws.OfflinePushInfo\n\tmsg.SendID = sendID\n\tmsg.RecvID = recvID\n\tmsg.Content = content\n\tmsg.MsgFrom = constant.SysMsgType\n\tmsg.ContentType = contentType\n\tmsg.SessionType = sessionType\n\tif msg.SessionType == constant.ReadGroupChatType {\n\t\tmsg.GroupID = recvID\n\t}\n\tmsg.CreateTime = timeutil.GetCurrentTimestampByMill()\n\tmsg.ClientMsgID = idutil.GetMsgIDByMD5(sendID)\n\toptionsConfig := s.contentTypeConf[contentType]\n\tif sendID == recvID && contentType == constant.HasReadReceipt {\n\t\toptionsConfig.ReliabilityLevel = constant.UnreliableNotification\n\t}\n\toptions := config.GetOptionsByNotification(optionsConfig, notificationOpt.SendMessage)\n\ts.SetOptionsByContentType(ctx, options, contentType)\n\tmsg.Options = options\n\t// fill Notification OfflinePush by config\n\tofflineInfo.Title = optionsConfig.OfflinePush.Title\n\tofflineInfo.Desc = optionsConfig.OfflinePush.Desc\n\tofflineInfo.Ex = optionsConfig.OfflinePush.Ext\n\tmsg.OfflinePushInfo = &offlineInfo\n\treq.MsgData = &msg\n\t_, err = s.sendMsg(ctx, &req)\n\tif err != nil {\n\t\tlog.ZWarn(ctx, \"SendMsg failed\", err, \"req\", req.String())\n\t}\n}\n\nfunc (s *NotificationSender) NotificationWithSessionType(ctx context.Context, sendID, recvID string, contentType, sessionType int32, m proto.Message, opts ...NotificationOptions) {\n\tif err := s.queue.Push(func() { s.send(ctx, sendID, recvID, contentType, sessionType, m, opts...) }); err != nil {\n\t\tlog.ZWarn(ctx, \"Push to queue failed\", err, \"sendID\", sendID, \"recvID\", recvID, \"msg\", jsonutil.StructToJsonString(m))\n\t}\n}\n\nfunc (s *NotificationSender) Notification(ctx context.Context, sendID, recvID string, contentType int32, m proto.Message, opts ...NotificationOptions) {\n\ts.NotificationWithSessionType(ctx, sendID, recvID, contentType, s.sessionTypeConf[contentType], m, opts...)\n}\n\nfunc (s *NotificationSender) SetOptionsByContentType(_ context.Context, options map[string]bool, contentType int32) {\n\tswitch contentType {\n\tcase constant.UserStatusChangeNotification:\n\t\toptions[constant.IsSenderSync] = false\n\tdefault:\n\t}\n}\n"
  },
  {
    "path": "pkg/rpccache/auth.go",
    "content": "package rpccache\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/convert\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewAuthLocalCache(client *rpcli.AuthClient, localCache *config.LocalCache, cli redis.UniversalClient) *AuthLocalCache {\n\tlc := localCache.Auth\n\tlog.ZDebug(context.Background(), \"AuthLocalCache\", \"topic\", lc.Topic, \"slotNum\", lc.SlotNum, \"slotSize\", lc.SlotSize, \"enable\", lc.Enable())\n\tx := &AuthLocalCache{\n\t\tclient: client,\n\t\tlocal: localcache.New[[]byte](\n\t\t\tlocalcache.WithLocalSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSlotSize(lc.SlotSize),\n\t\t\tlocalcache.WithLinkSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSuccessTTL(lc.Success()),\n\t\t\tlocalcache.WithLocalFailedTTL(lc.Failed()),\n\t\t),\n\t}\n\tif lc.Enable() {\n\t\tgo subscriberRedisDeleteCache(context.Background(), cli, lc.Topic, x.local.DelLocal)\n\t}\n\treturn x\n}\n\ntype AuthLocalCache struct {\n\tclient *rpcli.AuthClient\n\tlocal  localcache.Cache[[]byte]\n}\n\nfunc (a *AuthLocalCache) GetExistingToken(ctx context.Context, userID string, platformID int) (val map[string]int, err error) {\n\tresp, err := a.getExistingToken(ctx, userID, platformID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres := convert.TokenMapPb2DB(resp.TokenStates)\n\n\treturn res, nil\n}\n\nfunc (a *AuthLocalCache) getExistingToken(ctx context.Context, userID string, platformID int) (val *auth.GetExistingTokenResp, err error) {\n\tstart := time.Now()\n\tlog.ZDebug(ctx, \"AuthLocalCache GetExistingToken req\", \"userID\", userID, \"platformID\", platformID)\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"AuthLocalCache GetExistingToken error\", err, \"cost\", time.Since(start), \"userID\", userID, \"platformID\", platformID)\n\t\t} else {\n\t\t\tlog.ZDebug(ctx, \"AuthLocalCache GetExistingToken resp\", \"cost\", time.Since(start), \"userID\", userID, \"platformID\", platformID, \"val\", val)\n\t\t}\n\t}()\n\n\tvar cache cacheProto[auth.GetExistingTokenResp]\n\n\treturn cache.Unmarshal(a.local.Get(ctx, cachekey.GetTokenKey(userID, platformID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"AuthLocalCache GetExistingToken call rpc\", \"userID\", userID, \"platformID\", platformID)\n\t\treturn cache.Marshal(a.client.AuthClient.GetExistingToken(ctx, &auth.GetExistingTokenReq{UserID: userID, PlatformID: int32(platformID)}))\n\t}))\n}\n"
  },
  {
    "path": "pkg/rpccache/common.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage rpccache\n\nimport (\n\t\"github.com/openimsdk/tools/errs\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\nfunc newListMap[V comparable](values []V, err error) (*listMap[V], error) {\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlm := &listMap[V]{\n\t\tList: values,\n\t\tMap:  make(map[V]struct{}, len(values)),\n\t}\n\tfor _, value := range values {\n\t\tlm.Map[value] = struct{}{}\n\t}\n\treturn lm, nil\n}\n\ntype listMap[V comparable] struct {\n\tList []V\n\tMap  map[V]struct{}\n}\n\nfunc respProtoMarshal(resp proto.Message, err error) ([]byte, error) {\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn proto.Marshal(resp)\n}\n\nfunc cacheUnmarshal[V any](resp []byte, err error) (*V, error) {\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar val V\n\tif err := proto.Unmarshal(resp, any(&val).(proto.Message)); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"local cache proto.Unmarshal error\")\n\t}\n\treturn &val, nil\n}\n\ntype cacheProto[V any] struct{}\n\nfunc (cacheProto[V]) Marshal(resp *V, err error) ([]byte, error) {\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn proto.Marshal(any(resp).(proto.Message))\n}\n\nfunc (cacheProto[V]) Unmarshal(resp []byte, err error) (*V, error) {\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar val V\n\tif err := proto.Unmarshal(resp, any(&val).(proto.Message)); err != nil {\n\t\treturn nil, errs.WrapMsg(err, \"local cache proto.Unmarshal error\")\n\t}\n\treturn &val, nil\n}\n"
  },
  {
    "path": "pkg/rpccache/conversation.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage rpccache\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\tpbconversation \"github.com/openimsdk/protocol/conversation\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nconst (\n\tconversationWorkerCount = 20\n)\n\nfunc NewConversationLocalCache(client *rpcli.ConversationClient, localCache *config.LocalCache, cli redis.UniversalClient) *ConversationLocalCache {\n\tlc := localCache.Conversation\n\tlog.ZDebug(context.Background(), \"ConversationLocalCache\", \"topic\", lc.Topic, \"slotNum\", lc.SlotNum, \"slotSize\", lc.SlotSize, \"enable\", lc.Enable())\n\tx := &ConversationLocalCache{\n\t\tclient: client,\n\t\tlocal: localcache.New[[]byte](\n\t\t\tlocalcache.WithLocalSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSlotSize(lc.SlotSize),\n\t\t\tlocalcache.WithLinkSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSuccessTTL(lc.Success()),\n\t\t\tlocalcache.WithLocalFailedTTL(lc.Failed()),\n\t\t),\n\t}\n\tif lc.Enable() {\n\t\tgo subscriberRedisDeleteCache(context.Background(), cli, lc.Topic, x.local.DelLocal)\n\t}\n\treturn x\n}\n\ntype ConversationLocalCache struct {\n\tclient *rpcli.ConversationClient\n\tlocal  localcache.Cache[[]byte]\n}\n\nfunc (c *ConversationLocalCache) GetConversationIDs(ctx context.Context, ownerUserID string) (val []string, err error) {\n\tresp, err := c.getConversationIDs(ctx, ownerUserID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.ConversationIDs, nil\n}\n\nfunc (c *ConversationLocalCache) getConversationIDs(ctx context.Context, ownerUserID string) (val *pbconversation.GetConversationIDsResp, err error) {\n\tlog.ZDebug(ctx, \"ConversationLocalCache getConversationIDs req\", \"ownerUserID\", ownerUserID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"ConversationLocalCache getConversationIDs return\", \"ownerUserID\", ownerUserID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"ConversationLocalCache getConversationIDs return\", err, \"ownerUserID\", ownerUserID)\n\t\t}\n\t}()\n\tvar cache cacheProto[pbconversation.GetConversationIDsResp]\n\treturn cache.Unmarshal(c.local.Get(ctx, cachekey.GetConversationIDsKey(ownerUserID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"ConversationLocalCache getConversationIDs rpc\", \"ownerUserID\", ownerUserID)\n\t\treturn cache.Marshal(c.client.ConversationClient.GetConversationIDs(ctx, &pbconversation.GetConversationIDsReq{UserID: ownerUserID}))\n\t}))\n}\n\nfunc (c *ConversationLocalCache) GetConversation(ctx context.Context, userID, conversationID string) (val *pbconversation.Conversation, err error) {\n\tlog.ZDebug(ctx, \"ConversationLocalCache GetConversation req\", \"userID\", userID, \"conversationID\", conversationID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"ConversationLocalCache GetConversation return\", \"userID\", userID, \"conversationID\", conversationID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZWarn(ctx, \"ConversationLocalCache GetConversation return\", err, \"userID\", userID, \"conversationID\", conversationID)\n\t\t}\n\t}()\n\tvar cache cacheProto[pbconversation.Conversation]\n\treturn cache.Unmarshal(c.local.Get(ctx, cachekey.GetConversationKey(userID, conversationID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"ConversationLocalCache GetConversation rpc\", \"userID\", userID, \"conversationID\", conversationID)\n\t\treturn cache.Marshal(c.client.GetConversation(ctx, conversationID, userID))\n\t}))\n}\n\nfunc (c *ConversationLocalCache) GetSingleConversationRecvMsgOpt(ctx context.Context, userID, conversationID string) (int32, error) {\n\tconv, err := c.GetConversation(ctx, userID, conversationID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn conv.RecvMsgOpt, nil\n}\n\nfunc (c *ConversationLocalCache) GetConversations(ctx context.Context, ownerUserID string, conversationIDs []string) ([]*pbconversation.Conversation, error) {\n\tvar (\n\t\tconversations     = make([]*pbconversation.Conversation, 0, len(conversationIDs))\n\t\tconversationsChan = make(chan *pbconversation.Conversation, len(conversationIDs))\n\t)\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(conversationWorkerCount)\n\n\tfor _, conversationID := range conversationIDs {\n\t\tconversationID := conversationID\n\t\tg.Go(func() error {\n\t\t\tconversation, err := c.GetConversation(ctx, ownerUserID, conversationID)\n\t\t\tif err != nil {\n\t\t\t\tif errs.ErrRecordNotFound.Is(err) {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tconversationsChan <- conversation\n\t\t\treturn nil\n\t\t})\n\t}\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\tclose(conversationsChan)\n\tfor conversation := range conversationsChan {\n\t\tconversations = append(conversations, conversation)\n\t}\n\treturn conversations, nil\n}\n\nfunc (c *ConversationLocalCache) getConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) (val *pbconversation.GetConversationNotReceiveMessageUserIDsResp, err error) {\n\tlog.ZDebug(ctx, \"ConversationLocalCache getConversationNotReceiveMessageUserIDs req\", \"conversationID\", conversationID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"ConversationLocalCache getConversationNotReceiveMessageUserIDs return\", \"conversationID\", conversationID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"ConversationLocalCache getConversationNotReceiveMessageUserIDs return\", err, \"conversationID\", conversationID)\n\t\t}\n\t}()\n\tvar cache cacheProto[pbconversation.GetConversationNotReceiveMessageUserIDsResp]\n\treturn cache.Unmarshal(c.local.Get(ctx, cachekey.GetConversationNotReceiveMessageUserIDsKey(conversationID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"ConversationLocalCache getConversationNotReceiveMessageUserIDs rpc\", \"conversationID\", conversationID)\n\t\treturn cache.Marshal(c.client.ConversationClient.GetConversationNotReceiveMessageUserIDs(ctx, &pbconversation.GetConversationNotReceiveMessageUserIDsReq{ConversationID: conversationID}))\n\t}))\n}\n\nfunc (c *ConversationLocalCache) getPinnedConversationIDs(ctx context.Context, userID string) (val []string, err error) {\n\tlog.ZDebug(ctx, \"ConversationLocalCache getPinnedConversations req\", \"userID\", userID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"ConversationLocalCache getPinnedConversations return\", \"userID\", userID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"ConversationLocalCache getPinnedConversations return\", err, \"userID\", userID)\n\t\t}\n\t}()\n\tvar cache cacheProto[pbconversation.GetPinnedConversationIDsResp]\n\tresp, err := cache.Unmarshal(c.local.Get(ctx, cachekey.GetPinnedConversationIDs(userID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"ConversationLocalCache getConversationNotReceiveMessageUserIDs rpc\", \"userID\", userID)\n\t\treturn cache.Marshal(c.client.ConversationClient.GetPinnedConversationIDs(ctx, &pbconversation.GetPinnedConversationIDsReq{UserID: userID}))\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.ConversationIDs, nil\n}\n\nfunc (c *ConversationLocalCache) GetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error) {\n\tres, err := c.getConversationNotReceiveMessageUserIDs(ctx, conversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn res.UserIDs, nil\n}\n\nfunc (c *ConversationLocalCache) GetConversationNotReceiveMessageUserIDMap(ctx context.Context, conversationID string) (map[string]struct{}, error) {\n\tres, err := c.getConversationNotReceiveMessageUserIDs(ctx, conversationID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn datautil.SliceSet(res.UserIDs), nil\n}\n\nfunc (c *ConversationLocalCache) GetPinnedConversationIDs(ctx context.Context, userID string) ([]string, error) {\n\treturn c.getPinnedConversationIDs(ctx, userID)\n}\n"
  },
  {
    "path": "pkg/rpccache/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage rpccache // import \"github.com/openimsdk/open-im-server/v3/pkg/rpccache\"\n"
  },
  {
    "path": "pkg/rpccache/friend.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage rpccache\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/relation\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewFriendLocalCache(client *rpcli.RelationClient, localCache *config.LocalCache, cli redis.UniversalClient) *FriendLocalCache {\n\tlc := localCache.Friend\n\tlog.ZDebug(context.Background(), \"FriendLocalCache\", \"topic\", lc.Topic, \"slotNum\", lc.SlotNum, \"slotSize\", lc.SlotSize, \"enable\", lc.Enable())\n\tx := &FriendLocalCache{\n\t\tclient: client,\n\t\tlocal: localcache.New[[]byte](\n\t\t\tlocalcache.WithLocalSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSlotSize(lc.SlotSize),\n\t\t\tlocalcache.WithLinkSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSuccessTTL(lc.Success()),\n\t\t\tlocalcache.WithLocalFailedTTL(lc.Failed()),\n\t\t),\n\t}\n\tif lc.Enable() {\n\t\tgo subscriberRedisDeleteCache(context.Background(), cli, lc.Topic, x.local.DelLocal)\n\t}\n\treturn x\n}\n\ntype FriendLocalCache struct {\n\tclient *rpcli.RelationClient\n\tlocal  localcache.Cache[[]byte]\n}\n\nfunc (f *FriendLocalCache) IsFriend(ctx context.Context, possibleFriendUserID, userID string) (val bool, err error) {\n\tres, err := f.isFriend(ctx, possibleFriendUserID, userID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn res.InUser1Friends, nil\n}\n\nfunc (f *FriendLocalCache) isFriend(ctx context.Context, possibleFriendUserID, userID string) (val *relation.IsFriendResp, err error) {\n\tlog.ZDebug(ctx, \"FriendLocalCache isFriend req\", \"possibleFriendUserID\", possibleFriendUserID, \"userID\", userID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"FriendLocalCache isFriend return\", \"possibleFriendUserID\", possibleFriendUserID, \"userID\", userID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"FriendLocalCache isFriend return\", err, \"possibleFriendUserID\", possibleFriendUserID, \"userID\", userID)\n\t\t}\n\t}()\n\tvar cache cacheProto[relation.IsFriendResp]\n\treturn cache.Unmarshal(f.local.GetLink(ctx, cachekey.GetIsFriendKey(possibleFriendUserID, userID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"FriendLocalCache isFriend rpc\", \"possibleFriendUserID\", possibleFriendUserID, \"userID\", userID)\n\t\treturn cache.Marshal(f.client.FriendClient.IsFriend(ctx, &relation.IsFriendReq{UserID1: userID, UserID2: possibleFriendUserID}))\n\t}, cachekey.GetFriendIDsKey(possibleFriendUserID)))\n}\n\n// IsBlack possibleBlackUserID selfUserID.\nfunc (f *FriendLocalCache) IsBlack(ctx context.Context, possibleBlackUserID, userID string) (val bool, err error) {\n\tres, err := f.isBlack(ctx, possibleBlackUserID, userID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn res.InUser2Blacks, nil\n}\n\n// IsBlack possibleBlackUserID selfUserID.\nfunc (f *FriendLocalCache) isBlack(ctx context.Context, possibleBlackUserID, userID string) (val *relation.IsBlackResp, err error) {\n\tlog.ZDebug(ctx, \"FriendLocalCache isBlack req\", \"possibleBlackUserID\", possibleBlackUserID, \"userID\", userID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"FriendLocalCache isBlack return\", \"possibleBlackUserID\", possibleBlackUserID, \"userID\", userID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"FriendLocalCache isBlack return\", err, \"possibleBlackUserID\", possibleBlackUserID, \"userID\", userID)\n\t\t}\n\t}()\n\tvar cache cacheProto[relation.IsBlackResp]\n\treturn cache.Unmarshal(f.local.GetLink(ctx, cachekey.GetIsBlackIDsKey(possibleBlackUserID, userID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"FriendLocalCache IsBlack rpc\", \"possibleBlackUserID\", possibleBlackUserID, \"userID\", userID)\n\t\treturn cache.Marshal(f.client.FriendClient.IsBlack(ctx, &relation.IsBlackReq{UserID1: possibleBlackUserID, UserID2: userID}))\n\t}, cachekey.GetBlackIDsKey(userID)))\n}\n"
  },
  {
    "path": "pkg/rpccache/group.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage rpccache\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewGroupLocalCache(client *rpcli.GroupClient, localCache *config.LocalCache, cli redis.UniversalClient) *GroupLocalCache {\n\tlc := localCache.Group\n\tlog.ZDebug(context.Background(), \"GroupLocalCache\", \"topic\", lc.Topic, \"slotNum\", lc.SlotNum, \"slotSize\", lc.SlotSize, \"enable\", lc.Enable())\n\tx := &GroupLocalCache{\n\t\tclient: client,\n\t\tlocal: localcache.New[[]byte](\n\t\t\tlocalcache.WithLocalSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSlotSize(lc.SlotSize),\n\t\t\tlocalcache.WithLinkSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSuccessTTL(lc.Success()),\n\t\t\tlocalcache.WithLocalFailedTTL(lc.Failed()),\n\t\t),\n\t}\n\tif lc.Enable() {\n\t\tgo subscriberRedisDeleteCache(context.Background(), cli, lc.Topic, x.local.DelLocal)\n\t}\n\treturn x\n}\n\ntype GroupLocalCache struct {\n\tclient *rpcli.GroupClient\n\tlocal  localcache.Cache[[]byte]\n}\n\nfunc (g *GroupLocalCache) getGroupMemberIDs(ctx context.Context, groupID string) (val *group.GetGroupMemberUserIDsResp, err error) {\n\tlog.ZDebug(ctx, \"GroupLocalCache getGroupMemberIDs req\", \"groupID\", groupID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"GroupLocalCache getGroupMemberIDs return\", \"groupID\", groupID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"GroupLocalCache getGroupMemberIDs return\", err, \"groupID\", groupID)\n\t\t}\n\t}()\n\tvar cache cacheProto[group.GetGroupMemberUserIDsResp]\n\treturn cache.Unmarshal(g.local.Get(ctx, cachekey.GetGroupMemberIDsKey(groupID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"GroupLocalCache getGroupMemberIDs rpc\", \"groupID\", groupID)\n\t\treturn cache.Marshal(g.client.GroupClient.GetGroupMemberUserIDs(ctx, &group.GetGroupMemberUserIDsReq{GroupID: groupID}))\n\t}))\n}\n\nfunc (g *GroupLocalCache) GetGroupMember(ctx context.Context, groupID, userID string) (val *sdkws.GroupMemberFullInfo, err error) {\n\tlog.ZDebug(ctx, \"GroupLocalCache GetGroupInfo req\", \"groupID\", groupID, \"userID\", userID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"GroupLocalCache GetGroupInfo return\", \"groupID\", groupID, \"userID\", userID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"GroupLocalCache GetGroupInfo return\", err, \"groupID\", groupID, \"userID\", userID)\n\t\t}\n\t}()\n\tvar cache cacheProto[sdkws.GroupMemberFullInfo]\n\treturn cache.Unmarshal(g.local.Get(ctx, cachekey.GetGroupMemberInfoKey(groupID, userID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"GroupLocalCache GetGroupInfo rpc\", \"groupID\", groupID, \"userID\", userID)\n\t\treturn cache.Marshal(g.client.GetGroupMemberCache(ctx, groupID, userID))\n\t}))\n}\n\nfunc (g *GroupLocalCache) GetGroupInfo(ctx context.Context, groupID string) (val *sdkws.GroupInfo, err error) {\n\tlog.ZDebug(ctx, \"GroupLocalCache GetGroupInfo req\", \"groupID\", groupID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"GroupLocalCache GetGroupInfo return\", \"groupID\", groupID, \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"GroupLocalCache GetGroupInfo return\", err, \"groupID\", groupID)\n\t\t}\n\t}()\n\tvar cache cacheProto[sdkws.GroupInfo]\n\treturn cache.Unmarshal(g.local.Get(ctx, cachekey.GetGroupInfoKey(groupID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"GroupLocalCache GetGroupInfo rpc\", \"groupID\", groupID)\n\t\treturn cache.Marshal(g.client.GetGroupInfoCache(ctx, groupID))\n\t}))\n}\n\nfunc (g *GroupLocalCache) GetGroupMemberIDs(ctx context.Context, groupID string) ([]string, error) {\n\tres, err := g.getGroupMemberIDs(ctx, groupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn res.UserIDs, nil\n}\n\nfunc (g *GroupLocalCache) GetGroupMemberIDMap(ctx context.Context, groupID string) (map[string]struct{}, error) {\n\tres, err := g.getGroupMemberIDs(ctx, groupID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn datautil.SliceSet(res.UserIDs), nil\n}\n\nfunc (g *GroupLocalCache) GetGroupInfos(ctx context.Context, groupIDs []string) ([]*sdkws.GroupInfo, error) {\n\tgroupInfos := make([]*sdkws.GroupInfo, 0, len(groupIDs))\n\tfor _, groupID := range groupIDs {\n\t\tgroupInfo, err := g.GetGroupInfo(ctx, groupID)\n\t\tif err != nil {\n\t\t\tif errs.ErrRecordNotFound.Is(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tgroupInfos = append(groupInfos, groupInfo)\n\t}\n\treturn groupInfos, nil\n}\n\nfunc (g *GroupLocalCache) GetGroupMembers(ctx context.Context, groupID string, userIDs []string) ([]*sdkws.GroupMemberFullInfo, error) {\n\tmembers := make([]*sdkws.GroupMemberFullInfo, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tmember, err := g.GetGroupMember(ctx, groupID, userID)\n\t\tif err != nil {\n\t\t\tif errs.ErrRecordNotFound.Is(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tmembers = append(members, member)\n\t}\n\treturn members, nil\n}\n\nfunc (g *GroupLocalCache) GetGroupMemberInfoMap(ctx context.Context, groupID string, userIDs []string) (map[string]*sdkws.GroupMemberFullInfo, error) {\n\tmembers := make(map[string]*sdkws.GroupMemberFullInfo)\n\tfor _, userID := range userIDs {\n\t\tmember, err := g.GetGroupMember(ctx, groupID, userID)\n\t\tif err != nil {\n\t\t\tif errs.ErrRecordNotFound.Is(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tmembers[userID] = member\n\t}\n\treturn members, nil\n}\n"
  },
  {
    "path": "pkg/rpccache/online.go",
    "content": "package rpccache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/user\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache/lru\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/util/useronline\"\n\t\"github.com/openimsdk/tools/db/cacheutil\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/mcontext\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nconst (\n\tBegin uint32 = iota\n\tDoOnlineStatusOver\n\tDoSubscribeOver\n)\n\ntype OnlineCache interface {\n\tGetUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error)\n\tGetUserOnline(ctx context.Context, userID string) (bool, error)\n\tGetUsersOnline(ctx context.Context, userIDs []string) ([]string, []string, error)\n\tWaitCache()\n}\n\nfunc NewOnlineCache(client *rpcli.UserClient, group *GroupLocalCache, rdb redis.UniversalClient, fullUserCache bool, fn func(ctx context.Context, userID string, platformIDs []int32)) (OnlineCache, error) {\n\tif config.Standalone() {\n\t\treturn disableOnlineCache{client: client}, nil\n\t}\n\tl := &sync.Mutex{}\n\tx := &defaultOnlineCache{\n\t\tclient:        client,\n\t\tgroup:         group,\n\t\tfullUserCache: fullUserCache,\n\t\tLock:          l,\n\t\tCond:          sync.NewCond(l),\n\t}\n\n\tctx := mcontext.SetOperationID(context.TODO(), strconv.FormatInt(time.Now().UnixNano()+int64(rand.Uint32()), 10))\n\n\tswitch x.fullUserCache {\n\tcase true:\n\t\tlog.ZDebug(ctx, \"fullUserCache is true\")\n\t\tx.mapCache = cacheutil.NewCache[string, []int32]()\n\t\tgo func() {\n\t\t\tif err := x.initUsersOnlineStatus(ctx); err != nil {\n\t\t\t\tlog.ZError(ctx, \"initUsersOnlineStatus failed\", err)\n\t\t\t}\n\t\t}()\n\tcase false:\n\t\tlog.ZDebug(ctx, \"fullUserCache is false\")\n\t\tx.lruCache = lru.NewSlotLRU(1024, localcache.LRUStringHash, func() lru.LRU[string, []int32] {\n\t\t\treturn lru.NewLazyLRU[string, []int32](2048, cachekey.OnlineExpire/2, time.Second*3, localcache.EmptyTarget{}, func(key string, value []int32) {})\n\t\t})\n\t\tx.CurrentPhase.Store(DoSubscribeOver)\n\t\tx.Cond.Broadcast()\n\t}\n\tif rdb != nil {\n\t\tgo func() {\n\t\t\tx.doSubscribe(ctx, rdb, fn)\n\t\t}()\n\t}\n\treturn x, nil\n}\n\ntype defaultOnlineCache struct {\n\tclient *rpcli.UserClient\n\tgroup  *GroupLocalCache\n\n\t// fullUserCache if enabled, caches the online status of all users using mapCache;\n\t// otherwise, only a portion of users' online statuses (regardless of whether they are online) will be cached using lruCache.\n\tfullUserCache bool\n\n\tlruCache lru.LRU[string, []int32]\n\tmapCache *cacheutil.Cache[string, []int32]\n\n\tLock         *sync.Mutex\n\tCond         *sync.Cond\n\tCurrentPhase atomic.Uint32\n}\n\nfunc (o *defaultOnlineCache) initUsersOnlineStatus(ctx context.Context) (err error) {\n\tlog.ZDebug(ctx, \"init users online status begin\")\n\n\tvar (\n\t\ttotalSet      atomic.Int64\n\t\tmaxTries      = 5\n\t\tretryInterval = time.Second * 5\n\n\t\tresp *user.GetAllOnlineUsersResp\n\t)\n\n\tdefer func(t time.Time) {\n\t\tlog.ZInfo(ctx, \"init users online status end\", \"cost\", time.Since(t), \"totalSet\", totalSet.Load())\n\t\to.CurrentPhase.Store(DoOnlineStatusOver)\n\t\to.Cond.Broadcast()\n\t}(time.Now())\n\n\tretryOperation := func(operation func() error, operationName string) error {\n\t\tfor i := 0; i < maxTries; i++ {\n\t\t\tif err = operation(); err != nil {\n\t\t\t\tlog.ZWarn(ctx, fmt.Sprintf(\"initUsersOnlineStatus: %s failed\", operationName), err)\n\t\t\t\ttime.Sleep(retryInterval)\n\t\t\t} else {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn err\n\t}\n\n\tcursor := uint64(0)\n\tfor resp == nil || resp.NextCursor != 0 {\n\t\tif err = retryOperation(func() error {\n\t\t\tresp, err = o.client.GetAllOnlineUsers(ctx, cursor)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, u := range resp.StatusList {\n\t\t\t\tif u.Status == constant.Online {\n\t\t\t\t\to.setUserOnline(u.UserID, u.PlatformIDs)\n\t\t\t\t}\n\t\t\t\ttotalSet.Add(1)\n\t\t\t}\n\t\t\tcursor = resp.NextCursor\n\t\t\treturn nil\n\t\t}, \"getAllOnlineUsers\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (o *defaultOnlineCache) doSubscribe(ctx context.Context, rdb redis.UniversalClient, fn func(ctx context.Context, userID string, platformIDs []int32)) {\n\to.Lock.Lock()\n\tch := rdb.Subscribe(ctx, cachekey.OnlineChannel).Channel()\n\tfor o.CurrentPhase.Load() < DoOnlineStatusOver {\n\t\to.Cond.Wait()\n\t}\n\to.Lock.Unlock()\n\tlog.ZInfo(ctx, \"begin doSubscribe\")\n\n\tdoMessage := func(message *redis.Message) {\n\t\tuserID, platformIDs, err := useronline.ParseUserOnlineStatus(message.Payload)\n\t\tif err != nil {\n\t\t\tlog.ZError(ctx, \"OnlineCache setHasUserOnline redis subscribe parseUserOnlineStatus\", err, \"payload\", message.Payload, \"channel\", message.Channel)\n\t\t\treturn\n\t\t}\n\t\tlog.ZDebug(ctx, fmt.Sprintf(\"get subscribe %s message\", cachekey.OnlineChannel), \"useID\", userID, \"platformIDs\", platformIDs)\n\t\tswitch o.fullUserCache {\n\t\tcase true:\n\t\t\tif len(platformIDs) == 0 {\n\t\t\t\t// offline\n\t\t\t\to.mapCache.Delete(userID)\n\t\t\t} else {\n\t\t\t\to.mapCache.Store(userID, platformIDs)\n\t\t\t}\n\t\tcase false:\n\t\t\tstorageCache := o.setHasUserOnline(userID, platformIDs)\n\t\t\tlog.ZDebug(ctx, \"OnlineCache setHasUserOnline\", \"userID\", userID, \"platformIDs\", platformIDs, \"payload\", message.Payload, \"storageCache\", storageCache)\n\t\t\tif fn != nil {\n\t\t\t\tfn(ctx, userID, platformIDs)\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.CurrentPhase.Load() == DoOnlineStatusOver {\n\t\tfor done := false; !done; {\n\t\t\tselect {\n\t\t\tcase message := <-ch:\n\t\t\t\tdoMessage(message)\n\t\t\tdefault:\n\t\t\t\to.CurrentPhase.Store(DoSubscribeOver)\n\t\t\t\to.Cond.Broadcast()\n\t\t\t\tdone = true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor message := range ch {\n\t\tdoMessage(message)\n\t}\n}\n\nfunc (o *defaultOnlineCache) getUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) {\n\tplatformIDs, err := o.lruCache.Get(userID, func() ([]int32, error) {\n\t\treturn o.client.GetUserOnlinePlatform(ctx, userID)\n\t})\n\tif err != nil {\n\t\tlog.ZError(ctx, \"OnlineCache GetUserOnlinePlatform\", err, \"userID\", userID)\n\t\treturn nil, err\n\t}\n\t//log.ZDebug(ctx, \"OnlineCache GetUserOnlinePlatform\", \"userID\", userID, \"platformIDs\", platformIDs)\n\treturn platformIDs, nil\n}\n\nfunc (o *defaultOnlineCache) GetUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) {\n\tplatformIDs, err := o.getUserOnlinePlatform(ctx, userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttmp := make([]int32, len(platformIDs))\n\tcopy(tmp, platformIDs)\n\treturn platformIDs, nil\n}\n\nfunc (o *defaultOnlineCache) GetUserOnline(ctx context.Context, userID string) (bool, error) {\n\tplatformIDs, err := o.getUserOnlinePlatform(ctx, userID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn len(platformIDs) > 0, nil\n}\n\nfunc (o *defaultOnlineCache) getUserOnlinePlatformBatch(ctx context.Context, userIDs []string) (map[string][]int32, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\tplatformIDsMap, err := o.lruCache.GetBatch(userIDs, func(missingUsers []string) (map[string][]int32, error) {\n\t\tplatformIDsMap := make(map[string][]int32)\n\t\tusersStatus, err := o.client.GetUsersOnlinePlatform(ctx, missingUsers)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, u := range usersStatus {\n\t\t\tplatformIDsMap[u.UserID] = u.PlatformIDs\n\t\t}\n\n\t\treturn platformIDsMap, nil\n\t})\n\tif err != nil {\n\t\tlog.ZError(ctx, \"OnlineCache GetUserOnlinePlatform\", err, \"userID\", userIDs)\n\t\treturn nil, err\n\t}\n\treturn platformIDsMap, nil\n}\n\nfunc (o *defaultOnlineCache) GetUsersOnline(ctx context.Context, userIDs []string) ([]string, []string, error) {\n\tt := time.Now()\n\n\tvar (\n\t\tonlineUserIDs  = make([]string, 0, len(userIDs))\n\t\tofflineUserIDs = make([]string, 0, len(userIDs))\n\t)\n\n\tswitch o.fullUserCache {\n\tcase true:\n\t\tfor _, userID := range userIDs {\n\t\t\tif _, ok := o.mapCache.Load(userID); ok {\n\t\t\t\tonlineUserIDs = append(onlineUserIDs, userID)\n\t\t\t} else {\n\t\t\t\tofflineUserIDs = append(offlineUserIDs, userID)\n\t\t\t}\n\t\t}\n\tcase false:\n\t\tuserOnlineMap, err := o.getUserOnlinePlatformBatch(ctx, userIDs)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tfor key, value := range userOnlineMap {\n\t\t\tif len(value) > 0 {\n\t\t\t\tonlineUserIDs = append(onlineUserIDs, key)\n\t\t\t} else {\n\t\t\t\tofflineUserIDs = append(offlineUserIDs, key)\n\t\t\t}\n\t\t}\n\t}\n\n\tlog.ZInfo(ctx, \"get users online\", \"online users length\", len(onlineUserIDs), \"offline users length\", len(offlineUserIDs), \"cost\", time.Since(t))\n\treturn onlineUserIDs, offlineUserIDs, nil\n}\n\nfunc (o *defaultOnlineCache) setUserOnline(userID string, platformIDs []int32) {\n\tswitch o.fullUserCache {\n\tcase true:\n\t\to.mapCache.Store(userID, platformIDs)\n\tcase false:\n\t\to.lruCache.Set(userID, platformIDs)\n\t}\n}\n\nfunc (o *defaultOnlineCache) setHasUserOnline(userID string, platformIDs []int32) bool {\n\treturn o.lruCache.SetHas(userID, platformIDs)\n}\n\nfunc (o *defaultOnlineCache) WaitCache() {\n\to.Lock.Lock()\n\tfor o.CurrentPhase.Load() < DoSubscribeOver {\n\t\to.Cond.Wait()\n\t}\n\to.Lock.Unlock()\n}\n\ntype disableOnlineCache struct {\n\tclient *rpcli.UserClient\n}\n\nfunc (o disableOnlineCache) GetUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) {\n\treturn o.client.GetUserOnlinePlatform(ctx, userID)\n}\n\nfunc (o disableOnlineCache) GetUserOnline(ctx context.Context, userID string) (bool, error) {\n\tonlinePlatform, err := o.client.GetUserOnlinePlatform(ctx, userID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn len(onlinePlatform) > 0, err\n}\n\nfunc (o disableOnlineCache) GetUsersOnline(ctx context.Context, userIDs []string) ([]string, []string, error) {\n\tvar (\n\t\tonlineUserIDs  = make([]string, 0, len(userIDs))\n\t\tofflineUserIDs = make([]string, 0, len(userIDs))\n\t)\n\tfor _, userID := range userIDs {\n\t\tonline, err := o.GetUserOnline(ctx, userID)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tif online {\n\t\t\tonlineUserIDs = append(onlineUserIDs, userID)\n\t\t} else {\n\t\t\tofflineUserIDs = append(offlineUserIDs, userID)\n\t\t}\n\t}\n\treturn onlineUserIDs, offlineUserIDs, nil\n}\n\nfunc (o disableOnlineCache) WaitCache() {}\n"
  },
  {
    "path": "pkg/rpccache/subscriber.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage rpccache\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc subscriberRedisDeleteCache(ctx context.Context, client redis.UniversalClient, channel string, del func(ctx context.Context, key ...string)) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.ZPanic(ctx, \"subscriberRedisDeleteCache Panic\", errs.ErrPanic(r))\n\t\t}\n\t}()\n\tfor message := range client.Subscribe(ctx, channel).Channel() {\n\t\tlog.ZDebug(ctx, \"subscriberRedisDeleteCache\", \"channel\", channel, \"payload\", message.Payload)\n\t\tvar keys []string\n\t\tif err := json.Unmarshal([]byte(message.Payload), &keys); err != nil {\n\t\t\tlog.ZError(ctx, \"subscriberRedisDeleteCache json.Unmarshal error\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif len(keys) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tdel(ctx, keys...)\n\t}\n}\n"
  },
  {
    "path": "pkg/rpccache/user.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage rpccache\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/rpcli\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/localcache\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/redis/go-redis/v9\"\n)\n\nfunc NewUserLocalCache(client *rpcli.UserClient, localCache *config.LocalCache, cli redis.UniversalClient) *UserLocalCache {\n\tlc := localCache.User\n\tlog.ZDebug(context.Background(), \"UserLocalCache\", \"topic\", lc.Topic, \"slotNum\", lc.SlotNum, \"slotSize\", lc.SlotSize, \"enable\", lc.Enable())\n\tx := &UserLocalCache{\n\t\tclient: client,\n\t\tlocal: localcache.New[[]byte](\n\t\t\tlocalcache.WithLocalSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSlotSize(lc.SlotSize),\n\t\t\tlocalcache.WithLinkSlotNum(lc.SlotNum),\n\t\t\tlocalcache.WithLocalSuccessTTL(lc.Success()),\n\t\t\tlocalcache.WithLocalFailedTTL(lc.Failed()),\n\t\t),\n\t}\n\tif lc.Enable() {\n\t\tgo subscriberRedisDeleteCache(context.Background(), cli, lc.Topic, x.local.DelLocal)\n\t}\n\treturn x\n}\n\ntype UserLocalCache struct {\n\tclient *rpcli.UserClient\n\tlocal  localcache.Cache[[]byte]\n}\n\nfunc (u *UserLocalCache) GetUserInfo(ctx context.Context, userID string) (val *sdkws.UserInfo, err error) {\n\tlog.ZDebug(ctx, \"UserLocalCache GetUserInfo req\", \"userID\", userID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"UserLocalCache GetUserInfo return\", \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"UserLocalCache GetUserInfo return\", err)\n\t\t}\n\t}()\n\tvar cache cacheProto[sdkws.UserInfo]\n\treturn cache.Unmarshal(u.local.Get(ctx, cachekey.GetUserInfoKey(userID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"UserLocalCache GetUserInfo rpc\", \"userID\", userID)\n\t\treturn cache.Marshal(u.client.GetUserInfo(ctx, userID))\n\t}))\n}\n\nfunc (u *UserLocalCache) GetUserGlobalMsgRecvOpt(ctx context.Context, userID string) (val int32, err error) {\n\tresp, err := u.getUserGlobalMsgRecvOpt(ctx, userID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn resp.GlobalRecvMsgOpt, nil\n}\n\nfunc (u *UserLocalCache) getUserGlobalMsgRecvOpt(ctx context.Context, userID string) (val *user.GetGlobalRecvMessageOptResp, err error) {\n\tlog.ZDebug(ctx, \"UserLocalCache getUserGlobalMsgRecvOpt req\", \"userID\", userID)\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tlog.ZDebug(ctx, \"UserLocalCache getUserGlobalMsgRecvOpt return\", \"value\", val)\n\t\t} else {\n\t\t\tlog.ZError(ctx, \"UserLocalCache getUserGlobalMsgRecvOpt return\", err)\n\t\t}\n\t}()\n\tvar cache cacheProto[user.GetGlobalRecvMessageOptResp]\n\treturn cache.Unmarshal(u.local.Get(ctx, cachekey.GetUserGlobalRecvMsgOptKey(userID), func(ctx context.Context) ([]byte, error) {\n\t\tlog.ZDebug(ctx, \"UserLocalCache GetUserGlobalMsgRecvOpt rpc\", \"userID\", userID)\n\t\treturn cache.Marshal(u.client.UserClient.GetGlobalRecvMessageOpt(ctx, &user.GetGlobalRecvMessageOptReq{UserID: userID}))\n\t}))\n}\n\nfunc (u *UserLocalCache) GetUsersInfo(ctx context.Context, userIDs []string) ([]*sdkws.UserInfo, error) {\n\tusers := make([]*sdkws.UserInfo, 0, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tuser, err := u.GetUserInfo(ctx, userID)\n\t\tif err != nil {\n\t\t\tif errs.ErrRecordNotFound.Is(err) {\n\t\t\t\tlog.ZWarn(ctx, \"User info notFound\", err, \"userID\", userID)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tusers = append(users, user)\n\t}\n\treturn users, nil\n}\n\nfunc (u *UserLocalCache) GetUsersInfoMap(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error) {\n\tusers := make(map[string]*sdkws.UserInfo, len(userIDs))\n\tfor _, userID := range userIDs {\n\t\tuser, err := u.GetUserInfo(ctx, userID)\n\t\tif err != nil {\n\t\t\tif errs.ErrRecordNotFound.Is(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tusers[userID] = user\n\t}\n\treturn users, nil\n}\n"
  },
  {
    "path": "pkg/rpcli/auth.go",
    "content": "package rpcli\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/protocol/auth\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewAuthClient(cc grpc.ClientConnInterface) *AuthClient {\n\treturn &AuthClient{auth.NewAuthClient(cc)}\n}\n\ntype AuthClient struct {\n\tauth.AuthClient\n}\n\nfunc (x *AuthClient) KickTokens(ctx context.Context, tokens []string) error {\n\tif len(tokens) == 0 {\n\t\treturn nil\n\t}\n\treturn ignoreResp(x.AuthClient.KickTokens(ctx, &auth.KickTokensReq{Tokens: tokens}))\n}\n\nfunc (x *AuthClient) InvalidateToken(ctx context.Context, req *auth.InvalidateTokenReq) error {\n\treturn ignoreResp(x.AuthClient.InvalidateToken(ctx, req))\n}\n\nfunc (x *AuthClient) ParseToken(ctx context.Context, token string) (*auth.ParseTokenResp, error) {\n\treturn x.AuthClient.ParseToken(ctx, &auth.ParseTokenReq{Token: token})\n}\n"
  },
  {
    "path": "pkg/rpcli/conversation.go",
    "content": "package rpcli\n\nimport (\n\t\"context\"\n\n\t\"github.com/openimsdk/protocol/conversation\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewConversationClient(cc grpc.ClientConnInterface) *ConversationClient {\n\treturn &ConversationClient{conversation.NewConversationClient(cc)}\n}\n\ntype ConversationClient struct {\n\tconversation.ConversationClient\n}\n\nfunc (x *ConversationClient) SetConversationMaxSeq(ctx context.Context, conversationID string, ownerUserIDs []string, maxSeq int64) error {\n\tif len(ownerUserIDs) == 0 {\n\t\treturn nil\n\t}\n\treq := &conversation.SetConversationMaxSeqReq{ConversationID: conversationID, OwnerUserID: ownerUserIDs, MaxSeq: maxSeq}\n\treturn ignoreResp(x.ConversationClient.SetConversationMaxSeq(ctx, req))\n}\n\nfunc (x *ConversationClient) SetConversations(ctx context.Context, ownerUserIDs []string, info *conversation.ConversationReq) error {\n\tif len(ownerUserIDs) == 0 {\n\t\treturn nil\n\t}\n\treq := &conversation.SetConversationsReq{UserIDs: ownerUserIDs, Conversation: info}\n\treturn ignoreResp(x.ConversationClient.SetConversations(ctx, req))\n}\n\nfunc (x *ConversationClient) SetConversationMinSeq(ctx context.Context, conversationID string, ownerUserIDs []string, minSeq int64) error {\n\tif len(ownerUserIDs) == 0 {\n\t\treturn nil\n\t}\n\treq := &conversation.SetConversationMinSeqReq{ConversationID: conversationID, OwnerUserID: ownerUserIDs, MinSeq: minSeq}\n\treturn ignoreResp(x.ConversationClient.SetConversationMinSeq(ctx, req))\n}\n\nfunc (x *ConversationClient) GetConversation(ctx context.Context, conversationID string, ownerUserID string) (*conversation.Conversation, error) {\n\treq := &conversation.GetConversationReq{ConversationID: conversationID, OwnerUserID: ownerUserID}\n\treturn extractField(ctx, x.ConversationClient.GetConversation, req, (*conversation.GetConversationResp).GetConversation)\n}\n\nfunc (x *ConversationClient) GetConversations(ctx context.Context, conversationIDs []string, ownerUserID string) ([]*conversation.Conversation, error) {\n\tif len(conversationIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &conversation.GetConversationsReq{ConversationIDs: conversationIDs, OwnerUserID: ownerUserID}\n\treturn extractField(ctx, x.ConversationClient.GetConversations, req, (*conversation.GetConversationsResp).GetConversations)\n}\n\nfunc (x *ConversationClient) GetConversationIDs(ctx context.Context, ownerUserID string) ([]string, error) {\n\treq := &conversation.GetConversationIDsReq{UserID: ownerUserID}\n\treturn extractField(ctx, x.ConversationClient.GetConversationIDs, req, (*conversation.GetConversationIDsResp).GetConversationIDs)\n}\n\nfunc (x *ConversationClient) GetPinnedConversationIDs(ctx context.Context, ownerUserID string) ([]string, error) {\n\treq := &conversation.GetPinnedConversationIDsReq{UserID: ownerUserID}\n\treturn extractField(ctx, x.ConversationClient.GetPinnedConversationIDs, req, (*conversation.GetPinnedConversationIDsResp).GetConversationIDs)\n}\n\nfunc (x *ConversationClient) CreateGroupChatConversations(ctx context.Context, groupID string, userIDs []string) error {\n\tif len(userIDs) == 0 {\n\t\treturn nil\n\t}\n\treq := &conversation.CreateGroupChatConversationsReq{GroupID: groupID, UserIDs: userIDs}\n\treturn ignoreResp(x.ConversationClient.CreateGroupChatConversations(ctx, req))\n}\n\nfunc (x *ConversationClient) CreateSingleChatConversations(ctx context.Context, req *conversation.CreateSingleChatConversationsReq) error {\n\treturn ignoreResp(x.ConversationClient.CreateSingleChatConversations(ctx, req))\n}\n\nfunc (x *ConversationClient) GetConversationOfflinePushUserIDs(ctx context.Context, conversationID string, userIDs []string) ([]string, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &conversation.GetConversationOfflinePushUserIDsReq{ConversationID: conversationID, UserIDs: userIDs}\n\treturn extractField(ctx, x.ConversationClient.GetConversationOfflinePushUserIDs, req, (*conversation.GetConversationOfflinePushUserIDsResp).GetUserIDs)\n}\n"
  },
  {
    "path": "pkg/rpcli/group.go",
    "content": "package rpcli\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewGroupClient(cc grpc.ClientConnInterface) *GroupClient {\n\treturn &GroupClient{group.NewGroupClient(cc)}\n}\n\ntype GroupClient struct {\n\tgroup.GroupClient\n}\n\nfunc (x *GroupClient) GetGroupsInfo(ctx context.Context, groupIDs []string) ([]*sdkws.GroupInfo, error) {\n\tif len(groupIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &group.GetGroupsInfoReq{GroupIDs: groupIDs}\n\treturn extractField(ctx, x.GroupClient.GetGroupsInfo, req, (*group.GetGroupsInfoResp).GetGroupInfos)\n}\n\nfunc (x *GroupClient) GetGroupInfo(ctx context.Context, groupID string) (*sdkws.GroupInfo, error) {\n\treturn firstValue(x.GetGroupsInfo(ctx, []string{groupID}))\n}\n\nfunc (x *GroupClient) GetGroupInfoCache(ctx context.Context, groupID string) (*sdkws.GroupInfo, error) {\n\treq := &group.GetGroupInfoCacheReq{GroupID: groupID}\n\treturn extractField(ctx, x.GroupClient.GetGroupInfoCache, req, (*group.GetGroupInfoCacheResp).GetGroupInfo)\n}\n\nfunc (x *GroupClient) GetGroupMemberCache(ctx context.Context, groupID string, userID string) (*sdkws.GroupMemberFullInfo, error) {\n\treq := &group.GetGroupMemberCacheReq{GroupID: groupID, GroupMemberID: userID}\n\treturn extractField(ctx, x.GroupClient.GetGroupMemberCache, req, (*group.GetGroupMemberCacheResp).GetMember)\n}\n\nfunc (x *GroupClient) DismissGroup(ctx context.Context, groupID string, deleteMember bool) error {\n\treq := &group.DismissGroupReq{GroupID: groupID, DeleteMember: deleteMember}\n\treturn ignoreResp(x.GroupClient.DismissGroup(ctx, req))\n}\n\nfunc (x *GroupClient) GetGroupMemberUserIDs(ctx context.Context, groupID string) ([]string, error) {\n\treq := &group.GetGroupMemberUserIDsReq{GroupID: groupID}\n\treturn extractField(ctx, x.GroupClient.GetGroupMemberUserIDs, req, (*group.GetGroupMemberUserIDsResp).GetUserIDs)\n}\n\nfunc (x *GroupClient) GetGroupMembersInfo(ctx context.Context, groupID string, userIDs []string) ([]*sdkws.GroupMemberFullInfo, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &group.GetGroupMembersInfoReq{GroupID: groupID, UserIDs: userIDs}\n\treturn extractField(ctx, x.GroupClient.GetGroupMembersInfo, req, (*group.GetGroupMembersInfoResp).GetMembers)\n}\n\nfunc (x *GroupClient) GetGroupMemberInfo(ctx context.Context, groupID string, userID string) (*sdkws.GroupMemberFullInfo, error) {\n\treturn firstValue(x.GetGroupMembersInfo(ctx, groupID, []string{userID}))\n}\n\nfunc (x *GroupClient) GetGroupMemberMapInfo(ctx context.Context, groupID string, userIDs []string) (map[string]*sdkws.GroupMemberFullInfo, error) {\n\tmembers, err := x.GetGroupMembersInfo(ctx, groupID, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmemberMap := make(map[string]*sdkws.GroupMemberFullInfo)\n\tfor _, member := range members {\n\t\tmemberMap[member.UserID] = member\n\t}\n\treturn memberMap, nil\n}\n"
  },
  {
    "path": "pkg/rpcli/msg.go",
    "content": "package rpcli\n\nimport (\n\t\"context\"\n\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/openimsdk/protocol/msg\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n)\n\nfunc NewMsgClient(cc grpc.ClientConnInterface) *MsgClient {\n\treturn &MsgClient{msg.NewMsgClient(cc)}\n}\n\ntype MsgClient struct {\n\tmsg.MsgClient\n}\n\nfunc (x *MsgClient) GetMaxSeqs(ctx context.Context, conversationIDs []string) (map[string]int64, error) {\n\tif len(conversationIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &msg.GetMaxSeqsReq{ConversationIDs: conversationIDs}\n\treturn extractField(ctx, x.MsgClient.GetMaxSeqs, req, (*msg.SeqsInfoResp).GetMaxSeqs)\n}\n\nfunc (x *MsgClient) GetMsgByConversationIDs(ctx context.Context, conversationIDs []string, maxSeqs map[string]int64) (map[string]*sdkws.MsgData, error) {\n\tif len(conversationIDs) == 0 || len(maxSeqs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &msg.GetMsgByConversationIDsReq{ConversationIDs: conversationIDs, MaxSeqs: maxSeqs}\n\treturn extractField(ctx, x.MsgClient.GetMsgByConversationIDs, req, (*msg.GetMsgByConversationIDsResp).GetMsgDatas)\n}\n\nfunc (x *MsgClient) GetHasReadSeqs(ctx context.Context, conversationIDs []string, userID string) (map[string]int64, error) {\n\tif len(conversationIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &msg.GetHasReadSeqsReq{ConversationIDs: conversationIDs, UserID: userID}\n\treturn extractField(ctx, x.MsgClient.GetHasReadSeqs, req, (*msg.SeqsInfoResp).GetMaxSeqs)\n}\n\nfunc (x *MsgClient) SetUserConversationMaxSeq(ctx context.Context, conversationID string, ownerUserIDs []string, maxSeq int64) error {\n\tif len(ownerUserIDs) == 0 {\n\t\treturn nil\n\t}\n\treq := &msg.SetUserConversationMaxSeqReq{ConversationID: conversationID, OwnerUserID: ownerUserIDs, MaxSeq: maxSeq}\n\treturn ignoreResp(x.MsgClient.SetUserConversationMaxSeq(ctx, req))\n}\n\nfunc (x *MsgClient) SetUserConversationMin(ctx context.Context, conversationID string, ownerUserIDs []string, minSeq int64) error {\n\tif len(ownerUserIDs) == 0 {\n\t\treturn nil\n\t}\n\treq := &msg.SetUserConversationsMinSeqReq{ConversationID: conversationID, UserIDs: ownerUserIDs, Seq: minSeq}\n\treturn ignoreResp(x.MsgClient.SetUserConversationsMinSeq(ctx, req))\n}\n\nfunc (x *MsgClient) GetLastMessageSeqByTime(ctx context.Context, conversationID string, lastTime int64) (int64, error) {\n\treq := &msg.GetLastMessageSeqByTimeReq{ConversationID: conversationID, Time: lastTime}\n\treturn extractField(ctx, x.MsgClient.GetLastMessageSeqByTime, req, (*msg.GetLastMessageSeqByTimeResp).GetSeq)\n}\n\nfunc (x *MsgClient) GetConversationMaxSeq(ctx context.Context, conversationID string) (int64, error) {\n\treq := &msg.GetConversationMaxSeqReq{ConversationID: conversationID}\n\treturn extractField(ctx, x.MsgClient.GetConversationMaxSeq, req, (*msg.GetConversationMaxSeqResp).GetMaxSeq)\n}\n\nfunc (x *MsgClient) GetActiveConversation(ctx context.Context, conversationIDs []string) ([]*msg.ActiveConversation, error) {\n\tif len(conversationIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &msg.GetActiveConversationReq{ConversationIDs: conversationIDs}\n\treturn extractField(ctx, x.MsgClient.GetActiveConversation, req, (*msg.GetActiveConversationResp).GetConversations)\n}\n\nfunc (x *MsgClient) GetSeqMessage(ctx context.Context, userID string, conversations []*msg.ConversationSeqs) (map[string]*sdkws.PullMsgs, error) {\n\tif len(conversations) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &msg.GetSeqMessageReq{UserID: userID, Conversations: conversations}\n\treturn extractField(ctx, x.MsgClient.GetSeqMessage, req, (*msg.GetSeqMessageResp).GetMsgs)\n}\n\nfunc (x *MsgClient) SetUserConversationsMinSeq(ctx context.Context, conversationID string, userIDs []string, seq int64) error {\n\tif len(userIDs) == 0 {\n\t\treturn nil\n\t}\n\treq := &msg.SetUserConversationsMinSeqReq{ConversationID: conversationID, UserIDs: userIDs, Seq: seq}\n\treturn ignoreResp(x.MsgClient.SetUserConversationsMinSeq(ctx, req))\n}\n"
  },
  {
    "path": "pkg/rpcli/msggateway.go",
    "content": "package rpcli\n\nimport (\n\t\"github.com/openimsdk/protocol/msggateway\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewMsgGatewayClient(cc grpc.ClientConnInterface) *MsgGatewayClient {\n\treturn &MsgGatewayClient{msggateway.NewMsgGatewayClient(cc)}\n}\n\ntype MsgGatewayClient struct {\n\tmsggateway.MsgGatewayClient\n}\n"
  },
  {
    "path": "pkg/rpcli/push.go",
    "content": "package rpcli\n\nimport (\n\t\"github.com/openimsdk/protocol/push\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewPushMsgServiceClient(cc grpc.ClientConnInterface) *PushMsgServiceClient {\n\treturn &PushMsgServiceClient{push.NewPushMsgServiceClient(cc)}\n}\n\ntype PushMsgServiceClient struct {\n\tpush.PushMsgServiceClient\n}\n"
  },
  {
    "path": "pkg/rpcli/relation.go",
    "content": "package rpcli\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/protocol/relation\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewRelationClient(cc grpc.ClientConnInterface) *RelationClient {\n\treturn &RelationClient{relation.NewFriendClient(cc)}\n}\n\ntype RelationClient struct {\n\trelation.FriendClient\n}\n\nfunc (x *RelationClient) GetFriendsInfo(ctx context.Context, ownerUserID string, friendUserIDs []string) ([]*relation.FriendInfoOnly, error) {\n\tif len(friendUserIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &relation.GetFriendInfoReq{OwnerUserID: ownerUserID, FriendUserIDs: friendUserIDs}\n\treturn extractField(ctx, x.FriendClient.GetFriendInfo, req, (*relation.GetFriendInfoResp).GetFriendInfos)\n}\n"
  },
  {
    "path": "pkg/rpcli/rtc.go",
    "content": "package rpcli\n\nimport (\n\t\"github.com/openimsdk/protocol/rtc\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewRtcServiceClient(cc grpc.ClientConnInterface) *RtcServiceClient {\n\treturn &RtcServiceClient{rtc.NewRtcServiceClient(cc)}\n}\n\ntype RtcServiceClient struct {\n\trtc.RtcServiceClient\n}\n"
  },
  {
    "path": "pkg/rpcli/third.go",
    "content": "package rpcli\n\nimport (\n\t\"github.com/openimsdk/protocol/third\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewThirdClient(cc grpc.ClientConnInterface) *ThirdClient {\n\treturn &ThirdClient{third.NewThirdClient(cc)}\n}\n\ntype ThirdClient struct {\n\tthird.ThirdClient\n}\n"
  },
  {
    "path": "pkg/rpcli/tool.go",
    "content": "package rpcli\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc extractField[A, B, C any](ctx context.Context, fn func(ctx context.Context, req *A, opts ...grpc.CallOption) (*B, error), req *A, get func(*B) C) (C, error) {\n\tresp, err := fn(ctx, req)\n\tif err != nil {\n\t\tvar c C\n\t\treturn c, err\n\t}\n\treturn get(resp), nil\n}\n\nfunc firstValue[A any](val []A, err error) (A, error) {\n\tif err != nil {\n\t\tvar a A\n\t\treturn a, err\n\t}\n\tif len(val) == 0 {\n\t\tvar a A\n\t\treturn a, errs.ErrRecordNotFound.WrapMsg(\"record not found\")\n\t}\n\treturn val[0], nil\n}\n\nfunc ignoreResp(_ any, err error) error {\n\treturn err\n}\n"
  },
  {
    "path": "pkg/rpcli/user.go",
    "content": "package rpcli\n\nimport (\n\t\"context\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\t\"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/datautil\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc NewUserClient(cc grpc.ClientConnInterface) *UserClient {\n\treturn &UserClient{user.NewUserClient(cc)}\n}\n\ntype UserClient struct {\n\tuser.UserClient\n}\n\nfunc (x *UserClient) GetUsersInfo(ctx context.Context, userIDs []string) ([]*sdkws.UserInfo, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &user.GetDesignateUsersReq{UserIDs: userIDs}\n\treturn extractField(ctx, x.UserClient.GetDesignateUsers, req, (*user.GetDesignateUsersResp).GetUsersInfo)\n}\n\nfunc (x *UserClient) GetUserInfo(ctx context.Context, userID string) (*sdkws.UserInfo, error) {\n\treturn firstValue(x.GetUsersInfo(ctx, []string{userID}))\n}\n\nfunc (x *UserClient) CheckUser(ctx context.Context, userIDs []string) error {\n\tif len(userIDs) == 0 {\n\t\treturn nil\n\t}\n\tusers, err := x.GetUsersInfo(ctx, userIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(users) != len(userIDs) {\n\t\treturn errs.ErrRecordNotFound.WrapMsg(\"user not found\")\n\t}\n\treturn nil\n}\n\nfunc (x *UserClient) GetUsersInfoMap(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error) {\n\tusers, err := x.GetUsersInfo(ctx, userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn datautil.SliceToMap(users, func(e *sdkws.UserInfo) string {\n\t\treturn e.UserID\n\t}), nil\n}\n\nfunc (x *UserClient) GetAllOnlineUsers(ctx context.Context, cursor uint64) (*user.GetAllOnlineUsersResp, error) {\n\treq := &user.GetAllOnlineUsersReq{Cursor: cursor}\n\treturn x.UserClient.GetAllOnlineUsers(ctx, req)\n}\n\nfunc (x *UserClient) GetUsersOnlinePlatform(ctx context.Context, userIDs []string) ([]*user.OnlineStatus, error) {\n\tif len(userIDs) == 0 {\n\t\treturn nil, nil\n\t}\n\treq := &user.GetUserStatusReq{UserIDs: userIDs}\n\treturn extractField(ctx, x.UserClient.GetUserStatus, req, (*user.GetUserStatusResp).GetStatusList)\n\n}\n\nfunc (x *UserClient) GetUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) {\n\tstatus, err := x.GetUsersOnlinePlatform(ctx, []string{userID})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(status) == 0 {\n\t\treturn nil, nil\n\t}\n\treturn status[0].PlatformIDs, nil\n}\n\nfunc (x *UserClient) SetUserOnlineStatus(ctx context.Context, req *user.SetUserOnlineStatusReq) error {\n\tif len(req.Status) == 0 {\n\t\treturn nil\n\t}\n\treturn ignoreResp(x.UserClient.SetUserOnlineStatus(ctx, req))\n}\n\nfunc (x *UserClient) GetNotificationByID(ctx context.Context, userID string) error {\n\treturn ignoreResp(x.UserClient.GetNotificationAccount(ctx, &user.GetNotificationAccountReq{UserID: userID}))\n}\n\nfunc (x *UserClient) GetAllUserIDs(ctx context.Context, pageNumber, showNumber int32) ([]string, error) {\n\treq := &user.GetAllUserIDReq{Pagination: &sdkws.RequestPagination{PageNumber: pageNumber, ShowNumber: showNumber}}\n\treturn extractField(ctx, x.UserClient.GetAllUserID, req, (*user.GetAllUserIDResp).GetUserIDs)\n}\n"
  },
  {
    "path": "pkg/statistics/doc.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage statistics // import \"github.com/openimsdk/open-im-server/v3/pkg/statistics\"\n"
  },
  {
    "path": "pkg/statistics/statistics.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage statistics\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/openimsdk/tools/log\"\n)\n\ntype Statistics struct {\n\tAllCount   *uint64\n\tModuleName string\n\tPrintArgs  string\n\tSleepTime  uint64\n}\n\nfunc (s *Statistics) output() {\n\tvar intervalCount uint64\n\tt := time.NewTicker(time.Duration(s.SleepTime) * time.Second)\n\tdefer t.Stop()\n\tvar sum uint64\n\tvar timeIntervalNum uint64\n\tfor {\n\t\tsum = *s.AllCount\n\t\t<-t.C\n\t\tif *s.AllCount-sum <= 0 {\n\t\t\tintervalCount = 0\n\t\t} else {\n\t\t\tintervalCount = *s.AllCount - sum\n\t\t}\n\t\ttimeIntervalNum++\n\t\tlog.ZWarn(\n\t\t\tcontext.Background(),\n\t\t\t\" system stat \",\n\t\t\tnil,\n\t\t\t\"args\",\n\t\t\ts.PrintArgs,\n\t\t\t\"intervalCount\",\n\t\t\tintervalCount,\n\t\t\t\"total:\",\n\t\t\t*s.AllCount,\n\t\t\t\"intervalNum\",\n\t\t\ttimeIntervalNum,\n\t\t\t\"avg\",\n\t\t\t(*s.AllCount)/(timeIntervalNum)/s.SleepTime,\n\t\t)\n\t}\n}\n\nfunc NewStatistics(allCount *uint64, moduleName, printArgs string, sleepTime int) *Statistics {\n\tp := &Statistics{AllCount: allCount, ModuleName: moduleName, SleepTime: uint64(sleepTime), PrintArgs: printArgs}\n\tgo p.output()\n\treturn p\n}\n"
  },
  {
    "path": "pkg/tools/batcher/batcher.go",
    "content": "package batcher\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/authverify\"\n\t\"github.com/openimsdk/tools/errs\"\n\t\"github.com/openimsdk/tools/utils/idutil\"\n)\n\nvar (\n\tDefaultDataChanSize = 1000\n\tDefaultSize         = 100\n\tDefaultBuffer       = 100\n\tDefaultWorker       = 5\n\tDefaultInterval     = time.Second\n)\n\ntype Config struct {\n\tsize       int           // Number of message aggregations\n\tbuffer     int           // The number of caches running in a single coroutine\n\tdataBuffer int           // The size of the main data channel\n\tworker     int           // Number of coroutines processed in parallel\n\tinterval   time.Duration // Time of message aggregations\n\tsyncWait   bool          // Whether to wait synchronously after distributing messages have been consumed\n}\n\ntype Option func(c *Config)\n\nfunc WithSize(s int) Option {\n\treturn func(c *Config) {\n\t\tc.size = s\n\t}\n}\n\nfunc WithBuffer(b int) Option {\n\treturn func(c *Config) {\n\t\tc.buffer = b\n\t}\n}\n\nfunc WithWorker(w int) Option {\n\treturn func(c *Config) {\n\t\tc.worker = w\n\t}\n}\n\nfunc WithInterval(i time.Duration) Option {\n\treturn func(c *Config) {\n\t\tc.interval = i\n\t}\n}\n\nfunc WithSyncWait(wait bool) Option {\n\treturn func(c *Config) {\n\t\tc.syncWait = wait\n\t}\n}\n\nfunc WithDataBuffer(size int) Option {\n\treturn func(c *Config) {\n\t\tc.dataBuffer = size\n\t}\n}\n\ntype Batcher[T any] struct {\n\tconfig *Config\n\n\tglobalCtx  context.Context\n\tcancel     context.CancelFunc\n\tDo         func(ctx context.Context, channelID int, val *Msg[T])\n\tOnComplete func(lastMessage *T, totalCount int)\n\tSharding   func(key string) int\n\tKey        func(data *T) string\n\tHookFunc   func(triggerID string, messages map[string][]*T, totalCount int, lastMessage *T)\n\tdata       chan *T\n\tchArrays   []chan *Msg[T]\n\twait       sync.WaitGroup\n\tcounter    sync.WaitGroup\n}\n\nfunc emptyOnComplete[T any](*T, int) {}\nfunc emptyHookFunc[T any](string, map[string][]*T, int, *T) {\n}\n\nfunc New[T any](opts ...Option) *Batcher[T] {\n\tb := &Batcher[T]{\n\t\tOnComplete: emptyOnComplete[T],\n\t\tHookFunc:   emptyHookFunc[T],\n\t}\n\tconfig := &Config{\n\t\tsize:     DefaultSize,\n\t\tbuffer:   DefaultBuffer,\n\t\tworker:   DefaultWorker,\n\t\tinterval: DefaultInterval,\n\t}\n\tfor _, opt := range opts {\n\t\topt(config)\n\t}\n\tb.config = config\n\tb.data = make(chan *T, DefaultDataChanSize)\n\tb.globalCtx, b.cancel = context.WithCancel(context.Background())\n\n\tb.chArrays = make([]chan *Msg[T], b.config.worker)\n\tfor i := 0; i < b.config.worker; i++ {\n\t\tb.chArrays[i] = make(chan *Msg[T], b.config.buffer)\n\t}\n\treturn b\n}\n\nfunc (b *Batcher[T]) Worker() int {\n\treturn b.config.worker\n}\n\nfunc (b *Batcher[T]) Start() error {\n\tif b.Sharding == nil {\n\t\treturn errs.New(\"Sharding function is required\").Wrap()\n\t}\n\tif b.Do == nil {\n\t\treturn errs.New(\"Do function is required\").Wrap()\n\t}\n\tif b.Key == nil {\n\t\treturn errs.New(\"Key function is required\").Wrap()\n\t}\n\tb.wait.Add(b.config.worker)\n\tfor i := 0; i < b.config.worker; i++ {\n\t\tgo b.run(i, b.chArrays[i])\n\t}\n\tb.wait.Add(1)\n\tgo b.scheduler()\n\treturn nil\n}\n\nfunc (b *Batcher[T]) Put(ctx context.Context, data *T) error {\n\tif data == nil {\n\t\treturn errs.New(\"data can not be nil\").Wrap()\n\t}\n\tselect {\n\tcase <-b.globalCtx.Done():\n\t\treturn errs.New(\"data channel is closed\").Wrap()\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase b.data <- data:\n\t\treturn nil\n\t}\n}\n\nfunc (b *Batcher[T]) scheduler() {\n\tticker := time.NewTicker(b.config.interval)\n\tdefer func() {\n\t\tticker.Stop()\n\t\tfor _, ch := range b.chArrays {\n\t\t\tclose(ch)\n\t\t}\n\t\tclose(b.data)\n\t\tb.wait.Done()\n\t}()\n\n\tvals := make(map[string][]*T)\n\tcount := 0\n\tvar lastAny *T\n\n\tfor {\n\t\tselect {\n\t\tcase data, ok := <-b.data:\n\t\t\tif !ok {\n\t\t\t\t// If the data channel is closed unexpectedly\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif data == nil {\n\t\t\t\tif count > 0 {\n\t\t\t\t\tb.distributeMessage(vals, count, lastAny)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tkey := b.Key(data)\n\t\t\tvals[key] = append(vals[key], data)\n\t\t\tlastAny = data\n\n\t\t\tcount++\n\t\t\tif count >= b.config.size {\n\n\t\t\t\tb.distributeMessage(vals, count, lastAny)\n\t\t\t\tvals = make(map[string][]*T)\n\t\t\t\tcount = 0\n\t\t\t}\n\n\t\tcase <-ticker.C:\n\t\t\tif count > 0 {\n\n\t\t\t\tb.distributeMessage(vals, count, lastAny)\n\t\t\t\tvals = make(map[string][]*T)\n\t\t\t\tcount = 0\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype Msg[T any] struct {\n\tkey       string\n\ttriggerID string\n\tval       []*T\n}\n\nfunc (m Msg[T]) Key() string {\n\treturn m.key\n}\n\nfunc (m Msg[T]) TriggerID() string {\n\treturn m.triggerID\n}\n\nfunc (m Msg[T]) Val() []*T {\n\treturn m.val\n}\n\nfunc (m Msg[T]) String() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"Key: \")\n\tsb.WriteString(m.key)\n\tsb.WriteString(\", Values: [\")\n\tfor i, v := range m.val {\n\t\tif i > 0 {\n\t\t\tsb.WriteString(\", \")\n\t\t}\n\t\tsb.WriteString(fmt.Sprintf(\"%v\", *v))\n\t}\n\tsb.WriteString(\"]\")\n\treturn sb.String()\n}\n\nfunc (b *Batcher[T]) distributeMessage(messages map[string][]*T, totalCount int, lastMessage *T) {\n\ttriggerID := idutil.OperationIDGenerator()\n\tb.HookFunc(triggerID, messages, totalCount, lastMessage)\n\tfor key, data := range messages {\n\t\tif b.config.syncWait {\n\t\t\tb.counter.Add(1)\n\t\t}\n\t\tchannelID := b.Sharding(key)\n\t\tb.chArrays[channelID] <- &Msg[T]{key: key, triggerID: triggerID, val: data}\n\t}\n\tif b.config.syncWait {\n\t\tb.counter.Wait()\n\t}\n\tif b.OnComplete != nil {\n\t\tb.OnComplete(lastMessage, totalCount)\n\t}\n}\n\nfunc (b *Batcher[T]) run(channelID int, ch <-chan *Msg[T]) {\n\tdefer b.wait.Done()\n\tctx := authverify.WithTempAdmin(context.Background())\n\tfor {\n\t\tselect {\n\t\tcase messages, ok := <-ch:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tb.Do(ctx, channelID, messages)\n\t\t\tif b.config.syncWait {\n\t\t\t\tb.counter.Done()\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (b *Batcher[T]) Close() {\n\tb.cancel() // Signal to stop put data\n\tb.data <- nil\n\t//wait all goroutines exit\n\tb.wait.Wait()\n}\n"
  },
  {
    "path": "pkg/tools/batcher/batcher_test.go",
    "content": "package batcher\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/openimsdk/tools/utils/stringutil\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestBatcher(t *testing.T) {\n\tconfig := Config{\n\t\tsize:     1000,\n\t\tbuffer:   10,\n\t\tworker:   10,\n\t\tinterval: 5 * time.Millisecond,\n\t}\n\n\tb := New[string](\n\t\tWithSize(config.size),\n\t\tWithBuffer(config.buffer),\n\t\tWithWorker(config.worker),\n\t\tWithInterval(config.interval),\n\t\tWithSyncWait(true),\n\t)\n\n\t// Mock Do function to simply print values for demonstration\n\tb.Do = func(ctx context.Context, channelID int, vals *Msg[string]) {\n\t\tt.Logf(\"Channel %d Processed batch: %v\", channelID, vals)\n\t}\n\tb.OnComplete = func(lastMessage *string, totalCount int) {\n\t\tt.Logf(\"Completed processing with last message: %v, total count: %d\", *lastMessage, totalCount)\n\t}\n\tb.Sharding = func(key string) int {\n\t\thashCode := stringutil.GetHashCode(key)\n\t\treturn int(hashCode) % config.worker\n\t}\n\tb.Key = func(data *string) string {\n\t\treturn *data\n\t}\n\n\terr := b.Start()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Test normal data processing\n\tfor i := 0; i < 10000; i++ {\n\t\tdata := \"data\" + fmt.Sprintf(\"%d\", i)\n\t\tif err := b.Put(context.Background(), &data); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\n\ttime.Sleep(time.Duration(1) * time.Second)\n\tstart := time.Now()\n\t// Wait for all processing to finish\n\tb.Close()\n\n\telapsed := time.Since(start)\n\tt.Logf(\"Close took %s\", elapsed)\n\n\tif len(b.data) != 0 {\n\t\tt.Error(\"Data channel should be empty after closing\")\n\t}\n}\n"
  },
  {
    "path": "pkg/util/conversationutil/conversationutil.go",
    "content": "package conversationutil\n\nimport (\n\t\"sort\"\n\t\"strings\"\n)\n\nfunc GenConversationIDForSingle(sendID, recvID string) string {\n\tl := []string{sendID, recvID}\n\tsort.Strings(l)\n\treturn \"si_\" + strings.Join(l, \"_\")\n}\n\nfunc GenConversationUniqueKeyForGroup(groupID string) string {\n\treturn groupID\n}\n\nfunc GenGroupConversationID(groupID string) string {\n\treturn \"sg_\" + groupID\n}\n\nfunc IsGroupConversationID(conversationID string) bool {\n\treturn strings.HasPrefix(conversationID, \"sg_\")\n}\n\nfunc IsNotificationConversationID(conversationID string) bool {\n\treturn strings.HasPrefix(conversationID, \"n_\")\n}\n\nfunc GenConversationUniqueKeyForSingle(sendID, recvID string) string {\n\tl := []string{sendID, recvID}\n\tsort.Strings(l)\n\treturn strings.Join(l, \"_\")\n}\n\nfunc GetNotificationConversationIDByConversationID(conversationID string) string {\n\tl := strings.Split(conversationID, \"_\")\n\tif len(l) > 1 {\n\t\tl[0] = \"n\"\n\t\treturn strings.Join(l, \"_\")\n\t}\n\treturn \"\"\n}\n\nfunc GetSelfNotificationConversationID(userID string) string {\n\treturn \"n_\" + userID + \"_\" + userID\n}\n\nfunc GetSeqsBeginEnd(seqs []int64) (int64, int64) {\n\tif len(seqs) == 0 {\n\t\treturn 0, 0\n\t}\n\treturn seqs[0], seqs[len(seqs)-1]\n}\n"
  },
  {
    "path": "pkg/util/conversationutil/doc.go",
    "content": "package conversationutil // import \"github.com/openimsdk/open-im-server/v3/pkg/util/conversationutil\"\n"
  },
  {
    "path": "pkg/util/hashutil/id.go",
    "content": "package hashutil\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n)\n\nfunc IdHash(ids []string) uint64 {\n\tif len(ids) == 0 {\n\t\treturn 0\n\t}\n\tdata, _ := json.Marshal(ids)\n\tsum := md5.Sum(data)\n\treturn binary.BigEndian.Uint64(sum[:])\n}\n"
  },
  {
    "path": "pkg/util/useronline/split.go",
    "content": "package useronline\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc ParseUserOnlineStatus(payload string) (string, []int32, error) {\n\tarr := strings.Split(payload, \":\")\n\tif len(arr) == 0 {\n\t\treturn \"\", nil, errors.New(\"invalid data\")\n\t}\n\tuserID := arr[len(arr)-1]\n\tif userID == \"\" {\n\t\treturn \"\", nil, errors.New(\"userID is empty\")\n\t}\n\tplatformIDs := make([]int32, len(arr)-1)\n\tfor i := range platformIDs {\n\t\tplatformID, err := strconv.Atoi(arr[i])\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tplatformIDs[i] = int32(platformID)\n\t}\n\treturn userID, platformIDs, nil\n}\n"
  },
  {
    "path": "scripts/template/LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\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"
  },
  {
    "path": "scripts/template/LICENSE_TEMPLATES",
    "content": "Copyright © {{.Year}} {{.Holder}} All rights reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "scripts/template/boilerplate.txt",
    "content": "Copyright © {{.Year}} {{.Holder}} All rights reserved.\nUse of this source code is governed by a MIT style\nlicense that can be found in the LICENSE file.\n"
  },
  {
    "path": "scripts/template/footer.md.tmpl",
    "content": "**Full Changelog**: https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/compare/{{ .PreviousTag }}...{{ .Tag }}\n\n## Get Involved with OpenIM!\n\nYour patronage towards OpenIM is greatly appreciated 🎉🎉.\n\nIf you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/), we're committed to resolving your problem as soon as possible.\n\n**Here are some ways to get involved with the OpenIM community:**\n\n📢 **Slack Channel**: Join our Slack channels for discussions, communication, and support. Click [here](https://openimsdk.slack.com) to join the Open-IM-Server Slack team channel.\n\n📧 **Gmail Contact**: If you have any questions, suggestions, or feedback for our open-source projects, please feel free to [contact us via email](https://mail.google.com/mail/?view=cm&fs=1&tf=1&to=info@openim.io).\n\n📖 **Blog**: Stay up-to-date with OpenIM-Server projects and trends by reading our [blog](https://openim.io/). We share the latest developments, tech trends, and other interesting information related to OpenIM.\n\n📱 **WeChat**: Add us on WeChat (QR Code) and indicate that you are a user or developer of Open-IM-Server. We'll process your request as soon as possible.\n\nRemember, your contributions play a vital role in making OpenIM successful, and we look forward to your active participation in our community! 🙌"
  },
  {
    "path": "scripts/template/head.md.tmpl",
    "content": "## Welcome to the {{ .Tag }} release of [OpenIM](https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }})!🎉🎉!\n\nWe are excited to release {{.Tag}},  Branch: https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/tree/{{ .Tag }} , Git hash [{{ .ShortCommit }}], Install Address: [{{ .ReleaseURL }}]({{ .ReleaseURL }})\n\nLearn more about versions of OpenIM:\n\n+ We release logs are recorded on [✨CHANGELOG](https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/blob/main/CHANGELOG/CHANGELOG.md)\n\n+ For information on versions of OpenIM and how to maintain branches, read [📚this article](https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/blob/main/docs/contrib/version.md)\n\n+ If you wish to use mirroring, read OpenIM's [🤲image management policy](https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/blob/main/docs/contrib/images.md)\n\n**Want to be one of them 😘?**\n\n<p align=\"center\">\n<a href=\"https://github.com/kubbot\" style=\"float: left; margin-right: 10px;\">\n<img src=\"https://github.com/openimbot/openimbot/blob/main/assets/icon/blue%E9%80%8F%E6%98%8E.png\" width=\"50\" height=\"50\" />\n</a>\n<a href=\"https://openim.io\">\n<img src=\"https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/blob/main/assets/logo/openim-logo.png\" />\n</a>\n<a href=\"https://github.com/openimbot\" style=\"float: right; margin-left: 10px;\">\n<img src=\"https://github.com/openimbot/openimbot/blob/main/assets/icon/red%E9%80%8F%E6%98%8E.png\" width=\"50\" height=\"50\" />\n</a>\n</p>\n\n> **Note**\n> @openimbot and @kubbot have made great contributions to the community as community 🤖robots(@openimsdk/bot), respectively.\n> Thanks to the @openimsdk/openim team for all their hard work on this release.\n> Thank you to all the [💕developers and contributors](https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/graphs/contributors), people from all over the world, OpenIM brings us together\n> Contributions to this project are welcome! Please see [CONTRIBUTING.md](https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/blob/main/CONTRIBUTING.md) for details."
  },
  {
    "path": "scripts/template/project_README.md",
    "content": "# Project myproject\n\n<!-- Write one paragraph of this project description here -->\n\n## Features\n\n<!-- Tell others the features of this project -->\n\n## Getting Started\n\n### Prerequisites\n\n<!-- Describe packages, tools and everything we needed here -->\n\n### Building\n\n<!-- Describe how to build this project -->\n\n### Running\n\n<!-- Describe how to run this project -->\n\n## Using\n\n<!-- Place user documents here -->\n\n## Contributing\n\n<!-- Tell others how to contribute this project -->\n\n## Community(optional)\n\n<!-- Tell something about the community if needed -->\n\n## Authors\n\n<!-- Put authors here -->\n\n## License\n\n<!-- A link to license file -->\n"
  },
  {
    "path": "start-config.yml",
    "content": "serviceBinaries:\n  openim-api: 1\n  openim-crontask: 4\n  openim-rpc-user: 1\n  openim-msggateway: 1\n  openim-push: 8\n  openim-msgtransfer: 8\n  openim-rpc-conversation: 1\n  openim-rpc-auth: 1\n  openim-rpc-group: 1\n  openim-rpc-friend: 1\n  openim-rpc-msg: 1\n  openim-rpc-third: 1\ntoolBinaries:\n  - check-free-memory\n  - check-component\n  - seq\nmaxFileDescriptors: 10000\n"
  },
  {
    "path": "test/e2e/README.md",
    "content": "# OpenIM End-to-End (E2E) Testing Module\n\n## Overview\n\nThis repository contains the End-to-End (E2E) testing suite for OpenIM, a comprehensive instant messaging platform. The E2E tests are designed to simulate real-world usage scenarios to ensure that all components of the OpenIM system are functioning correctly in an integrated environment.\n\nThe tests cover various aspects of the system, including API endpoints, chat services, web interfaces, and RPC components, as well as performance and scalability under different load conditions.\n\n## Directory Structure\n\n```bash\n❯ tree e2e\ntest/e2e/\n├── conformance/             # Contains tests for verifying OpenIM API conformance\n├── framework/               # Provides auxiliary code and libraries for building and running E2E tests\n│   ├── config/              # Test configuration files and management\n│   ├── ginkgowrapper/       # Functions wrapping the testing library for handling test failures and skips\n│   └── helpers/             # Helper functions such as user creation, message sending, etc.\n├── api/                     # End-to-end tests for OpenIM API\n├── chat/                    # Tests for the business server (including login, registration, and other logic)\n├── web/                     # Tests for the web frontend (login, registration, message sending and receiving)\n├── rpc/                     # End-to-end tests for various RPC components\n│   ├── auth/                # Tests for the authentication service\n│   ├── conversation/        # Tests for conversation management\n│   ├── friend/              # Tests for friend relationship management\n│   ├── group/               # Tests for group management\n│   └── message/             # Tests for message handling\n├── scalability/             # Tests for the scalability of the OpenIM system\n├── performance/             # Performance tests such as load testing and stress testing\n└── upgrade/                 # Tests for compatibility and stability during OpenIM upgrades\n```\n\nThe E2E tests are organized into the following directory structure:\n\n- `conformance/`: Contains tests to verify the conformance of OpenIM API implementations.\n- `framework/`: Provides helper code for constructing and running E2E tests using the Ginkgo framework.\n  - `config/`: Manages test configurations and options.\n  - `ginkgowrapper/`: Wrappers for Ginkgo's `Fail` and `Skip` functions to handle structured data panics.\n  - `helpers/`: Utility functions for common test actions like user creation, message dispatching, etc.\n- `api/`: E2E tests for the OpenIM API endpoints.\n- `chat/`: Tests for the chat service, including authentication, session management, and messaging logic.\n- `web/`: Tests for the web interface, including user interactions and information exchange.\n- `rpc/`: E2E tests for each of the RPC components.\n  - `auth/`: Tests for the authentication service.\n  - `conversation/`: Tests for conversation management.\n  - `friend/`: Tests for friend relationship management.\n  - `group/`: Tests for group management.\n  - `message/`: Tests for message handling.\n- `scalability/`: Tests for the scalability of the OpenIM system.\n- `performance/`: Performance tests, including load and stress tests.\n- `upgrade/`: Tests for the upgrade process of OpenIM, ensuring compatibility and stability.\n\n## Prerequisites\n\nSince the deployment of OpenIM requires some components such as Mongo and Kafka, you should think a bit before using E2E tests\n\n```bash\ndocker compose up -d\n```\n\nOR User [kubernetes deployment](https://github.com/openimsdk/helm-charts)\n\nBefore running the E2E tests, ensure that you have the following prerequisites installed:\n\n- Docker\n- Kubernetes\n- Ginkgo test framework\n- Go (version 1.19 or higher)\n\n## Configuration\n\nTest configurations can be customized via the `config/` directory. The configuration files are in YAML format and allow you to set parameters such as API endpoints, user credentials, and test data.\n\n## Running the Tests\n\nTo run a single test or set of tests, you'll need the [Ginkgo](https://github.com/onsi/ginkgo) tool installed on your machine:\n\n```\nginkgo --help\n  --focus value\n    \tIf set, ginkgo will only run specs that match this regular expression. Can be specified multiple times, values are ORed.\n```\n\nTo run the entire suite of E2E tests, use the following command:\n\n```sh\nginkgo -v --randomizeAllSpecs --randomizeSuites --failOnPending --cover --trace --race --progress\n```\n\nYou can also run a specific test or group of tests by specifying the path to the test directory:\n\n```bash\nginkgo -v ./test/e2e/chat\n```\n\nOr you can use Makefile to run the tests:\n\n```bash\nmake test-e2e\n```\n\n## Test Development\n\nTo contribute to the E2E tests:\n\n1. Clone the repository and navigate to the `test/e2e/` directory.\n2. Create a new test file or modify an existing test to cover a new scenario.\n3. Write test cases using the Ginkgo BDD style, ensuring that they are clear and descriptive.\n4. Run the tests locally to ensure they pass.\n5. Submit a pull request with your changes.\n\nPlease refer to the `CONTRIBUTING.md` file for more detailed instructions on contributing to the test suite.\n\n\n## Reporting Issues\n\nIf you encounter any issues while running the E2E tests, please open an issue on the GitHub repository with the following information:\n\nOpen issue: https://github.com/openimsdk/open-im-server/issues/new/choose, choose \"Failing Test\" template.\n\n+ A clear and concise description of the issue.\n+ Steps to reproduce the behavior.\n+ Relevant logs and test output.\n+ Any other context that could be helpful in troubleshooting.\n\n\n## Continuous Integration (CI)\n\nThe E2E test suite is integrated with CI, which runs the tests automatically on each code commit. The results are reported back to the pull request or commit to provide immediate feedback on the impact of the changes.\n\n[![OpenIM Linux System E2E Test](https://github.com/openimsdk/open-im-server/actions/workflows/e2e-test.yml/badge.svg)](https://github.com/openimsdk/open-im-server/actions/workflows/e2e-test.yml)\n\n\n## Contact\n\nFor any queries or assistance, please reach out to the OpenIM development team at [support@openim.com](mailto:support@openim.com)."
  },
  {
    "path": "test/e2e/api/token/token.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage token\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// API endpoints and other constants.\nconst (\n\tAPIHost         = \"http://127.0.0.1:10002\"\n\tUserTokenURL    = APIHost + \"/auth/user_token\"\n\tUserRegisterURL = APIHost + \"/user/user_register\"\n\tSecretKey       = \"openIM123\"\n\tOperationID     = \"1646445464564\"\n)\n\n// UserTokenRequest represents a request to get a user token.\ntype UserTokenRequest struct {\n\tSecret     string `json:\"secret\"`\n\tPlatformID int    `json:\"platformID\"`\n\tUserID     string `json:\"userID\"`\n}\n\n// UserTokenResponse represents a response containing a user token.\ntype UserTokenResponse struct {\n\tToken   string `json:\"token\"`\n\tErrCode int    `json:\"errCode\"`\n}\n\n// User represents user data for registration.\ntype User struct {\n\tUserID   string `json:\"userID\"`\n\tNickname string `json:\"nickname\"`\n\tFaceURL  string `json:\"faceURL\"`\n}\n\n// UserRegisterRequest represents a request to register a user.\ntype UserRegisterRequest struct {\n\tUsers []User `json:\"users\"`\n}\n\n/* func main() {\n\t// Example usage of functions\n\ttoken, err := GetUserToken(\"openIM123456\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Error getting user token: %v\", err)\n\t}\n\tfmt.Println(\"Token:\", token)\n\n\terr = RegisterUser(token, \"testUserID\", \"TestNickname\", \"https://example.com/image.jpg\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Error registering user: %v\", err)\n\t}\n} */\n\n// GetUserToken requests a user token from the API.\nfunc GetUserToken(userID string) (string, error) {\n\treqBody := UserTokenRequest{\n\t\tSecret:     SecretKey,\n\t\tPlatformID: 1,\n\t\tUserID:     userID,\n\t}\n\treqBytes, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := http.Post(UserTokenURL, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar tokenResp UserTokenResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif tokenResp.ErrCode != 0 {\n\t\treturn \"\", fmt.Errorf(\"error in token response: %v\", tokenResp.ErrCode)\n\t}\n\n\treturn tokenResp.Token, nil\n}\n\n// RegisterUser registers a new user using the API.\nfunc RegisterUser(token, userID, nickname, faceURL string) error {\n\tuser := User{\n\t\tUserID:   userID,\n\t\tNickname: nickname,\n\t\tFaceURL:  faceURL,\n\t}\n\treqBody := UserRegisterRequest{\n\t\tUsers: []User{user},\n\t}\n\treqBytes, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient := &http.Client{}\n\treq, err := http.NewRequest(\"POST\", UserRegisterURL, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"operationID\", OperationID)\n\treq.Header.Add(\"token\", token)\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar respData map[string]any\n\tif err := json.Unmarshal(respBody, &respData); err != nil {\n\t\treturn err\n\t}\n\n\tif errCode, ok := respData[\"errCode\"].(float64); ok && errCode != 0 {\n\t\treturn fmt.Errorf(\"error in user registration response: %v\", respData)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "test/e2e/api/user/curd.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage user\n\nimport (\n\t\"fmt\"\n\n\tgettoken \"github.com/openimsdk/open-im-server/v3/test/e2e/api/token\"\n\t\"github.com/openimsdk/open-im-server/v3/test/e2e/framework/config\"\n)\n\n// UserInfoRequest represents a request to get or update user information.\ntype UserInfoRequest struct {\n\tUserIDs  []string       `json:\"userIDs,omitempty\"`\n\tUserInfo *gettoken.User `json:\"userInfo,omitempty\"`\n}\n\n// GetUsersOnlineStatusRequest represents a request to get users' online status.\ntype GetUsersOnlineStatusRequest struct {\n\tUserIDs []string `json:\"userIDs\"`\n}\n\n// GetUsersInfo retrieves detailed information for a list of user IDs.\nfunc GetUsersInfo(token string, userIDs []string) error {\n\n\turl := fmt.Sprintf(\"http://%s:%s/user/get_users_info\", config.LoadConfig().APIHost, config.LoadConfig().APIPort)\n\n\trequestBody := UserInfoRequest{\n\t\tUserIDs: userIDs,\n\t}\n\treturn sendPostRequestWithToken(url, token, requestBody)\n}\n\n// UpdateUserInfo updates the information for a user.\nfunc UpdateUserInfo(token, userID, nickname, faceURL string) error {\n\n\turl := fmt.Sprintf(\"http://%s:%s/user/update_user_info\", config.LoadConfig().APIHost, config.LoadConfig().APIPort)\n\n\trequestBody := UserInfoRequest{\n\t\tUserInfo: &gettoken.User{\n\t\t\tUserID:   userID,\n\t\t\tNickname: nickname,\n\t\t\tFaceURL:  faceURL,\n\t\t},\n\t}\n\treturn sendPostRequestWithToken(url, token, requestBody)\n}\n\n// GetUsersOnlineStatus retrieves the online status for a list of user IDs.\nfunc GetUsersOnlineStatus(token string, userIDs []string) error {\n\n\turl := fmt.Sprintf(\"http://%s:%s/user/get_users_online_status\", config.LoadConfig().APIHost, config.LoadConfig().APIPort)\n\n\trequestBody := GetUsersOnlineStatusRequest{\n\t\tUserIDs: userIDs,\n\t}\n\n\treturn sendPostRequestWithToken(url, token, requestBody)\n}\n"
  },
  {
    "path": "test/e2e/api/user/user.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage user\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\tgettoken \"github.com/openimsdk/open-im-server/v3/test/e2e/api/token\"\n\t\"github.com/openimsdk/open-im-server/v3/test/e2e/framework/config\"\n)\n\n// ForceLogoutRequest represents a request to force a user logout.\ntype ForceLogoutRequest struct {\n\tPlatformID int    `json:\"platformID\"`\n\tUserID     string `json:\"userID\"`\n}\n\n// CheckUserAccountRequest represents a request to check a user account.\ntype CheckUserAccountRequest struct {\n\tCheckUserIDs []string `json:\"checkUserIDs\"`\n}\n\n// GetUsersRequest represents a request to get a list of users.\ntype GetUsersRequest struct {\n\tPagination Pagination `json:\"pagination\"`\n}\n\n// Pagination specifies the page number and number of items per page.\ntype Pagination struct {\n\tPageNumber int `json:\"pageNumber\"`\n\tShowNumber int `json:\"showNumber\"`\n}\n\n// ForceLogout forces a user to log out.\nfunc ForceLogout(token, userID string, platformID int) error {\n\n\turl := fmt.Sprintf(\"http://%s:%s/auth/force_logout\", config.LoadConfig().APIHost, config.LoadConfig().APIPort)\n\n\trequestBody := ForceLogoutRequest{\n\t\tPlatformID: platformID,\n\t\tUserID:     userID,\n\t}\n\treturn sendPostRequestWithToken(url, token, requestBody)\n}\n\n// CheckUserAccount checks if the user accounts exist.\nfunc CheckUserAccount(token string, userIDs []string) error {\n\n\turl := fmt.Sprintf(\"http://%s:%s/user/account_check\", config.LoadConfig().APIHost, config.LoadConfig().APIPort)\n\n\trequestBody := CheckUserAccountRequest{\n\t\tCheckUserIDs: userIDs,\n\t}\n\treturn sendPostRequestWithToken(url, token, requestBody)\n}\n\n// GetUsers retrieves a list of users with pagination.\nfunc GetUsers(token string, pageNumber, showNumber int) error {\n\n\turl := fmt.Sprintf(\"http://%s:%s/user/account_check\", config.LoadConfig().APIHost, config.LoadConfig().APIPort)\n\n\trequestBody := GetUsersRequest{\n\t\tPagination: Pagination{\n\t\t\tPageNumber: pageNumber,\n\t\t\tShowNumber: showNumber,\n\t\t},\n\t}\n\treturn sendPostRequestWithToken(url, token, requestBody)\n}\n\n// sendPostRequestWithToken sends a POST request with a token in the header.\nfunc sendPostRequestWithToken(url, token string, body any) error {\n\treqBytes, err := json.Marshal(body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient := &http.Client{}\n\treq, err := http.NewRequest(\"POST\", url, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"operationID\", gettoken.OperationID)\n\treq.Header.Add(\"token\", token)\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar respData map[string]any\n\tif err := json.Unmarshal(respBody, &respData); err != nil {\n\t\treturn err\n\t}\n\n\tif errCode, ok := respData[\"errCode\"].(float64); ok && errCode != 0 {\n\t\treturn fmt.Errorf(\"error in response: %v\", respData)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "test/e2e/conformance/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/e2e.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage e2e\n\nimport (\n\t\"testing\"\n\n\tgettoken \"github.com/openimsdk/open-im-server/v3/test/e2e/api/token\"\n\t\"github.com/openimsdk/open-im-server/v3/test/e2e/api/user\"\n)\n\n// RunE2ETests checks configuration parameters (specified through flags) and then runs\n// E2E tests using the Ginkgo runner.\n// If a \"report directory\" is specified, one or more JUnit test reports will be\n// generated in this directory, and cluster logs will also be saved.\n// This function is called on each Ginkgo node in parallel mode.\nfunc RunE2ETests(t *testing.T) {\n\n\t// Example usage of new functions\n\ttoken, _ := gettoken.GetUserToken(\"openIM123456\")\n\n\t// Example of getting user info\n\t_ = user.GetUsersInfo(token, []string{\"user1\", \"user2\"})\n\n\t// Example of updating user info\n\t_ = user.UpdateUserInfo(token, \"user1\", \"NewNickname\", \"https://github.com/openimsdk/open-im-server/blob/main/assets/logo/openim-logo.png\")\n\n\t// Example of getting users' online status\n\t_ = user.GetUsersOnlineStatus(token, []string{\"user1\", \"user2\"})\n\n\t// Example of forcing a logout\n\t_ = user.ForceLogout(token, \"4950983283\", 2)\n\n\t// Example of checking user account\n\t_ = user.CheckUserAccount(token, []string{\"openIM123456\", \"anotherUserID\"})\n\n\t// Example of getting users\n\t_ = user.GetUsers(token, 1, 100)\n}\n"
  },
  {
    "path": "test/e2e/e2e_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage e2e\n\nimport (\n\t\"flag\"\n\t\"testing\"\n\n\t\"github.com/openimsdk/open-im-server/v3/test/e2e/framework/config\"\n)\n\n// handleFlags sets up all flags and parses the command line.\nfunc handleFlags() {\n\tconfig.CopyFlags(config.Flags, flag.CommandLine)\n\tflag.Parse()\n}\n\nfunc TestMain(m *testing.M) {\n\thandleFlags()\n\tm.Run()\n}\n\nfunc TestE2E(t *testing.T) {\n\tRunE2ETests(t)\n}\n"
  },
  {
    "path": "test/e2e/framework/config/config.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage config\n\nimport (\n\t\"flag\"\n\t\"os\"\n)\n\n// Flags is the flag set that AddOptions adds to. Test authors should\n// also use it instead of directly adding to the global command line.\nvar Flags = flag.NewFlagSet(\"\", flag.ContinueOnError)\n\n// CopyFlags ensures that all flags that are defined in the source flag\n// set appear in the target flag set as if they had been defined there\n// directly. From the flag package it inherits the behavior that there\n// is a panic if the target already contains a flag from the source.\nfunc CopyFlags(source *flag.FlagSet, target *flag.FlagSet) {\n\tsource.VisitAll(func(flag *flag.Flag) {\n\t\t// We don't need to copy flag.DefValue. The original\n\t\t// default (from, say, flag.String) was stored in\n\t\t// the value and gets extracted by Var for the help\n\t\t// message.\n\t\ttarget.Var(flag.Value, flag.Name, flag.Usage)\n\t})\n}\n\n// Config defines the configuration structure for the OpenIM components.\ntype Config struct {\n\tAPIHost             string\n\tAPIPort             string\n\tMsgGatewayHost      string\n\tMsgTransferHost     string\n\tPushHost            string\n\tRPCAuthHost         string\n\tRPCConversationHost string\n\tRPCFriendHost       string\n\tRPCGroupHost        string\n\tRPCMsgHost          string\n\tRPCThirdHost        string\n\tRPCUserHost         string\n\t// Add other configuration fields as needed\n}\n\n// LoadConfig loads the configurations from environment variables or default values.\nfunc LoadConfig() *Config {\n\treturn &Config{\n\t\tAPIHost: getEnv(\"OPENIM_API_HOST\", \"127.0.0.1\"),\n\t\tAPIPort: getEnv(\"API_OPENIM_PORT\", \"10002\"),\n\n\t\t// TODO: Set default variable\n\t\tMsgGatewayHost:      getEnv(\"OPENIM_MSGGATEWAY_HOST\", \"default-msggateway-host\"),\n\t\tMsgTransferHost:     getEnv(\"OPENIM_MSGTRANSFER_HOST\", \"default-msgtransfer-host\"),\n\t\tPushHost:            getEnv(\"OPENIM_PUSH_HOST\", \"default-push-host\"),\n\t\tRPCAuthHost:         getEnv(\"OPENIM_RPC_AUTH_HOST\", \"default-rpc-auth-host\"),\n\t\tRPCConversationHost: getEnv(\"OPENIM_RPC_CONVERSATION_HOST\", \"default-rpc-conversation-host\"),\n\t\tRPCFriendHost:       getEnv(\"OPENIM_RPC_FRIEND_HOST\", \"default-rpc-friend-host\"),\n\t\tRPCGroupHost:        getEnv(\"OPENIM_RPC_GROUP_HOST\", \"default-rpc-group-host\"),\n\t\tRPCMsgHost:          getEnv(\"OPENIM_RPC_MSG_HOST\", \"default-rpc-msg-host\"),\n\t\tRPCThirdHost:        getEnv(\"OPENIM_RPC_THIRD_HOST\", \"default-rpc-third-host\"),\n\t\tRPCUserHost:         getEnv(\"OPENIM_RPC_USER_HOST\", \"default-rpc-user-host\"),\n\t}\n}\n\n// getEnv is a helper function to read an environment variable or return a default value.\nfunc getEnv(key, defaultValue string) string {\n\tvalue, exists := os.LookupEnv(key)\n\tif !exists {\n\t\treturn defaultValue\n\t}\n\treturn value\n}\n"
  },
  {
    "path": "test/e2e/framework/config/config_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage config\n\nimport (\n\t\"flag\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestCopyFlags(t *testing.T) {\n\ttype args struct {\n\t\tsource *flag.FlagSet\n\t\ttarget *flag.FlagSet\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Copy empty source to empty target\",\n\t\t\targs: args{\n\t\t\t\tsource: flag.NewFlagSet(\"source\", flag.ContinueOnError),\n\t\t\t\ttarget: flag.NewFlagSet(\"target\", flag.ContinueOnError),\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Copy non-empty source to empty target\",\n\t\t\targs: args{\n\t\t\t\tsource: func() *flag.FlagSet {\n\t\t\t\t\tfs := flag.NewFlagSet(\"source\", flag.ContinueOnError)\n\t\t\t\t\tfs.String(\"test-flag\", \"default\", \"test usage\")\n\t\t\t\t\treturn fs\n\t\t\t\t}(),\n\t\t\t\ttarget: flag.NewFlagSet(\"target\", flag.ContinueOnError),\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Copy source to target with existing flag\",\n\t\t\targs: args{\n\t\t\t\tsource: func() *flag.FlagSet {\n\t\t\t\t\tfs := flag.NewFlagSet(\"source\", flag.ContinueOnError)\n\t\t\t\t\tfs.String(\"test-flag\", \"default\", \"test usage\")\n\t\t\t\t\treturn fs\n\t\t\t\t}(),\n\t\t\t\ttarget: func() *flag.FlagSet {\n\t\t\t\t\tfs := flag.NewFlagSet(\"target\", flag.ContinueOnError)\n\t\t\t\t\tfs.String(\"test-flag\", \"default\", \"test usage\")\n\t\t\t\t\treturn fs\n\t\t\t\t}(),\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); (r != nil) != tt.wantErr {\n\t\t\t\t\tt.Errorf(\"CopyFlags() panic = %v, wantErr %v\", r, tt.wantErr)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tCopyFlags(tt.args.source, tt.args.target)\n\n\t\t\t// Verify the replicated tag\n\t\t\tif !tt.wantErr {\n\t\t\t\ttt.args.source.VisitAll(func(f *flag.Flag) {\n\t\t\t\t\tif gotFlag := tt.args.target.Lookup(f.Name); gotFlag == nil || !reflect.DeepEqual(gotFlag, f) {\n\t\t\t\t\t\tt.Errorf(\"CopyFlags() failed to copy flag %s\", f.Name)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/e2e/framework/ginkgowrapper/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/framework/ginkgowrapper/ginkgowrapper.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage ginkgowrapper\n"
  },
  {
    "path": "test/e2e/framework/ginkgowrapper/ginkgowrapper_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage ginkgowrapper\n"
  },
  {
    "path": "test/e2e/framework/helpers/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/framework/helpers/chat/chat.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n)\n\nvar (\n\t// The default template version.\n\tdefaultTemplateVersion = \"v1.3.0\"\n)\n\nfunc main() {\n\t// Define the URL to get the latest version\n\t// latestVersionURL := \"https://github.com/openimsdk/chat/releases/latest\"\n\t// latestVersion, err := getLatestVersion(latestVersionURL)\n\t// if err != nil {\n\t// \tfmt.Printf(\"Failed to get the latest version: %v\\n\", err)\n\t// \treturn\n\t// }\n\tlatestVersion := defaultTemplateVersion\n\n\t// getLatestVersion\n\n\t// Construct the download URL\n\tdownloadURL := fmt.Sprintf(\"https://github.com/openimsdk/chat/releases/download/%s/chat_Linux_x86_64.tar.gz\", latestVersion)\n\n\t// Set the installation directory\n\tinstallDir := \"/tmp/chat\"\n\n\t// Clear the installation directory before proceeding\n\terr := os.RemoveAll(installDir)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to clear installation directory: %v\\n\", err)\n\t\treturn\n\t}\n\n\t// Create the installation directory\n\terr = os.MkdirAll(installDir, 0755)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to create installation directory: %v\\n\", err)\n\t\treturn\n\t}\n\n\t// Download and extract OpenIM Chat to the installation directory\n\terr = downloadAndExtract(downloadURL, installDir)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to download and extract OpenIM Chat: %v\\n\", err)\n\t\treturn\n\t}\n\n\t// Create configuration file directory\n\tconfigDir := filepath.Join(installDir, \"config\")\n\terr = os.MkdirAll(configDir, 0755)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to create configuration directory: %v\\n\", err)\n\t\treturn\n\t}\n\n\t// Download configuration files\n\tconfigURL := \"https://raw.githubusercontent.com/openimsdk/chat/main/config/config.yaml\"\n\terr = downloadAndExtract(configURL, configDir)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to download and extract configuration files: %v\\n\", err)\n\t\treturn\n\t}\n\n\t// Define the processes to be started\n\tcmds := []string{\n\t\t\"admin-api\",\n\t\t\"admin-rpc\",\n\t\t\"chat-api\",\n\t\t\"chat-rpc\",\n\t}\n\n\t// Start each process in a new goroutine\n\tfor _, cmd := range cmds {\n\t\tgo startProcess(filepath.Join(installDir, cmd))\n\t}\n\n\t// Block the main thread indefinitely\n\tselect {}\n}\n\n/* func getLatestVersion(url string) (string, error) {\n\tresp, err := webhook.Get(url)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n// \tlocation := resp.Header.Get(\"Location\")\n// \tif location == \"\" {\n// \t\treturn defaultTemplateVersion, nil\n// \t}\n\n\t// Extract the version number from the URL\n\tlatestVersion := filepath.Base(location)\n\treturn latestVersion, nil\n} */\n\n// downloadAndExtract downloads a file from a URL and extracts it to a destination directory.\nfunc downloadAndExtract(url, destDir string) error {\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"error downloading file, HTTP status code: %d\", resp.StatusCode)\n\t}\n\n\t// Create the destination directory\n\terr = os.MkdirAll(destDir, 0755)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Define the path for the downloaded file\n\tfilePath := filepath.Join(destDir, \"downloaded_file.tar.gz\")\n\tfile, err := os.Create(filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\t// Copy the downloaded file\n\t_, err = io.Copy(file, resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Extract the file\n\tcmd := exec.Command(\"tar\", \"xzvf\", filePath, \"-C\", destDir)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Run()\n}\n\n// startProcess starts a process and prints any errors encountered.\nfunc startProcess(cmdPath string) {\n\tcmd := exec.Command(cmdPath)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\tfmt.Printf(\"Failed to start process %s: %v\\n\", cmdPath, err)\n\t}\n}\n"
  },
  {
    "path": "test/e2e/page/chat_page.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage page\n"
  },
  {
    "path": "test/e2e/page/login_page.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage page\n"
  },
  {
    "path": "test/e2e/performance/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/rpc/auth/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/rpc/conversation/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/rpc/friend/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/rpc/group/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/rpc/message/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/scalability/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/upgrade/.keep",
    "content": ".keep"
  },
  {
    "path": "test/e2e/web/Readme.md",
    "content": "# OpenIM Web E2E\n\n"
  },
  {
    "path": "test/jwt/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/golang-jwt/jwt/v4\"\n)\n\nfunc main() {\n\trawJWT := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOiI4MjkzODEzMTgzIiwiUGxhdGZvcm1JRCI6NSwiZXhwIjoxNzA2NTk0MTU0LCJuYmYiOjE2OTg4MTc4NTQsImlhdCI6MTY5ODgxODE1NH0.QCJHzU07SC6iYBoFO6Zsm61TNDor2D89I4E3zg8HHHU`\n\n\t// Verify the token\n\tclaims := &jwt.MapClaims{}\n\tparsedT, err := jwt.ParseWithClaims(rawJWT, claims, func(token *jwt.Token) (any, error) {\n\t\t// Validate the alg is HMAC signature\n\t\tif _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\n\t\t\treturn nil, fmt.Errorf(\"unexpected signing method: %v\", token.Header[\"alg\"])\n\t\t}\n\n\t\tif kid, ok := token.Header[\"kid\"].(string); ok {\n\t\t\tfmt.Println(\"kid\", kid)\n\t\t}\n\n\t\treturn []byte(\"key1\"), nil\n\t})\n\n\tif err != nil || !parsedT.Valid {\n\t\tfmt.Println(\"token valid failed\", err)\n\n\t\treturn\n\t}\n\n\tfmt.Println(\"ok\")\n}\n"
  },
  {
    "path": "test/readme",
    "content": "## Run the Tests\n\nread: [Test Docs](./docs/contrib/test.md)\n\nTo run a single test or set of tests, you'll need the [Ginkgo](https://github.com/onsi/ginkgo) tool installed on your\nmachine:\n\n```console\ngo install github.com/onsi/ginkgo/ginkgo@latest\n```\n\n```shell\nginkgo --help\n  --focus value\n    \tIf set, ginkgo will only run specs that match this regular expression. Can be specified multiple times, values are ORed.\n\n```\n"
  },
  {
    "path": "test/stress-test/README.md",
    "content": "# Stress Test\n\n## Usage\n\nYou need set `TestTargetUserList` and `DefaultGroupID` variables.\n\n### Build\n\n```bash\n\ngo build -o test/stress-test/stress-test test/stress-test/main.go\n```\n\n### Excute\n\n```bash\n\ntools/stress-test/stress-test -c config/\n```\n"
  },
  {
    "path": "test/stress-test/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/relation\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\tpbuser \"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\n/*\n 1. Create one user every minute\n 2. Import target users as friends\n 3. Add users to the default group\n 4. Send a message to the default group every second, containing index and current timestamp\n 5. Create a new group every minute and invite target users to join\n*/\n\n// !!! ATTENTION: This variable is must be added!\nvar (\n\t//  Use default userIDs List for testing, need to be created.\n\tTestTargetUserList = []string{\n\t\t\"<need-update-it>\",\n\t}\n\tDefaultGroupID = \"<need-update-it>\" // Use default group ID for testing, need to be created.\n)\n\nvar (\n\tApiAddress string\n\n\t// API method\n\tGetAdminToken = \"/auth/get_admin_token\"\n\tCreateUser    = \"/user/user_register\"\n\tImportFriend  = \"/friend/import_friend\"\n\tInviteToGroup = \"/group/invite_user_to_group\"\n\tSendMsg       = \"/msg/send_msg\"\n\tCreateGroup   = \"/group/create_group\"\n\tGetUserToken  = \"/auth/user_token\"\n)\n\nconst (\n\tMaxUser  = 10000\n\tMaxGroup = 1000\n\n\tCreateUserTicker  = 1 * time.Minute // Ticker is 1min in create user\n\tSendMessageTicker = 1 * time.Second // Ticker is 1s in send message\n\tCreateGroupTicker = 1 * time.Minute\n)\n\ntype BaseResp struct {\n\tErrCode int             `json:\"errCode\"`\n\tErrMsg  string          `json:\"errMsg\"`\n\tData    json.RawMessage `json:\"data\"`\n}\n\ntype StressTest struct {\n\tConf           *conf\n\tAdminUserID    string\n\tAdminToken     string\n\tDefaultGroupID string\n\tDefaultUserID  string\n\tUserCounter    int\n\tGroupCounter   int\n\tMsgCounter     int\n\tCreatedUsers   []string\n\tCreatedGroups  []string\n\tMutex          sync.Mutex\n\tCtx            context.Context\n\tCancel         context.CancelFunc\n\tHttpClient     *http.Client\n\tWg             sync.WaitGroup\n\tOnce           sync.Once\n}\n\ntype conf struct {\n\tShare config.Share\n\tApi   config.API\n}\n\nfunc initConfig(configDir string) (*config.Share, *config.API, error) {\n\tvar (\n\t\tshare     = &config.Share{}\n\t\tapiConfig = &config.API{}\n\t)\n\n\terr := config.Load(configDir, config.ShareFileName, config.EnvPrefixMap[config.ShareFileName], share)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\terr = config.Load(configDir, config.OpenIMAPICfgFileName, config.EnvPrefixMap[config.OpenIMAPICfgFileName], apiConfig)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn share, apiConfig, nil\n}\n\n// Post Request\nfunc (st *StressTest) PostRequest(ctx context.Context, url string, reqbody any) ([]byte, error) {\n\t// Marshal body\n\tjsonBody, err := json.Marshal(reqbody)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to marshal request body\", err, \"url\", url, \"reqbody\", reqbody)\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"operationID\", st.AdminUserID)\n\tif st.AdminToken != \"\" {\n\t\treq.Header.Set(\"token\", st.AdminToken)\n\t}\n\n\t// log.ZInfo(ctx, \"Header info is \", \"Content-Type\", \"application/json\", \"operationID\", st.AdminUserID, \"token\", st.AdminToken)\n\n\tresp, err := st.HttpClient.Do(req)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to send request\", err, \"url\", url, \"reqbody\", reqbody)\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to read response body\", err, \"url\", url)\n\t\treturn nil, err\n\t}\n\n\tvar baseResp BaseResp\n\tif err := json.Unmarshal(respBody, &baseResp); err != nil {\n\t\tlog.ZError(ctx, \"Failed to unmarshal response body\", err, \"url\", url, \"respBody\", string(respBody))\n\t\treturn nil, err\n\t}\n\n\tif baseResp.ErrCode != 0 {\n\t\terr = fmt.Errorf(baseResp.ErrMsg)\n\t\tlog.ZError(ctx, \"Failed to send request\", err, \"url\", url, \"reqbody\", reqbody, \"resp\", baseResp)\n\t\treturn nil, err\n\t}\n\n\treturn baseResp.Data, nil\n}\n\nfunc (st *StressTest) GetAdminToken(ctx context.Context) (string, error) {\n\treq := auth.GetAdminTokenReq{\n\t\tSecret: st.Conf.Share.Secret,\n\t\tUserID: st.AdminUserID,\n\t}\n\n\tresp, err := st.PostRequest(ctx, ApiAddress+GetAdminToken, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdata := &auth.GetAdminTokenResp{}\n\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn data.Token, nil\n}\n\nfunc (st *StressTest) CreateUser(ctx context.Context, userID string) (string, error) {\n\tuser := &sdkws.UserInfo{\n\t\tUserID:   userID,\n\t\tNickname: userID,\n\t}\n\n\treq := pbuser.UserRegisterReq{\n\t\tUsers: []*sdkws.UserInfo{user},\n\t}\n\n\t_, err := st.PostRequest(ctx, ApiAddress+CreateUser, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tst.UserCounter++\n\treturn userID, nil\n}\n\nfunc (st *StressTest) ImportFriend(ctx context.Context, userID string) error {\n\treq := relation.ImportFriendReq{\n\t\tOwnerUserID:   userID,\n\t\tFriendUserIDs: TestTargetUserList,\n\t}\n\n\t_, err := st.PostRequest(ctx, ApiAddress+ImportFriend, &req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (st *StressTest) InviteToGroup(ctx context.Context, userID string) error {\n\treq := group.InviteUserToGroupReq{\n\t\tGroupID:        st.DefaultGroupID,\n\t\tInvitedUserIDs: []string{userID},\n\t}\n\t_, err := st.PostRequest(ctx, ApiAddress+InviteToGroup, &req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (st *StressTest) SendMsg(ctx context.Context, userID string) error {\n\tcontentObj := map[string]any{\n\t\t\"content\": fmt.Sprintf(\"index %d. The current time is %s\", st.MsgCounter, time.Now().Format(\"2006-01-02 15:04:05.000\")),\n\t}\n\n\treq := &apistruct.SendMsgReq{\n\t\tSendMsg: apistruct.SendMsg{\n\t\t\tSendID:         userID,\n\t\t\tSenderNickname: userID,\n\t\t\tGroupID:        st.DefaultGroupID,\n\t\t\tContentType:    constant.Text,\n\t\t\tSessionType:    constant.ReadGroupChatType,\n\t\t\tContent:        contentObj,\n\t\t},\n\t}\n\n\t_, err := st.PostRequest(ctx, ApiAddress+SendMsg, &req)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to send message\", err, \"userID\", userID, \"req\", &req)\n\t\treturn err\n\t}\n\n\tst.MsgCounter++\n\n\treturn nil\n}\n\nfunc (st *StressTest) CreateGroup(ctx context.Context, userID string) (string, error) {\n\tgroupID := fmt.Sprintf(\"StressTestGroup_%d_%s\", st.GroupCounter, time.Now().Format(\"20060102150405\"))\n\n\tgroupInfo := &sdkws.GroupInfo{\n\t\tGroupID:   groupID,\n\t\tGroupName: groupID,\n\t\tGroupType: constant.WorkingGroup,\n\t}\n\n\treq := group.CreateGroupReq{\n\t\tOwnerUserID:   userID,\n\t\tMemberUserIDs: TestTargetUserList,\n\t\tGroupInfo:     groupInfo,\n\t}\n\n\tresp := group.CreateGroupResp{}\n\n\tresponse, err := st.PostRequest(ctx, ApiAddress+CreateGroup, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := json.Unmarshal(response, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tst.GroupCounter++\n\n\treturn resp.GroupInfo.GroupID, nil\n}\n\nfunc main() {\n\tvar configPath string\n\t// defaultConfigDir := filepath.Join(\"..\", \"..\", \"..\", \"..\", \"..\", \"config\")\n\t// flag.StringVar(&configPath, \"c\", defaultConfigDir, \"config path\")\n\tflag.StringVar(&configPath, \"c\", \"\", \"config path\")\n\tflag.Parse()\n\n\tif configPath == \"\" {\n\t\t_, _ = fmt.Fprintln(os.Stderr, \"config path is empty\")\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\n\tfmt.Printf(\" Config Path: %s\\n\", configPath)\n\n\tshare, apiConfig, err := initConfig(configPath)\n\tif err != nil {\n\t\tprogram.ExitWithError(err)\n\t\treturn\n\t}\n\n\tApiAddress = fmt.Sprintf(\"http://%s:%s\", \"127.0.0.1\", fmt.Sprint(apiConfig.Api.Ports[0]))\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tch := make(chan struct{})\n\n\tdefer cancel()\n\n\tst := &StressTest{\n\t\tConf: &conf{\n\t\t\tShare: *share,\n\t\t\tApi:   *apiConfig,\n\t\t},\n\t\tAdminUserID: share.IMAdminUser.UserIDs[0],\n\t\tCtx:         ctx,\n\t\tCancel:      cancel,\n\t\tHttpClient: &http.Client{\n\t\t\tTimeout: 50 * time.Second,\n\t\t},\n\t}\n\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt, syscall.SIGTERM)\n\tgo func() {\n\t\t<-c\n\t\tfmt.Println(\"\\nReceived stop signal, stopping...\")\n\n\t\tselect {\n\t\tcase <-ch:\n\t\tdefault:\n\t\t\tclose(ch)\n\t\t}\n\n\t\tst.Cancel()\n\t}()\n\n\ttoken, err := st.GetAdminToken(st.Ctx)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Get Admin Token failed.\", err, \"AdminUserID\", st.AdminUserID)\n\t}\n\n\tst.AdminToken = token\n\tfmt.Println(\"Admin Token:\", st.AdminToken)\n\tfmt.Println(\"ApiAddress:\", ApiAddress)\n\n\tst.DefaultGroupID = DefaultGroupID\n\n\tst.Wg.Add(1)\n\tgo func() {\n\t\tdefer st.Wg.Done()\n\n\t\tticker := time.NewTicker(CreateUserTicker)\n\t\tdefer ticker.Stop()\n\n\t\tfor st.UserCounter < MaxUser {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Create user\", \"reason\", \"context done\")\n\t\t\t\treturn\n\n\t\t\tcase <-ticker.C:\n\t\t\t\t// Create User\n\t\t\t\tuserID := fmt.Sprintf(\"%d_Stresstest_%s\", st.UserCounter, time.Now().Format(\"0102150405\"))\n\n\t\t\t\tuserCreatedID, err := st.CreateUser(st.Ctx, userID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.ZError(st.Ctx, \"Create User failed.\", err, \"UserID\", userID)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// fmt.Println(\"User Created ID:\", userCreatedID)\n\n\t\t\t\t// Import Friend\n\t\t\t\tif err = st.ImportFriend(st.Ctx, userCreatedID); err != nil {\n\t\t\t\t\tlog.ZError(st.Ctx, \"Import Friend failed.\", err, \"UserID\", userCreatedID)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Invite To Group\n\t\t\t\tif err = st.InviteToGroup(st.Ctx, userCreatedID); err != nil {\n\t\t\t\t\tlog.ZError(st.Ctx, \"Invite To Group failed.\", err, \"UserID\", userCreatedID)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tst.Once.Do(func() {\n\t\t\t\t\tst.DefaultUserID = userCreatedID\n\t\t\t\t\tfmt.Println(\"Default Send User Created ID:\", userCreatedID)\n\t\t\t\t\tclose(ch)\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}()\n\n\tst.Wg.Add(1)\n\tgo func() {\n\t\tdefer st.Wg.Done()\n\n\t\tticker := time.NewTicker(SendMessageTicker)\n\t\tdefer ticker.Stop()\n\t\t<-ch\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Send message\", \"reason\", \"context done\")\n\t\t\t\treturn\n\n\t\t\tcase <-ticker.C:\n\t\t\t\t// Send Message\n\t\t\t\tif err = st.SendMsg(st.Ctx, st.DefaultUserID); err != nil {\n\t\t\t\t\tlog.ZError(st.Ctx, \"Send Message failed.\", err, \"UserID\", st.DefaultUserID)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\tst.Wg.Add(1)\n\tgo func() {\n\t\tdefer st.Wg.Done()\n\n\t\tticker := time.NewTicker(CreateGroupTicker)\n\t\tdefer ticker.Stop()\n\t\t<-ch\n\n\t\tfor st.GroupCounter < MaxGroup {\n\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Create Group\", \"reason\", \"context done\")\n\t\t\t\treturn\n\n\t\t\tcase <-ticker.C:\n\n\t\t\t\t// Create Group\n\t\t\t\t_, err := st.CreateGroup(st.Ctx, st.DefaultUserID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.ZError(st.Ctx, \"Create Group failed.\", err, \"UserID\", st.DefaultUserID)\n\t\t\t\t\tos.Exit(1)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// fmt.Println(\"Group Created ID:\", groupID)\n\t\t\t}\n\t\t}\n\t}()\n\n\tst.Wg.Wait()\n}\n"
  },
  {
    "path": "test/stress-test-v2/README.md",
    "content": "# Stress Test V2\n\n## Usage\n\nYou need set `TestTargetUserList` variables.\n\n### Build\n\n```bash\n\ngo build -o test/stress-test-v2/stress-test-v2 test/stress-test-v2/main.go\n```\n\n### Excute\n\n```bash\n\ntools/stress-test-v2/stress-test-v2 -c config/\n```\n"
  },
  {
    "path": "test/stress-test-v2/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\tpbuser \"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\n// 1. Create 100K New Users\n// 2. Create 100 100K Groups\n// 3. Create 1000 999 Groups\n// 4. Send message to 100K Groups every second\n// 5. Send message to 999 Groups every minute\n\nvar (\n\t//  Use default userIDs List for testing, need to be created.\n\tTestTargetUserList = []string{\n\t\t// \"<need-update-it>\",\n\t}\n\t// DefaultGroupID = \"<need-update-it>\" // Use default group ID for testing, need to be created.\n)\n\nvar (\n\tApiAddress string\n\n\t// API method\n\tGetAdminToken      = \"/auth/get_admin_token\"\n\tUserCheck          = \"/user/account_check\"\n\tCreateUser         = \"/user/user_register\"\n\tImportFriend       = \"/friend/import_friend\"\n\tInviteToGroup      = \"/group/invite_user_to_group\"\n\tGetGroupMemberInfo = \"/group/get_group_members_info\"\n\tSendMsg            = \"/msg/send_msg\"\n\tCreateGroup        = \"/group/create_group\"\n\tGetUserToken       = \"/auth/user_token\"\n)\n\nconst (\n\tMaxUser            = 100000\n\tMax1kUser          = 1000\n\tMax100KGroup       = 100\n\tMax999Group        = 1000\n\tMaxInviteUserLimit = 999\n\n\tCreateUserTicker         = 1 * time.Second\n\tCreateGroupTicker        = 1 * time.Second\n\tCreate100KGroupTicker    = 1 * time.Second\n\tCreate999GroupTicker     = 1 * time.Second\n\tSendMsgTo100KGroupTicker = 1 * time.Second\n\tSendMsgTo999GroupTicker  = 1 * time.Minute\n)\n\ntype BaseResp struct {\n\tErrCode int             `json:\"errCode\"`\n\tErrMsg  string          `json:\"errMsg\"`\n\tData    json.RawMessage `json:\"data\"`\n}\n\ntype StressTest struct {\n\tConf                   *conf\n\tAdminUserID            string\n\tAdminToken             string\n\tDefaultGroupID         string\n\tDefaultUserID          string\n\tUserCounter            int\n\tCreateUserCounter      int\n\tCreate100kGroupCounter int\n\tCreate999GroupCounter  int\n\tMsgCounter             int\n\tCreatedUsers           []string\n\tCreatedGroups          []string\n\tMutex                  sync.Mutex\n\tCtx                    context.Context\n\tCancel                 context.CancelFunc\n\tHttpClient             *http.Client\n\tWg                     sync.WaitGroup\n\tOnce                   sync.Once\n}\n\ntype conf struct {\n\tShare config.Share\n\tApi   config.API\n}\n\nfunc initConfig(configDir string) (*config.Share, *config.API, error) {\n\tvar (\n\t\tshare     = &config.Share{}\n\t\tapiConfig = &config.API{}\n\t)\n\n\terr := config.Load(configDir, config.ShareFileName, config.EnvPrefixMap[config.ShareFileName], share)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\terr = config.Load(configDir, config.OpenIMAPICfgFileName, config.EnvPrefixMap[config.OpenIMAPICfgFileName], apiConfig)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn share, apiConfig, nil\n}\n\n// Post Request\nfunc (st *StressTest) PostRequest(ctx context.Context, url string, reqbody any) ([]byte, error) {\n\t// Marshal body\n\tjsonBody, err := json.Marshal(reqbody)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to marshal request body\", err, \"url\", url, \"reqbody\", reqbody)\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"operationID\", st.AdminUserID)\n\tif st.AdminToken != \"\" {\n\t\treq.Header.Set(\"token\", st.AdminToken)\n\t}\n\n\t// log.ZInfo(ctx, \"Header info is \", \"Content-Type\", \"application/json\", \"operationID\", st.AdminUserID, \"token\", st.AdminToken)\n\n\tresp, err := st.HttpClient.Do(req)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to send request\", err, \"url\", url, \"reqbody\", reqbody)\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to read response body\", err, \"url\", url)\n\t\treturn nil, err\n\t}\n\n\tvar baseResp BaseResp\n\tif err := json.Unmarshal(respBody, &baseResp); err != nil {\n\t\tlog.ZError(ctx, \"Failed to unmarshal response body\", err, \"url\", url, \"respBody\", string(respBody))\n\t\treturn nil, err\n\t}\n\n\tif baseResp.ErrCode != 0 {\n\t\terr = fmt.Errorf(baseResp.ErrMsg)\n\t\t// log.ZError(ctx, \"Failed to send request\", err, \"url\", url, \"reqbody\", reqbody, \"resp\", baseResp)\n\t\treturn nil, err\n\t}\n\n\treturn baseResp.Data, nil\n}\n\nfunc (st *StressTest) GetAdminToken(ctx context.Context) (string, error) {\n\treq := auth.GetAdminTokenReq{\n\t\tSecret: st.Conf.Share.Secret,\n\t\tUserID: st.AdminUserID,\n\t}\n\n\tresp, err := st.PostRequest(ctx, ApiAddress+GetAdminToken, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdata := &auth.GetAdminTokenResp{}\n\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn data.Token, nil\n}\n\nfunc (st *StressTest) CheckUser(ctx context.Context, userIDs []string) ([]string, error) {\n\treq := pbuser.AccountCheckReq{\n\t\tCheckUserIDs: userIDs,\n\t}\n\n\tresp, err := st.PostRequest(ctx, ApiAddress+UserCheck, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata := &pbuser.AccountCheckResp{}\n\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\treturn nil, err\n\t}\n\n\tunRegisteredUserIDs := make([]string, 0)\n\n\tfor _, res := range data.Results {\n\t\tif res.AccountStatus == constant.UnRegistered {\n\t\t\tunRegisteredUserIDs = append(unRegisteredUserIDs, res.UserID)\n\t\t}\n\t}\n\n\treturn unRegisteredUserIDs, nil\n}\n\nfunc (st *StressTest) CreateUser(ctx context.Context, userID string) (string, error) {\n\tuser := &sdkws.UserInfo{\n\t\tUserID:   userID,\n\t\tNickname: userID,\n\t}\n\n\treq := pbuser.UserRegisterReq{\n\t\tUsers: []*sdkws.UserInfo{user},\n\t}\n\n\t_, err := st.PostRequest(ctx, ApiAddress+CreateUser, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tst.UserCounter++\n\treturn userID, nil\n}\n\nfunc (st *StressTest) CreateUserBatch(ctx context.Context, userIDs []string) error {\n\t// The method can import a large number of users at once.\n\tvar userList []*sdkws.UserInfo\n\n\tdefer st.Once.Do(\n\t\tfunc() {\n\t\t\tst.DefaultUserID = userIDs[0]\n\t\t\tfmt.Println(\"Default Send User Created ID:\", st.DefaultUserID)\n\t\t})\n\n\tneedUserIDs, err := st.CheckUser(ctx, userIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, userID := range needUserIDs {\n\t\tuser := &sdkws.UserInfo{\n\t\t\tUserID:   userID,\n\t\t\tNickname: userID,\n\t\t}\n\t\tuserList = append(userList, user)\n\t}\n\n\treq := pbuser.UserRegisterReq{\n\t\tUsers: userList,\n\t}\n\n\t_, err = st.PostRequest(ctx, ApiAddress+CreateUser, &req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tst.UserCounter += len(userList)\n\treturn nil\n}\n\nfunc (st *StressTest) GetGroupMembersInfo(ctx context.Context, groupID string, userIDs []string) ([]string, error) {\n\tneedInviteUserIDs := make([]string, 0)\n\n\tconst maxBatchSize = 500\n\tif len(userIDs) > maxBatchSize {\n\t\tfor i := 0; i < len(userIDs); i += maxBatchSize {\n\t\t\tend := min(i+maxBatchSize, len(userIDs))\n\t\t\tbatchUserIDs := userIDs[i:end]\n\n\t\t\t// log.ZInfo(ctx, \"Processing group members batch\", \"groupID\", groupID, \"batch\", i/maxBatchSize+1,\n\t\t\t// \t\"batchUserCount\", len(batchUserIDs))\n\n\t\t\t// Process a single batch\n\t\t\tbatchReq := group.GetGroupMembersInfoReq{\n\t\t\t\tGroupID: groupID,\n\t\t\t\tUserIDs: batchUserIDs,\n\t\t\t}\n\n\t\t\tresp, err := st.PostRequest(ctx, ApiAddress+GetGroupMemberInfo, &batchReq)\n\t\t\tif err != nil {\n\t\t\t\tlog.ZError(ctx, \"Batch query failed\", err, \"batch\", i/maxBatchSize+1)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdata := &group.GetGroupMembersInfoResp{}\n\t\t\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\t\t\tlog.ZError(ctx, \"Failed to parse batch response\", err, \"batch\", i/maxBatchSize+1)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Process the batch results\n\t\t\texistingMembers := make(map[string]bool)\n\t\t\tfor _, member := range data.Members {\n\t\t\t\texistingMembers[member.UserID] = true\n\t\t\t}\n\n\t\t\tfor _, userID := range batchUserIDs {\n\t\t\t\tif !existingMembers[userID] {\n\t\t\t\t\tneedInviteUserIDs = append(needInviteUserIDs, userID)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn needInviteUserIDs, nil\n\t}\n\n\treq := group.GetGroupMembersInfoReq{\n\t\tGroupID: groupID,\n\t\tUserIDs: userIDs,\n\t}\n\n\tresp, err := st.PostRequest(ctx, ApiAddress+GetGroupMemberInfo, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata := &group.GetGroupMembersInfoResp{}\n\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\treturn nil, err\n\t}\n\n\texistingMembers := make(map[string]bool)\n\tfor _, member := range data.Members {\n\t\texistingMembers[member.UserID] = true\n\t}\n\n\tfor _, userID := range userIDs {\n\t\tif !existingMembers[userID] {\n\t\t\tneedInviteUserIDs = append(needInviteUserIDs, userID)\n\t\t}\n\t}\n\n\treturn needInviteUserIDs, nil\n}\n\nfunc (st *StressTest) InviteToGroup(ctx context.Context, groupID string, userIDs []string) error {\n\treq := group.InviteUserToGroupReq{\n\t\tGroupID:        groupID,\n\t\tInvitedUserIDs: userIDs,\n\t}\n\t_, err := st.PostRequest(ctx, ApiAddress+InviteToGroup, &req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (st *StressTest) SendMsg(ctx context.Context, userID string, groupID string) error {\n\tcontentObj := map[string]any{\n\t\t// \"content\": fmt.Sprintf(\"index %d. The current time is %s\", st.MsgCounter, time.Now().Format(\"2006-01-02 15:04:05.000\")),\n\t\t\"content\": fmt.Sprintf(\"The current time is %s\", time.Now().Format(\"2006-01-02 15:04:05.000\")),\n\t}\n\n\treq := &apistruct.SendMsgReq{\n\t\tSendMsg: apistruct.SendMsg{\n\t\t\tSendID:         userID,\n\t\t\tSenderNickname: userID,\n\t\t\tGroupID:        groupID,\n\t\t\tContentType:    constant.Text,\n\t\t\tSessionType:    constant.ReadGroupChatType,\n\t\t\tContent:        contentObj,\n\t\t},\n\t}\n\n\t_, err := st.PostRequest(ctx, ApiAddress+SendMsg, &req)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to send message\", err, \"userID\", userID, \"req\", &req)\n\t\treturn err\n\t}\n\n\tst.MsgCounter++\n\n\treturn nil\n}\n\n// Max userIDs number is 1000\nfunc (st *StressTest) CreateGroup(ctx context.Context, groupID string, userID string, userIDsList []string) (string, error) {\n\tgroupInfo := &sdkws.GroupInfo{\n\t\tGroupID:   groupID,\n\t\tGroupName: groupID,\n\t\tGroupType: constant.WorkingGroup,\n\t}\n\n\treq := group.CreateGroupReq{\n\t\tOwnerUserID:   userID,\n\t\tMemberUserIDs: userIDsList,\n\t\tGroupInfo:     groupInfo,\n\t}\n\n\tresp := group.CreateGroupResp{}\n\n\tresponse, err := st.PostRequest(ctx, ApiAddress+CreateGroup, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := json.Unmarshal(response, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// st.GroupCounter++\n\n\treturn resp.GroupInfo.GroupID, nil\n}\n\nfunc main() {\n\tvar configPath string\n\t// defaultConfigDir := filepath.Join(\"..\", \"..\", \"..\", \"..\", \"..\", \"config\")\n\t// flag.StringVar(&configPath, \"c\", defaultConfigDir, \"config path\")\n\tflag.StringVar(&configPath, \"c\", \"\", \"config path\")\n\tflag.Parse()\n\n\tif configPath == \"\" {\n\t\t_, _ = fmt.Fprintln(os.Stderr, \"config path is empty\")\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\n\tfmt.Printf(\" Config Path: %s\\n\", configPath)\n\n\tshare, apiConfig, err := initConfig(configPath)\n\tif err != nil {\n\t\tprogram.ExitWithError(err)\n\t\treturn\n\t}\n\n\tApiAddress = fmt.Sprintf(\"http://%s:%s\", \"127.0.0.1\", fmt.Sprint(apiConfig.Api.Ports[0]))\n\n\tctx, cancel := context.WithCancel(context.Background())\n\t// ch := make(chan struct{})\n\n\tst := &StressTest{\n\t\tConf: &conf{\n\t\t\tShare: *share,\n\t\t\tApi:   *apiConfig,\n\t\t},\n\t\tAdminUserID: share.IMAdminUser.UserIDs[0],\n\t\tCtx:         ctx,\n\t\tCancel:      cancel,\n\t\tHttpClient: &http.Client{\n\t\t\tTimeout: 50 * time.Second,\n\t\t},\n\t}\n\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt, syscall.SIGTERM)\n\tgo func() {\n\t\t<-c\n\t\tfmt.Println(\"\\nReceived stop signal, stopping...\")\n\n\t\tgo func() {\n\t\t\t// time.Sleep(5 * time.Second)\n\t\t\tfmt.Println(\"Force exit\")\n\t\t\tos.Exit(0)\n\t\t}()\n\n\t\tst.Cancel()\n\t}()\n\n\ttoken, err := st.GetAdminToken(st.Ctx)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Get Admin Token failed.\", err, \"AdminUserID\", st.AdminUserID)\n\t}\n\n\tst.AdminToken = token\n\tfmt.Println(\"Admin Token:\", st.AdminToken)\n\tfmt.Println(\"ApiAddress:\", ApiAddress)\n\n\tfor i := range MaxUser {\n\t\tuserID := fmt.Sprintf(\"v2_StressTest_User_%d\", i)\n\t\tst.CreatedUsers = append(st.CreatedUsers, userID)\n\t\tst.CreateUserCounter++\n\t}\n\n\t// err = st.CreateUserBatch(st.Ctx, st.CreatedUsers)\n\t// if err != nil {\n\t// \tlog.ZError(ctx, \"Create user failed.\", err)\n\t// }\n\n\tconst batchSize = 1000\n\ttotalUsers := len(st.CreatedUsers)\n\tsuccessCount := 0\n\n\tif st.DefaultUserID == \"\" && len(st.CreatedUsers) > 0 {\n\t\tst.DefaultUserID = st.CreatedUsers[0]\n\t}\n\n\tfor i := 0; i < totalUsers; i += batchSize {\n\t\tend := min(i+batchSize, totalUsers)\n\n\t\tuserBatch := st.CreatedUsers[i:end]\n\t\tlog.ZInfo(st.Ctx, \"Creating user batch\", \"batch\", i/batchSize+1, \"count\", len(userBatch))\n\n\t\terr = st.CreateUserBatch(st.Ctx, userBatch)\n\t\tif err != nil {\n\t\t\tlog.ZError(st.Ctx, \"Batch user creation failed\", err, \"batch\", i/batchSize+1)\n\t\t} else {\n\t\t\tsuccessCount += len(userBatch)\n\t\t\tlog.ZInfo(st.Ctx, \"Batch user creation succeeded\", \"batch\", i/batchSize+1,\n\t\t\t\t\"progress\", fmt.Sprintf(\"%d/%d\", successCount, totalUsers))\n\t\t}\n\t}\n\n\t// Execute create 100k group\n\tst.Wg.Add(1)\n\tgo func() {\n\t\tdefer st.Wg.Done()\n\n\t\tcreate100kGroupTicker := time.NewTicker(Create100KGroupTicker)\n\t\tdefer create100kGroupTicker.Stop()\n\n\t\tfor i := range Max100KGroup {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Create 100K Group\")\n\t\t\t\treturn\n\n\t\t\tcase <-create100kGroupTicker.C:\n\t\t\t\t// Create 100K groups\n\t\t\t\tst.Wg.Add(1)\n\t\t\t\tgo func(idx int) {\n\t\t\t\t\tstartTime := time.Now()\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\telapsedTime := time.Since(startTime)\n\t\t\t\t\t\tlog.ZInfo(st.Ctx, \"100K group creation completed\",\n\t\t\t\t\t\t\t\"groupID\", fmt.Sprintf(\"v2_StressTest_Group_100K_%d\", idx),\n\t\t\t\t\t\t\t\"index\", idx,\n\t\t\t\t\t\t\t\"duration\", elapsedTime.String())\n\t\t\t\t\t}()\n\n\t\t\t\t\tdefer st.Wg.Done()\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\tst.Mutex.Lock()\n\t\t\t\t\t\tst.Create100kGroupCounter++\n\t\t\t\t\t\tst.Mutex.Unlock()\n\t\t\t\t\t}()\n\n\t\t\t\t\tgroupID := fmt.Sprintf(\"v2_StressTest_Group_100K_%d\", idx)\n\n\t\t\t\t\tif _, err = st.CreateGroup(st.Ctx, groupID, st.DefaultUserID, TestTargetUserList); err != nil {\n\t\t\t\t\t\tlog.ZError(st.Ctx, \"Create group failed.\", err)\n\t\t\t\t\t\t// continue\n\t\t\t\t\t}\n\n\t\t\t\t\tfor i := 0; i <= MaxUser/MaxInviteUserLimit; i++ {\n\t\t\t\t\t\tInviteUserIDs := make([]string, 0)\n\t\t\t\t\t\t// ensure TargetUserList is in group\n\t\t\t\t\t\tInviteUserIDs = append(InviteUserIDs, TestTargetUserList...)\n\n\t\t\t\t\t\tstartIdx := max(i*MaxInviteUserLimit, 1)\n\t\t\t\t\t\tendIdx := min((i+1)*MaxInviteUserLimit, MaxUser)\n\n\t\t\t\t\t\tfor j := startIdx; j < endIdx; j++ {\n\t\t\t\t\t\t\tuserCreatedID := fmt.Sprintf(\"v2_StressTest_User_%d\", j)\n\t\t\t\t\t\t\tInviteUserIDs = append(InviteUserIDs, userCreatedID)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(InviteUserIDs) == 0 {\n\t\t\t\t\t\t\t// log.ZWarn(st.Ctx, \"InviteUserIDs is empty\", nil, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tInviteUserIDs, err := st.GetGroupMembersInfo(ctx, groupID, InviteUserIDs)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"GetGroupMembersInfo failed.\", err, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(InviteUserIDs) == 0 {\n\t\t\t\t\t\t\t// log.ZWarn(st.Ctx, \"InviteUserIDs is empty\", nil, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Invite To Group\n\t\t\t\t\t\tif err = st.InviteToGroup(st.Ctx, groupID, InviteUserIDs); err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"Invite To Group failed.\", err, \"UserID\", InviteUserIDs)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t// os.Exit(1)\n\t\t\t\t\t\t\t// return\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}(i)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// create 999 groups\n\tst.Wg.Add(1)\n\tgo func() {\n\t\tdefer st.Wg.Done()\n\n\t\tcreate999GroupTicker := time.NewTicker(Create999GroupTicker)\n\t\tdefer create999GroupTicker.Stop()\n\n\t\tfor i := range Max999Group {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Create 999 Group\")\n\t\t\t\treturn\n\n\t\t\tcase <-create999GroupTicker.C:\n\t\t\t\t// Create 999 groups\n\t\t\t\tst.Wg.Add(1)\n\t\t\t\tgo func(idx int) {\n\t\t\t\t\tstartTime := time.Now()\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\telapsedTime := time.Since(startTime)\n\t\t\t\t\t\tlog.ZInfo(st.Ctx, \"999 group creation completed\",\n\t\t\t\t\t\t\t\"groupID\", fmt.Sprintf(\"v2_StressTest_Group_1K_%d\", idx),\n\t\t\t\t\t\t\t\"index\", idx,\n\t\t\t\t\t\t\t\"duration\", elapsedTime.String())\n\t\t\t\t\t}()\n\n\t\t\t\t\tdefer st.Wg.Done()\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\tst.Mutex.Lock()\n\t\t\t\t\t\tst.Create999GroupCounter++\n\t\t\t\t\t\tst.Mutex.Unlock()\n\t\t\t\t\t}()\n\n\t\t\t\t\tgroupID := fmt.Sprintf(\"v2_StressTest_Group_1K_%d\", idx)\n\n\t\t\t\t\tif _, err = st.CreateGroup(st.Ctx, groupID, st.DefaultUserID, TestTargetUserList); err != nil {\n\t\t\t\t\t\tlog.ZError(st.Ctx, \"Create group failed.\", err)\n\t\t\t\t\t\t// continue\n\t\t\t\t\t}\n\t\t\t\t\tfor i := 0; i <= Max1kUser/MaxInviteUserLimit; i++ {\n\t\t\t\t\t\tInviteUserIDs := make([]string, 0)\n\t\t\t\t\t\t// ensure TargetUserList is in group\n\t\t\t\t\t\tInviteUserIDs = append(InviteUserIDs, TestTargetUserList...)\n\n\t\t\t\t\t\tstartIdx := max(i*MaxInviteUserLimit, 1)\n\t\t\t\t\t\tendIdx := min((i+1)*MaxInviteUserLimit, Max1kUser)\n\n\t\t\t\t\t\tfor j := startIdx; j < endIdx; j++ {\n\t\t\t\t\t\t\tuserCreatedID := fmt.Sprintf(\"v2_StressTest_User_%d\", j)\n\t\t\t\t\t\t\tInviteUserIDs = append(InviteUserIDs, userCreatedID)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(InviteUserIDs) == 0 {\n\t\t\t\t\t\t\t// log.ZWarn(st.Ctx, \"InviteUserIDs is empty\", nil, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tInviteUserIDs, err := st.GetGroupMembersInfo(ctx, groupID, InviteUserIDs)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"GetGroupMembersInfo failed.\", err, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(InviteUserIDs) == 0 {\n\t\t\t\t\t\t\t// log.ZWarn(st.Ctx, \"InviteUserIDs is empty\", nil, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Invite To Group\n\t\t\t\t\t\tif err = st.InviteToGroup(st.Ctx, groupID, InviteUserIDs); err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"Invite To Group failed.\", err, \"UserID\", InviteUserIDs)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t// os.Exit(1)\n\t\t\t\t\t\t\t// return\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}(i)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Send message to 100K groups\n\tst.Wg.Wait()\n\tfmt.Println(\"All groups created successfully, starting to send messages...\")\n\tlog.ZInfo(ctx, \"All groups created successfully, starting to send messages...\")\n\n\tvar groups100K []string\n\tvar groups999 []string\n\n\tfor i := range Max100KGroup {\n\t\tgroupID := fmt.Sprintf(\"v2_StressTest_Group_100K_%d\", i)\n\t\tgroups100K = append(groups100K, groupID)\n\t}\n\n\tfor i := range Max999Group {\n\t\tgroupID := fmt.Sprintf(\"v2_StressTest_Group_1K_%d\", i)\n\t\tgroups999 = append(groups999, groupID)\n\t}\n\n\tsend100kGroupLimiter := make(chan struct{}, 20)\n\tsend999GroupLimiter := make(chan struct{}, 100)\n\n\t// execute Send message to 100K groups\n\tgo func() {\n\t\tticker := time.NewTicker(SendMsgTo100KGroupTicker)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Send Message to 100K Group\")\n\t\t\t\treturn\n\n\t\t\tcase <-ticker.C:\n\t\t\t\t// Send message to 100K groups\n\t\t\t\tfor _, groupID := range groups100K {\n\t\t\t\t\tsend100kGroupLimiter <- struct{}{}\n\t\t\t\t\tgo func(groupID string) {\n\t\t\t\t\t\tdefer func() { <-send100kGroupLimiter }()\n\t\t\t\t\t\tif err := st.SendMsg(st.Ctx, st.DefaultUserID, groupID); err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"Send message to 100K group failed.\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}(groupID)\n\t\t\t\t}\n\t\t\t\t// log.ZInfo(st.Ctx, \"Send message to 100K groups successfully.\")\n\t\t\t}\n\t\t}\n\t}()\n\n\t// execute Send message to 999 groups\n\tgo func() {\n\t\tticker := time.NewTicker(SendMsgTo999GroupTicker)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Send Message to 999 Group\")\n\t\t\t\treturn\n\n\t\t\tcase <-ticker.C:\n\t\t\t\t// Send message to 999 groups\n\t\t\t\tfor _, groupID := range groups999 {\n\t\t\t\t\tsend999GroupLimiter <- struct{}{}\n\t\t\t\t\tgo func(groupID string) {\n\t\t\t\t\t\tdefer func() { <-send999GroupLimiter }()\n\n\t\t\t\t\t\tif err := st.SendMsg(st.Ctx, st.DefaultUserID, groupID); err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"Send message to 999 group failed.\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}(groupID)\n\t\t\t\t}\n\t\t\t\t// log.ZInfo(st.Ctx, \"Send message to 999 groups successfully.\")\n\t\t\t}\n\t\t}\n\t}()\n\n\t<-st.Ctx.Done()\n\tfmt.Println(\"Received signal to exit, shutting down...\")\n}\n"
  },
  {
    "path": "test/testdata/README.md",
    "content": "\n# Test Data for OpenIM Server\n\nThis directory (`testdata`) contains various JSON formatted data files that are used for testing the OpenIM Server.\n\n## Structure\n\n```bash\ntestdata/\n│\n├── README.md         # 描述该目录下各子目录和文件的作用\n│\n├── storage/              # 存储模拟的数据库数据\n│   ├── users.json   # 用户的模拟数据\n│   └── messages.json # 消息的模拟数据\n│\n├── requests/        # 存储模拟的请求数据\n│   ├── login.json   # 模拟登陆请求\n│   ├── register.json # 模拟注册请求\n│   └── sendMessage.json # 模拟发送消息请求\n│\n└── responses/       # 存储模拟的响应数据\n    ├── login.json   # 模拟登陆响应\n    ├── register.json # 模拟注册响应\n    └── sendMessage.json # 模拟发送消息响应\n```\n\nHere is an overview of what each subdirectory or file represents:\n\n- `db/` - This directory contains mock data mimicking the actual database contents.\n  - `users.json` - Represents a list of users in the system. Each entry contains user-specific information such as user ID, username, password hash, etc.\n  - `messages.json` - Contains a list of messages exchanged between users. Each message entry includes the sender's and receiver's user IDs, message content, timestamp, etc.\n- `requests/` - This directory contains mock requests that a client might send to the server.\n  - `login.json` - Represents a user login request. It includes fields such as username and password.\n  - `register.json` - Mimics a user registration request. Contains details such as username, password, email, etc.\n  - `sendMessage.json` - Simulates a message sending request from a user to another user.\n- `responses/` - This directory holds the expected server responses for the respective requests.\n  - `login.json` - Represents a successful login response from the server. It typically includes a session token and user-specific information.\n  - `register.json` - Simulates a successful registration response from the server, usually containing the new user's ID, username, etc.\n  - `sendMessage.json` - Depicts a successful message sending response from the server, confirming the delivery of the message.\n\n## JSON Format\n\nAll the data files in this directory are in JSON format. JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse and generate.\n\nHere is a simple example of what a JSON file might look like:\n\n```bash\n  \"users\": [\n    {\n      \"id\": 1,\n      \"username\": \"user1\",\n      \"password\": \"password1\"\n    },\n    {\n      \"id\": 2,\n      \"username\": \"user2\",\n      \"password\": \"password2\"\n    }\n  ]\n\n```\n\nIn this example, \"users\" is an array of user objects. Each user object has an \"id\", \"username\", and \"password\".\n"
  },
  {
    "path": "test/testdata/db/messages.json",
    "content": ""
  },
  {
    "path": "test/testdata/db/users.json",
    "content": ""
  },
  {
    "path": "test/testdata/requests/login.json",
    "content": ""
  },
  {
    "path": "test/testdata/requests/register.json",
    "content": ""
  },
  {
    "path": "test/testdata/requests/send-message.json",
    "content": ""
  },
  {
    "path": "test/testdata/responses/login.json",
    "content": ""
  },
  {
    "path": "test/testdata/responses/register.json",
    "content": ""
  },
  {
    "path": "test/testdata/responses/sendMessage.json",
    "content": ""
  },
  {
    "path": "test/webhook/msgmodify/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n\tcbapi \"github.com/openimsdk/open-im-server/v3/pkg/callbackstruct\"\n\t\"github.com/openimsdk/protocol/constant\"\n)\n\nfunc main() {\n\tg := gin.Default()\n\tg.POST(\"/callbackExample/callbackBeforeMsgModifyCommand\", toGin(handlerMsg))\n\tif err := g.Run(\":10006\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc toGin[R any](fn func(c *gin.Context, req *R)) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tbody, err := io.ReadAll(c.Request.Body)\n\t\tif err != nil {\n\t\t\tc.String(http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\tfmt.Printf(\"HTTP %s %s %s\\n\", c.Request.Method, c.Request.URL, body)\n\t\tvar req R\n\t\tif err := json.Unmarshal(body, &req); err != nil {\n\t\t\tc.String(http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\tfn(c, &req)\n\t}\n}\n\nfunc handlerMsg(c *gin.Context, req *cbapi.CallbackMsgModifyCommandReq) {\n\tvar resp cbapi.CallbackMsgModifyCommandResp\n\tif req.ContentType != constant.Text {\n\t\tc.JSON(http.StatusOK, &resp)\n\t\treturn\n\t}\n\tvar textElem struct {\n\t\tContent string `json:\"content\"`\n\t}\n\tif err := json.Unmarshal([]byte(req.Content), &textElem); err != nil {\n\t\tc.String(http.StatusInternalServerError, err.Error())\n\t\treturn\n\t}\n\tconst word = \"xxx\"\n\tif strings.Contains(textElem.Content, word) {\n\t\ttextElem.Content = strings.ReplaceAll(textElem.Content, word, strings.Repeat(\"*\", len(word)))\n\t\tcontent, err := json.Marshal(&textElem)\n\t\tif err != nil {\n\t\t\tc.String(http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\ttmp := string(content)\n\t\tresp.Content = &tmp\n\t}\n\tc.JSON(http.StatusOK, &resp)\n}\n"
  },
  {
    "path": "tools/README.md",
    "content": "# Notes about go workspace\n\nAs openim is using go1.18's [workspace feature](https://go.dev/doc/tutorial/workspaces), once you add a new module, you need to run `go work use -r .` at root directory to update the workspace synced.\n\n### Create a new extensions\n\n1. Create your tools_name directory in pkg `/tools` first and cd into it.\n2. Init the project.\n3. Then `go work use -r .` at current directory to update the workspace.\n4. Create your tools\n\nYou can execute the following commands to do things above:\n\n```bash\n# edit the CRD_NAME and CRD_GROUP to your own\nexport OPENIM_TOOLS_NAME=<Changeme>\n\n# copy and paste to create a new CRD and Controller\nmkdir tools/${OPENIM_TOOLS_NAME}\ncd tools/${OPENIM_TOOLS_NAME}\ngo mod init github.com/openimsdk/open-im-server/tools/${OPENIM_TOOLS_NAME}\ngo mod tidy\ngo work use -r .\ncd ../..\n```"
  },
  {
    "path": "tools/changelog/changelog.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// You can specify a tag as a command line argument to generate the changelog for a specific version.\n// Example: go run tools/changelog/changelog.go v0.0.33\n// If no tag is provided, the latest release will be used.\n\n// Setting repo owner and repo name by generate changelog\nconst (\n\trepoOwner = \"openimsdk\"\n\trepoName  = \"open-im-server\"\n)\n\n// GitHubRepo struct represents the repo details.\ntype GitHubRepo struct {\n\tOwner         string\n\tRepo          string\n\tFullChangelog string\n}\n\n// ReleaseData represents the JSON structure for release data.\ntype ReleaseData struct {\n\tTagName   string `json:\"tag_name\"`\n\tBody      string `json:\"body\"`\n\tHtmlUrl   string `json:\"html_url\"`\n\tPublished string `json:\"published_at\"`\n}\n\n// Method to classify and format release notes.\nfunc (g *GitHubRepo) classifyReleaseNotes(body string) map[string][]string {\n\tresult := map[string][]string{\n\t\t\"feat\":     {},\n\t\t\"fix\":      {},\n\t\t\"chore\":    {},\n\t\t\"refactor\": {},\n\t\t\"build\":    {},\n\t\t\"other\":    {},\n\t}\n\n\t// Regular expression to extract PR number and URL (case insensitive)\n\trePR := regexp.MustCompile(`(?i)in (https://github\\.com/[^\\s]+/pull/(\\d+))`)\n\n\t// Split the body into individual lines.\n\tlines := strings.Split(body, \"\\n\")\n\n\tfor _, line := range lines {\n\t\t// Skip lines that contain \"deps: Merge\"\n\t\tif strings.Contains(strings.ToLower(line), \"deps: merge #\") {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Use a regular expression to extract Full Changelog link and its title (case insensitive).\n\t\tif strings.Contains(strings.ToLower(line), \"**full changelog**\") {\n\t\t\tmatches := regexp.MustCompile(`(?i)\\*\\*full changelog\\*\\*: (https://github\\.com/[^\\s]+/compare/([^\\s]+))`).FindStringSubmatch(line)\n\t\t\tif len(matches) > 2 {\n\t\t\t\t// Format the Full Changelog link with title\n\t\t\t\tg.FullChangelog = fmt.Sprintf(\"[%s](%s)\", matches[2], matches[1])\n\t\t\t}\n\t\t\tcontinue // Skip further processing for this line.\n\t\t}\n\n\t\tif strings.HasPrefix(line, \"*\") {\n\t\t\tvar category string\n\n\t\t\t// Use strings.ToLower to make the matching case insensitive\n\t\t\tlowerLine := strings.ToLower(line)\n\n\t\t\t// Determine the category based on the prefix (case insensitive).\n\t\t\tif strings.HasPrefix(lowerLine, \"* feat\") {\n\t\t\t\tcategory = \"feat\"\n\t\t\t} else if strings.HasPrefix(lowerLine, \"* fix\") {\n\t\t\t\tcategory = \"fix\"\n\t\t\t} else if strings.HasPrefix(lowerLine, \"* chore\") {\n\t\t\t\tcategory = \"chore\"\n\t\t\t} else if strings.HasPrefix(lowerLine, \"* refactor\") {\n\t\t\t\tcategory = \"refactor\"\n\t\t\t} else if strings.HasPrefix(lowerLine, \"* build\") {\n\t\t\t\tcategory = \"build\"\n\t\t\t} else {\n\t\t\t\tcategory = \"other\"\n\t\t\t}\n\n\t\t\t// Extract PR number and URL (case insensitive)\n\t\t\tmatches := rePR.FindStringSubmatch(line)\n\t\t\tif len(matches) == 3 {\n\t\t\t\tprURL := matches[1]\n\t\t\t\tprNumber := matches[2]\n\t\t\t\t// Format the line with the PR link and use original content for the final result\n\t\t\t\tformattedLine := fmt.Sprintf(\"* %s [#%s](%s)\", strings.Split(line, \" by \")[0][2:], prNumber, prURL)\n\t\t\t\tresult[category] = append(result[category], formattedLine)\n\t\t\t} else {\n\t\t\t\t// If no PR link is found, just add the line as is\n\t\t\t\tresult[category] = append(result[category], line)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\n// Method to generate the final changelog.\nfunc (g *GitHubRepo) generateChangelog(tag, date, htmlURL, body string) string {\n\tsections := g.classifyReleaseNotes(body)\n\n\t// Convert ISO 8601 date to simpler format (YYYY-MM-DD)\n\tformattedDate := date[:10]\n\n\t// Changelog header with tag, date, and links.\n\tchangelog := fmt.Sprintf(\"## [%s](%s) \\t(%s)\\n\\n\", tag, htmlURL, formattedDate)\n\n\tif len(sections[\"feat\"]) > 0 {\n\t\tchangelog += \"### New Features\\n\" + strings.Join(sections[\"feat\"], \"\\n\") + \"\\n\\n\"\n\t}\n\tif len(sections[\"fix\"]) > 0 {\n\t\tchangelog += \"### Bug Fixes\\n\" + strings.Join(sections[\"fix\"], \"\\n\") + \"\\n\\n\"\n\t}\n\tif len(sections[\"chore\"]) > 0 {\n\t\tchangelog += \"### Chores\\n\" + strings.Join(sections[\"chore\"], \"\\n\") + \"\\n\\n\"\n\t}\n\tif len(sections[\"refactor\"]) > 0 {\n\t\tchangelog += \"### Refactors\\n\" + strings.Join(sections[\"refactor\"], \"\\n\") + \"\\n\\n\"\n\t}\n\tif len(sections[\"build\"]) > 0 {\n\t\tchangelog += \"### Builds\\n\" + strings.Join(sections[\"build\"], \"\\n\") + \"\\n\\n\"\n\t}\n\tif len(sections[\"other\"]) > 0 {\n\t\tchangelog += \"### Others\\n\" + strings.Join(sections[\"other\"], \"\\n\") + \"\\n\\n\"\n\t}\n\n\tif g.FullChangelog != \"\" {\n\t\tchangelog += fmt.Sprintf(\"**Full Changelog**: %s\\n\", g.FullChangelog)\n\t}\n\n\treturn changelog\n}\n\n// Method to fetch release data from GitHub API.\nfunc (g *GitHubRepo) fetchReleaseData(version string) (*ReleaseData, error) {\n\tvar apiURL string\n\n\tif version == \"\" {\n\t\t// Fetch the latest release.\n\t\tapiURL = fmt.Sprintf(\"https://api.github.com/repos/%s/%s/releases/latest\", g.Owner, g.Repo)\n\t} else {\n\t\t// Fetch a specific version.\n\t\tapiURL = fmt.Sprintf(\"https://api.github.com/repos/%s/%s/releases/tags/%s\", g.Owner, g.Repo, version)\n\t}\n\n\tresp, err := http.Get(apiURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar releaseData ReleaseData\n\terr = json.Unmarshal(body, &releaseData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &releaseData, nil\n}\n\nfunc main() {\n\trepo := &GitHubRepo{Owner: repoOwner, Repo: repoName}\n\n\t// Get the version from command line arguments, if provided\n\tvar version string // Default is use latest\n\n\tif len(os.Args) > 1 {\n\t\tversion = os.Args[1] // Use the provided version\n\t}\n\n\t// Fetch release data (either for latest or specific version)\n\treleaseData, err := repo.fetchReleaseData(version)\n\tif err != nil {\n\t\tfmt.Println(\"Error fetching release data:\", err)\n\t\treturn\n\t}\n\n\t// Generate and print the formatted changelog\n\tchangelog := repo.generateChangelog(releaseData.TagName, releaseData.Published, releaseData.HtmlUrl, releaseData.Body)\n\tfmt.Println(changelog)\n}\n"
  },
  {
    "path": "tools/check-component/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/redisutil\"\n\t\"github.com/openimsdk/tools/discovery/etcd\"\n\t\"github.com/openimsdk/tools/discovery/zookeeper\"\n\t\"github.com/openimsdk/tools/mq/kafka\"\n\t\"github.com/openimsdk/tools/s3/minio\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\nconst maxRetry = 180\n\nconst (\n\tMountConfigFilePath = \"CONFIG_PATH\"\n\tDeploymentType      = \"DEPLOYMENT_TYPE\"\n\tKUBERNETES          = \"kubernetes\"\n)\n\nfunc CheckZookeeper(ctx context.Context, config *config.ZooKeeper) error {\n\t// Temporary disable logging\n\toriginalLogger := log.Default().Writer()\n\tlog.SetOutput(io.Discard)\n\tdefer log.SetOutput(originalLogger) // Ensure logging is restored\n\treturn zookeeper.Check(ctx, config.Address, config.Schema, zookeeper.WithUserNameAndPassword(config.Username, config.Password))\n}\n\nfunc CheckEtcd(ctx context.Context, config *config.Etcd) error {\n\treturn etcd.Check(ctx, config.Address, \"/check_openim_component\",\n\t\ttrue,\n\t\tetcd.WithDialTimeout(10*time.Second),\n\t\tetcd.WithMaxCallSendMsgSize(20*1024*1024),\n\t\tetcd.WithUsernameAndPassword(config.Username, config.Password))\n}\n\nfunc CheckMongo(ctx context.Context, config *config.Mongo) error {\n\treturn mongoutil.Check(ctx, config.Build())\n}\n\nfunc CheckRedis(ctx context.Context, config *config.Redis) error {\n\treturn redisutil.Check(ctx, config.Build())\n}\n\nfunc CheckMinIO(ctx context.Context, config *config.Minio) error {\n\treturn minio.Check(ctx, config.Build())\n}\n\nfunc CheckKafka(ctx context.Context, conf *config.Kafka) error {\n\treturn kafka.CheckHealth(ctx, conf.Build())\n}\n\nfunc initConfig(configDir string) (*config.Mongo, *config.Redis, *config.Kafka, *config.Minio, *config.Discovery, error) {\n\tvar (\n\t\tmongoConfig = &config.Mongo{}\n\t\tredisConfig = &config.Redis{}\n\t\tkafkaConfig = &config.Kafka{}\n\t\tminioConfig = &config.Minio{}\n\t\tdiscovery   = &config.Discovery{}\n\t\tthirdConfig = &config.Third{}\n\t)\n\n\terr := config.Load(configDir, config.MongodbConfigFileName, config.EnvPrefixMap[config.MongodbConfigFileName], mongoConfig)\n\tif err != nil {\n\t\treturn nil, nil, nil, nil, nil, err\n\t}\n\n\terr = config.Load(configDir, config.RedisConfigFileName, config.EnvPrefixMap[config.RedisConfigFileName], redisConfig)\n\tif err != nil {\n\t\treturn nil, nil, nil, nil, nil, err\n\t}\n\n\terr = config.Load(configDir, config.KafkaConfigFileName, config.EnvPrefixMap[config.KafkaConfigFileName], kafkaConfig)\n\tif err != nil {\n\t\treturn nil, nil, nil, nil, nil, err\n\t}\n\n\terr = config.Load(configDir, config.OpenIMRPCThirdCfgFileName, config.EnvPrefixMap[config.OpenIMRPCThirdCfgFileName], thirdConfig)\n\tif err != nil {\n\t\treturn nil, nil, nil, nil, nil, err\n\t}\n\n\tif thirdConfig.Object.Enable == \"minio\" {\n\t\terr = config.Load(configDir, config.MinioConfigFileName, config.EnvPrefixMap[config.MinioConfigFileName], minioConfig)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, nil, nil, err\n\t\t}\n\t} else {\n\t\tminioConfig = nil\n\t}\n\terr = config.Load(configDir, config.DiscoveryConfigFilename, config.EnvPrefixMap[config.DiscoveryConfigFilename], discovery)\n\tif err != nil {\n\t\treturn nil, nil, nil, nil, nil, err\n\t}\n\treturn mongoConfig, redisConfig, kafkaConfig, minioConfig, discovery, nil\n}\n\nfunc main() {\n\tvar index int\n\tvar configDir string\n\tflag.IntVar(&index, \"i\", 0, \"Index number\")\n\tdefaultConfigDir := filepath.Join(\"..\", \"..\", \"..\", \"..\", \"..\", \"config\")\n\tflag.StringVar(&configDir, \"c\", defaultConfigDir, \"Configuration dir\")\n\tflag.Parse()\n\n\tfmt.Printf(\"%s Index: %d, Config Path: %s\\n\", filepath.Base(os.Args[0]), index, configDir)\n\n\tmongoConfig, redisConfig, kafkaConfig, minioConfig, zookeeperConfig, err := initConfig(configDir)\n\tif err != nil {\n\t\tprogram.ExitWithError(err)\n\t}\n\n\tctx := context.Background()\n\terr = performChecks(ctx, mongoConfig, redisConfig, kafkaConfig, minioConfig, zookeeperConfig, maxRetry)\n\tif err != nil {\n\t\t// Assume program.ExitWithError logs the error and exits.\n\t\t// Replace with your error handling logic as necessary.\n\t\tprogram.ExitWithError(err)\n\t}\n}\n\nfunc performChecks(ctx context.Context, mongoConfig *config.Mongo, redisConfig *config.Redis, kafkaConfig *config.Kafka, minioConfig *config.Minio, discovery *config.Discovery, maxRetry int) error {\n\tchecksDone := make(map[string]bool)\n\n\tchecks := map[string]func(ctx context.Context) error{\n\t\t\"Mongo\": func(ctx context.Context) error {\n\t\t\treturn CheckMongo(ctx, mongoConfig)\n\t\t},\n\t\t\"Redis\": func(ctx context.Context) error {\n\t\t\treturn CheckRedis(ctx, redisConfig)\n\t\t},\n\t\t\"Kafka\": func(ctx context.Context) error {\n\t\t\treturn CheckKafka(ctx, kafkaConfig)\n\t\t},\n\t}\n\tif minioConfig != nil {\n\t\tchecks[\"MinIO\"] = func(ctx context.Context) error {\n\t\t\treturn CheckMinIO(ctx, minioConfig)\n\t\t}\n\t}\n\tif discovery.Enable == \"etcd\" {\n\t\tchecks[\"Etcd\"] = func(ctx context.Context) error {\n\t\t\treturn CheckEtcd(ctx, &discovery.Etcd)\n\t\t}\n\t}\n\n\tfor i := 0; i < maxRetry; i++ {\n\t\tallSuccess := true\n\t\tfor name, check := range checks {\n\t\t\tif !checksDone[name] {\n\t\t\t\tif err := check(ctx); err != nil {\n\t\t\t\t\tfmt.Printf(\"%s check failed: %v\\n\", name, err)\n\t\t\t\t\tallSuccess = false\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"%s check succeeded.\\n\", name)\n\t\t\t\t\tchecksDone[name] = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif allSuccess {\n\t\t\tfmt.Println(\"All components checks passed successfully.\")\n\t\t\treturn nil\n\t\t}\n\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\n\treturn fmt.Errorf(\"not all components checks passed successfully after %d attempts\", maxRetry)\n}\n"
  },
  {
    "path": "tools/check-free-memory/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/shirou/gopsutil/mem\"\n)\n\nfunc main() {\n\tvMem, err := mem.VirtualMemory()\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to get virtual memory info: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n\n\t// Use the Available field to get the available memory\n\tavailableMemoryGB := float64(vMem.Available) / float64(1024*1024*1024)\n\n\tif availableMemoryGB < 1.0 {\n\t\tfmt.Fprintf(os.Stderr, \"System available memory is less than 1GB: %.2fGB\\n\", availableMemoryGB)\n\t\tos.Exit(1)\n\t} else {\n\t\tfmt.Printf(\"System available memory is sufficient: %.2fGB\\n\", availableMemoryGB)\n\t}\n}\n"
  },
  {
    "path": "tools/imctl/.gitignore",
    "content": "# Copyright © 2023 OpenIMSDK.\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# ==============================================================================\n# For the entire design of.gitignore, ignore git commits and ignore files\n#===============================================================================\n#\n\n### OpenIM developer supplement ###\nlogs\n.devcontainer\ncomponents\nout-test\nDockerfile.cross\n\n### Makefile ###\ntmp/\nbin/\noutput/\n_output/\n\n### OpenIM Config ###\nconfig/config.yaml\n./config/config.yaml\n.env\n./.env\n\n# files used by the developer\n.idea.md\n.todo.md\n.note.md\n\n# ==============================================================================\n# Created by https://www.toptal.com/developers/gitignore/api/go,git,vim,tags,test,emacs,backup,jetbrains\n# Edit at https://www.toptal.com/developers/gitignore?templates=go,git,vim,tags,test,emacs,backup,jetbrains\n\ncmd/\ninternal/\npkg/\n"
  },
  {
    "path": "tools/imctl/README.md",
    "content": "# [RFC #0005] OpenIM CTL Module Proposal\n\n## Meta\n\n- Name: OpenIM CTL Module Enhancement\n- Start Date: 2023-08-23\n- Author(s): @cubxxw\n- Status: Draft\n- RFC Pull Request: (leave blank)\n- OpenIMSDK Pull Request: (leave blank)\n- OpenIMSDK Issue: https://github.com/openimsdk/open-im-server/issues/924\n- Supersedes: N/A\n\n## 📇Topics\n\n- RFC #0000 OpenIMSDK CTL Module Proposal\n  - [Meta](#meta)\n  - [Summary](#summary)\n  - [Definitions](#definitions)\n  - [Motivation](#motivation)\n  - [What it is](#what-it-is)\n  - [How it Works](#how-it-works)\n  - [Migration](#migration)\n  - [Drawbacks](#drawbacks)\n  - [Alternatives](#alternatives)\n  - [Prior Art](#prior-art)\n  - [Unresolved Questions](#unresolved-questions)\n  - [Spec. Changes (OPTIONAL)](#spec-changes-optional)\n  - [History](#history)\n\n## Summary\n\nThe OpenIM CTL module proposal aims to provide an integrated tool for the OpenIM system, offering utilities for user management, system monitoring, debugging, configuration, and more. This tool will enhance the extensibility of the OpenIM system and reduce dependencies on individual modules.\n\n## Definitions\n\n- **OpenIM**: An Instant Messaging system.\n- **`imctl`**: The control command-line tool for OpenIM.\n- **E2E Testing**: End-to-End Testing.\n- **API**: Application Programming Interface.\n\n## Motivation\n\n- Improve the OpenIM system's extensibility and reduce dependencies on individual modules.\n- Simplify the process for testers to perform automated tests.\n- Enhance interaction with scripts and reduce the system's coupling.\n- Implement a consistent tool similar to kubectl for a streamlined user experience.\n\n## What it is\n\n`imctl` is a command-line utility designed for OpenIM to provide functionalities including:\n\n- User Management: Add, delete, or disable user accounts.\n- System Monitoring: View metrics like online users, message transfer rate.\n- Debugging: View logs, adjust log levels, check system states.\n- Configuration Management: Update system settings, manage plugins/modules.\n- Data Management: Backup, restore, import, or export data.\n- System Maintenance: Update, restart services, or maintenance mode.\n\n## How it Works\n\n`imctl`, inspired by kubectl, will have sub-commands and options for the functionalities mentioned. Developers, operations, and testers can invoke these commands to manage and monitor the OpenIM system.\n\n## Migration\n\nCurrently, the `imctl` will be housed in `tools/imctl`, and later on, the plan is to move it to `cmd/imctl`. Migration guidelines will be provided to ensure smooth transitions.\n\n## Drawbacks\n\n- Overhead in learning and adapting to a new tool for existing users.\n- Potential complexities in implementing some of the advanced functionalities.\n\n## Alternatives\n\n- Continue using individual modules for OpenIM management.\n- Utilize third-party tools or platforms with similar functionalities, customizing them for OpenIM.\n\n## Prior Art\n\nKubectl from Kubernetes is a significant inspiration for `imctl`, offering a comprehensive command-line tool for managing clusters.\n\n## Unresolved Questions\n\n- What other functionalities might be required in future versions of `imctl`?\n- What's the expected timeline for transitioning from `tools/imctl` to `cmd/imctl`?\n\n## Spec. Changes (OPTIONAL)\n\nAs of now, there are no proposed changes to the core specifications or extensions. Future changes based on community feedback might necessitate spec changes, which will be documented accordingly."
  },
  {
    "path": "tools/imctl/main.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n\n\tfmt.Println(\"imctl\")\n}\n"
  },
  {
    "path": "tools/infra/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/fatih/color\"\n)\n\n// Define a function to print important link information\nfunc printLinks() {\n\tblue := color.New(color.FgBlue).SprintFunc()\n\tfmt.Printf(\"OpenIM Github: %s\\n\", blue(\"https://github.com/OpenIMSDK/Open-IM-Server\"))\n\tfmt.Printf(\"Slack Invitation: %s\\n\", blue(\"https://openimsdk.slack.com\"))\n\tfmt.Printf(\"Follow Twitter: %s\\n\", blue(\"https://twitter.com/founder_im63606\"))\n}\n\nfunc main() {\n\tyellow := color.New(color.FgYellow)\n\tblue := color.New(color.FgBlue, color.Bold)\n\n\tyellow.Println(\"Please use the release branch or tag for production environments!\")\n\n\tmessage := `\n____                       _____  __  __ \n/ __ \\                     |_   _||  \\/  |\n| |  | | _ __    ___  _ __    | |  | \\  / |\n| |  | || '_ \\  / _ \\| '_ \\   | |  | |\\/| |\n| |__| || |_) ||  __/| | | | _| |_ | |  | |\n\\____/ | .__/  \\___||_| |_||_____||_|  |_|\n\t   | |                                \n\t   |_|                                \n\nKeep checking for updates!\n`\n\n\tblue.Println(message)\n\tprintLinks() // Call the function to print the link information\n}\n"
  },
  {
    "path": "tools/ncpu/README.md",
    "content": "# ncpu\n\n**ncpu** is a simple utility to fetch the number of CPU cores across different operating systems.\n\n## Introduction\n\nIn various scenarios, especially while compiling code, it's beneficial to know the number of available CPU cores to optimize the build process. However, the command to fetch the CPU core count differs between operating systems. For example, on Linux, we use `nproc`, while on macOS, it's `sysctl -n hw.ncpu`. The `ncpu` utility provides a unified way to obtain this number, regardless of the platform.\n\n## Usage\n\nTo retrieve the number of CPU cores, simply use the `ncpu` command:\n\n```bash\n$ ncpu\n```\n\nThis will return an integer representing the number of available CPU cores.\n\n### Example:\n\nLet's say you're compiling a project using `make`. To utilize all the CPU cores for the compilation process, you can use:\n\n```bash\n$ make -j $(ncpu) build # or any other build command\n```\n\nThe above command will ensure the build process takes advantage of all the available CPU cores, thereby potentially speeding up the compilation.\n\n## Why use `ncpu`?\n\n- **Cross-platform compatibility**: No need to remember or detect which OS-specific command to use. Just use `ncpu`!\n  \n- **Ease of use**: A simple and intuitive command that's easy to incorporate into scripts or command-line operations.\n\n- **Consistency**: Ensures consistent behavior and output across different systems and environments.\n\n## Installation\n\n(Include installation steps here, e.g., how to clone the repo, build the tool, or install via package manager.)\n"
  },
  {
    "path": "tools/ncpu/main.go",
    "content": "// Copyright © 2024 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\n\t\"go.uber.org/automaxprocs/maxprocs\"\n)\n\nfunc main() {\n\t// Set maxprocs with a custom logger that does nothing to ignore logs.\n\tmaxprocs.Set(maxprocs.Logger(func(string, ...interface{}) {\n\t\t// Intentionally left blank to suppress all log output from automaxprocs.\n\t}))\n\n\t// Now this will print the GOMAXPROCS value without printing the automaxprocs log message.\n\tfmt.Println(runtime.GOMAXPROCS(0))\n}\n"
  },
  {
    "path": "tools/ncpu/main_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport \"testing\"\n\nfunc Test_main(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t}{\n\t\t{\n\t\t\tname: \"Test_main\",\n\t\t},\n\t\t{\n\t\t\tname: \"Test_main2\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmain()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "tools/s3/README.md",
    "content": "# After s3 switches the storage engine, convert the data\n\n- build\n```shell\ngo build -o s3convert main.go\n```\n\n- start\n```shell\n./s3convert -config <config dir path> -name <old s3 name>\n# ./s3convert -config ./../../config -name minio\n```\n"
  },
  {
    "path": "tools/s3/internal/conversion.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/mitchellh/mapstructure\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/redisutil\"\n\t\"github.com/openimsdk/tools/s3\"\n\t\"github.com/openimsdk/tools/s3/aws\"\n\t\"github.com/openimsdk/tools/s3/cos\"\n\t\"github.com/openimsdk/tools/s3/kodo\"\n\t\"github.com/openimsdk/tools/s3/minio\"\n\t\"github.com/openimsdk/tools/s3/oss\"\n\t\"github.com/spf13/viper\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n)\n\nconst defaultTimeout = time.Second * 10\n\nfunc readConf(path string, val any) error {\n\tv := viper.New()\n\tv.SetConfigFile(path)\n\tif err := v.ReadInConfig(); err != nil {\n\t\treturn err\n\t}\n\tfn := func(config *mapstructure.DecoderConfig) {\n\t\tconfig.TagName = \"mapstructure\"\n\t}\n\treturn v.Unmarshal(val, fn)\n}\n\nfunc getS3(path string, name string, thirdConf *config.Third) (s3.Interface, error) {\n\tswitch name {\n\tcase \"minio\":\n\t\tctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)\n\t\tdefer cancel()\n\t\tvar minioConf config.Minio\n\t\tif err := readConf(filepath.Join(path, minioConf.GetConfigFileName()), &minioConf); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar redisConf config.Redis\n\t\tif err := readConf(filepath.Join(path, redisConf.GetConfigFileName()), &redisConf); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trdb, err := redisutil.NewRedisClient(ctx, redisConf.Build())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn minio.NewMinio(ctx, redis.NewMinioCache(rdb), *minioConf.Build())\n\tcase \"cos\":\n\t\treturn cos.NewCos(*thirdConf.Object.Cos.Build())\n\tcase \"oss\":\n\t\treturn oss.NewOSS(*thirdConf.Object.Oss.Build())\n\tcase \"kodo\":\n\t\treturn kodo.NewKodo(*thirdConf.Object.Kodo.Build())\n\tcase \"aws\":\n\t\treturn aws.NewAws(*thirdConf.Object.Aws.Build())\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid object enable: %s\", name)\n\t}\n}\n\nfunc getMongo(path string) (database.ObjectInfo, error) {\n\tvar mongoConf config.Mongo\n\tif err := readConf(filepath.Join(path, mongoConf.GetConfigFileName()), &mongoConf); err != nil {\n\t\treturn nil, err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)\n\tdefer cancel()\n\tmgocli, err := mongoutil.NewMongoDB(ctx, mongoConf.Build())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn mgo.NewS3Mongo(mgocli.GetDB())\n}\n\nfunc Main(path string, engine string) error {\n\tvar thirdConf config.Third\n\tif err := readConf(filepath.Join(path, thirdConf.GetConfigFileName()), &thirdConf); err != nil {\n\t\treturn err\n\t}\n\tif thirdConf.Object.Enable == engine {\n\t\treturn errors.New(\"same s3 storage\")\n\t}\n\ts3db, err := getMongo(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\toldS3, err := getS3(path, engine, &thirdConf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tnewS3, err := getS3(path, thirdConf.Object.Enable, &thirdConf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcount, err := getEngineCount(s3db, oldS3.Engine())\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Printf(\"engine %s count: %d\", oldS3.Engine(), count)\n\tvar skip int\n\tfor i := 1; i <= count+1; i++ {\n\t\tlog.Printf(\"start %d/%d\", i, count)\n\t\tstart := time.Now()\n\t\tres, err := doObject(s3db, newS3, oldS3, skip)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"end [%s] %d/%d error %s\", time.Since(start), i, count, err)\n\t\t\treturn err\n\t\t}\n\t\tlog.Printf(\"end [%s] %d/%d result %+v\", time.Since(start), i, count, *res)\n\t\tif res.Skip {\n\t\t\tskip++\n\t\t}\n\t\tif res.End {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getEngineCount(db database.ObjectInfo, name string) (int, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)\n\tdefer cancel()\n\tcount, err := db.GetEngineCount(ctx, name)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int(count), nil\n}\n\nfunc doObject(db database.ObjectInfo, newS3, oldS3 s3.Interface, skip int) (*Result, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)\n\tdefer cancel()\n\tinfos, err := db.GetEngineInfo(ctx, oldS3.Engine(), 1, skip)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(infos) == 0 {\n\t\treturn &Result{End: true}, nil\n\t}\n\tobj := infos[0]\n\tif _, err := db.Take(ctx, newS3.Engine(), obj.Name); err == nil {\n\t\treturn &Result{Skip: true}, nil\n\t} else if !errors.Is(err, mongo.ErrNoDocuments) {\n\t\treturn nil, err\n\t}\n\tdownloadURL, err := oldS3.AccessURL(ctx, obj.Key, time.Hour, &s3.AccessURLOption{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tputURL, err := newS3.PresignedPutObject(ctx, obj.Key, time.Hour, &s3.PutOption{ContentType: obj.ContentType})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdownloadResp, err := http.Get(downloadURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer downloadResp.Body.Close()\n\tswitch downloadResp.StatusCode {\n\tcase http.StatusNotFound:\n\t\treturn &Result{Skip: true}, nil\n\tcase http.StatusOK:\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"download object failed %s\", downloadResp.Status)\n\t}\n\tlog.Printf(\"file size %d\", obj.Size)\n\trequest, err := http.NewRequest(http.MethodPut, putURL.URL, downloadResp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tputResp, err := http.DefaultClient.Do(request)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer putResp.Body.Close()\n\tif putResp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"put object failed %s\", putResp.Status)\n\t}\n\tctx, cancel = context.WithTimeout(context.Background(), defaultTimeout)\n\tdefer cancel()\n\tif err := db.UpdateEngine(ctx, obj.Engine, obj.Name, newS3.Engine()); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Result{}, nil\n}\n\ntype Result struct {\n\tSkip bool\n\tEnd  bool\n}\n"
  },
  {
    "path": "tools/s3/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"github.com/openimsdk/open-im-server/v3/tools/s3/internal\"\n\t\"os\"\n)\n\nfunc main() {\n\tvar (\n\t\tname   string\n\t\tconfig string\n\t)\n\tflag.StringVar(&name, \"name\", \"\", \"old previous storage name\")\n\tflag.StringVar(&config, \"config\", \"\", \"config directory\")\n\tflag.Parse()\n\tif err := internal.Main(config, name); err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n\tfmt.Fprintln(os.Stdout, \"success\")\n}\n"
  },
  {
    "path": "tools/seq/internal/seq.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/mitchellh/mapstructure\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo\"\n\t\"github.com/openimsdk/tools/db/mongoutil\"\n\t\"github.com/openimsdk/tools/db/redisutil\"\n\t\"github.com/openimsdk/tools/utils/runtimeenv\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/spf13/viper\"\n\t\"go.mongodb.org/mongo-driver/bson\"\n\t\"go.mongodb.org/mongo-driver/mongo\"\n\t\"go.mongodb.org/mongo-driver/mongo/options\"\n)\n\nconst StructTagName = \"yaml\"\n\nconst (\n\tMaxSeq                 = \"MAX_SEQ:\"\n\tMinSeq                 = \"MIN_SEQ:\"\n\tConversationUserMinSeq = \"CON_USER_MIN_SEQ:\"\n\tHasReadSeq             = \"HAS_READ_SEQ:\"\n)\n\nconst (\n\tbatchSize             = 100\n\tdataVersionCollection = \"data_version\"\n\tseqKey                = \"seq\"\n\tseqVersion            = 38\n)\n\nfunc readConfig[T any](dir string, name string) (*T, error) {\n\tif runtimeenv.RuntimeEnvironment() == config.KUBERNETES {\n\t\tdir = os.Getenv(config.MountConfigFilePath)\n\t}\n\tv := viper.New()\n\tv.SetEnvPrefix(config.EnvPrefixMap[name])\n\tv.AutomaticEnv()\n\tv.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))\n\tv.SetConfigFile(filepath.Join(dir, name))\n\tif err := v.ReadInConfig(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar conf T\n\tif err := v.Unmarshal(&conf, func(config *mapstructure.DecoderConfig) {\n\t\tconfig.TagName = StructTagName\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &conf, nil\n}\n\nfunc Main(conf string, del time.Duration) error {\n\tredisConfig, err := readConfig[config.Redis](conf, config.RedisConfigFileName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmongodbConfig, err := readConfig[config.Mongo](conf, config.MongodbConfigFileName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\n\tdefer cancel()\n\trdb, err := redisutil.NewRedisClient(ctx, redisConfig.Build())\n\tif err != nil {\n\t\treturn err\n\t}\n\tmgocli, err := mongoutil.NewMongoDB(ctx, mongodbConfig.Build())\n\tif err != nil {\n\t\treturn err\n\t}\n\tversionColl := mgocli.GetDB().Collection(dataVersionCollection)\n\tconverted, err := CheckVersion(versionColl, seqKey, seqVersion)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif converted {\n\t\tfmt.Println(\"[seq] seq data has been converted\")\n\t\treturn nil\n\t}\n\tif _, err := mgo.NewSeqConversationMongo(mgocli.GetDB()); err != nil {\n\t\treturn err\n\t}\n\tcSeq, err := mgo.NewSeqConversationMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tuSeq, err := mgo.NewSeqUserMongo(mgocli.GetDB())\n\tif err != nil {\n\t\treturn err\n\t}\n\tuSpitHasReadSeq := func(id string) (conversationID string, userID string, err error) {\n\t\t// HasReadSeq + userID + \":\" + conversationID\n\t\tarr := strings.Split(id, \":\")\n\t\tif len(arr) != 2 || arr[0] == \"\" || arr[1] == \"\" {\n\t\t\treturn \"\", \"\", fmt.Errorf(\"invalid has read seq id %s\", id)\n\t\t}\n\t\tuserID = arr[0]\n\t\tconversationID = arr[1]\n\t\treturn\n\t}\n\tuSpitConversationUserMinSeq := func(id string) (conversationID string, userID string, err error) {\n\t\t// ConversationUserMinSeq + conversationID + \"u:\" + userID\n\t\tarr := strings.Split(id, \"u:\")\n\t\tif len(arr) != 2 || arr[0] == \"\" || arr[1] == \"\" {\n\t\t\treturn \"\", \"\", fmt.Errorf(\"invalid has read seq id %s\", id)\n\t\t}\n\t\tconversationID = arr[0]\n\t\tuserID = arr[1]\n\t\treturn\n\t}\n\n\tts := []*taskSeq{\n\t\t{\n\t\t\tPrefix: MaxSeq,\n\t\t\tGetSeq: cSeq.GetMaxSeq,\n\t\t\tSetSeq: cSeq.SetMaxSeq,\n\t\t},\n\t\t{\n\t\t\tPrefix: MinSeq,\n\t\t\tGetSeq: cSeq.GetMinSeq,\n\t\t\tSetSeq: cSeq.SetMinSeq,\n\t\t},\n\t\t{\n\t\t\tPrefix: HasReadSeq,\n\t\t\tGetSeq: func(ctx context.Context, id string) (int64, error) {\n\t\t\t\tconversationID, userID, err := uSpitHasReadSeq(id)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn 0, err\n\t\t\t\t}\n\t\t\t\treturn uSeq.GetUserReadSeq(ctx, conversationID, userID)\n\t\t\t},\n\t\t\tSetSeq: func(ctx context.Context, id string, seq int64) error {\n\t\t\t\tconversationID, userID, err := uSpitHasReadSeq(id)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn uSeq.SetUserReadSeq(ctx, conversationID, userID, seq)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tPrefix: ConversationUserMinSeq,\n\t\t\tGetSeq: func(ctx context.Context, id string) (int64, error) {\n\t\t\t\tconversationID, userID, err := uSpitConversationUserMinSeq(id)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn 0, err\n\t\t\t\t}\n\t\t\t\treturn uSeq.GetUserMinSeq(ctx, conversationID, userID)\n\t\t\t},\n\t\t\tSetSeq: func(ctx context.Context, id string, seq int64) error {\n\t\t\t\tconversationID, userID, err := uSpitConversationUserMinSeq(id)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn uSeq.SetUserMinSeq(ctx, conversationID, userID, seq)\n\t\t\t},\n\t\t},\n\t}\n\n\tcancel()\n\tctx = context.Background()\n\n\tvar wg sync.WaitGroup\n\twg.Add(len(ts))\n\n\tfor i := range ts {\n\t\tgo func(task *taskSeq) {\n\t\t\tdefer wg.Done()\n\t\t\terr := seqRedisToMongo(ctx, rdb, task.GetSeq, task.SetSeq, task.Prefix, del, &task.Count)\n\t\t\ttask.End = time.Now()\n\t\t\ttask.Error = err\n\t\t}(ts[i])\n\t}\n\tstart := time.Now()\n\tdone := make(chan struct{})\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(done)\n\t}()\n\n\tsigs := make(chan os.Signal, 1)\n\tsignal.Notify(sigs, syscall.SIGTERM)\n\n\tticker := time.NewTicker(time.Second)\n\tdefer ticker.Stop()\n\tvar buf bytes.Buffer\n\n\tprintTaskInfo := func(now time.Time) {\n\t\tbuf.Reset()\n\t\tbuf.WriteString(now.Format(time.DateTime))\n\t\tbuf.WriteString(\" \\n\")\n\t\tfor i := range ts {\n\t\t\ttask := ts[i]\n\t\t\tif task.Error == nil {\n\t\t\t\tif task.End.IsZero() {\n\t\t\t\t\tbuf.WriteString(fmt.Sprintf(\"[%s] converting %s* count %d\", now.Sub(start), task.Prefix, atomic.LoadInt64(&task.Count)))\n\t\t\t\t} else {\n\t\t\t\t\tbuf.WriteString(fmt.Sprintf(\"[%s] success %s* count %d\", task.End.Sub(start), task.Prefix, atomic.LoadInt64(&task.Count)))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbuf.WriteString(fmt.Sprintf(\"[%s] failed %s* count %d error %s\", task.End.Sub(start), task.Prefix, atomic.LoadInt64(&task.Count), task.Error))\n\t\t\t}\n\t\t\tbuf.WriteString(\"\\n\")\n\t\t}\n\t\tfmt.Println(buf.String())\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tcase s := <-sigs:\n\t\t\treturn fmt.Errorf(\"exit by signal %s\", s)\n\t\tcase <-done:\n\t\t\terrs := make([]error, 0, len(ts))\n\t\t\tfor i := range ts {\n\t\t\t\ttask := ts[i]\n\t\t\t\tif task.Error != nil {\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"seq %s failed %w\", task.Prefix, task.Error))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(errs) > 0 {\n\t\t\t\treturn errors.Join(errs...)\n\t\t\t}\n\t\t\tprintTaskInfo(time.Now())\n\t\t\tif err := SetVersion(versionColl, seqKey, seqVersion); err != nil {\n\t\t\t\treturn fmt.Errorf(\"set mongodb seq version %w\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\tcase now := <-ticker.C:\n\t\t\tprintTaskInfo(now)\n\t\t}\n\t}\n}\n\ntype taskSeq struct {\n\tPrefix string\n\tCount  int64\n\tError  error\n\tEnd    time.Time\n\tGetSeq func(ctx context.Context, id string) (int64, error)\n\tSetSeq func(ctx context.Context, id string, seq int64) error\n}\n\nfunc seqRedisToMongo(ctx context.Context, rdb redis.UniversalClient, getSeq func(ctx context.Context, id string) (int64, error), setSeq func(ctx context.Context, id string, seq int64) error, prefix string, delAfter time.Duration, count *int64) error {\n\tvar (\n\t\tcursor uint64\n\t\tkeys   []string\n\t\terr    error\n\t)\n\tfor {\n\t\tkeys, cursor, err = rdb.Scan(ctx, cursor, prefix+\"*\", batchSize).Result()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(keys) > 0 {\n\t\t\tfor _, key := range keys {\n\t\t\t\tseqStr, err := rdb.Get(ctx, key).Result()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"redis get %s failed %w\", key, err)\n\t\t\t\t}\n\t\t\t\tseq, err := strconv.Atoi(seqStr)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"invalid %s seq %s\", key, seqStr)\n\t\t\t\t}\n\t\t\t\tif seq < 0 {\n\t\t\t\t\treturn fmt.Errorf(\"invalid %s seq %s\", key, seqStr)\n\t\t\t\t}\n\t\t\t\tid := strings.TrimPrefix(key, prefix)\n\t\t\t\tredisSeq := int64(seq)\n\t\t\t\tmongoSeq, err := getSeq(ctx, id)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"get mongo seq %s failed %w\", key, err)\n\t\t\t\t}\n\t\t\t\tif mongoSeq < redisSeq {\n\t\t\t\t\tif err := setSeq(ctx, id, redisSeq); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"set mongo seq %s failed %w\", key, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif delAfter > 0 {\n\t\t\t\t\tif err := rdb.Expire(ctx, key, delAfter).Err(); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"redis expire key %s failed %w\", key, err)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif err := rdb.Del(ctx, key).Err(); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"redis del key %s failed %w\", key, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tatomic.AddInt64(count, 1)\n\t\t\t}\n\t\t}\n\t\tif cursor == 0 {\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc CheckVersion(coll *mongo.Collection, key string, currentVersion int) (converted bool, err error) {\n\ttype VersionTable struct {\n\t\tKey   string `bson:\"key\"`\n\t\tValue string `bson:\"value\"`\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*5)\n\tdefer cancel()\n\tres, err := mongoutil.FindOne[VersionTable](ctx, coll, bson.M{\"key\": key})\n\tif err == nil {\n\t\tver, err := strconv.Atoi(res.Value)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"version %s parse error %w\", res.Value, err)\n\t\t}\n\t\tif ver >= currentVersion {\n\t\t\treturn true, nil\n\t\t}\n\t\treturn false, nil\n\t} else if errors.Is(err, mongo.ErrNoDocuments) {\n\t\treturn false, nil\n\t} else {\n\t\treturn false, err\n\t}\n}\n\nfunc SetVersion(coll *mongo.Collection, key string, version int) error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*5)\n\tdefer cancel()\n\toption := options.Update().SetUpsert(true)\n\tfilter := bson.M{\"key\": key}\n\tupdate := bson.M{\"$set\": bson.M{\"key\": key, \"value\": strconv.Itoa(version)}}\n\treturn mongoutil.UpdateOne(ctx, coll, filter, update, false, option)\n}\n"
  },
  {
    "path": "tools/seq/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/tools/seq/internal\"\n)\n\nfunc main() {\n\tvar (\n\t\tconfig string\n\t\tsecond int\n\t)\n\tflag.StringVar(&config, \"c\", \"\", \"config directory\")\n\tflag.IntVar(&second, \"sec\", 3600*24, \"delayed deletion of the original seq key after conversion\")\n\tflag.Parse()\n\tif err := internal.Main(config, time.Duration(second)*time.Second); err != nil {\n\t\tfmt.Println(\"seq task\", err)\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\tfmt.Println(\"seq task success!\")\n}\n"
  },
  {
    "path": "tools/stress-test-v2/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/pkg/apistruct\"\n\t\"github.com/openimsdk/open-im-server/v3/pkg/common/config\"\n\t\"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/protocol/constant\"\n\t\"github.com/openimsdk/protocol/group\"\n\t\"github.com/openimsdk/protocol/sdkws\"\n\tpbuser \"github.com/openimsdk/protocol/user\"\n\t\"github.com/openimsdk/tools/log\"\n\t\"github.com/openimsdk/tools/system/program\"\n)\n\n// 1. Create 100K New Users\n// 2. Create 100 100K Groups\n// 3. Create 1000 999 Groups\n// 4. Send message to 100K Groups every second\n// 5. Send message to 999 Groups every minute\n\nvar (\n\t//  Use default userIDs List for testing, need to be created.\n\tTestTargetUserList = []string{\n\t\t// \"<need-update-it>\",\n\t}\n\t// DefaultGroupID = \"<need-update-it>\" // Use default group ID for testing, need to be created.\n)\n\nvar (\n\tApiAddress string\n\n\t// API method\n\tGetAdminToken      = \"/auth/get_admin_token\"\n\tUserCheck          = \"/user/account_check\"\n\tCreateUser         = \"/user/user_register\"\n\tImportFriend       = \"/friend/import_friend\"\n\tInviteToGroup      = \"/group/invite_user_to_group\"\n\tGetGroupMemberInfo = \"/group/get_group_members_info\"\n\tSendMsg            = \"/msg/send_msg\"\n\tCreateGroup        = \"/group/create_group\"\n\tGetUserToken       = \"/auth/user_token\"\n)\n\nconst (\n\tMaxUser            = 100000\n\tMax100KGroup       = 100\n\tMax999Group        = 1000\n\tMaxInviteUserLimit = 999\n\n\tCreateUserTicker         = 1 * time.Second\n\tCreateGroupTicker        = 1 * time.Second\n\tCreate100KGroupTicker    = 1 * time.Second\n\tCreate999GroupTicker     = 1 * time.Second\n\tSendMsgTo100KGroupTicker = 1 * time.Second\n\tSendMsgTo999GroupTicker  = 1 * time.Minute\n)\n\ntype BaseResp struct {\n\tErrCode int             `json:\"errCode\"`\n\tErrMsg  string          `json:\"errMsg\"`\n\tData    json.RawMessage `json:\"data\"`\n}\n\ntype StressTest struct {\n\tConf                   *conf\n\tAdminUserID            string\n\tAdminToken             string\n\tDefaultGroupID         string\n\tDefaultUserID          string\n\tUserCounter            int\n\tCreateUserCounter      int\n\tCreate100kGroupCounter int\n\tCreate999GroupCounter  int\n\tMsgCounter             int\n\tCreatedUsers           []string\n\tCreatedGroups          []string\n\tMutex                  sync.Mutex\n\tCtx                    context.Context\n\tCancel                 context.CancelFunc\n\tHttpClient             *http.Client\n\tWg                     sync.WaitGroup\n\tOnce                   sync.Once\n}\n\ntype conf struct {\n\tShare config.Share\n\tApi   config.API\n}\n\nfunc initConfig(configDir string) (*config.Share, *config.API, error) {\n\tvar (\n\t\tshare     = &config.Share{}\n\t\tapiConfig = &config.API{}\n\t)\n\n\terr := config.Load(configDir, config.ShareFileName, config.EnvPrefixMap[config.ShareFileName], share)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\terr = config.Load(configDir, config.OpenIMAPICfgFileName, config.EnvPrefixMap[config.OpenIMAPICfgFileName], apiConfig)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn share, apiConfig, nil\n}\n\n// Post Request\nfunc (st *StressTest) PostRequest(ctx context.Context, url string, reqbody any) ([]byte, error) {\n\t// Marshal body\n\tjsonBody, err := json.Marshal(reqbody)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to marshal request body\", err, \"url\", url, \"reqbody\", reqbody)\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"operationID\", st.AdminUserID)\n\tif st.AdminToken != \"\" {\n\t\treq.Header.Set(\"token\", st.AdminToken)\n\t}\n\n\t// log.ZInfo(ctx, \"Header info is \", \"Content-Type\", \"application/json\", \"operationID\", st.AdminUserID, \"token\", st.AdminToken)\n\n\tresp, err := st.HttpClient.Do(req)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to send request\", err, \"url\", url, \"reqbody\", reqbody)\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to read response body\", err, \"url\", url)\n\t\treturn nil, err\n\t}\n\n\tvar baseResp BaseResp\n\tif err := json.Unmarshal(respBody, &baseResp); err != nil {\n\t\tlog.ZError(ctx, \"Failed to unmarshal response body\", err, \"url\", url, \"respBody\", string(respBody))\n\t\treturn nil, err\n\t}\n\n\tif baseResp.ErrCode != 0 {\n\t\terr = fmt.Errorf(baseResp.ErrMsg)\n\t\tlog.ZError(ctx, \"Failed to send request\", err, \"url\", url, \"reqbody\", reqbody, \"resp\", baseResp)\n\t\treturn nil, err\n\t}\n\n\treturn baseResp.Data, nil\n}\n\nfunc (st *StressTest) GetAdminToken(ctx context.Context) (string, error) {\n\treq := auth.GetAdminTokenReq{\n\t\tSecret: st.Conf.Share.Secret,\n\t\tUserID: st.AdminUserID,\n\t}\n\n\tresp, err := st.PostRequest(ctx, ApiAddress+GetAdminToken, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdata := &auth.GetAdminTokenResp{}\n\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn data.Token, nil\n}\n\nfunc (st *StressTest) CheckUser(ctx context.Context, userIDs []string) ([]string, error) {\n\treq := pbuser.AccountCheckReq{\n\t\tCheckUserIDs: userIDs,\n\t}\n\n\tresp, err := st.PostRequest(ctx, ApiAddress+UserCheck, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata := &pbuser.AccountCheckResp{}\n\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\treturn nil, err\n\t}\n\n\tunRegisteredUserIDs := make([]string, 0)\n\n\tfor _, res := range data.Results {\n\t\tif res.AccountStatus == constant.UnRegistered {\n\t\t\tunRegisteredUserIDs = append(unRegisteredUserIDs, res.UserID)\n\t\t}\n\t}\n\n\treturn unRegisteredUserIDs, nil\n}\n\nfunc (st *StressTest) CreateUser(ctx context.Context, userID string) (string, error) {\n\tuser := &sdkws.UserInfo{\n\t\tUserID:   userID,\n\t\tNickname: userID,\n\t}\n\n\treq := pbuser.UserRegisterReq{\n\t\tUsers: []*sdkws.UserInfo{user},\n\t}\n\n\t_, err := st.PostRequest(ctx, ApiAddress+CreateUser, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tst.UserCounter++\n\treturn userID, nil\n}\n\nfunc (st *StressTest) CreateUserBatch(ctx context.Context, userIDs []string) error {\n\t// The method can import a large number of users at once.\n\tvar userList []*sdkws.UserInfo\n\n\tdefer st.Once.Do(\n\t\tfunc() {\n\t\t\tst.DefaultUserID = userIDs[0]\n\t\t\tfmt.Println(\"Default Send User Created ID:\", st.DefaultUserID)\n\t\t})\n\n\tneedUserIDs, err := st.CheckUser(ctx, userIDs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, userID := range needUserIDs {\n\t\tuser := &sdkws.UserInfo{\n\t\t\tUserID:   userID,\n\t\t\tNickname: userID,\n\t\t}\n\t\tuserList = append(userList, user)\n\t}\n\n\treq := pbuser.UserRegisterReq{\n\t\tUsers: userList,\n\t}\n\n\t_, err = st.PostRequest(ctx, ApiAddress+CreateUser, &req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tst.UserCounter += len(userList)\n\treturn nil\n}\n\nfunc (st *StressTest) GetGroupMembersInfo(ctx context.Context, groupID string, userIDs []string) ([]string, error) {\n\tneedInviteUserIDs := make([]string, 0)\n\n\tconst maxBatchSize = 500\n\tif len(userIDs) > maxBatchSize {\n\t\tfor i := 0; i < len(userIDs); i += maxBatchSize {\n\t\t\tend := min(i+maxBatchSize, len(userIDs))\n\t\t\tbatchUserIDs := userIDs[i:end]\n\n\t\t\t// log.ZInfo(ctx, \"Processing group members batch\", \"groupID\", groupID, \"batch\", i/maxBatchSize+1,\n\t\t\t// \t\"batchUserCount\", len(batchUserIDs))\n\n\t\t\t// Process a single batch\n\t\t\tbatchReq := group.GetGroupMembersInfoReq{\n\t\t\t\tGroupID: groupID,\n\t\t\t\tUserIDs: batchUserIDs,\n\t\t\t}\n\n\t\t\tresp, err := st.PostRequest(ctx, ApiAddress+GetGroupMemberInfo, &batchReq)\n\t\t\tif err != nil {\n\t\t\t\tlog.ZError(ctx, \"Batch query failed\", err, \"batch\", i/maxBatchSize+1)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdata := &group.GetGroupMembersInfoResp{}\n\t\t\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\t\t\tlog.ZError(ctx, \"Failed to parse batch response\", err, \"batch\", i/maxBatchSize+1)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Process the batch results\n\t\t\texistingMembers := make(map[string]bool)\n\t\t\tfor _, member := range data.Members {\n\t\t\t\texistingMembers[member.UserID] = true\n\t\t\t}\n\n\t\t\tfor _, userID := range batchUserIDs {\n\t\t\t\tif !existingMembers[userID] {\n\t\t\t\t\tneedInviteUserIDs = append(needInviteUserIDs, userID)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn needInviteUserIDs, nil\n\t}\n\n\treq := group.GetGroupMembersInfoReq{\n\t\tGroupID: groupID,\n\t\tUserIDs: userIDs,\n\t}\n\n\tresp, err := st.PostRequest(ctx, ApiAddress+GetGroupMemberInfo, &req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata := &group.GetGroupMembersInfoResp{}\n\tif err := json.Unmarshal(resp, &data); err != nil {\n\t\treturn nil, err\n\t}\n\n\texistingMembers := make(map[string]bool)\n\tfor _, member := range data.Members {\n\t\texistingMembers[member.UserID] = true\n\t}\n\n\tfor _, userID := range userIDs {\n\t\tif !existingMembers[userID] {\n\t\t\tneedInviteUserIDs = append(needInviteUserIDs, userID)\n\t\t}\n\t}\n\n\treturn needInviteUserIDs, nil\n}\n\nfunc (st *StressTest) InviteToGroup(ctx context.Context, groupID string, userIDs []string) error {\n\treq := group.InviteUserToGroupReq{\n\t\tGroupID:        groupID,\n\t\tInvitedUserIDs: userIDs,\n\t}\n\t_, err := st.PostRequest(ctx, ApiAddress+InviteToGroup, &req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (st *StressTest) SendMsg(ctx context.Context, userID string, groupID string) error {\n\tcontentObj := map[string]any{\n\t\t// \"content\": fmt.Sprintf(\"index %d. The current time is %s\", st.MsgCounter, time.Now().Format(\"2006-01-02 15:04:05.000\")),\n\t\t\"content\": fmt.Sprintf(\"The current time is %s\", time.Now().Format(\"2006-01-02 15:04:05.000\")),\n\t}\n\n\treq := &apistruct.SendMsgReq{\n\t\tSendMsg: apistruct.SendMsg{\n\t\t\tSendID:         userID,\n\t\t\tSenderNickname: userID,\n\t\t\tGroupID:        groupID,\n\t\t\tContentType:    constant.Text,\n\t\t\tSessionType:    constant.ReadGroupChatType,\n\t\t\tContent:        contentObj,\n\t\t},\n\t}\n\n\t_, err := st.PostRequest(ctx, ApiAddress+SendMsg, &req)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Failed to send message\", err, \"userID\", userID, \"req\", &req)\n\t\treturn err\n\t}\n\n\tst.MsgCounter++\n\n\treturn nil\n}\n\n// Max userIDs number is 1000\nfunc (st *StressTest) CreateGroup(ctx context.Context, groupID string, userID string, userIDsList []string) (string, error) {\n\tgroupInfo := &sdkws.GroupInfo{\n\t\tGroupID:   groupID,\n\t\tGroupName: groupID,\n\t\tGroupType: constant.WorkingGroup,\n\t}\n\n\treq := group.CreateGroupReq{\n\t\tOwnerUserID:   userID,\n\t\tMemberUserIDs: userIDsList,\n\t\tGroupInfo:     groupInfo,\n\t}\n\n\tresp := group.CreateGroupResp{}\n\n\tresponse, err := st.PostRequest(ctx, ApiAddress+CreateGroup, &req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := json.Unmarshal(response, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// st.GroupCounter++\n\n\treturn resp.GroupInfo.GroupID, nil\n}\n\nfunc main() {\n\tvar configPath string\n\t// defaultConfigDir := filepath.Join(\"..\", \"..\", \"..\", \"..\", \"..\", \"config\")\n\t// flag.StringVar(&configPath, \"c\", defaultConfigDir, \"config path\")\n\tflag.StringVar(&configPath, \"c\", \"\", \"config path\")\n\tflag.Parse()\n\n\tif configPath == \"\" {\n\t\t_, _ = fmt.Fprintln(os.Stderr, \"config path is empty\")\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\n\tfmt.Printf(\" Config Path: %s\\n\", configPath)\n\n\tshare, apiConfig, err := initConfig(configPath)\n\tif err != nil {\n\t\tprogram.ExitWithError(err)\n\t\treturn\n\t}\n\n\tApiAddress = fmt.Sprintf(\"http://%s:%s\", \"127.0.0.1\", fmt.Sprint(apiConfig.Api.Ports[0]))\n\n\tctx, cancel := context.WithCancel(context.Background())\n\t// ch := make(chan struct{})\n\n\tst := &StressTest{\n\t\tConf: &conf{\n\t\t\tShare: *share,\n\t\t\tApi:   *apiConfig,\n\t\t},\n\t\tAdminUserID: share.IMAdminUser.UserIDs[0],\n\t\tCtx:         ctx,\n\t\tCancel:      cancel,\n\t\tHttpClient: &http.Client{\n\t\t\tTimeout: 50 * time.Second,\n\t\t},\n\t}\n\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt, syscall.SIGTERM)\n\tgo func() {\n\t\t<-c\n\t\tfmt.Println(\"\\nReceived stop signal, stopping...\")\n\n\t\tgo func() {\n\t\t\t// time.Sleep(5 * time.Second)\n\t\t\tfmt.Println(\"Force exit\")\n\t\t\tos.Exit(0)\n\t\t}()\n\n\t\tst.Cancel()\n\t}()\n\n\ttoken, err := st.GetAdminToken(st.Ctx)\n\tif err != nil {\n\t\tlog.ZError(ctx, \"Get Admin Token failed.\", err, \"AdminUserID\", st.AdminUserID)\n\t}\n\n\tst.AdminToken = token\n\tfmt.Println(\"Admin Token:\", st.AdminToken)\n\tfmt.Println(\"ApiAddress:\", ApiAddress)\n\tfor i := 0; i < MaxUser; i++ {\n\t\tuserID := fmt.Sprintf(\"v2_StressTest_User_%d\", i)\n\t\tst.CreatedUsers = append(st.CreatedUsers, userID)\n\t\tst.CreateUserCounter++\n\t}\n\n\t// err = st.CreateUserBatch(st.Ctx, st.CreatedUsers)\n\t// if err != nil {\n\t// \tlog.ZError(ctx, \"Create user failed.\", err)\n\t// }\n\n\tconst batchSize = 1000\n\ttotalUsers := len(st.CreatedUsers)\n\tsuccessCount := 0\n\n\tif st.DefaultUserID == \"\" && len(st.CreatedUsers) > 0 {\n\t\tst.DefaultUserID = st.CreatedUsers[0]\n\t}\n\n\tfor i := 0; i < totalUsers; i += batchSize {\n\t\tend := min(i+batchSize, totalUsers)\n\n\t\tuserBatch := st.CreatedUsers[i:end]\n\t\tlog.ZInfo(st.Ctx, \"Creating user batch\", \"batch\", i/batchSize+1, \"count\", len(userBatch))\n\n\t\terr = st.CreateUserBatch(st.Ctx, userBatch)\n\t\tif err != nil {\n\t\t\tlog.ZError(st.Ctx, \"Batch user creation failed\", err, \"batch\", i/batchSize+1)\n\t\t} else {\n\t\t\tsuccessCount += len(userBatch)\n\t\t\tlog.ZInfo(st.Ctx, \"Batch user creation succeeded\", \"batch\", i/batchSize+1,\n\t\t\t\t\"progress\", fmt.Sprintf(\"%d/%d\", successCount, totalUsers))\n\t\t}\n\t}\n\n\t// Execute create 100k group\n\tst.Wg.Add(1)\n\tgo func() {\n\t\tdefer st.Wg.Done()\n\n\t\tcreate100kGroupTicker := time.NewTicker(Create100KGroupTicker)\n\t\tdefer create100kGroupTicker.Stop()\n\n\t\tfor i := 0; i < Max100KGroup; i++ {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Create 100K Group\")\n\t\t\t\treturn\n\n\t\t\tcase <-create100kGroupTicker.C:\n\t\t\t\t// Create 100K groups\n\t\t\t\tst.Wg.Add(1)\n\t\t\t\tgo func(idx int) {\n\t\t\t\t\tdefer st.Wg.Done()\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\tst.Create100kGroupCounter++\n\t\t\t\t\t}()\n\n\t\t\t\t\tgroupID := fmt.Sprintf(\"v2_StressTest_Group_100K_%d\", idx)\n\n\t\t\t\t\tif _, err = st.CreateGroup(st.Ctx, groupID, st.DefaultUserID, TestTargetUserList); err != nil {\n\t\t\t\t\t\tlog.ZError(st.Ctx, \"Create group failed.\", err)\n\t\t\t\t\t\t// continue\n\t\t\t\t\t}\n\n\t\t\t\t\tfor i := 0; i < MaxUser/MaxInviteUserLimit; i++ {\n\t\t\t\t\t\tInviteUserIDs := make([]string, 0)\n\t\t\t\t\t\t// ensure TargetUserList is in group\n\t\t\t\t\t\tInviteUserIDs = append(InviteUserIDs, TestTargetUserList...)\n\n\t\t\t\t\t\tstartIdx := max(i*MaxInviteUserLimit, 1)\n\t\t\t\t\t\tendIdx := min((i+1)*MaxInviteUserLimit, MaxUser)\n\n\t\t\t\t\t\tfor j := startIdx; j < endIdx; j++ {\n\t\t\t\t\t\t\tuserCreatedID := fmt.Sprintf(\"v2_StressTest_User_%d\", j)\n\t\t\t\t\t\t\tInviteUserIDs = append(InviteUserIDs, userCreatedID)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(InviteUserIDs) == 0 {\n\t\t\t\t\t\t\tlog.ZWarn(st.Ctx, \"InviteUserIDs is empty\", nil, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tInviteUserIDs, err := st.GetGroupMembersInfo(ctx, groupID, InviteUserIDs)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"GetGroupMembersInfo failed.\", err, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(InviteUserIDs) == 0 {\n\t\t\t\t\t\t\tlog.ZWarn(st.Ctx, \"InviteUserIDs is empty\", nil, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Invite To Group\n\t\t\t\t\t\tif err = st.InviteToGroup(st.Ctx, groupID, InviteUserIDs); err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"Invite To Group failed.\", err, \"UserID\", InviteUserIDs)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t// os.Exit(1)\n\t\t\t\t\t\t\t// return\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}(i)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// create 999 groups\n\tst.Wg.Add(1)\n\tgo func() {\n\t\tdefer st.Wg.Done()\n\n\t\tcreate999GroupTicker := time.NewTicker(Create999GroupTicker)\n\t\tdefer create999GroupTicker.Stop()\n\n\t\tfor i := 0; i < Max999Group; i++ {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Create 999 Group\")\n\t\t\t\treturn\n\n\t\t\tcase <-create999GroupTicker.C:\n\t\t\t\t// Create 999 groups\n\t\t\t\tst.Wg.Add(1)\n\t\t\t\tgo func(idx int) {\n\t\t\t\t\tdefer st.Wg.Done()\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\tst.Create999GroupCounter++\n\t\t\t\t\t}()\n\n\t\t\t\t\tgroupID := fmt.Sprintf(\"v2_StressTest_Group_1K_%d\", idx)\n\n\t\t\t\t\tif _, err = st.CreateGroup(st.Ctx, groupID, st.DefaultUserID, TestTargetUserList); err != nil {\n\t\t\t\t\t\tlog.ZError(st.Ctx, \"Create group failed.\", err)\n\t\t\t\t\t\t// continue\n\t\t\t\t\t}\n\t\t\t\t\tfor i := 0; i < MaxUser/MaxInviteUserLimit; i++ {\n\t\t\t\t\t\tInviteUserIDs := make([]string, 0)\n\t\t\t\t\t\t// ensure TargetUserList is in group\n\t\t\t\t\t\tInviteUserIDs = append(InviteUserIDs, TestTargetUserList...)\n\n\t\t\t\t\t\tstartIdx := max(i*MaxInviteUserLimit, 1)\n\t\t\t\t\t\tendIdx := min((i+1)*MaxInviteUserLimit, MaxUser)\n\n\t\t\t\t\t\tfor j := startIdx; j < endIdx; j++ {\n\t\t\t\t\t\t\tuserCreatedID := fmt.Sprintf(\"v2_StressTest_User_%d\", j)\n\t\t\t\t\t\t\tInviteUserIDs = append(InviteUserIDs, userCreatedID)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(InviteUserIDs) == 0 {\n\t\t\t\t\t\t\tlog.ZWarn(st.Ctx, \"InviteUserIDs is empty\", nil, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tInviteUserIDs, err := st.GetGroupMembersInfo(ctx, groupID, InviteUserIDs)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"GetGroupMembersInfo failed.\", err, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(InviteUserIDs) == 0 {\n\t\t\t\t\t\t\tlog.ZWarn(st.Ctx, \"InviteUserIDs is empty\", nil, \"groupID\", groupID)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Invite To Group\n\t\t\t\t\t\tif err = st.InviteToGroup(st.Ctx, groupID, InviteUserIDs); err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"Invite To Group failed.\", err, \"UserID\", InviteUserIDs)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t// os.Exit(1)\n\t\t\t\t\t\t\t// return\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}(i)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Send message to 100K groups\n\tst.Wg.Wait()\n\tfmt.Println(\"All groups created successfully, starting to send messages...\")\n\tlog.ZInfo(ctx, \"All groups created successfully, starting to send messages...\")\n\n\tvar groups100K []string\n\tvar groups999 []string\n\n\tfor i := 0; i < Max100KGroup; i++ {\n\t\tgroupID := fmt.Sprintf(\"v2_StressTest_Group_100K_%d\", i)\n\t\tgroups100K = append(groups100K, groupID)\n\t}\n\n\tfor i := 0; i < Max999Group; i++ {\n\t\tgroupID := fmt.Sprintf(\"v2_StressTest_Group_1K_%d\", i)\n\t\tgroups999 = append(groups999, groupID)\n\t}\n\n\tsend100kGroupLimiter := make(chan struct{}, 20)\n\tsend999GroupLimiter := make(chan struct{}, 100)\n\n\t// execute Send message to 100K groups\n\tgo func() {\n\t\tticker := time.NewTicker(SendMsgTo100KGroupTicker)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Send Message to 100K Group\")\n\t\t\t\treturn\n\n\t\t\tcase <-ticker.C:\n\t\t\t\t// Send message to 100K groups\n\t\t\t\tfor _, groupID := range groups100K {\n\t\t\t\t\tsend100kGroupLimiter <- struct{}{}\n\t\t\t\t\tgo func(groupID string) {\n\t\t\t\t\t\tdefer func() { <-send100kGroupLimiter }()\n\t\t\t\t\t\tif err := st.SendMsg(st.Ctx, st.DefaultUserID, groupID); err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"Send message to 100K group failed.\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}(groupID)\n\t\t\t\t}\n\t\t\t\t// log.ZInfo(st.Ctx, \"Send message to 100K groups successfully.\")\n\t\t\t}\n\t\t}\n\t}()\n\n\t// execute Send message to 999 groups\n\tgo func() {\n\t\tticker := time.NewTicker(SendMsgTo999GroupTicker)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-st.Ctx.Done():\n\t\t\t\tlog.ZInfo(st.Ctx, \"Stop Send Message to 999 Group\")\n\t\t\t\treturn\n\n\t\t\tcase <-ticker.C:\n\t\t\t\t// Send message to 999 groups\n\t\t\t\tfor _, groupID := range groups999 {\n\t\t\t\t\tsend999GroupLimiter <- struct{}{}\n\t\t\t\t\tgo func(groupID string) {\n\t\t\t\t\t\tdefer func() { <-send999GroupLimiter }()\n\n\t\t\t\t\t\tif err := st.SendMsg(st.Ctx, st.DefaultUserID, groupID); err != nil {\n\t\t\t\t\t\t\tlog.ZError(st.Ctx, \"Send message to 999 group failed.\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}(groupID)\n\t\t\t\t}\n\t\t\t\t// log.ZInfo(st.Ctx, \"Send message to 999 groups successfully.\")\n\t\t\t}\n\t\t}\n\t}()\n\n\t<-st.Ctx.Done()\n\tfmt.Println(\"Received signal to exit, shutting down...\")\n}\n"
  },
  {
    "path": "tools/url2im/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/openimsdk/open-im-server/v3/tools/url2im/pkg\"\n)\n\n/*take.txt\n{\"url\":\"http://xxx/xxxx\",\"name\":\"xxxx\",\"contentType\":\"image/jpeg\"}\n{\"url\":\"http://xxx/xxxx\",\"name\":\"xxxx\",\"contentType\":\"image/jpeg\"}\n{\"url\":\"http://xxx/xxxx\",\"name\":\"xxxx\",\"contentType\":\"image/jpeg\"}\n*/\n\nfunc main() {\n\tvar conf pkg.Config // Configuration object, '*' denotes required fields\n\n\t// *Required*: Path for the task log file\n\tflag.StringVar(&conf.TaskPath, \"task\", \"take.txt\", \"Path for the task log file\")\n\n\t// Optional: Path for the progress log file\n\tflag.StringVar(&conf.ProgressPath, \"progress\", \"\", \"Path for the progress log file\")\n\n\t// Number of concurrent operations\n\tflag.IntVar(&conf.Concurrency, \"concurrency\", 1, \"Number of concurrent operations\")\n\n\t// Number of retry attempts\n\tflag.IntVar(&conf.Retry, \"retry\", 1, \"Number of retry attempts\")\n\n\t// Optional: Path for the temporary directory\n\tflag.StringVar(&conf.TempDir, \"temp\", \"\", \"Path for the temporary directory\")\n\n\t// Cache size in bytes (downloads move to disk when exceeded)\n\tflag.Int64Var(&conf.CacheSize, \"cache\", 1024*1024*100, \"Cache size in bytes\")\n\n\t// Request timeout in milliseconds\n\tflag.Int64Var((*int64)(&conf.Timeout), \"timeout\", 5000, \"Request timeout in milliseconds\")\n\n\t// *Required*: API endpoint for the IM service\n\tflag.StringVar(&conf.Api, \"api\", \"http://127.0.0.1:10002\", \"API endpoint for the IM service\")\n\n\t// IM administrator's user ID\n\tflag.StringVar(&conf.UserID, \"userID\", \"openIM123456\", \"IM administrator's user ID\")\n\n\t// Secret for the IM configuration\n\tflag.StringVar(&conf.Secret, \"secret\", \"openIM123\", \"Secret for the IM configuration\")\n\n\tflag.Parse()\n\tif !filepath.IsAbs(conf.TaskPath) {\n\t\tvar err error\n\t\tconf.TaskPath, err = filepath.Abs(conf.TaskPath)\n\t\tif err != nil {\n\t\t\tlog.Println(\"get abs path err:\", err)\n\t\t\treturn\n\t\t}\n\t}\n\tif conf.ProgressPath == \"\" {\n\t\tconf.ProgressPath = conf.TaskPath + \".progress.txt\"\n\t} else if !filepath.IsAbs(conf.ProgressPath) {\n\t\tvar err error\n\t\tconf.ProgressPath, err = filepath.Abs(conf.ProgressPath)\n\t\tif err != nil {\n\t\t\tlog.Println(\"get abs path err:\", err)\n\t\t\treturn\n\t\t}\n\t}\n\tif conf.TempDir == \"\" {\n\t\tconf.TempDir = conf.TaskPath + \".temp\"\n\t}\n\tif info, err := os.Stat(conf.TempDir); err == nil {\n\t\tif !info.IsDir() {\n\t\t\tlog.Printf(\"temp dir %s is not dir\\n\", err)\n\t\t\treturn\n\t\t}\n\t} else if os.IsNotExist(err) {\n\t\tif err := os.MkdirAll(conf.TempDir, os.ModePerm); err != nil {\n\t\t\tlog.Printf(\"mkdir temp dir %s err %+v\\n\", conf.TempDir, err)\n\t\t\treturn\n\t\t}\n\t\tdefer os.RemoveAll(conf.TempDir)\n\t} else {\n\t\tlog.Println(\"get temp dir err:\", err)\n\t\treturn\n\t}\n\tif conf.Concurrency <= 0 {\n\t\tconf.Concurrency = 1\n\t}\n\tif conf.Retry <= 0 {\n\t\tconf.Retry = 1\n\t}\n\tif conf.CacheSize <= 0 {\n\t\tconf.CacheSize = 1024 * 1024 * 100 // 100M\n\t}\n\tif conf.Timeout <= 0 {\n\t\tconf.Timeout = 5000\n\t}\n\tconf.Timeout = conf.Timeout * time.Millisecond\n\tif err := pkg.Run(conf); err != nil {\n\t\tlog.Println(\"main err:\", err)\n\t}\n}\n"
  },
  {
    "path": "tools/url2im/pkg/api.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage pkg\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/openimsdk/protocol/auth\"\n\t\"github.com/openimsdk/protocol/third\"\n\t\"github.com/openimsdk/tools/errs\"\n)\n\ntype Api struct {\n\tApi    string\n\tUserID string\n\tSecret string\n\tToken  string\n\tClient *http.Client\n}\n\nfunc (a *Api) apiPost(ctx context.Context, path string, req any, resp any) error {\n\toperationID, _ := ctx.Value(\"operationID\").(string)\n\tif operationID == \"\" {\n\t\treturn errs.New(\"call api operationID is empty\")\n\t}\n\treqBody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\trequest, err := http.NewRequestWithContext(ctx, http.MethodPost, a.Api+path, bytes.NewReader(reqBody))\n\tif err != nil {\n\t\treturn err\n\t}\n\tDefaultRequestHeader(request.Header)\n\trequest.ContentLength = int64(len(reqBody))\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\trequest.Header.Set(\"operationID\", operationID)\n\tif a.Token != \"\" {\n\t\trequest.Header.Set(\"token\", a.Token)\n\t}\n\tresponse, err := a.Client.Do(request)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer response.Body.Close()\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif response.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"api %s status %s body %s\", path, response.Status, body)\n\t}\n\tvar baseResponse struct {\n\t\tErrCode int             `json:\"errCode\"`\n\t\tErrMsg  string          `json:\"errMsg\"`\n\t\tErrDlt  string          `json:\"errDlt\"`\n\t\tData    json.RawMessage `json:\"data\"`\n\t}\n\tif err := json.Unmarshal(body, &baseResponse); err != nil {\n\t\treturn err\n\t}\n\tif baseResponse.ErrCode != 0 {\n\t\treturn fmt.Errorf(\"api %s errCode %d errMsg %s errDlt %s\", path, baseResponse.ErrCode, baseResponse.ErrMsg, baseResponse.ErrDlt)\n\t}\n\tif resp != nil {\n\t\tif err := json.Unmarshal(baseResponse.Data, resp); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *Api) GetAdminToken(ctx context.Context) (string, error) {\n\treq := auth.GetAdminTokenReq{\n\t\tUserID: a.UserID,\n\t\tSecret: a.Secret,\n\t}\n\tvar resp auth.GetAdminTokenResp\n\tif err := a.apiPost(ctx, \"/auth/get_admin_token\", &req, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn resp.Token, nil\n}\n\nfunc (a *Api) GetPartLimit(ctx context.Context) (*third.PartLimitResp, error) {\n\tvar resp third.PartLimitResp\n\tif err := a.apiPost(ctx, \"/object/part_limit\", &third.PartLimitReq{}, &resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (a *Api) InitiateMultipartUpload(ctx context.Context, req *third.InitiateMultipartUploadReq) (*third.InitiateMultipartUploadResp, error) {\n\tvar resp third.InitiateMultipartUploadResp\n\tif err := a.apiPost(ctx, \"/object/initiate_multipart_upload\", req, &resp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &resp, nil\n}\n\nfunc (a *Api) CompleteMultipartUpload(ctx context.Context, req *third.CompleteMultipartUploadReq) (string, error) {\n\tvar resp third.CompleteMultipartUploadResp\n\tif err := a.apiPost(ctx, \"/object/complete_multipart_upload\", req, &resp); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn resp.Url, nil\n}\n"
  },
  {
    "path": "tools/url2im/pkg/buffer.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage pkg\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n)\n\ntype ReadSeekSizeCloser interface {\n\tio.ReadSeekCloser\n\tSize() int64\n}\n\nfunc NewReader(r io.Reader, max int64, path string) (ReadSeekSizeCloser, error) {\n\tbuf := make([]byte, max+1)\n\tn, err := io.ReadFull(r, buf)\n\tif err == nil {\n\t\tf, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o666)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar ok bool\n\t\tdefer func() {\n\t\t\tif !ok {\n\t\t\t\t_ = f.Close()\n\t\t\t\t_ = os.Remove(path)\n\t\t\t}\n\t\t}()\n\t\tif _, err := f.Write(buf[:n]); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcn, err := io.Copy(f, r)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif _, err := f.Seek(0, io.SeekStart); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tok = true\n\t\treturn &fileBuffer{\n\t\t\tf: f,\n\t\t\tn: cn + int64(n),\n\t\t}, nil\n\t} else if err == io.EOF || err == io.ErrUnexpectedEOF {\n\t\treturn &memoryBuffer{\n\t\t\tr: bytes.NewReader(buf[:n]),\n\t\t}, nil\n\t} else {\n\t\treturn nil, err\n\t}\n}\n\ntype fileBuffer struct {\n\tn int64\n\tf *os.File\n}\n\nfunc (r *fileBuffer) Read(p []byte) (n int, err error) {\n\treturn r.f.Read(p)\n}\n\nfunc (r *fileBuffer) Seek(offset int64, whence int) (int64, error) {\n\treturn r.f.Seek(offset, whence)\n}\n\nfunc (r *fileBuffer) Size() int64 {\n\treturn r.n\n}\n\nfunc (r *fileBuffer) Close() error {\n\tname := r.f.Name()\n\tif err := r.f.Close(); err != nil {\n\t\treturn err\n\t}\n\treturn os.Remove(name)\n}\n\ntype memoryBuffer struct {\n\tr *bytes.Reader\n}\n\nfunc (r *memoryBuffer) Read(p []byte) (n int, err error) {\n\treturn r.r.Read(p)\n}\n\nfunc (r *memoryBuffer) Seek(offset int64, whence int) (int64, error) {\n\treturn r.r.Seek(offset, whence)\n}\n\nfunc (r *memoryBuffer) Close() error {\n\treturn nil\n}\n\nfunc (r *memoryBuffer) Size() int64 {\n\treturn r.r.Size()\n}\n"
  },
  {
    "path": "tools/url2im/pkg/config.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage pkg\n\nimport \"time\"\n\ntype Config struct {\n\tTaskPath     string\n\tProgressPath string\n\tConcurrency  int\n\tRetry        int\n\tTimeout      time.Duration\n\tApi          string\n\tUserID       string\n\tSecret       string\n\tTempDir      string\n\tCacheSize    int64\n}\n"
  },
  {
    "path": "tools/url2im/pkg/http.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage pkg\n\nimport \"net/http\"\n\nfunc DefaultRequestHeader(header http.Header) {\n\theader.Set(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36\")\n}\n"
  },
  {
    "path": "tools/url2im/pkg/manage.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage pkg\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/openimsdk/tools/errs\"\n\n\t\"github.com/openimsdk/protocol/third\"\n)\n\ntype Upload struct {\n\tURL         string `json:\"url\"`\n\tName        string `json:\"name\"`\n\tContentType string `json:\"contentType\"`\n}\n\ntype Task struct {\n\tIndex  int\n\tUpload Upload\n}\n\ntype PartInfo struct {\n\tContentType string\n\tPartSize    int64\n\tPartNum     int\n\tFileMd5     string\n\tPartMd5     string\n\tPartSizes   []int64\n\tPartMd5s    []string\n}\n\nfunc Run(conf Config) error {\n\tm := &Manage{\n\t\tprefix: time.Now().Format(\"20060102150405\"),\n\t\tconf:   &conf,\n\t\tctx:    context.Background(),\n\t}\n\treturn m.Run()\n}\n\ntype Manage struct {\n\tconf      *Config\n\tctx       context.Context\n\tapi       *Api\n\tpartLimit *third.PartLimitResp\n\tprefix    string\n\ttasks     chan Task\n\tid        uint64\n\tsuccess   int64\n\tfailed    int64\n}\n\nfunc (m *Manage) tempFilePath() string {\n\treturn filepath.Join(m.conf.TempDir, fmt.Sprintf(\"%s_%d\", m.prefix, atomic.AddUint64(&m.id, 1)))\n}\n\nfunc (m *Manage) Run() error {\n\tdefer func(start time.Time) {\n\t\tlog.Printf(\"run time %s\\n\", time.Since(start))\n\t}(time.Now())\n\tm.api = &Api{\n\t\tApi:    m.conf.Api,\n\t\tUserID: m.conf.UserID,\n\t\tSecret: m.conf.Secret,\n\t\tClient: &http.Client{Timeout: m.conf.Timeout},\n\t}\n\tvar err error\n\tctx := context.WithValue(m.ctx, \"operationID\", fmt.Sprintf(\"%s_init\", m.prefix))\n\tm.api.Token, err = m.api.GetAdminToken(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tm.partLimit, err = m.api.GetPartLimit(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tprogress, err := ReadProgress(m.conf.ProgressPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tprogressFile, err := os.OpenFile(m.conf.ProgressPath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar mutex sync.Mutex\n\twriteSuccessIndex := func(index int) {\n\t\tmutex.Lock()\n\t\tdefer mutex.Unlock()\n\t\tif _, err := progressFile.Write([]byte(strconv.Itoa(index) + \"\\n\")); err != nil {\n\t\t\tlog.Printf(\"write progress err: %v\\n\", err)\n\t\t}\n\t}\n\tfile, err := os.Open(m.conf.TaskPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tm.tasks = make(chan Task, m.conf.Concurrency*2)\n\tgo func() {\n\t\tdefer file.Close()\n\t\tdefer close(m.tasks)\n\t\tscanner := bufio.NewScanner(file)\n\t\tvar (\n\t\t\tindex int\n\t\t\tnum   int\n\t\t)\n\t\tfor scanner.Scan() {\n\t\t\tline := strings.TrimSpace(scanner.Text())\n\t\t\tif line == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tindex++\n\t\t\tif progress.IsUploaded(index) {\n\t\t\t\tlog.Printf(\"index: %d already uploaded %s\\n\", index, line)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar upload Upload\n\t\t\tif err := json.Unmarshal([]byte(line), &upload); err != nil {\n\t\t\t\tlog.Printf(\"index: %d json.Unmarshal(%s) err: %v\", index, line, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnum++\n\t\t\tm.tasks <- Task{\n\t\t\t\tIndex:  index,\n\t\t\t\tUpload: upload,\n\t\t\t}\n\t\t}\n\t\tif num == 0 {\n\t\t\tlog.Println(\"mark all completed\")\n\t\t}\n\t}()\n\tvar wg sync.WaitGroup\n\twg.Add(m.conf.Concurrency)\n\tfor i := 0; i < m.conf.Concurrency; i++ {\n\t\tgo func(tid int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor task := range m.tasks {\n\t\t\t\tvar success bool\n\t\t\t\tfor n := 0; n < m.conf.Retry; n++ {\n\t\t\t\t\tctx := context.WithValue(m.ctx, \"operationID\", fmt.Sprintf(\"%s_%d_%d_%d\", m.prefix, tid, task.Index, n+1))\n\t\t\t\t\tif urlRaw, err := m.RunTask(ctx, task); err == nil {\n\t\t\t\t\t\twriteSuccessIndex(task.Index)\n\t\t\t\t\t\tlog.Println(\"index:\", task.Index, \"upload success\", \"urlRaw\", urlRaw)\n\t\t\t\t\t\tsuccess = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Printf(\"index: %d upload: %+v err: %v\", task.Index, task.Upload, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif success {\n\t\t\t\t\tatomic.AddInt64(&m.success, 1)\n\t\t\t\t} else {\n\t\t\t\t\tatomic.AddInt64(&m.failed, 1)\n\t\t\t\t\tlog.Printf(\"index: %d upload: %+v failed\", task.Index, task.Upload)\n\t\t\t\t}\n\t\t\t}\n\t\t}(i + 1)\n\t}\n\twg.Wait()\n\tlog.Printf(\"execution completed success %d failed %d\\n\", m.success, m.failed)\n\treturn nil\n}\n\nfunc (m *Manage) RunTask(ctx context.Context, task Task) (string, error) {\n\tresp, err := m.HttpGet(ctx, task.Upload.URL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\treader, err := NewReader(resp.Body, m.conf.CacheSize, m.tempFilePath())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer reader.Close()\n\tpart, err := m.getPartInfo(ctx, reader, reader.Size())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar contentType string\n\tif task.Upload.ContentType == \"\" {\n\t\tcontentType = part.ContentType\n\t} else {\n\t\tcontentType = task.Upload.ContentType\n\t}\n\tinitiateMultipartUploadResp, err := m.api.InitiateMultipartUpload(ctx, &third.InitiateMultipartUploadReq{\n\t\tHash:        part.PartMd5,\n\t\tSize:        reader.Size(),\n\t\tPartSize:    part.PartSize,\n\t\tMaxParts:    -1,\n\t\tCause:       \"batch-import\",\n\t\tName:        task.Upload.Name,\n\t\tContentType: contentType,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif initiateMultipartUploadResp.Upload == nil {\n\t\treturn initiateMultipartUploadResp.Url, nil\n\t}\n\tif _, err := reader.Seek(0, io.SeekStart); err != nil {\n\t\treturn \"\", err\n\t}\n\tuploadParts := make([]*third.SignPart, part.PartNum)\n\tfor _, part := range initiateMultipartUploadResp.Upload.Sign.Parts {\n\t\tuploadParts[part.PartNumber-1] = part\n\t}\n\tfor i, currentPartSize := range part.PartSizes {\n\t\tmd5Reader := NewMd5Reader(io.LimitReader(reader, currentPartSize))\n\t\tif err := m.doPut(ctx, m.api.Client, initiateMultipartUploadResp.Upload.Sign, uploadParts[i], md5Reader, currentPartSize); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif md5val := md5Reader.Md5(); md5val != part.PartMd5s[i] {\n\t\t\treturn \"\", fmt.Errorf(\"upload part %d failed, md5 not match, expect %s, got %s\", i, part.PartMd5s[i], md5val)\n\t\t}\n\t}\n\turlRaw, err := m.api.CompleteMultipartUpload(ctx, &third.CompleteMultipartUploadReq{\n\t\tUploadID:    initiateMultipartUploadResp.Upload.UploadID,\n\t\tParts:       part.PartMd5s,\n\t\tName:        task.Upload.Name,\n\t\tContentType: contentType,\n\t\tCause:       \"batch-import\",\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn urlRaw, nil\n}\n\nfunc (m *Manage) partSize(size int64) (int64, error) {\n\tif size <= 0 {\n\t\treturn 0, errs.New(\"size must be greater than 0\")\n\t}\n\tif size > m.partLimit.MaxPartSize*int64(m.partLimit.MaxNumSize) {\n\t\treturn 0, errs.New(\"size must be less than\", \"size\", m.partLimit.MaxPartSize*int64(m.partLimit.MaxNumSize))\n\t}\n\tif size <= m.partLimit.MinPartSize*int64(m.partLimit.MaxNumSize) {\n\t\treturn m.partLimit.MinPartSize, nil\n\t}\n\tpartSize := size / int64(m.partLimit.MaxNumSize)\n\tif size%int64(m.partLimit.MaxNumSize) != 0 {\n\t\tpartSize++\n\t}\n\treturn partSize, nil\n}\n\nfunc (m *Manage) partMD5(parts []string) string {\n\ts := strings.Join(parts, \",\")\n\tmd5Sum := md5.Sum([]byte(s))\n\treturn hex.EncodeToString(md5Sum[:])\n}\n\nfunc (m *Manage) getPartInfo(ctx context.Context, r io.Reader, fileSize int64) (*PartInfo, error) {\n\tpartSize, err := m.partSize(fileSize)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpartNum := int(fileSize / partSize)\n\tif fileSize%partSize != 0 {\n\t\tpartNum++\n\t}\n\tpartSizes := make([]int64, partNum)\n\tfor i := 0; i < partNum; i++ {\n\t\tpartSizes[i] = partSize\n\t}\n\tpartSizes[partNum-1] = fileSize - partSize*(int64(partNum)-1)\n\tpartMd5s := make([]string, partNum)\n\tbuf := make([]byte, 1024*8)\n\tfileMd5 := md5.New()\n\tvar contentType string\n\tfor i := 0; i < partNum; i++ {\n\t\th := md5.New()\n\t\tr := io.LimitReader(r, partSize)\n\t\tfor {\n\t\t\tif n, err := r.Read(buf); err == nil {\n\t\t\t\tif contentType == \"\" {\n\t\t\t\t\tcontentType = http.DetectContentType(buf[:n])\n\t\t\t\t}\n\t\t\t\th.Write(buf[:n])\n\t\t\t\tfileMd5.Write(buf[:n])\n\t\t\t} else if err == io.EOF {\n\t\t\t\tbreak\n\t\t\t} else {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tpartMd5s[i] = hex.EncodeToString(h.Sum(nil))\n\t}\n\tpartMd5Val := m.partMD5(partMd5s)\n\tfileMd5val := hex.EncodeToString(fileMd5.Sum(nil))\n\treturn &PartInfo{\n\t\tContentType: contentType,\n\t\tPartSize:    partSize,\n\t\tPartNum:     partNum,\n\t\tFileMd5:     fileMd5val,\n\t\tPartMd5:     partMd5Val,\n\t\tPartSizes:   partSizes,\n\t\tPartMd5s:    partMd5s,\n\t}, nil\n}\n\nfunc (m *Manage) doPut(ctx context.Context, client *http.Client, sign *third.AuthSignParts, part *third.SignPart, reader io.Reader, size int64) error {\n\trawURL := part.Url\n\tif rawURL == \"\" {\n\t\trawURL = sign.Url\n\t}\n\tif len(sign.Query)+len(part.Query) > 0 {\n\t\tu, err := url.Parse(rawURL)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tquery := u.Query()\n\t\tfor i := range sign.Query {\n\t\t\tv := sign.Query[i]\n\t\t\tquery[v.Key] = v.Values\n\t\t}\n\t\tfor i := range part.Query {\n\t\t\tv := part.Query[i]\n\t\t\tquery[v.Key] = v.Values\n\t\t}\n\t\tu.RawQuery = query.Encode()\n\t\trawURL = u.String()\n\t}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPut, rawURL, reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor i := range sign.Header {\n\t\tv := sign.Header[i]\n\t\treq.Header[v.Key] = v.Values\n\t}\n\tfor i := range part.Header {\n\t\tv := part.Header[i]\n\t\treq.Header[v.Key] = v.Values\n\t}\n\treq.ContentLength = size\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = resp.Body.Close()\n\t}()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode/200 != 1 {\n\t\treturn fmt.Errorf(\"PUT %s part %d failed, status code %d, body %s\", rawURL, part.PartNumber, resp.StatusCode, string(body))\n\t}\n\treturn nil\n}\n\nfunc (m *Manage) HttpGet(ctx context.Context, url string) (*http.Response, error) {\n\treqUrl := url\n\tfor {\n\t\trequest, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tDefaultRequestHeader(request.Header)\n\t\tresponse, err := m.api.Client.Do(request)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif response.StatusCode != http.StatusOK {\n\t\t\t_ = response.Body.Close()\n\t\t\treturn nil, fmt.Errorf(\"webhook get %s status %s\", url, response.Status)\n\t\t}\n\t\treturn response, nil\n\t}\n}\n"
  },
  {
    "path": "tools/url2im/pkg/md5.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage pkg\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"hash\"\n\t\"io\"\n)\n\nfunc NewMd5Reader(r io.Reader) *Md5Reader {\n\treturn &Md5Reader{h: md5.New(), r: r}\n}\n\ntype Md5Reader struct {\n\th hash.Hash\n\tr io.Reader\n}\n\nfunc (r *Md5Reader) Read(p []byte) (n int, err error) {\n\tn, err = r.r.Read(p)\n\tif err == nil && n > 0 {\n\t\tr.h.Write(p[:n])\n\t}\n\treturn\n}\n\nfunc (r *Md5Reader) Md5() string {\n\treturn hex.EncodeToString(r.h.Sum(nil))\n}\n"
  },
  {
    "path": "tools/url2im/pkg/progress.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage pkg\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/kelindar/bitmap\"\n)\n\nfunc ReadProgress(path string) (*Progress, error) {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn &Progress{}, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tdefer file.Close()\n\tscanner := bufio.NewScanner(file)\n\tvar upload bitmap.Bitmap\n\tfor scanner.Scan() {\n\t\tindex, err := strconv.Atoi(scanner.Text())\n\t\tif err != nil || index < 0 {\n\t\t\tcontinue\n\t\t}\n\t\tupload.Set(uint32(index))\n\t}\n\treturn &Progress{upload: upload}, nil\n}\n\ntype Progress struct {\n\tupload bitmap.Bitmap\n}\n\nfunc (p *Progress) IsUploaded(index int) bool {\n\tif p == nil {\n\t\treturn false\n\t}\n\treturn p.upload.Contains(uint32(index))\n}\n"
  },
  {
    "path": "tools/versionchecker/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"runtime\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/openimsdk/tools/utils/timeutil\"\n)\n\nfunc ExecuteCommand(cmdName string, args ...string) (string, error) {\n\tcmd := exec.Command(cmdName, args...)\n\tvar out bytes.Buffer\n\tvar stderr bytes.Buffer\n\tcmd.Stdout = &out\n\tcmd.Stderr = &stderr\n\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error executing %s: %v, stderr: %s\", cmdName, err, stderr.String())\n\t}\n\treturn out.String(), nil\n}\n\nfunc printTime() string {\n\tformattedTime := timeutil.GetCurrentTimeFormatted()\n\treturn fmt.Sprintf(\"Current Date & Time: %s\", formattedTime)\n}\n\nfunc getGoVersion() string {\n\tversion := runtime.Version()\n\tgoos := runtime.GOOS\n\tgoarch := runtime.GOARCH\n\treturn fmt.Sprintf(\"Go Version: %s\\nOS: %s\\nArchitecture: %s\", version, goos, goarch)\n}\n\nfunc getDockerVersion() string {\n\tversion, err := ExecuteCommand(\"docker\", \"--version\")\n\tif err != nil {\n\t\treturn \"Docker is not installed. Please install it to get the version.\"\n\t}\n\treturn version\n}\n\nfunc getKubernetesVersion() string {\n\tversion, err := ExecuteCommand(\"kubectl\", \"version\", \"--client\", \"--short\")\n\tif err != nil {\n\t\treturn \"Kubernetes is not installed. Please install it to get the version.\"\n\t}\n\treturn version\n}\n\nfunc getGitVersion() string {\n\tversion, err := ExecuteCommand(\"git\", \"branch\", \"--show-current\")\n\tif err != nil {\n\t\treturn \"Git is not installed. Please install it to get the version.\"\n\t}\n\treturn version\n}\n\n// // NOTE: You'll need to provide appropriate commands for OpenIM versions.\n// func getOpenIMServerVersion() string {\n// \t// Placeholder\n// \topenimVersion := version.GetSingleVersion()\n// \treturn \"OpenIM Server: \" + openimVersion + \"\\n\"\n// }\n\n// func getOpenIMClientVersion() (string, error) {\n// \topenIMClientVersion, err := version.GetClientVersion()\n// \tif err != nil {\n// \t\treturn \"\", err\n// \t}\n// \treturn \"OpenIM Client: \" + openIMClientVersion.ClientVersion + \"\\n\", nil\n// }\n\nfunc main() {\n\t// red := color.New(color.FgRed).SprintFunc()\n\t//\tgreen := color.New(color.FgGreen).SprintFunc()\n\tblue := color.New(color.FgBlue).SprintFunc()\n\t//\tyellow := color.New(color.FgYellow).SprintFunc()\n\tfmt.Println(blue(\"## Go Version\"))\n\tfmt.Println(getGoVersion())\n\tfmt.Println(blue(\"## Branch Type\"))\n\tfmt.Println(getGitVersion())\n\tfmt.Println(blue(\"## Docker Version\"))\n\tfmt.Println(getDockerVersion())\n\tfmt.Println(blue(\"## Kubernetes Version\"))\n\tfmt.Println(getKubernetesVersion())\n\t// fmt.Println(blue(\"## OpenIM Versions\"))\n\t// fmt.Println(getOpenIMServerVersion())\n\t// clientVersion, err := getOpenIMClientVersion()\n\t// if err != nil {\n\t// \tfmt.Println(red(\"Error getting OpenIM Client Version: \"), err)\n\t// } else {\n\t// \tfmt.Println(clientVersion)\n\t// }\n}\n"
  },
  {
    "path": "tools/yamlfmt/main.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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// OPENIM plan on prow tools\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc main() {\n\t// Prow OWNERs file defines the default indent as 2 spaces.\n\tindent := flag.Int(\"indent\", 2, \"default indent\")\n\tflag.Parse()\n\tfor _, path := range flag.Args() {\n\t\tsourceYaml, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"%s: %v\\n\", path, err)\n\t\t\tcontinue\n\t\t}\n\t\trootNode, err := fetchYaml(sourceYaml)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"%s: %v\\n\", path, err)\n\t\t\tcontinue\n\t\t}\n\t\twriter, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"%s: %v\\n\", path, err)\n\t\t\tcontinue\n\t\t}\n\t\terr = streamYaml(writer, indent, rootNode)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"%s: %v\\n\", path, err)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc fetchYaml(sourceYaml []byte) (*yaml.Node, error) {\n\trootNode := yaml.Node{}\n\terr := yaml.Unmarshal(sourceYaml, &rootNode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &rootNode, nil\n}\n\nfunc streamYaml(writer io.Writer, indent *int, in *yaml.Node) error {\n\tencoder := yaml.NewEncoder(writer)\n\tencoder.SetIndent(*indent)\n\terr := encoder.Encode(in)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn encoder.Close()\n}\n"
  },
  {
    "path": "tools/yamlfmt/main_test.go",
    "content": "// Copyright © 2023 OpenIM. All rights reserved.\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\npackage main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/likexian/gokit/assert\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc Test_main(t *testing.T) {\n\tsourceYaml := ` # See the OWNERS docs at https://go.k8s.io/owners\napprovers:\n- dep-approvers\n- thockin         # Network\n- liggitt\n\nlabels:\n- sig/architecture\n`\n\n\toutputYaml := `# See the OWNERS docs at https://go.k8s.io/owners\napprovers:\n  - dep-approvers\n  - thockin # Network\n  - liggitt\nlabels:\n  - sig/architecture\n`\n\tnode, _ := fetchYaml([]byte(sourceYaml))\n\tvar output bytes.Buffer\n\tindent := 2\n\twriter := bufio.NewWriter(&output)\n\t_ = streamYaml(writer, &indent, node)\n\t_ = writer.Flush()\n\tassert.Equal(t, outputYaml, string(output.Bytes()), \"yaml was not formatted correctly\")\n}\n\nfunc Test_fetchYaml(t *testing.T) {\n\ttype args struct {\n\t\tsourceYaml []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *yaml.Node\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid YAML\",\n\t\t\targs: args{sourceYaml: []byte(\"key: value\")},\n\t\t\twant: &yaml.Node{\n\t\t\t\tKind:  yaml.MappingNode,\n\t\t\t\tTag:   \"!!map\",\n\t\t\t\tValue: \"\",\n\t\t\t\tContent: []*yaml.Node{\n\t\t\t\t\t{\n\t\t\t\t\t\tKind:  yaml.ScalarNode,\n\t\t\t\t\t\tTag:   \"!!str\",\n\t\t\t\t\t\tValue: \"key\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tKind:  yaml.ScalarNode,\n\t\t\t\t\t\tTag:   \"!!str\",\n\t\t\t\t\t\tValue: \"value\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Invalid YAML\",\n\t\t\targs:    args{sourceYaml: []byte(\"key:\")},\n\t\t\twant:    nil,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := fetchYaml(tt.args.sourceYaml)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"fetchYaml() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"fetchYaml() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_streamYaml(t *testing.T) {\n\ttype args struct {\n\t\tindent *int\n\t\tin     *yaml.Node\n\t}\n\tdefaultIndent := 2\n\ttests := []struct {\n\t\tname       string\n\t\targs       args\n\t\twantWriter string\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid YAML node with default indent\",\n\t\t\targs: args{\n\t\t\t\tindent: &defaultIndent,\n\t\t\t\tin: &yaml.Node{\n\t\t\t\t\tKind:  yaml.MappingNode,\n\t\t\t\t\tTag:   \"!!map\",\n\t\t\t\t\tValue: \"\",\n\t\t\t\t\tContent: []*yaml.Node{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKind:  yaml.ScalarNode,\n\t\t\t\t\t\t\tTag:   \"!!str\",\n\t\t\t\t\t\t\tValue: \"key\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tKind:  yaml.ScalarNode,\n\t\t\t\t\t\t\tTag:   \"!!str\",\n\t\t\t\t\t\t\tValue: \"value\",\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\twantWriter: \"key: value\\n\",\n\t\t\twantErr:    false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\twriter := &bytes.Buffer{}\n\t\t\tif err := streamYaml(writer, tt.args.indent, tt.args.in); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"streamYaml() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotWriter := writer.String(); gotWriter != tt.wantWriter {\n\t\t\t\tt.Errorf(\"streamYaml() = %v, want %v\", gotWriter, tt.wantWriter)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "version/version",
    "content": "main"
  },
  {
    "path": "version/version.go",
    "content": "package version\n\nimport (\n\t_ \"embed\"\n\t\"strings\"\n)\n\n//go:embed version\nvar Version string\n\nfunc init() {\n\tVersion = strings.Trim(Version, \"\\n\")\n\tVersion = strings.TrimSpace(Version)\n}\n"
  }
]