feat: incr sync version.

dev_1
Gordon 1 year ago
parent e8ccae6349
commit 88b8043224
  1. 23
      go/chao-sdk-core/.github/.codecov.yml
  2. 43
      go/chao-sdk-core/.github/labels.yml
  3. 53
      go/chao-sdk-core/.github/workflows/auto-invite.yml
  4. 60
      go/chao-sdk-core/.github/workflows/check-coverage.yml
  5. 76
      go/chao-sdk-core/.github/workflows/codeql-analysis.yml
  6. 15
      go/chao-sdk-core/.github/workflows/e2e-test.yml
  7. 44
      go/chao-sdk-core/.github/workflows/gosec.yml
  8. 31
      go/chao-sdk-core/.github/workflows/issue-robot.yml
  9. 61
      go/chao-sdk-core/.github/workflows/link-pr.yml
  10. 52
      go/chao-sdk-core/.github/workflows/opencommit.yml
  11. 144
      go/chao-sdk-core/.github/workflows/openimci.yml
  12. 82
      go/chao-sdk-core/.github/workflows/release.yml
  13. 48
      go/chao-sdk-core/.github/workflows/stale.yml
  14. 172
      go/chao-sdk-core/.gitignore
  15. 934
      go/chao-sdk-core/.golangci.yml
  16. 223
      go/chao-sdk-core/.goreleaser.yaml
  17. 62
      go/chao-sdk-core/CHANGELOG/.chglog/CHANGELOG.tpl.md
  18. 81
      go/chao-sdk-core/CHANGELOG/.chglog/config.yml
  19. 42
      go/chao-sdk-core/CHANGELOG/CHANGELOG-1.0.md
  20. 0
      go/chao-sdk-core/CHANGELOG/CHANGELOG-1.1.md
  21. 92
      go/chao-sdk-core/CHANGELOG/CHANGELOG-2.0.md
  22. 23
      go/chao-sdk-core/CHANGELOG/CHANGELOG-2.1.md
  23. 24
      go/chao-sdk-core/CHANGELOG/CHANGELOG-2.2.md
  24. 32
      go/chao-sdk-core/CHANGELOG/CHANGELOG-2.3.md
  25. 52
      go/chao-sdk-core/CHANGELOG/CHANGELOG-2.9.md
  26. 128
      go/chao-sdk-core/CHANGELOG/CHANGELOG-3.0.md
  27. 104
      go/chao-sdk-core/CHANGELOG/CHANGELOG.md
  28. 435
      go/chao-sdk-core/CONTRIBUTING.md
  29. 201
      go/chao-sdk-core/LICENSE
  30. 515
      go/chao-sdk-core/Makefile
  31. 144
      go/chao-sdk-core/README.md
  32. 26
      go/chao-sdk-core/README_zh-CN.md
  33. 234
      go/chao-sdk-core/cmd/gordon_main.go
  34. 61
      go/chao-sdk-core/cmd/main.go
  35. 25
      go/chao-sdk-core/cmd/online_open_im.go
  36. 67
      go/chao-sdk-core/cmd/parse.go
  37. 38
      go/chao-sdk-core/cmd/press_open_im.go
  38. 38
      go/chao-sdk-core/cmd/reliability_open_im.go
  39. 192
      go/chao-sdk-core/cmd/sk_main.go
  40. 74
      go/chao-sdk-core/docs/.generated_docs
  41. 1
      go/chao-sdk-core/docs/CODEOWNERS
  42. 129
      go/chao-sdk-core/docs/contrib/cicd-actions.md
  43. 38
      go/chao-sdk-core/docs/contrib/code_conventions.md
  44. 80
      go/chao-sdk-core/docs/contrib/development.md
  45. 102
      go/chao-sdk-core/docs/contrib/git_workflow.md
  46. 43
      go/chao-sdk-core/go.mod
  47. 77
      go/chao-sdk-core/go.sum
  48. 0
      go/chao-sdk-core/identifier.sqlite
  49. 49
      go/chao-sdk-core/internal/business/business.go
  50. 23
      go/chao-sdk-core/internal/business/open_im_sdk_business.go
  51. 72
      go/chao-sdk-core/internal/cache/cahe.go
  52. 33
      go/chao-sdk-core/internal/common/common.go
  53. 33
      go/chao-sdk-core/internal/common/object_storage.go
  54. 1178
      go/chao-sdk-core/internal/conversation_msg/conversation.go
  55. 1064
      go/chao-sdk-core/internal/conversation_msg/conversation_msg.go
  56. 714
      go/chao-sdk-core/internal/conversation_msg/conversation_notification.go
  57. 57
      go/chao-sdk-core/internal/conversation_msg/convert.go
  58. 492
      go/chao-sdk-core/internal/conversation_msg/create_message.go
  59. 245
      go/chao-sdk-core/internal/conversation_msg/delete.go
  60. 217
      go/chao-sdk-core/internal/conversation_msg/entering.go
  61. 32
      go/chao-sdk-core/internal/conversation_msg/image.go
  62. 52
      go/chao-sdk-core/internal/conversation_msg/max_seq_recorder.go
  63. 347
      go/chao-sdk-core/internal/conversation_msg/message_check.go
  64. 124
      go/chao-sdk-core/internal/conversation_msg/message_controller.go
  65. 102
      go/chao-sdk-core/internal/conversation_msg/progress.go
  66. 281
      go/chao-sdk-core/internal/conversation_msg/read_drawing.go
  67. 203
      go/chao-sdk-core/internal/conversation_msg/revoke.go
  68. 1180
      go/chao-sdk-core/internal/conversation_msg/sdk.go
  69. 148
      go/chao-sdk-core/internal/conversation_msg/sync.go
  70. 89
      go/chao-sdk-core/internal/file/bitmap.go
  71. 62
      go/chao-sdk-core/internal/file/cb.go
  72. 24
      go/chao-sdk-core/internal/file/file.go
  73. 73
      go/chao-sdk-core/internal/file/file_default.go
  74. 156
      go/chao-sdk-core/internal/file/file_js.go
  75. 40
      go/chao-sdk-core/internal/file/file_test.go
  76. 43
      go/chao-sdk-core/internal/file/md5.go
  77. 44
      go/chao-sdk-core/internal/file/progress.go
  78. 576
      go/chao-sdk-core/internal/file/upload.go
  79. 71
      go/chao-sdk-core/internal/friend/conversion.go
  80. 199
      go/chao-sdk-core/internal/friend/friend.go
  81. 33
      go/chao-sdk-core/internal/friend/hash.go
  82. 136
      go/chao-sdk-core/internal/friend/notification.go
  83. 344
      go/chao-sdk-core/internal/friend/sdk.go
  84. 180
      go/chao-sdk-core/internal/friend/sync.go
  85. 72
      go/chao-sdk-core/internal/friend/sync2.go
  86. 13
      go/chao-sdk-core/internal/friend/sync2_test.go
  87. 62
      go/chao-sdk-core/internal/full/full.go
  88. 29
      go/chao-sdk-core/internal/full/full_model.go
  89. 199
      go/chao-sdk-core/internal/full/open_im_sdk_full.go
  90. 97
      go/chao-sdk-core/internal/group/conversion.go
  91. 343
      go/chao-sdk-core/internal/group/group.go
  92. 245
      go/chao-sdk-core/internal/group/notification.go
  93. 383
      go/chao-sdk-core/internal/group/sdk.go
  94. 310
      go/chao-sdk-core/internal/group/sync.go
  95. 347
      go/chao-sdk-core/internal/group/sync2.go
  96. 1
      go/chao-sdk-core/internal/group/sync2_test.go
  97. 265
      go/chao-sdk-core/internal/incrversion/option.go
  98. 61
      go/chao-sdk-core/internal/interaction/compressor.go
  99. 39
      go/chao-sdk-core/internal/interaction/constant.go
  100. 52
      go/chao-sdk-core/internal/interaction/context.go
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,23 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
coverage:
status:
project:
default: false # disable the default status that measures entire project
pkg: # declare a new status context "pkg"
paths:
- pkg/* # only include coverage in "pkg/" folder
informational: true # Always pass check
patch: off # disable the commit only checks

@ -0,0 +1,43 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Refer to Kubernetes for size/* Settings
# https://github.com/Kubernetes/Kubernetes
XS:
name: size/XS
lines: 0
color: 3CBF00
S:
name: size/S
lines: 10
color: 5D9801
M:
name: size/M
lines: 30
color: 7F7203
L:
name: size/L
lines: 100
color: A14C05
XL:
name: size/XL
lines: 500
color: C32607
XXL:
name: size/XXL
lines: 1000
color: E50009
comment: |
# Whoa! Easy there, Partner!
This PR is too big. Please break it up into smaller PRs.

@ -0,0 +1,53 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Invite users to join our group
on:
issue_comment:
types:
- created
jobs:
issue_comment:
name: Invite users to join our group
if: ${{ github.event.comment.body == '/invite' || github.event.comment.body == '/close' || github.event.comment.body == '/comment' }}
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Invite user to join our group
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
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.
Our most recommended /root/workspaces/openim/Open-IM-Server/docs copyway to get in touch is through [Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg). 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.
In addition to Slack, we also offer the following ways to get in touch:
+ <a href="https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg" 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-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg) team channel.
+ <a href="https://mail.google.com/mail/u/0/?fs=1&tf=cm&to=winxu81@gmail.com" 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.
+ <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.
+ <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.
- name: Close Issue
uses: peter-evans/close-issue@v3
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
comment: 🤖 Auto-closing issue, if you still need help please reopen the issue or ask for help in the community above
labels: |
triage/accepted

@ -0,0 +1,60 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# name: Check-Coverage
# on:
# workflow_dispatch:
# push:
# branches: [ "main" ]
# paths-ignore:
# - "docs/**"
# - "**/*.md"
# - "**/*.yaml"
# - "CONTRIBUTORS"
# - "CHANGELOG/**"
# pull_request:
# branches: [ "*" ]
# paths-ignore:
# - "docs/**"
# - "**/*.md"
# - "**/*.yaml"
# - "CONTRIBUTORS"
# - "CHANGELOG/**"
# env:
# # Common versions
# GO_VERSION: "1.20"
# jobs:
# coverage:
# runs-on: ubuntu-20.04
# steps:
# - name: Checkout
# uses: actions/checkout@v3
# - name: Setup Golang with cache
# uses: magnetikonline/action-golang-cache@v3
# with:
# go-version: ${{ env.GO_VERSION }}
# token: ${{ secrets.BOT_GITHUB_TOKEN }}
# - name: Install Dependencies
# run: sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev
# - name: Run Cover
# run: make cover
# - name: Upload Coverage to Codecov
# uses: codecov/codecov-action@v3

@ -0,0 +1,76 @@
# Copyright © 2023 OpenIM open source community. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: "Code Scanning - Action"
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: '30 1 * * 0'
jobs:
CodeQL-Build:
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
runs-on: ubuntu-latest
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java, ruby
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below).
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# ✏ If the Autobuild fails above, remove it and uncomment the following
# three lines and modify them (or add more) to build your code if your
# project uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

@ -0,0 +1,15 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

@ -0,0 +1,44 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# name: Run gosec
# # gosec is a source code security audit tool for the Go language. It performs a static
# # analysis of the Go code, looking for potential security problems. The main functions of gosec are:
# # 1. Find common security vulnerabilities, such as SQL injection, command injection, and cross-site scripting (XSS).
# # 2. Audit codes according to common security standards and find non-standard codes.
# # 3. Assist the Go language engineer to write safe and reliable code.
# on:
# push:
# branches: "*"
# pull_request:
# branches: "*"
# paths-ignore:
# - '*.md'
# - '*.yml'
# - '.github'
# jobs:
# golang-security-action:
# runs-on: ubuntu-latest
# env:
# GO111MODULE: on
# steps:
# - name: Check out code
# uses: actions/checkout@v3
# - name: Run Gosec Security Scanner
# uses: securego/gosec@master
# with:
# args: ./...

@ -0,0 +1,31 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: 'issue translator'
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
# it is not necessary to decide whether you need to modify the issue header content
IS_MODIFY_TITLE: true
BOT_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
# Required, input your bot github token

@ -0,0 +1,61 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# name: Github Rebot for Link check error
# on:
# pull_request:
# branches: [ main ]
# paths:
# - '**.md'
# - 'docs/**'
# - '.lycheeignore'
# push:
# branches: [ main ]
# schedule:
# - cron: '0 11 * * *'
# jobs:
# linkChecker:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - name: Link Checker
# id: lychee
# uses: lycheeverse/lychee-action@v1.7.0
# with:
# # For parameter description, see https://github.com/lycheeverse/lychee#commandline-parameters
# # Actions Link address -> https://github.com/lycheeverse/lychee-action
# # -E, --exclude-all-private Exclude all private IPs from checking.
# # -i, --insecure Proceed for server connections considered insecure (invalid TLS)
# # -n, --no-progress Do not show progress bar.
# # -t, --timeout <timeout> Website timeout in seconds from connect to response finished [default:20]
# # --max-concurrency <max-concurrency> Maximum number of concurrent network requests [default: 128]
# # -a --accept <accept> Comma-separated list of accepted status codes for valid links
# # docs/.vitepress/dist the site directory to check
# # ./*.md all markdown files in the root directory
# args: --verbose -E -i --no-progress --exclude-path './CHANGELOG' './**/*.md'
# env:
# GITHUB_TOKEN: ${{secrets.GH_PAT}}
# - name: Create Issue From File
# if: env.lychee_exit_code != 0
# uses: peter-evans/create-issue-from-file@v4
# with:
# title: Bug reports for links in OpenIM docs
# content-filepath: ./lychee/out.md
# labels: kind/documentation, triage/unresolved, report
# token: ${{ secrets.BOT_GITHUB_TOKEN }}

@ -0,0 +1,52 @@
# Copyright © 2023 OpenIM open source community. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: 'OpenIM Commit Action'
on:
push:
branches:
- main
jobs:
opencommit:
timeout-minutes: 10
name: OpenCommit
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Setup Node.js Environment
uses: actions/setup-node@v2
with:
node-version: '16'
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: di-sukharev/opencommit@github-action-v1.0.4
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
# set openAI api key in repo actions secrets,
# for openAI keys go to: https://platform.openai.com/account/api-keys
# for repo secret go to: https://github.com/kuebcub/settings/secrets/actions
OCO_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# customization
OCO_OPENAI_MAX_TOKENS: 500
OCO_OPENAI_BASE_PATH: ''
OCO_DESCRIPTION: false
OCO_EMOJI: false
OCO_MODEL: gpt-3.5-turbo
OCO_LANGUAGE: en

@ -0,0 +1,144 @@
# Copyright © 2023 OpenIM open source community. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: OpenIM CI Aotu Build and Install
on:
push:
branches:
- main
paths-ignore:
- "docs/**"
- "README.md"
- "README_zh-CN.md"
- "CONTRIBUTING.md"
pull_request:
branches:
- main
paths-ignore:
- "README.md"
- "README_zh-CN.md"
- "CONTRIBUTING.md"
- "docs/**"
env:
GO_VERSION: "1.19"
GOLANGCI_VERSION: "v1.50.1"
jobs:
openim:
name: Test with go ${{ matrix.go_version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the changed files back to the repository.
contents: write
environment:
name: openim
strategy:
matrix:
go_version: ["1.18","1.19","1.20","1.21"]
os: [ubuntu-latest]
steps:
- name: Set up Go ${{ matrix.go_version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go_version }}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Install Task
uses: arduino/setup-task@v1
with:
version: 2.x
- name: Run go format
run: |
sudo make format
echo "Run go format successfully"
continue-on-error: true
- name: Generate all necessary files, such as error code files
run: |
make generate
echo "Generate all necessary files successfully"
continue-on-error: true
- name: Run unit test and get test coverage
run: |
make cover
echo "Run unit test and get test coverage successfully"
continue-on-error: true
- name: Clean all build
run: |
sudo make clean
echo "Clean all build successfully"
- name: Build source code for host platform
run: |
sudo make build
echo "Build source code for host platform successfully"
- name: Build wasm source code
run: |
sudo make build-wasm
echo "Build wasm source code successfully"
- name: OpenIM verify copyright
run: |
sudo make verify-copyright
sudo make add-copyright
echo "OpenIM verify successfully"
continue-on-error: true
- name: push OpenIM
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "cicd: robot automated Change"
# commit_options: '--no-verify --signoff'
branch: main
# create_branch: true
# # Optional commit user and author settings
# commit_user_name: kubbot # defaults to "github-actions[bot]"
# commit_user_email: 3293172751ysy@gmail.com # defaults to "41898282+github-actions[bot]@users.noreply.github.com"
# commit_author: Kubbot # defaults to author of the commit that triggered the run
continue-on-error: true
- name: Set Current Directory
id: set_directory
run: |
echo "::set-output name=directory::$(pwd)"
continue-on-error: true
- name: Collect Test Coverage File
id: collect_coverage
run: |
cd ${{ steps.set_directory.outputs.directory }}
make cover
echo "::set-output name=coverage_file::./_output/tmp/coverage.out"
continue-on-error: true
- name: Display Test Coverage
run: |
echo "Test Coverage:"
cat ${{ steps.collect_coverage.outputs.coverage_file }}
continue-on-error: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
continue-on-error: true

@ -0,0 +1,82 @@
# Copyright © 2023 OpenIM open source community. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: OpenIM OpenIM Core release
on:
push:
# run only against tags
tags:
- '*'
permissions:
contents: write
packages: write
issues: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v4
with:
go-version: stable
# More assembly might be required: Docker logins, GPG, etc. It all depends
# on your needs.
- uses: goreleaser/goreleaser-action@v4
with:
# either 'goreleaser' (default) or 'goreleaser-pro':
distribution: goreleaser
version: latest
workdir: .
args: release --clean --clean --release-footer-tmpl=scripts/template/footer.md.tmpl --release-header-tmpl=scripts/template/head.md.tmpl
env:
USERNAME: ${{ github.repository_owner }}
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
# Your GoReleaser Pro key, if you are using the 'goreleaser-pro'
# distribution:
# GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
goreleaser-check-pkgs:
runs-on: ubuntu-latest
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
needs: [ goreleaser ]
if: github.ref == 'refs/heads/main'
strategy:
matrix:
format: [ deb, rpm, apk ]
steps:
- uses: actions/checkout@v3 # v3
with:
fetch-depth: 0
- uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2
- uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3
with:
path: |
./_output/dist/*.deb
./_output/dist/*.rpm
./_output/dist/*.apk
key: ${{ github.ref }}
- run: task goreleaser:test:${{ matrix.format }}

@ -0,0 +1,48 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
- cron: '0 8 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.BOT_GITHUB_TOKEN }}
days-before-stale: 60
days-before-close: 7
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'This issue is stale because it has been open 60 days with no activity.'
close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity. You can reopen it if you want.'
stale-pr-label: lifecycle/stale
stale-issue-label: lifecycle/stale
exempt-issue-labels: 'openim'
exempt-pr-labels: 'openim'
exempt-draft-pr: true

@ -0,0 +1,172 @@
logs
.devcontainer
components
logs
out-test
*.db
### Backup ###
*.bak
*.gho
*.ori
*.orig
*.tmp
### deploy dir ###
deploy/open_im_api
deploy/open_im_msg_gateway
deploy/open_im_msg_transfer
deploy/open_im_push
deploy/open_im_rpc_user
deploy/open_im_rpc_friend
deploy/open_im_rpc_group
deploy/open_im_rpc_msg
deploy/open_im_rpc_auth
deploy/Open-IM-SDK-Core
### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
vendor/
bin/
tools/
tmp/
### vscode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
### JetBrains ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml

@ -0,0 +1,934 @@
# Copyright © 2023 OpenIMSDK open source community. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file contains all available configuration options
# with their default values.
# options for analysis running
run:
# default concurrency is a available CPU number
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 5m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# list of build tags, all linters use it. Default is empty list.
build-tags:
- mytag
# which dirs to skip: issues from them won't be reported;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but default dirs are skipped independently
# from this option's value (see skip-dirs-use-default).
# "/" will be replaced by current OS file path separator to properly work
# on Windows.
skip-dirs:
- util
- .*~
- api/swagger/docs
- server/docs
# default is true. Enables skipping of directories:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs-use-default: true
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
# "/" will be replaced by current OS file path separator to properly work
# on Windows.
skip-files:
- ".*\\.my\\.go$"
- _test.go
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
# If invoked with -mod=readonly, the go command is disallowed from the implicit
# automatic updating of go.mod described above. Instead, it fails when any changes
# to go.mod are needed. This setting is most useful to check that go.mod does
# not need updates, such as in a continuous integration and testing system.
# If invoked with -mod=vendor, the go command assumes that the vendor
# directory holds the correct copies of dependencies and ignores
# the dependency descriptions in go.mod.
#modules-download-mode: release|readonly|vendor
# Allow multiple parallel golangci-lint instances running.
# If false (default) - golangci-lint acquires file lock on start.
allow-parallel-runners: true
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
# make issues output unique by line, default is true
uniq-by-line: true
# add a prefix to the output file references; default is no prefix
path-prefix: ""
# sorts results by: filepath, line and column
sort-results: true
# all available settings of specific linters
linters-settings:
bidichk:
# The following configurations check for all mentioned invisible unicode
# runes. It can be omitted because all runes are enabled by default.
left-to-right-embedding: true
right-to-left-embedding: true
pop-directional-formatting: true
left-to-right-override: true
right-to-left-override: true
left-to-right-isolate: true
right-to-left-isolate: true
first-strong-isolate: true
pop-directional-isolate: true
dogsled:
# checks assignments with too many blank identifiers; default is 2
max-blank-identifiers: 2
dupl:
# tokens count to trigger issue, 150 by default
threshold: 100
errcheck:
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
# [deprecated] comma-separated list of pairs of the form pkg:regex
# the regex is used to ignore names within pkg. (default "fmt:.*").
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
#ignore: GenMarkdownTree,os:.*,BindPFlags,WriteTo,Help
#ignore: (os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv
# path to a file containing a list of functions to exclude from checking
# see https://github.com/kisielk/errcheck#excluding-functions for details
#exclude: errcheck.txt
errorlint:
# Check whether fmt.Errorf uses the %w verb for formatting errors. See the readme for caveats
errorf: true
# Check for plain type assertions and type switches
asserts: true
# Check for plain error comparisons
comparison: true
exhaustive:
# check switch statements in generated files also
check-generated: false
# indicates that switch statements are to be considered exhaustive if a
# 'default' case is present, even if all enum members aren't listed in the
# switch
default-signifies-exhaustive: false
# enum members matching the supplied regex do not have to be listed in
# switch statements to satisfy exhaustiveness
ignore-enum-members: ""
# consider enums only in package scopes, not in inner scopes
package-scope-only: false
exhaustivestruct:
struct-patterns:
- '*.Test'
- '*.Test2'
- '*.Embedded'
- '*.External'
# forbidigo:
# # Forbid the following identifiers (identifiers are written using regexp):
# forbid:
# - ^print.*$
# - 'fmt\.Print.*'
# - fmt.Println.* # too much log noise
# - ginkgo\\.F.* # these are used just for local development
# # Exclude godoc examples from forbidigo checks. Default is true.
# exclude_godoc_examples: false
funlen:
lines: 150
statements: 50
gci:
# put imports beginning with prefix after 3rd-party packages;
# only support one prefix
# if not set, use goimports.local-prefixes
prefix: github.com/openimsdk/openim-sdk-core
gocognit:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 30
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
# ignore test files, false by default
ignore-tests: false
# look for existing constants matching the values, true by default
match-constant: true
# search also for duplicated numbers, false by default
numbers: false
# minimum value, only works with goconst.numbers, 3 by default
min: 3
# maximum value, only works with goconst.numbers, 3 by default
max: 3
# ignore when constant is not used as function argument, true by default
ignore-calls: true
gocritic:
# Which checks should be enabled; can't be combined with 'disabled-checks';
# See https://go-critic.github.io/overview#checks-overview
# To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`
# By default list of stable checks is used.
enabled-checks:
#- rangeValCopy
- nestingreduce
- truncatecmp
- unnamedresult
- ruleguard
# Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty
disabled-checks:
- regexpMust
- ifElseChain
#- exitAfterDefer
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
enabled-tags:
- performance
disabled-tags:
- experimental
# Settings passed to gocritic.
# The settings key is the name of a supported gocritic checker.
# The list of supported checkers can be find in https://go-critic.github.io/overview.
settings:
captLocal: # must be valid enabled check name
# whether to restrict checker to params only (default true)
paramsOnly: true
elseif:
# whether to skip balanced if-else pairs (default true)
skipBalanced: true
hugeParam:
# size in bytes that makes the warning trigger (default 80)
sizeThreshold: 80
nestingReduce:
# min number of statements inside a branch to trigger a warning (default 5)
bodyWidth: 5
rangeExprCopy:
# size in bytes that makes the warning trigger (default 512)
sizeThreshold: 512
# whether to check test functions (default true)
skipTestFuncs: true
rangeValCopy:
# size in bytes that makes the warning trigger (default 128)
sizeThreshold: 32
# whether to check test functions (default true)
skipTestFuncs: true
ruleguard:
# path to a gorules file for the ruleguard checker
rules: ''
truncateCmp:
# whether to skip int/uint/uintptr types (default true)
skipArchDependent: true
underef:
# whether to skip (*x).method() calls where x is a pointer receiver (default true)
skipRecvDeref: true
unnamedResult:
# whether to check exported functions
checkExported: true
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 30
cyclop:
# the maximal code complexity to report
max-complexity: 50
# the maximal average package complexity. If it's higher than 0.0 (float) the check is enabled (default 0.0)
package-average: 0.0
# should ignore tests (default false)
skip-tests: false
godot:
# comments to be checked: `declarations`, `toplevel`, or `all`
scope: declarations
# list of regexps for excluding particular comment lines from check
exclude:
# example: exclude comments which contain numbers
# - '[0-9]+'
# check that each sentence starts with a capital letter
capital: false
godox:
# report any comments starting with keywords, this is useful for TODO or FIXME comments that
# might be left in the code accidentally and should be resolved before merging
keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting
#- TODO
- BUG
- FIXME
#- NOTE
- OPTIMIZE # marks code that should be optimized before merging
- HACK # marks hack-arounds that should be removed before merging
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
gofumpt:
# Select the Go version to target. The default is `1.18`.
lang-version: "1.20"
# Choose whether or not to use the extra rules that are disabled
# by default
extra-rules: false
goheader:
values:
const:
# define here const type values in format k:v, for example:
# COMPANY: MY COMPANY
regexp:
# define here regexp type values, for example
# AUTHOR: .*@mycompany\.com
template: # |-
# put here copyright header template for source code files, for example:
# Note: {{ YEAR }} is a builtin value that returns the year relative to the current machine time.
#
# {{ AUTHOR }} {{ COMPANY }} {{ YEAR }}
# SPDX-License-Identifier: Apache-2.0
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
template-path:
# also as alternative of directive 'template' you may put the path to file with the template source
goimports:
# put imports beginning with prefix after 3rd-party packages;
# it's a comma-separated list of prefixes
local-prefixes: github.com/openimsdk/openim-sdk-core
golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.9
gomnd:
settings:
mnd:
# the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description.
checks: argument,case,condition,operation,return,assign
# ignored-numbers: 1000
# ignored-files: magic_.*.go
# ignored-functions: math.*
gomoddirectives:
# Allow local `replace` directives. Default is false.
replace-local: true
# List of allowed `replace` directives. Default is empty.
replace-allow-list:
- google.golang.org/grpc
# Allow to not explain why the version has been retracted in the `retract` directives. Default is false.
retract-allow-no-explanation: false
# Forbid the use of the `exclude` directives. Default is false.
exclude-forbidden: false
gomodguard:
allowed:
modules:
- gorm.io/gen # List of allowed modules
- gorm.io/gorm
- gorm.io/driver/mysql
- k8s.io/klog
# - gopkg.in/yaml.v2
domains: # List of allowed module domains
- google.golang.org
- gopkg.in
- golang.org
- github.com
- go.uber.org
- go.etcd.io
blocked:
versions:
- github.com/MakeNowJust/heredoc:
version: "> 2.0.9"
reason: "use the latest version"
local_replace_directives: false # Set to true to raise lint issues for packages that are loaded from a local path via replace directive
gosec:
# To select a subset of rules to run.
# Available rules: https://github.com/securego/gosec#available-rules
includes:
- G401
- G306
- G101
# To specify a set of rules to explicitly exclude.
# Available rules: https://github.com/securego/gosec#available-rules
excludes:
- G204
# Exclude generated files
exclude-generated: true
# Filter out the issues with a lower severity than the given value. Valid options are: low, medium, high.
severity: "low"
# Filter out the issues with a lower confidence than the given value. Valid options are: low, medium, high.
confidence: "low"
# To specify the configuration of rules.
# The configuration of rules is not fully documented by gosec:
# https://github.com/securego/gosec#configuration
# https://github.com/securego/gosec/blob/569328eade2ccbad4ce2d0f21ee158ab5356a5cf/rules/rulelist.go#L60-L102
config:
G306: "0600"
G101:
pattern: "(?i)example"
ignore_entropy: false
entropy_threshold: "80.0"
per_char_threshold: "3.0"
truncate: "32"
gosimple:
# Select the Go version to target. The default is '1.13'.
go: "1.20"
# https://staticcheck.io/docs/options#checks
checks: [ "all" ]
govet:
# report about shadowed variables
check-shadowing: true
# settings per analyzer
settings:
printf: # analyzer name, run `go tool vet help` to see all analyzers
funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
# enable or disable analyzers by name
enable:
- atomicalign
enable-all: false
disable:
- shadow
disable-all: false
# depguard:
# list-type: blacklist
# include-go-root: false
# packages:
# - github.com/Sirupsen/logrus
# packages-with-error-message:
# # specify an error message to output when a blacklisted package is used
# - github.com/Sirupsen/logrus: "logging is allowed only by logutils.Log"
ifshort:
# Maximum length of variable declaration measured in number of lines, after which linter won't suggest using short syntax.
# Has higher priority than max-decl-chars.
max-decl-lines: 1
# Maximum length of variable declaration measured in number of characters, after which linter won't suggest using short syntax.
max-decl-chars: 30
importas:
# if set to `true`, force to use alias.
no-unaliased: true
# List of aliases
alias:
# using `servingv1` alias for `knative.dev/serving/pkg/apis/serving/v1` package
- pkg: knative.dev/serving/pkg/apis/serving/v1
alias: servingv1
# using `autoscalingv1alpha1` alias for `knative.dev/serving/pkg/apis/autoscaling/v1alpha1` package
- pkg: knative.dev/serving/pkg/apis/autoscaling/v1alpha1
alias: autoscalingv1alpha1
# You can specify the package path by regular expression,
# and alias by regular expression expansion syntax like below.
# see https://github.com/julz/importas#use-regular-expression for details
- pkg: knative.dev/serving/pkg/apis/(\w+)/(v[\w\d]+)
alias: $1$2
# using `jwt` alias for `github.com/appleboy/gin-jwt/v2` package
jwt: github.com/appleboy/gin-jwt/v2
ireturn:
# ireturn allows using `allow` and `reject` settings at the same time.
# Both settings are lists of the keywords and regular expressions matched to interface or package names.
# keywords:
# - `empty` for `interface{}`
# - `error` for errors
# - `stdlib` for standard library
# - `anon` for anonymous interfaces
# By default, it allows using errors, empty interfaces, anonymous interfaces,
# and interfaces provided by the standard library.
allow:
- anon
- error
- empty
- stdlib
# You can specify idiomatic endings for interface
- (or|er)$
# Reject patterns
reject:
- github.com\/user\/package\/v4\.Type
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 240
# tab width in spaces. Default to 1.
tab-width: 4
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
ignore-words:
- someword
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 30
nestif:
# minimal complexity of if statements to report, 5 by default
min-complexity: 4
nilnil:
# By default, nilnil checks all returned types below.
checked-types:
- ptr
- func
- iface
- map
- chan
nlreturn:
# size of the block (including return statement that is still "OK")
# so no return split required.
block-size: 1
nolintlint:
# Disable to ensure that all nolint directives actually have an effect. Default is true.
allow-unused: false
# Disable to ensure that nolint directives don't have a leading space. Default is true.
allow-leading-space: true
# Exclude following linters from requiring an explanation. Default is [].
allow-no-explanation: [ ]
# Enable to require an explanation of nonzero length after each nolint directive. Default is false.
require-explanation: false
# Enable to require nolint directives to mention the specific linter being suppressed. Default is false.
require-specific: true
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true # Report preallocation suggestions on range loops, true by default
for-loops: false # Report preallocation suggestions on for loops, false by default
promlinter:
# Promlinter cannot infer all metrics name in static analysis.
# Enable strict mode will also include the errors caused by failing to parse the args.
strict: false
# Please refer to https://github.com/yeya24/promlinter#usage for detailed usage.
disabled-linters:
# - "Help"
# - "MetricUnits"
# - "Counter"
# - "HistogramSummaryReserved"
# - "MetricTypeInName"
# - "ReservedChars"
# - "CamelCase"
# - "lintUnitAbbreviations"
predeclared:
# comma-separated list of predeclared identifiers to not report on
ignore: ""
# include method names and field names (i.e., qualified names) in checks
q: false
rowserrcheck:
packages:
- github.com/jmoiron/sqlx
revive:
# see https://github.com/mgechev/revive#available-rules for details.
ignore-generated-header: true
severity: warning
rules:
- name: indent-error-flow
severity: warning
staticcheck:
# Select the Go version to target. The default is '1.13'.
go: "1.16"
# https://staticcheck.io/docs/options#checks
checks: [ "all" ]
stylecheck:
# Select the Go version to target. The default is '1.13'.
go: "1.16"
# https://staticcheck.io/docs/options#checks
checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022" ]
# https://staticcheck.io/docs/options#dot_import_whitelist
dot-import-whitelist:
- fmt
# https://staticcheck.io/docs/options#initialisms
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" ]
# https://staticcheck.io/docs/options#http_status_code_whitelist
http-status-code-whitelist: [ "200", "400", "404", "500" ]
tagliatelle:
# check the struck tag name case
case:
# use the struct field name to check the name of the struct tag
use-field-name: true
rules:
# any struct tag type can be used.
# support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower`
json: camel
yaml: camel
xml: camel
bson: camel
avro: snake
mapstructure: kebab
testpackage:
# regexp pattern to skip files
skip-regexp: (id|export|internal)_test\.go
thelper:
# The following configurations enable all checks. It can be omitted because all checks are enabled by default.
# You can enable only required checks deleting unnecessary checks.
test:
first: true
name: true
begin: true
benchmark:
first: true
name: true
begin: true
tb:
first: true
name: true
begin: true
tenv:
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
# By default, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
all: false
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
whitespace:
multi-if: false # Enforces newlines (or comments) after every multi-line if statement
multi-func: false # Enforces newlines (or comments) after every multi-line function signature
wrapcheck:
# An array of strings that specify substrings of signatures to ignore.
# If this set, it will override the default set of ignored signatures.
# See https://github.com/tomarrell/wrapcheck#configuration for more information.
ignoreSigs:
- .Errorf(
- errors.New(
- errors.Unwrap(
- .Wrap(
- .Wrapf(
- .WithMessage(
- .WithMessagef(
- .WithStack(
ignorePackageGlobs:
- encoding/*
- github.com/pkg/*
wsl:
# If true append is only allowed to be cuddled if appending value is
# matching variables, fields or types on line above. Default is true.
strict-append: true
# Allow calls and assignments to be cuddled as long as the lines have any
# matching variables, fields or types. Default is true.
allow-assign-and-call: true
# Allow assignments to be cuddled with anything. Default is false.
allow-assign-and-anything: false
# Allow multiline assignments to be cuddled. Default is true.
allow-multiline-assign: true
# Allow declarations (var) to be cuddled.
allow-cuddle-declarations: false
# Allow trailing comments in ending of blocks
allow-trailing-comment: false
# Force newlines in end of case at this limit (0 = never).
force-case-trailing-whitespace: 0
# Force cuddling of err checks with err var assignment
force-err-cuddling: false
# Allow leading comments to be separated with empty liens
allow-separated-leading-comment: false
makezero:
# Allow only slices initialized with a length of zero. Default is false.
always: false
# The custom section can be used to define linter plugins to be loaded at runtime. See README doc
# for more info.
#custom:
# Each custom linter should have a unique name.
#example:
# The path to the plugin *.so. Can be absolute or local. Required for each custom linter
#path: /path/to/example.so
# The description of the linter. Optional, just for documentation purposes.
#description: This is an example usage of a plugin linter.
# Intended to point to the repo location of the linter. Optional, just for documentation purposes.
#original-url: github.com/golangci/example-linter
linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon.
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
# enable-all: true
disable-all: true
enable:
- typecheck
- asciicheck
- bodyclose
- cyclop
- deadcode
# - depguard
- dogsled
- dupl
- durationcheck
- errcheck
- errorlint
- exhaustive
- exportloopref
# - forbidigo
- funlen
# - gci
# - gochecknoinits
- gocognit
- goconst
- gocyclo
- godot
- godox
- gofmt
- gofumpt
- goheader
- goimports
- gomoddirectives
- gomodguard
- goprintffuncname
- gosec
- gosimple
- govet
- ifshort
- importas
- ineffassign
- lll
- makezero
- misspell
- nakedret
- nestif
- nilerr
- nlreturn
- noctx
- nolintlint
- paralleltest
- prealloc
- predeclared
- promlinter
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- structcheck
- stylecheck
- thelper
- tparallel
- unconvert
- unparam
- unused
- varcheck
- wastedassign
- whitespace
- bidichk
- wastedassign
- golint
- execinquery
- nosprintfhostport
- grouper
- decorder
- errchkjson
- maintidx
#- containedctx
#- tagliatelle
#- nonamedreturns
#- nilnil
#- tenv
#- varnamelen
#- contextcheck
#- errname
#- ForceTypeAssert
#- nilassign
fast: false
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
- tools/.*
- test/.*
- third_party/.*
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- linters:
- golint
path: (internal/api/.*)\.go # exclude golint for internal/api/... files
- linters:
- revive
path: (log/.*)\.go
- linters:
- wrapcheck
path: (cmd/.*|pkg/.*)\.go
- linters:
- typecheck
#path: (pkg/storage/.*)\.go
path: (internal/.*|pkg/.*)\.go
- path: (cmd/.*|test/.*|tools/.*|internal/pump/pumps/.*)\.go
linters:
- forbidigo
- path: (cmd/[a-z]*/.*|store/.*)\.go
linters:
- dupl
- linters:
- gocritic
text: (hugeParam:|rangeValCopy:)
- path: (cmd/[a-z]*/.*)\.go
linters:
- lll
- path: (validator/.*|code/.*|validator/.*|watcher/watcher/.*)
linters:
- gochecknoinits
- path: (internal/.*/options|internal/pump|pkg/log/options.go|internal/authzserver|tools/)
linters:
- tagliatelle
- path: (pkg/app/.*)\.go
linters:
- deadcode
- unused
- varcheck
- forbidigo
# Exclude some staticcheck messages
- linters:
- staticcheck
text: "SA9003:"
# Exclude lll issues for long lines with go:generate
- linters:
- lll
source: "^//go:generate "
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: true
# The default value is false. If set to true exclude and exclude-rules
# regular expressions become case sensitive.
exclude-case-sensitive: false
# The list of ids of default excludes to include or disable. By default it's empty.
include:
- EXC0002 # disable excluding of issues about comments from golint
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false
# Show only new issues created after git revision `REV`
# new-from-rev: REV
# Show only new issues created in git patch with set file path.
#new-from-patch: path/to/patch/file
# Fix found issues (if it's supported by the linter)
fix: true
severity:
# Default value is empty string.
# Set the default severity for issues. If severity rules are defined and the issues
# do not match or no severity is provided to the rule this will be the default
# severity applied. Severities should match the supported severity names of the
# selected out format.
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity
# - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
default-severity: error
# The default value is false.
# If set to true severity-rules regular expressions become case sensitive.
case-sensitive: false
# Default value is empty list.
# When a list of severity rules are provided, severity information will be added to lint
# issues. Severity rules have the same filtering capability as exclude rules except you
# are allowed to specify one matcher per severity rule.
# Only affects out formats that support setting severity information.
rules:
- linters:
- dupl
severity: info

@ -0,0 +1,223 @@
# Copyright © 2023 OpenIM open source community. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
git:
# What should be used to sort tags when gathering the current and previous
# tags if there are more than one tag in the same commit.
#
# Default: '-version:refname'
tag_sort: -version:creatordate
# What should be used to specify prerelease suffix while sorting tags when gathering
# the current and previous tags if there are more than one tag in the same commit.
#
# Since: v1.17
prerelease_suffix: "-"
# Tags to be ignored by GoReleaser.
# This means that GoReleaser will not pick up tags that match any of the
# provided values as either previous or current tags.
#
# Templates: allowed.
# Since: v1.21.
ignore_tags:
- nightly
# - "{{.Env.IGNORE_TAG}}"
report_sizes: true
builds:
- binary: openim-sdk-core
id: openim-sdk-core
main: ./cmd/main.go
goos:
- linux
goarch:
- amd64
- arm64
goarm:
- "6"
- "7"
- id: openIM.wasm
main: wasm/cmd/main.go # 指定 wasm 主文件路径
binary: openIM.wasm
ldflags: "-s -w"
goos:
- js
goarch:
- wasm
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
files:
- LICENSE
- README.md
# a more complete example, check the globbing deep dive below
- src: "*.md"
dst: docs
# Strip parent folders when adding files to the archive.
strip_parent: true
# File info.
# Not all fields are supported by all formats available formats.
#
# Default: copied from the source file
info:
# Templates: allowed (since v1.14)
owner: root
# Templates: allowed (since v1.14)
group: root
# Must be in time.RFC3339Nano format.
#
# Templates: allowed (since v1.14)
mtime: "{{ .CommitDate }}"
# File mode.
mode: 0644
format_overrides:
- goos: windows
format: zip
changelog:
sort: asc
use: github
filters:
exclude:
- "^test:"
- "^chore"
- "merge conflict"
- Merge pull request
- Merge remote-tracking branch
- Merge branch
- go mod tidy
groups:
- title: Dependency updates
regexp: '^.*?(feat|fix)\(deps\)!?:.+$'
order: 300
- title: "New Features"
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 100
- title: "Security updates"
regexp: '^.*?sec(\([[:word:]]+\))??!?:.+$'
order: 150
- title: "Bug fixes"
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
order: 200
- title: "Documentation updates"
regexp: ^.*?doc(\([[:word:]]+\))??!?:.+$
order: 400
- title: "Build process updates"
regexp: ^.*?build(\([[:word:]]+\))??!?:.+$
order: 400
- title: Other work
order: 9999
nfpms:
- id: packages
builds:
- openim-sdk-core
- openIM.wasm
# Your app's vendor.
vendor: OpenIMSDK
homepage: https://github.com/openimsdk/openim-sdk-core
maintainer: kubbot <https://github.com/kubbot>
description: |-
Auto sync github labels
kubbot && openimbot
license: Apache-2.0
formats:
- apk
- deb
- rpm
- termux.deb # Since: v1.11
- archlinux # Since: v1.13
dependencies:
- git
recommends:
- golang
# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
# Default: './dist'
dist: ./_output/dist
# .goreleaser.yaml
milestones:
# You can have multiple milestone configs
-
# Repository for the milestone
# Default is extracted from the origin remote URL
repo:
owner: user
name: repo
# Whether to close the milestone
close: true
# Fail release on errors, such as missing milestone.
fail_on_error: false
# Name of the milestone
#
# Default: '{{ .Tag }}'
name_template: "Current Release"
# publishers:
# - name: "fury.io"
# ids:
# - packages
# dir: "{{ dir .ArtifactPath }}"
# cmd: |
# bash -c '
# if [[ "{{ .Tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/{{ .Env.USERNAME }}/
# else
# echo "Skipping deployment: Non-production release detected"
# fi'
checksum:
name_template: "{{ .ProjectName }}_checksums.txt"
algorithm: sha256
release:
prerelease: auto

@ -0,0 +1,62 @@
# Version logging for OpenIM
<!-- BEGIN MUNGE: GENERATED_TOC -->
<!-- END MUNGE: GENERATED_TOC -->
{{ if .Versions -}}
<a name="unreleased"></a>
## [Unreleased]
{{ if .Unreleased.CommitGroups -}}
{{ range .Unreleased.CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
{{ range .Versions }}
<a name="{{ .Tag.Name }}"></a>
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
{{ end }}
{{ end -}}
{{- if .RevertCommits -}}
### Reverts
{{ range .RevertCommits -}}
- {{ .Revert.Header }}
{{ end }}
{{ end -}}
{{- if .MergeCommits -}}
### Pull Requests
{{ range .MergeCommits -}}
- {{ .Header }}
{{ end }}
{{ end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
{{- if .Versions }}
[Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD
{{ range .Versions -}}
{{ if .Tag.Previous -}}
[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}
{{ end -}}
{{ end -}}
{{ end -}}

@ -0,0 +1,81 @@
# Copyright © 2023 OpenIM SDK. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
bin: git
style: github
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://github.com/openimsdk/openim-sdk-core
options:
tag_filter_pattern: '^v'
sort: "date"
commits:
filters:
Type:
- feat
- fix
- perf
- refactor
- docs
- test
- chore
- ci
- build
sort_by: Scope
commit_groups:
group_by: Type
sort_by: Title
title_order:
- feat
- fix
- perf
- refactor
- docs
- test
- chore
- ci
- build
title_maps:
feat: Features
header:
pattern: "<regexp>"
pattern_maps:
- PropName
issues:
prefix:
- #
refs:
actions:
- Closes
- Fixes
merges:
pattern: "^Merge branch '(\\w+)'$"
pattern_maps:
- Source
reverts:
pattern: "^Revert \"([\\s\\S]*)\"$"
pattern_maps:
- Header
notes:
keywords:
- BREAKING CHANGE

@ -0,0 +1,42 @@
# Version logging for OpenIM
<!-- BEGIN MUNGE: GENERATED_TOC -->
<!-- END MUNGE: GENERATED_TOC -->
<a name="unreleased"></a>
## [Unreleased]
<a name="v1.0.7"></a>
## [v1.0.7] - 2021-12-10
<a name="v1.0.6"></a>
## [v1.0.6] - 2021-12-03
<a name="v1.0.5"></a>
## [v1.0.5] - 2021-11-25
<a name="v1.0.4"></a>
## [v1.0.4] - 2021-11-12
<a name="v1.0.3"></a>
## [v1.0.3] - 2021-11-05
<a name="v1.0.2"></a>
## [v1.0.2] - 2021-11-04
<a name="v1.0.1"></a>
## [v1.0.1] - 2021-11-03
<a name="v1.0.0"></a>
## v1.0.0 - 2021-10-28
[Unreleased]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.7...HEAD
[v1.0.7]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.6...v1.0.7
[v1.0.6]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.5...v1.0.6
[v1.0.5]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.4...v1.0.5
[v1.0.4]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.3...v1.0.4
[v1.0.3]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.2...v1.0.3
[v1.0.2]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.1...v1.0.2
[v1.0.1]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.0...v1.0.1

@ -0,0 +1,92 @@
# Version logging for OpenIM
<!-- BEGIN MUNGE: GENERATED_TOC -->
<!-- END MUNGE: GENERATED_TOC -->
<a name="unreleased"></a>
## [Unreleased]
<a name="v2.3.3"></a>
## [v2.3.3] - 2022-09-16
<a name="v2.3.2"></a>
## [v2.3.2] - 2022-09-09
<a name="v2.3.0-rc0"></a>
## [v2.3.0-rc0] - 2022-07-15
<a name="v2.2.0"></a>
## [v2.2.0] - 2022-07-01
### Pull Requests
- Merge branch 'tuoyun'
<a name="v2.1.0"></a>
## [v2.1.0] - 2022-06-17
<a name="v2.0.9"></a>
## [v2.0.9] - 2022-05-11
<a name="v2.0.8"></a>
## [v2.0.8] - 2022-04-29
### Pull Requests
- Merge branch 'tuoyun'
<a name="v2.0.7"></a>
## [v2.0.7] - 2022-04-22
<a name="v2.0.6"></a>
## [v2.0.6] - 2022-04-08
<a name="v2.0.5"></a>
## [v2.0.5] - 2022-04-01
### Pull Requests
- Merge branch 'tuoyun'
<a name="v2.0.4"></a>
## [v2.0.4] - 2022-03-25
<a name="v2.0.3"></a>
## [v2.0.3] - 2022-03-18
### Pull Requests
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
<a name="v2.0.2"></a>
## [v2.0.2] - 2022-02-24
<a name="v2.0.1"></a>
## [v2.0.1] - 2022-02-24
### Pull Requests
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
<a name="v2.0.0"></a>
## v2.0.0 - 2022-02-23
[Unreleased]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.3...HEAD
[v2.3.3]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.2...v2.3.3
[v2.3.2]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.0-rc0...v2.3.2
[v2.3.0-rc0]: https://github.com/openimsdk/openim-sdk-core/compare/v2.2.0...v2.3.0-rc0
[v2.2.0]: https://github.com/openimsdk/openim-sdk-core/compare/v2.1.0...v2.2.0
[v2.1.0]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.9...v2.1.0
[v2.0.9]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.8...v2.0.9
[v2.0.8]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.7...v2.0.8
[v2.0.7]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.6...v2.0.7
[v2.0.6]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.5...v2.0.6
[v2.0.5]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.4...v2.0.5
[v2.0.4]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.3...v2.0.4
[v2.0.3]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.2...v2.0.3
[v2.0.2]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.1...v2.0.2
[v2.0.1]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.0...v2.0.1

@ -0,0 +1,23 @@
# Version logging for OpenIM
<!-- BEGIN MUNGE: GENERATED_TOC -->
<!-- END MUNGE: GENERATED_TOC -->
<a name="unreleased"></a>
## [Unreleased]
<a name="v2.1.0"></a>
## v2.1.0 - 2022-06-17
### Pull Requests
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
[Unreleased]: https://github.com/openimsdk/openim-sdk-core/compare/v2.1.0...HEAD

@ -0,0 +1,24 @@
# Version logging for OpenIM
<!-- BEGIN MUNGE: GENERATED_TOC -->
<!-- END MUNGE: GENERATED_TOC -->
<a name="unreleased"></a>
## [Unreleased]
<a name="v2.2.0"></a>
## v2.2.0 - 2022-07-01
### Pull Requests
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
[Unreleased]: https://github.com/openimsdk/openim-sdk-core/compare/v2.2.0...HEAD

@ -0,0 +1,32 @@
# Version logging for OpenIM
<!-- BEGIN MUNGE: GENERATED_TOC -->
<!-- END MUNGE: GENERATED_TOC -->
<a name="unreleased"></a>
## [Unreleased]
<a name="v2.3.3"></a>
## [v2.3.3] - 2022-09-16
<a name="v2.3.2"></a>
## [v2.3.2] - 2022-09-09
<a name="v2.3.0-rc0"></a>
## v2.3.0-rc0 - 2022-07-15
### Pull Requests
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
[Unreleased]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.3...HEAD
[v2.3.3]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.2...v2.3.3
[v2.3.2]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.0-rc0...v2.3.2

@ -0,0 +1,52 @@
# Version logging for OpenIM
<!-- BEGIN MUNGE: GENERATED_TOC -->
- [Version logging for OpenIM](#version-logging-for-openim)
- [Unreleased](#unreleased)
- [v2.9.0+1.839643f - 2023-07-07](#v2901839643f---2023-07-07)
- [v2.9.0+2.35f07fe - 2023-07-06](#v290235f07fe---2023-07-06)
- [v2.9.0+1.b5072b1 - 2023-07-05](#v2901b5072b1---2023-07-05)
- [v2.9.0+3.2667a3a - 2023-07-05](#v29032667a3a---2023-07-05)
- [v2.9.0+7.04818ca - 2023-07-05](#v290704818ca---2023-07-05)
- [v2.9.0 - 2023-07-04](#v290---2023-07-04)
- [Reverts](#reverts)
- [Pull Requests](#pull-requests)
<!-- END MUNGE: GENERATED_TOC -->
<a name="unreleased"></a>
## [Unreleased]
<a name="v2.9.0+1.839643f"></a>
## [v2.9.0+1.839643f] - 2023-07-07
<a name="v2.9.0+2.35f07fe"></a>
## [v2.9.0+2.35f07fe] - 2023-07-06
<a name="v2.9.0+1.b5072b1"></a>
## [v2.9.0+1.b5072b1] - 2023-07-05
<a name="v2.9.0+3.2667a3a"></a>
## [v2.9.0+3.2667a3a] - 2023-07-05
<a name="v2.9.0+7.04818ca"></a>
## [v2.9.0+7.04818ca] - 2023-07-05
<a name="v2.9.0"></a>
## v2.9.0 - 2023-07-04
### Reverts
- update etcd to v3.5.2 ([#206](https://github.com/openimsdk/Open-IM-Server/issues/206))
### Pull Requests
- Merge branch 'tuoyun'
[Unreleased]: https://github.com/openimsdk/Open-IM-Server/compare/v2.9.0+1.839643f...HEAD
[v2.9.0+1.839643f]: https://github.com/openimsdk/Open-IM-Server/compare/v2.9.0+2.35f07fe...v2.9.0+1.839643f
[v2.9.0+2.35f07fe]: https://github.com/openimsdk/Open-IM-Server/compare/v2.9.0+1.b5072b1...v2.9.0+2.35f07fe
[v2.9.0+1.b5072b1]: https://github.com/openimsdk/Open-IM-Server/compare/v2.9.0+3.2667a3a...v2.9.0+1.b5072b1
[v2.9.0+3.2667a3a]: https://github.com/openimsdk/Open-IM-Server/compare/v2.9.0+7.04818ca...v2.9.0+3.2667a3a
[v2.9.0+7.04818ca]: https://github.com/openimsdk/Open-IM-Server/compare/v2.9.0...v2.9.0+7.04818ca

@ -0,0 +1,128 @@
# Version logging for OpenIM
<!-- BEGIN MUNGE: GENERATED_TOC -->
<!-- END MUNGE: GENERATED_TOC -->
<a name="unreleased"></a>
## [Unreleased]
<a name="v3.0.0-rc.1"></a>
## [v3.0.0-rc.1] - 2023-07-13
<a name="v2.3.3"></a>
## [v2.3.3] - 2022-09-16
<a name="v2.3.2"></a>
## [v2.3.2] - 2022-09-09
<a name="v2.3.0-rc0"></a>
## [v2.3.0-rc0] - 2022-07-15
<a name="v2.2.0"></a>
## [v2.2.0] - 2022-07-01
### Pull Requests
- Merge branch 'tuoyun'
<a name="v2.1.0"></a>
## [v2.1.0] - 2022-06-17
<a name="v2.0.9"></a>
## [v2.0.9] - 2022-05-11
<a name="v2.0.8"></a>
## [v2.0.8] - 2022-04-29
### Pull Requests
- Merge branch 'tuoyun'
<a name="v2.0.7"></a>
## [v2.0.7] - 2022-04-22
<a name="v2.0.6"></a>
## [v2.0.6] - 2022-04-08
<a name="v2.0.5"></a>
## [v2.0.5] - 2022-04-01
### Pull Requests
- Merge branch 'tuoyun'
<a name="v2.0.4"></a>
## [v2.0.4] - 2022-03-25
<a name="v2.0.3"></a>
## [v2.0.3] - 2022-03-18
### Pull Requests
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
<a name="v2.0.2"></a>
## [v2.0.2] - 2022-02-24
<a name="v2.0.1"></a>
## [v2.0.1] - 2022-02-24
### Pull Requests
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
- Merge branch 'tuoyun'
<a name="v2.0.0"></a>
## [v2.0.0] - 2022-02-23
<a name="v1.0.7"></a>
## [v1.0.7] - 2021-12-10
<a name="v1.0.6"></a>
## [v1.0.6] - 2021-12-03
<a name="v1.0.5"></a>
## [v1.0.5] - 2021-11-25
<a name="v1.0.4"></a>
## [v1.0.4] - 2021-11-12
<a name="v1.0.3"></a>
## [v1.0.3] - 2021-11-05
<a name="v1.0.2"></a>
## [v1.0.2] - 2021-11-04
<a name="v1.0.1"></a>
## [v1.0.1] - 2021-11-03
<a name="v1.0.0"></a>
## v1.0.0 - 2021-10-28
[Unreleased]: https://github.com/openimsdk/openim-sdk-core/compare/v3.0.0-rc.1...HEAD
[v3.0.0-rc.1]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.3...v3.0.0-rc.1
[v2.3.3]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.2...v2.3.3
[v2.3.2]: https://github.com/openimsdk/openim-sdk-core/compare/v2.3.0-rc0...v2.3.2
[v2.3.0-rc0]: https://github.com/openimsdk/openim-sdk-core/compare/v2.2.0...v2.3.0-rc0
[v2.2.0]: https://github.com/openimsdk/openim-sdk-core/compare/v2.1.0...v2.2.0
[v2.1.0]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.9...v2.1.0
[v2.0.9]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.8...v2.0.9
[v2.0.8]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.7...v2.0.8
[v2.0.7]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.6...v2.0.7
[v2.0.6]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.5...v2.0.6
[v2.0.5]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.4...v2.0.5
[v2.0.4]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.3...v2.0.4
[v2.0.3]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.2...v2.0.3
[v2.0.2]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.1...v2.0.2
[v2.0.1]: https://github.com/openimsdk/openim-sdk-core/compare/v2.0.0...v2.0.1
[v2.0.0]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.7...v2.0.0
[v1.0.7]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.6...v1.0.7
[v1.0.6]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.5...v1.0.6
[v1.0.5]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.4...v1.0.5
[v1.0.4]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.3...v1.0.4
[v1.0.3]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.2...v1.0.3
[v1.0.2]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.1...v1.0.2
[v1.0.1]: https://github.com/openimsdk/openim-sdk-core/compare/v1.0.0...v1.0.1

@ -0,0 +1,104 @@
# Changelog
- [Changelog](#changelog)
- [command](#command)
- [create next tag](#create-next-tag)
- [Release version logs](#release-version-logs)
- [Introduction](#introduction)
- [Naming Format](#naming-format)
- [Examples](#examples)
- [Version Modifiers](#version-modifiers)
- [Versioning Strategy](#versioning-strategy)
All notable changes to this project will be documented in this file.
+ [https://github.com/openimsdk/Open-IM-Server/releases](https://github.com/openimsdk/Open-IM-Server/releases)
## command
```bash
git-chglog --tag-filter-pattern 'v2.0.*' -o CHANGELOG-2.0.md
```
## create next tag
```bash
git-chglog --next-tag 2.0.0 -o CHANGELOG.md
git commit -am "release 2.0.0"
git tag 2.0.0
```
| Query | Description | Example |
| -------------- | ---------------------------------------------- | --------------------------- |
| `<old>..<new>` | Commit contained in `<new>` tags from `<old>`. | `$ git-chglog 1.0.0..2.0.0` |
| `<name>..` | Commit from the `<name>` to the latest tag. | `$ git-chglog 1.0.0..` |
| `..<name>` | Commit from the oldest tag to `<name>`. | `$ git-chglog ..2.0.0` |
| `<name>` | Commit contained in `<name>`. | `$ git-chglog 1.0.0` |
## Release version logs
+ [OpenIM CHANGELOG-V1.0](CHANGELOG-1.0.md)
+ [OpenIM CHANGELOG-V2.0](CHANGELOG-2.0.md)
+ [OpenIM CHANGELOG-V2.1](CHANGELOG-2.1.md)
+ [OpenIM CHANGELOG-V2.2](CHANGELOG-2.2.md)
+ [OpenIM CHANGELOG-V2.3](CHANGELOG-2.3.md)
+ [OpenIM CHANGELOG-V2.9](CHANGELOG-2.9.md)
+ [OpenIM CHANGELOG-V3.0](CHANGELOG-3.0.md)
## Introduction
In both the open-source and closed-source software development communities, it is important to follow a consistent and understandable versioning scheme for software projects. This ensures clear communication of changes, compatibility, and stability across different releases. One widely adopted naming convention is the Semantic Versioning 2.0.0.
## Naming Format
The most common format for version numbers is as follows:
```
major.minor[.patch[.build]]
```
Let's take a closer look at each component:
1. **Major Version**: This is the first number in the versioning scheme and indicates significant changes that may not be backward compatible (specific to each project).
2. **Minor Version**: The second number signifies the addition of new features while maintaining backward compatibility.
3. **Patch Version**: The third number represents bug fixes or code optimizations without introducing new features. It is generally backward compatible.
4. **Build Version**: Typically an automatically generated number that increments with each code commit.
## Examples
Here are a few examples to illustrate the versioning scheme:
1. `1.0`
2. `2.14.0.1478`
3. `3.2.1 build-354`
## Version Modifiers
Apart from the version numbers, there are also version modifiers used to indicate specific stages or statuses of a release. Some commonly used version modifiers include:
- **alpha**: An internal testing version with numerous known bugs. It is primarily used for communication among developers.
- **beta**: A testing version released to enthusiastic users for feedback and bug detection.
- **rc (release candidate)**: The final testing version before the official release.
- **ga (general availability)**: The initial stable release for public distribution.
- **r/release** (or no modifier at all): The final released version intended for general users.
- **lts (long-term support)**: Designates a version that will receive extended maintenance and bug fixes for a specified number of years.
## Versioning Strategy
To effectively manage version numbers, the following strategies are commonly employed:
- The initial version of a project can be either `0.1` or `1.0`.
- When fixing bugs, the patch version is incremented by 1.
- When adding new features, the minor version is incremented by 1, and the patch version is reset to 0.
- In the case of significant modifications, the major version is incremented by 1.
- The build version is usually automatically generated by the compilation process and follows a defined format. It does not require manual control.
By adhering to these strategies and guidelines, developers can maintain consistency and clarity in versioning their software projects. This enables users and collaborators to understand the nature of changes between different releases and ensure compatibility with their systems.
(Note: Markdown formatting has been used to structure this article. Markdown is a lightweight markup language used to format text on platforms like GitHub.)
------
**Note**: The above article is based on the given content and aims to provide a Markdown-formatted English article explaining the naming conventions for software project versions, specifically focusing on the Semantic Versioning 2.0.0.

@ -0,0 +1,435 @@
# Contributing to OpenIM
So, you want to hack on OpenIM? Yay!
First of all, thank you for considering contributing to our project! We appreciate your time and effort, and we value any contribution, whether it's reporting a bug, suggesting a new feature, or submitting a pull request.
This document provides guidelines and best practices to help you contribute effectively.
## 📇Topics
- [What we expect of you](#What-we-expect-of-you)
- [Code of Conduct](#Code-of-Conduct)
- [Getting Started](#Getting-Started)
- [Style and Specification](#Style-and-Specification)
- [Engage to help anything](#Engage-to-help-anything)
- [Release version](#Release-version)
- [Contact Us](#Contact-Us)
## What we expect of you
We hope that anyone can join OpenIM , even if you are a student, writer, translator
Please meet the minimum version of the Go language published in [go.mod](./go.mod). If you want to manage the Go language version, we provide tools to install [gvm](https://github.com/moovweb/gvm) in our [Makefile](./Makefile)
You'd better use Linux as the development environment, Linux with [Makefile](./Makefile) can help you quickly build and test OpenIM project.
If you are familiar with [Makefile](./Makefile) , you can easily see the clever design of the OpenIM Makefile. Storing the necessary tools such as golangci in the `/tools` directory can avoid some tool version issues.
The [Makefile](./Makefile) is for every developer, even if you don't know how to use the Makefile tool, don't worry, we provide two great commands to get you up to speed with the Makefile architecture, `make help` and `make help-all`, it can reduce problems of the developing environment.
## Code of Conduct
#### Code and doc contribution
Every action to make project OpenIM better is encouraged. On GitHub, every improvement for OpenIM could be via a [PR](https://github.com/openimsdk/openim-sdk-core/pulls) (short for pull request).
+ If you find a typo, try to fix it!
+ If you find a bug, try to fix it!
+ If you find some redundant codes, try to remove them!
+ If you find some test cases missing, try to add them!
+ If you could enhance a feature, please **DO NOT** hesitate!
+ If you find code implicit, try to add comments to make it clear!
+ If you find code ugly, try to refactor that!
+ If you can help to improve documents, it could not be better!
+ If you find document incorrect, just do it and fix that!
+ ...
#### Where should I start?
Getting good at GitHub is the first step, we have a [list of labes](https://github.com/openimsdk/openim-sdk-core/labels) and reading some of the [common tags](https://github.com/openimsdk/openim-sdk-core/labels?sort=count-desc) helps us get involved in the community quickly.GitHub allows you to filter out types of issues and pull requests, which helps you discover items in need of triaging. This table includes some predetermined searches for convenience:
| Search | What it sorts |
| ------------------------------------------------------------ | ------------------------------------------------------- |
| [created-asc](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+sort%3Acreated-asc) | Untriaged issues by age |
| [needs-triage](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+no%3Alabel) | Issues that need to be assigned to a Labels |
| [`is:open is:issue`](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aopen+is%3Aissue+sort%3Aupdated-desc) | Newest incoming issues |
| [comments-desc](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-desc) | Busiest untriaged issues, sorted by # of comments |
| [comments-asc](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-asc) | Issues that need more attention, based on # of comments |
We suggest preparing your triage by filtering out the oldest, unlabelled issues and pull requests first.
1. If you are new to the project, don't know how to contribute OpenIM, please check out the [good first issue](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aopen+label%3A"good+first+issue"+sort%3Aupdated-desc) label and [help wanted](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted"+sort%3Aupdated-desc).
2. You should be good at filtering the OpenIM issue tags and finding the ones you like, such as [RFC](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+RFC+label%3ARFC) for big initiatives, features for [feature](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+label%3Akind%2Ffeature+sort%3Aupdated-desc+) proposals, and [bug](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+label%3Akind%2Fbug+sort%3Aupdated-desc+) fixes.
3. If you are looking for something to work on, check out our [open issues](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
4. If you have an idea for a new feature, please [open an issue](https://github.com/openimsdk/openim-sdk-core/issues/new/choose), and we can discuss it.
> **Note**
> Reply to `/assign` or `/assign @yourself` with a question you wish to resolve, and we'll assign the question to you and your name will be listed under `Assignees`
#### Design documents
For any substantial design, there should be a well-crafted design document. This document is not just a simple record, but also a detailed description and manifestation, which can help team members better understand the design thinking and grasp the design direction. In the process of writing the design document, we can choose to use tools such as `Google Docs` or `Notion`, and even mark **RFC** in [issues](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+RFC+label%3ARFC) or [discussions](https://github.com/openimsdk/openim-sdk-core/discussions) for better collaboration. Of course, after completing the design document, we should also add it to our [Shared Drive](https://drive.google.com/drive/) and notify the appropriate working group to let everyone know of its existence. Only by doing so can we maximize the effectiveness of the design document and provide strong support for the smooth progress of the project.
Anybody can access the shared Drive for reading. To get access to comment. Once you've done that, head to the [shared Drive](https://drive.google.com/) and behold all the docs.
In addition to that, we'd love to invite you to [Join Our Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg) where you can play with your imagination, tell us what you're working on, and get a quick response.
When documenting a new design, we recommend a 2-step approach:
1. Use the short-form **RFC** template to outline your ideas and get early feedback.
2. Once you have received sufficient feedback and consensus, you may use the **longer-form design doc template** to specify and discuss your design in more details.
**In order to contribute a feature to OpenIM you'll need to go through the following steps:**
+ Discuss your idea with the appropriate [working groups](https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg) on the working group's Slack channel.
+ Once there is general agreement that the feature is useful, create a GitHub issue to track the discussion. The issue should include information about the requirements and use cases that it is trying to address.
+ Include a discussion of the proposed design and technical details of the implementation in the issue.
But keep in mind that there is no guarantee of it being accepted and so it is usually best to get agreement on the **idea/design** before time is spent coding it. However, sometimes seeing the exact code change can help focus discussions, so the choice is up to you.
## Getting Started
> **Note**
>
> A simple example allows you to quickly contribute your first **PR** to OpenIM.
To propose PR for the OpenIM item, we assume you have registered a GitHub ID. Then you could finish the preparation in the following steps:
1. **Fork** the repository(OpenIM)
2. **CLONE** your own repository to master locally. Use `git clone https://github.com/<your-username>/OpenIM.git` to clone repository to your local machine. Then you can create new branches to finish the change you wish to make.
3. **Set Remote** upstream to be `https://github.com/openimsdk/openim-sdk-core.git` using the following two commands:
```bash
❯ git remote add upstream https://github.com/openimsdk/openim-sdk-core.git
❯ git remote set-url --push upstream no-pushing
```
With this remote setting, you can check your **git remote configuration** like this:
```go
❯ git remote -v
origin https://github.com/<your-username>/OpenIM.git (fetch)
origin https://github.com/<your-username>/OpenIM.git (push)
upstream https://github.com/openimsdk/openim-sdk-core.git (fetch)
upstream no-pushing (push)
```
Adding this, we can easily synchronize local branches with upstream branches.
4. Create a new branch for your changes (use a descriptive name, such as `fix-bug-123` or `add-new-feature`).
```bash
❯ cd OpenIM
❯ git fetch upstream
❯ git checkout upstream/main
```
> **Note**
>
> Please don't use `git pull` instead of the above `fetch` and `rebase`. Since `git pull` executes a merge, it creates merge commits. These make the commit history messy and violate the principle that commits ought to be individually understandable and useful.
>
> You might also consider changing your `.git/config` file via `git config branch.autoSetupRebase always` to change the behavior of `git pull`, or another non-merge option such as `git pull --rebase`.
Create a new branch:
```bash
❯ git checkout -b <new-branch>
```
Make any change on the `new-branch` then use [Makefile](./Makefile) **build** and **test** your codes.
5. **Commit your changes** to your local branch, lint before committing and commit with sign-off
```bash
❯ git rebase upstream/main
❯ make lint # golangci-lint run -c .golangci.yml
❯ git add -A # add changes to staging
❯ git commit -a -s -m "fix: message for your changes" # -s adds a Signed-off-by trailer
```
6. **Push your branch** to your forked repository, it is recommended to have only one commit for a **PR**.
```bash
# sync up with upstream
❯ git fetch upstream
❯ git rebase upstream/main
❯ git rebase -i <commit-id> # rebase with interactive mode to `squash` your commits into a single one
❯ git push # push to the remote repository, if it's a first time push, run git push --set-upstream origin <new-branch>
```
You can also use `git commit -s --amend && git push -f` to update modifications on the previous commit.
If you have developed multiple features in the same branch, you should create PR separately by rebasing to the main branch between each push:
```bash
# create new branch, for example git checkout -b feature/infra
❯ git checkout -b <new branch>
# update some code, feature1
❯ git add -A
❯ git commit -m -s "feat: feature one"
❯ git push # if it's first time push, run git push --set-upstream origin <new-branch>
# then create pull request, and merge
# update some new feature, feature2, rebase main branch first.
❯ git rebase upstream/main # rebase the current branch to upstream/main branch
❯ git add -A
❯ git commit -m -s "feat: feature two"
# then create pull request, and merge
```
7. **Open a pull request** to `OpenIMSDK/openim-sdk-core:main`
It is recommended to review your changes before filing a pull request. Check if your code doesn't conflict with the main branch and no redundant code is included.
## Style and Specification
We divide the problem into security and general problems:
#### Reporting security issues
Security issues are always treated seriously. As our usual principle, we discourage anyone to spread security issues. If you find a security issue of OpenIM, please do not discuss it in public and even do not open a public issue.
Instead we encourage you to send us a private email to 📮[winxu81@gmail.com](https://mail.google.com/mail/u/0/?fs=1&tf=cm&to=winxu81@gmail.com) to report this.
#### Reporting general issues
To be honest, we regard every user of OpenIMas a very kind contributor. After experiencing OpenIM, you may have some feedback for the project. Then feel free to open an issue via [NEW ISSUE](https://github.com/openimsdk/openim-sdk-core/issues/new/choose).
Since we collaborate project OpenIM in a distributed way, we appreciate **WELL-WRITTEN**, **DETAILED**, **EXPLICIT** issue reports. To make the communication more efficient, we wish everyone could search if your issue is an existing one in the searching list. If you find it existing, please add your details in comments under the existing issue instead of opening a brand new one.
To make the issue details as standard as possible, we setup an [ISSUE TEMPLATE](https://github.com/openimsdk/.github/tree/main/.github/ISSUE_TEMPLATE) for issue reporters. You can find three kinds of issue templates there: question, bug report and feature request. Please **BE SURE** to follow the instructions to fill fields in template.
**There are a lot of cases when you could open an issue:**
+ bug report
+ feature request
+ OpenIM performance issues
+ feature proposal
+ feature design
+ help wanted
+ doc incomplete
+ test improvement
+ any questions on OpenIM project
+ and so on
Also, we must be reminded when submitting a new question about OpenIM, please remember to remove the sensitive data from your post. Sensitive data could be password, secret key, network locations, private business data and so on.
> **Note**
> We have requirements for **Commits**, **PR**, **Docs**, and good standards help us collaborate better and understand what you're doing.
#### Commit Rules
Actually in OpenIM, we take two rules serious when committing:
**🥇 Commit Message:**
Commit message could help reviewers better understand what the purpose of submitted PR is. It could help accelerate the code review procedure as well. We encourage contributors to use **EXPLICIT** commit message rather than ambiguous message. In general, we advocate the following commit message type:
We use [Semantic Commits](https://www.conventionalcommits.org/en/v1.0.0/) to make it easier to understand what a commit does and to build pretty changelogs. Please use the following prefixes for your commits:
+ `docs: xxxx`. For example, "docs: add docs about storage installation".
+ `feature: xxxx`.For example, "feature: make result show in sorted order".
+ `bugfix: xxxx`. For example, "bugfix: fix panic when input nil parameter".
+ `style: xxxx`. For example, "style: format the code style of Constants.java".
+ `refactor: xxxx.` For example, "refactor: simplify to make codes more readable".
+ `test: xxx`. For example, "test: add unit test case for func InsertIntoArray".
+ `chore: xxx.` For example, "chore: integrate travis-ci". It's the type of mantainance change.
+ other readable and explicit expression ways.
On the other side, we discourage contributors from committing message like the following ways:
+ ~~fix bug~~
+ ~~update~~
+ ~~add doc~~
**🥈 Commit Content:**
Commit content represents all content changes included in one commit. We had better include things in one single commit which could support reviewer's complete review without any other commits' help.
In another word, contents in one single commit can pass the CI to avoid code mess. In brief, there are two minor rules for us to keep in mind:
1. avoid very large change in a commit.
2. complete and reviewable for each commit.
3. words are written in lowercase English, not uppercase English or other languages such as Chinese.
No matter what the commit message, or commit content is, we do take more emphasis on code review.
An example for this could be:
```bash
❯ git commit -a -s -m "docs: add a new section to the readme"
```
#### PR Description
PR is the only way to make change to OpenIM project files. To help reviewers better get your purpose, **PR** description could not be too detailed. We encourage contributors to follow the [PR template](https://github.com/openimsdk/.github/tree/main/.github/PULL_REQUEST_TEMPLATE.md) to finish the pull request.
You can find some very formal PR in [RFC](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+RFC+label%3ARFC) issues and learn about them.
**📖 Opening PRs:**
+ As long as you are working on your **PR**, please mark it as a draft.
+ Please make sure that your **PR** is up-to-date with the latest changes in `main`
+ Mention the issue that your **PR** is addressing. For example, `Fixes: #{ID_1}, #{ID_2}`
+ Make sure that your **PR** passes all checks.
**🈴 Reviewing PRs:**
+ Be respectful and constructive.
+ Assign yourself to the **PR**.
+ Check if all checks are passing.
+ Suggest changes instead of simply commenting on found issues.
+ If you are unsure about something, ask the author.
+ If you are not sure if the changes work, try them out.
+ Reach out to other reviewers if you are unsure about something.
+ If you are happy with the changes, approve the **PR**.
+ Merge the **PR** once it has all approvals and the checks are passing.
**⚠ DCO check:**
We have a [DCO check](https://github.com/apps/dco) which runs on every **PR** to verify that the commit has been signed off.
Once [installed](https://github.com/apps/dco#usage), this integration will set the [status](https://developer.github.com/v3/repos/statuses/) to `failed` if commits in a **Pull Request** do not contain a valid `Signed-off-by` line.
To sign off the last commit you made, you can use:
```bash
❯ git commit --amend --signoff
```
Contributors *sign-off* that they adhere to these requirements by adding a `Signed-off-by` line to commit messages.
Git even has a `-s` command line option to append this automatically to your commit message:
```bash
❯ git commit -s -m "docs: this is my commit message"
```
You can also automate signing off your commits by adding the following to your `.zshrc` or `.bashrc`:
```bash
❯ cat ~/.bashrc || cat ~/.zshrc
git() {
if [ $# -gt 0 ] && [[ "$1" == "commit" ]] ; then
shift
command git commit --signoff "$@"
else
command git "$@"
fi
}
```
#### CI actions
We host CI on [GitHub Actions](https://github.com/openimsdk/openim-sdk-core/actions), we will make sure PR pass tests before we can merge it.
These two kind of tests: `lint` and `unit test`
`lint` tests if your code matches our code conventions, please consult [golangci-lint](https://golangci-lint.run/) and [lint config](https://github.com/openimsdk/openim-sdk-core/blob/main/.golangci.yml)
> **Note**
>
> You can use the [Makefile](./Makefile) to run Lint with the command `make lint`.
`unit test` runs all the test in code, and the code coverage should not less than 60 percent, record us in [codeclimate](https://codeclimate.com/github/OpenIMSDK/openim-sdk-core) OpenIM the unit test coverage data.
> **Note**
>
> We use the [Makefile](./Makefile) utility, `make tese` to run the unit tests, and the `make cover` utility to check the unit test coverage.
Try your best to keep every function has been tested, it keeps the function behaves as intended.
#### Docs Contribution
**The documentation for OpenIM includes:**
+ [README.md](https://github.com/openimsdk/openim-sdk-core/blob/main/README.md): This file includes the basic information and instructions for getting started with OpenIM.
+ [README_zh-CN.md](https://github.com/openimsdk/openim-sdk-core/blob/main/README_zh-CN.md): This file includes the basic information and instructions for getting started with OpenIM in Chinese.
+ [CONTRIBUTING.md](https://github.com/openimsdk/openim-sdk-core/blob/main/CONTRIBUTING.md): This file contains guidelines for contributing to OpenIM's codebase, such as how to submit issues, pull requests, and code reviews.
+ [Official Documentation](https://doc.rentsoft.cn): This is the official documentation for OpenIM, which includes comprehensive information on all of its features, configuration options, and troubleshooting tips.
**Please obey the following rules to better format the docs, which would greatly improve the reading experience.**
1. Please do not use Chinese punctuations in English docs, and vice versa.
2. Please use upper case letters where applicable, like the first letter of sentences / headings, etc.
3. Please specify a language for each Markdown code blocks, unless there's no associated languages.
4. Please insert a whitespace between Chinese and English words.
5. Please use the correct case for technical terms, such as using `HTTP` instead of http, `MySQL` rather than mysql, `Kubernetes` instead of kubernetes, etc.
6. Please check if there's any typos in the docs before submitting PRs.
**Markfile Lint:**
We integrated in the CICD actions [markdownlint](https://github.com/markdownlint/markdownlint), it detects Markfile specification.
> **Note**
> We recommend reading [markdownlint rules](https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md), This document contains a description of all rules, what they are checking for, as well as an examples of documents that break the rule and corrected versions of the examples.
## Engage to help anything
We choose GitHub as the primary place for OpenIM to collaborate. So the latest updates of OpenIM are always here. Although contributions via **PR** is an explicit way to help, we still call for any other ways.
+ reply to other's [issues](https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) if you could;
+ help solve other user's problems;
+ help review other's [PR](https://github.com/openimsdk/openim-sdk-core/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc) design;
+ discuss about OpenIM to make things clearer;
+ advocate [OpenIM](https://google.com/search?q=OpenIM) technology beyond GitHub;
+ write blogs on OpenIM and so on.
In a word, **ANY HELP IS CONTRIBUTION.**
## Release version
Releases of OpenIM are done using [Release Please](https://github.com/googleapis/release-please) and [GoReleaser](https://goreleaser.com/). The workflow looks like this:
🎯 **A PR is merged to the `main` branch:**
+ Release please is triggered, creates or updates a new release PR
+ This is done with every merge to main, the current release PR is updated every time
🎯 **Merging the 'release please' PR to `main`:**
+ Release please is triggered, creates a new release and updates the changelog based on the commit messages
+ GoReleaser is triggered, builds the binaries and attaches them to the release
+ Containers are created and pushed to the container registry
With the next relevant merge, a new release PR will be created and the process starts again
👀 **Manually setting the version:**
If you want to manually set the version, you can create a PR with an empty commit message that contains the version number in the commit message. For example:
Such a commit can get produced as follows:
````bash
❯ git commit --allow-empty -m "chore: release 0.0.3" -m "Release-As: 0.0.3
````
## Contact Us
We value close connections with our users, developers, and contributors here at openim-sdk-core. 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.
Our most recommended way to get in touch is through [Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg). 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 openim-sdk-core. You can ask technical questions, seek help, or share your experiences with other users of openim-sdk-core.
In addition to Slack, we also offer the following ways to get in touch:
+ <a href="https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg" 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 [👀 openim-sdk-core slack](https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg) team channel.
+ <a href="https://mail.google.com/mail/u/0/?fs=1&tf=cm&to=winxu81@gmail.com" 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.
+ <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 openim-sdk-core projects and trends. On the blog, we share our latest developments, tech trends, and other interesting information.
+ <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 openim-sdk-core. We will process your request as soon as possible.
Whether you're looking to join our community or have any questions or suggestions, we welcome you to get in touch with us.

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,515 @@
# ==============================================================================
# define the default goal
#
ROOT_PACKAGE=github.com/openimsdk/Open-IM-SDK-Core
# Copyright 2023 OpenIM. All rights reserved.
# Use of this source code is governed by a MIT style
# license that can be found in the LICENSE file.
###################################=> common commands <=#############################################
# ========================== Capture Environment ===============================
# get the repo root and output path
ROOT_PACKAGE=github.com/OpenIM/chat
OUT_DIR=$(REPO_ROOT)/_output
# ==============================================================================
# define the default goal
#
SHELL := /bin/bash
DIRS=$(shell ls)
GO=go
.DEFAULT_GOAL := help
# include the common makefile
COMMON_SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
# ROOT_DIR: root directory of the code base
ifeq ($(origin ROOT_DIR),undefined)
ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/. && pwd -P))
endif
# OUTPUT_DIR: The directory where the build output is stored.
ifeq ($(origin OUTPUT_DIR),undefined)
OUTPUT_DIR := $(ROOT_DIR)/_output
$(shell mkdir -p $(OUTPUT_DIR))
endif
# BIN_DIR: The directory where the build output is stored.
ifeq ($(origin BIN_DIR),undefined)
BIN_DIR := $(OUTPUT_DIR)/bin
$(shell mkdir -p $(BIN_DIR))
endif
ifeq ($(origin TOOLS_DIR),undefined)
TOOLS_DIR := $(OUTPUT_DIR)/tools
$(shell mkdir -p $(TOOLS_DIR))
endif
ifeq ($(origin TMP_DIR),undefined)
TMP_DIR := $(OUTPUT_DIR)/tmp
$(shell mkdir -p $(TMP_DIR))
endif
ifeq ($(origin VERSION), undefined)
VERSION := $(shell git describe --tags --always --match="v*" --dirty | sed 's/-/./g') #v2.3.3.631.g00abdc9b.dirty
endif
# Check if the tree is dirty. default to dirty(maybe u should commit?)
GIT_TREE_STATE:="dirty"
ifeq (, $(shell git status --porcelain 2>/dev/null))
GIT_TREE_STATE="clean"
endif
GIT_COMMIT:=$(shell git rev-parse HEAD)
IMG ?= openim_chat:latest
BUILDFILE = "./main.go"
BUILDAPP = "$(OUTPUT_DIR)/"
# Define the directory you want to copyright
CODE_DIRS := $(ROOT_DIR)/ #$(ROOT_DIR)/pkg $(ROOT_DIR)/core $(ROOT_DIR)/integrationtest $(ROOT_DIR)/lib $(ROOT_DIR)/mock $(ROOT_DIR)/db $(ROOT_DIR)/openapi
FINDS := find $(CODE_DIRS)
ifndef V
MAKEFLAGS += --no-print-directory
endif
# The OS must be linux when building docker images
# !WARNING: linux_mips64 linux_mips64le
PLATFORMS ?= linux_s390x darwin_amd64 windows_amd64 linux_amd64 linux_arm64 linux_ppc64le
# The OS can be linux/windows/darwin when building binaries
# PLATFORMS ?= darwin_amd64 windows_amd64 linux_amd64 linux_arm64
# Set a specific PLATFORM
ifeq ($(origin PLATFORM), undefined)
ifeq ($(origin GOOS), undefined)
GOOS := $(shell go env GOOS)
endif
ifeq ($(origin GOARCH), undefined)
GOARCH := $(shell go env GOARCH)
endif
PLATFORM := $(GOOS)_$(GOARCH)
# Use linux as the default OS when building images
IMAGE_PLAT := linux_$(GOARCH)
else
GOOS := $(word 1, $(subst _, ,$(PLATFORM)))
GOARCH := $(word 2, $(subst _, ,$(PLATFORM)))
IMAGE_PLAT := $(PLATFORM)
endif
# Copy githook scripts when execute makefile
# TODO! GIT_FILE_SIZE_LIMIT=42000000 git commit -m "This commit is allowed file sizes up to 42MB"
COPY_GITHOOK:=$(shell cp -f scripts/githooks/* .git/hooks/; chmod +x .git/hooks/*)
# Linux command settings
FIND := find . ! -path './image/*' ! -path './vendor/*' ! -path './bin/*'
XARGS := xargs -r
# ==============================================================================
# TODO: License selection
# LICENSE_TEMPLATE ?= $(ROOT_DIR)/scripts/LICENSE/license_templates.txt # MIT License
LICENSE_TEMPLATE ?= $(ROOT_DIR)/scripts/LICENSE/LICENSE_TEMPLATES # Apache License
# COMMA: Concatenate multiple strings to form a list of strings
COMMA := ,
# SPACE: Used to separate strings
SPACE :=
# SPACE: Replace multiple consecutive Spaces with a single space
SPACE +=
# ==============================================================================
# Build definition
GO_SUPPORTED_VERSIONS ?= 1.18|1.19|1.20|1.21
GO_LDFLAGS += -X $(VERSION_PACKAGE).GitVersion=$(VERSION) \
-X $(VERSION_PACKAGE).GitCommit=$(GIT_COMMIT) \
-X $(VERSION_PACKAGE).GitTreeState=$(GIT_TREE_STATE) \
-X $(VERSION_PACKAGE).BuildDate=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
ifneq ($(DLV),)
GO_BUILD_FLAGS += -gcflags "all=-N -l"
LDFLAGS = ""
endif
GO_BUILD_FLAGS += -ldflags "$(GO_LDFLAGS)"
ifeq ($(GOOS),windows)
GO_OUT_EXT := .exe
endif
ifeq ($(ROOT_PACKAGE),)
$(error the variable ROOT_PACKAGE must be set prior to including golang.mk)
endif
GOPATH := $(shell go env GOPATH)
ifeq ($(origin GOBIN), undefined)
GOBIN := $(GOPATH)/bin
endif
COMMANDS ?= $(filter-out %.md, $(wildcard ${ROOT_DIR}/cmd/*))
BINS ?= $(foreach cmd,${COMMANDS},$(notdir ${cmd}))
ifeq (${COMMANDS},)
$(error Could not determine COMMANDS, set ROOT_DIR or run in source dir)
endif
ifeq (${BINS},)
$(error Could not determine BINS, set ROOT_DIR or run in source dir)
endif
EXCLUDE_TESTS=github.com/openimsdk/openim-sdk-core/test
# ==============================================================================
# Build
## all: Build all the necessary targets.
.PHONY: all
all: copyright-verify build # tidy lint cover
# Define available OS and ARCH
OSES = linux
ARCHS = amd64 arm64
ifeq ($(ARCH),arm64)
export CC=aarch64-linux-gnu-gcc
export CXX=aarch64-linux-gnu-g++
endif
# Set default OS and ARCH (e.g., current platform)
OS ?= $(shell go env GOOS)
ARCH ?= $(shell go env GOARCH)
BIN_DIR ?= ./_output/bin
TARGET ?= ./cmd/main.go
## build: Build for current platform by default
.PHONY: build
build:
@echo "===========> Building for $(OS)/$(ARCH)"
@CGO_ENABLED=1 GOOS=$(OS) GOARCH=$(ARCH) go build -o $(BIN_DIR)/openim-sdk-core-$(OS)-$(ARCH) $(TARGET)
# sudo apt-get install gcc-aarch64-linux-gnu
## build-multiple: Build for all supported platforms
.PHONY: build-multiple
build-multiple:
@for os in $(OSES); do \
for arch in $(ARCHS); do \
$(MAKE) build OS=$$os ARCH=$$arch; \
done \
done
.PHONY: build-wasm
build-wasm:
GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" -o ${BIN_DIR}/openIM.wasm wasm/cmd/main.go
## install: Install the binary to the BIN_DIR
.PHONY: install
install: build
mv ${BINARY_NAME} ${BIN_DIR}
## reset_remote_branch: Reset the remote branch
.PHONY: reset_remote_branch
reset_remote_branch:
remote_branch=$(shell git rev-parse --abbrev-ref --symbolic-full-name @{u})
git reset --hard $(remote_branch)
git pull $(remote_branch)
## ios: Build the iOS framework
.PHONY: ios
ios:
go get golang.org/x/mobile
rm -rf build/ open_im_sdk/t_friend_sdk.go open_im_sdk/t_group_sdk.go open_im_sdk/ws_wrapper/
GOARCH=arm64 gomobile bind -v -trimpath -ldflags "-s -w" -o build/OpenIMCore.xcframework -target=ios ./open_im_sdk/ ./open_im_sdk_callback/
## android: Build the Android library
# Note: to build an AAR on Windows, gomobile, Android Studio, and the NDK must be installed.
# The NDK version tested by the OpenIM team was r20b.
# To build an AAR on Mac, gomobile, Android Studio, and the NDK version 20.0.5594570 must be installed.
.PHONY: android
android:
go get golang.org/x/mobile/bind
GOARCH=amd64 gomobile bind -v -trimpath -ldflags="-s -w" -o ./open_im_sdk.aar -target=android ./open_im_sdk/ ./open_im_sdk_callback/
# Targets
.PHONY: release
release: release.verify release.ensure-tag
@scripts/release.sh
.PHONY: install.gsemver
release.verify: install.git-chglog install.github-release install.coscmd
.PHONY: release.tag
release.tag: install.gsemver release.ensure-tag
@git push origin `git describe --tags --abbrev=0`
.PHONY: release.ensure-tag
release.ensure-tag: install.gsemver
@scripts/ensure_tag.sh
## tidy: tidy go.mod
.PHONY: tidy
tidy:
@$(GO) mod tidy
## style: Code style -> fmt,vet,lint
.PHONY: style
style: fmt vet lint
## fmt: Run go fmt against code.
.PHONY: fmt
fmt:
@$(GO) fmt ./...
## vet: Run go vet against code.
.PHONY: vet
vet:
@$(GO) vet ./...
## generate: Run go generate against code.
.PHONY: generate
generate:
@$(GO) generate ./...
## lint: Run go lint against code.
.PHONY: lint
lint: tools.verify.golangci-lint
@echo "===========> Run golangci to lint source codes"
@$(TOOLS_DIR)/golangci-lint run -c $(ROOT_DIR)/.golangci.yml $(ROOT_DIR)/...
## test: Run unit test
.PHONY: test
test:
@$(GO) test ./...
## cover: Run unit test with coverage.
.PHONY: cover
cover: test
@$(GO) test -cover
## docker-build: Build docker image with the manager.
.PHONY: docker-build
docker-build:
docker build -t ${IMG} .
## docker-push: Push docker image with the manager.
.PHONY: docker-push
docker-push:
docker push ${IMG}
## docker-buildx-push: Push docker image with the manager using buildx.
.PHONY: docker-buildx-push
docker-buildx-push:
docker buildx build --platform linux/arm64,linux/amd64 -t ${IMG} . --push
## copyright-verify: Validate boilerplate headers for assign files.
.PHONY: copyright-verify
copyright-verify: tools.verify.addlicense copyright-add
@echo "===========> Validate boilerplate headers for assign files starting in the $(ROOT_DIR) directory"
@$(TOOLS_DIR)/addlicense -v -check -ignore **/test/** -f $(LICENSE_TEMPLATE) $(CODE_DIRS)
@echo "===========> End of boilerplate headers check..."
## copyright-add: Add the boilerplate headers for all files.
.PHONY: copyright-add
copyright-add: tools.verify.addlicense
@echo "===========> Adding $(LICENSE_TEMPLATE) the boilerplate headers for all files"
@$(TOOLS_DIR)/addlicense -y $(shell date +"%Y") -v -c "OpenIM open source community." -f $(LICENSE_TEMPLATE) $(CODE_DIRS)
@echo "===========> End the copyright is added..."
## clean: Clean all builds.
.PHONY: clean
clean:
@echo "===========> Cleaning all builds TMP_DIR($(TMP_DIR)) AND BIN_DIR($(BIN_DIR))"
@-rm -vrf $(TMP_DIR) $(BIN_DIR)
@echo "===========> End clean..."
## help: Show this help info.
.PHONY: help
help: Makefile
@printf "\n\033[1mUsage: make <TARGETS> ...\033[0m\n\n\\033[1mTargets:\\033[0m\n\n"
@sed -n 's/^##//p' $< | awk -F':' '{printf "\033[36m%-28s\033[0m %s\n", $$1, $$2}' | sed -e 's/^/ /'
######################################=> common tools<= ############################################
# tools
BUILD_TOOLS ?= go-gitlint golangci-lint goimports addlicense deepcopy-gen conversion-gen ginkgo go-junit-report
## tools.verify.%: Check if a tool is installed and install it
.PHONY: tools.verify.%
tools.verify.%:
@echo "===========> Verifying $* is installed"
@if [ ! -f $(TOOLS_DIR)/$* ]; then GOBIN=$(TOOLS_DIR) $(MAKE) tools.install.$*; fi
@echo "===========> $* is install in $(TOOLS_DIR)/$*"
# tools: Install a must tools
.PHONY: tools
tools: $(addprefix tools.verify., $(BUILD_TOOLS))
# tools.install.%: Install a single tool in $GOBIN/
.PHONY: tools.install.%
tools.install.%:
@echo "===========> Installing $,The default installation path is $(GOBIN)/$*"
@$(MAKE) install.$*
.PHONY: install.golangci-lint
install.golangci-lint:
@$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
.PHONY: install.goimports
install.goimports:
@$(GO) install golang.org/x/tools/cmd/goimports@latest
.PHONY: install.addlicense
install.addlicense:
@$(GO) install github.com/google/addlicense@latest
.PHONY: install.deepcopy-gen
install.deepcopy-gen:
@$(GO) install k8s.io/code-generator/cmd/deepcopy-gen@latest
.PHONY: install.conversion-gen
install.conversion-gen:
@$(GO) install k8s.io/code-generator/cmd/conversion-gen@latest
.PHONY: install.ginkgo
install.ginkgo:
@$(GO) install github.com/onsi/ginkgo/ginkgo@v1.16.2
.PHONY: install.go-gitlint
# wget -P _output/tools/ https://openim-1306374445.cos.ap-guangzhou.myqcloud.com/openim/tools/go-gitlint
# go install github.com/antham/go-gitlint/cmd/gitlint@latest
install.go-gitlint:
@wget -q https://openim-1306374445.cos.ap-guangzhou.myqcloud.com/openim/tools/go-gitlint -O ${TOOLS_DIR}/go-gitlint
@chmod +x ${TOOLS_DIR}/go-gitlint
.PHONY: install.go-junit-report
install.go-junit-report:
@$(GO) install github.com/jstemmer/go-junit-report@latest
# ==============================================================================
# Tools that might be used include go gvm, cos
#
## install.kube-score: Install kube-score, used to check kubernetes yaml files
.PHONY: install.kube-score
install.kube-score:
@$(GO) install github.com/zegl/kube-score/cmd/kube-score@latest
## install.kubeconform: Install kubeconform, used to check kubernetes yaml files
.PHONY: install.kubeconform
install.kubeconform:
@$(GO) install github.com/yannh/kubeconform/cmd/kubeconform@latest
## install.gsemver: Install gsemver, used to generate semver
.PHONY: install.gsemver
install.gsemver:
@$(GO) install github.com/arnaud-deprez/gsemver@latest
## install.git-chglog: Install git-chglog, used to generate changelog
.PHONY: install.git-chglog
install.git-chglog:
@$(GO) install github.com/git-chglog/git-chglog/cmd/git-chglog@latest
## install.github-release: Install github-release, used to create github release
.PHONY: install.github-release
install.github-release:
@$(GO) install github.com/github-release/github-release@latest
## install.coscli: Install coscli, used to upload files to cos
# example: ./coscli cp/sync -r /root/workspaces/kubecub/chat/ cos://kubecub-1306374445/code/ -e cos.ap-hongkong.myqcloud.com
# https://cloud.tencent.com/document/product/436/71763
# kubecub/*
# - code/
# - docs/
# - images/
# - scripts/
.PHONY: install.coscli
install.coscli:
@wget -q https://github.com/tencentyun/coscli/releases/download/v0.13.0-beta/coscli-linux -O ${TOOLS_DIR}/coscli
@chmod +x ${TOOLS_DIR}/coscli
## install.coscmd: Install coscmd, used to upload files to cos
.PHONY: install.coscmd
install.coscmd:
@if which pip &>/dev/null; then pip install coscmd; else pip3 install coscmd; fi
## install.delve: Install delve, used to debug go program
.PHONY: install.delve
install.delve:
@$(GO) install github.com/go-delve/delve/cmd/dlv@latest
## install.air: Install air, used to hot reload go program
.PHONY: install.air
install.air:
@$(GO) install github.com/cosmtrek/air@latest
## install.gvm: Install gvm, gvm is a Go version manager, built on top of the official go tool.
.PHONY: install.gvm
install.gvm:
@echo "===========> Installing gvm,The default installation path is ~/.gvm/scripts/gvm"
@bash < <(curl -s -S -L https://raw.gitee.com/moovweb/gvm/master/binscripts/gvm-installer)
@$(shell source /root/.gvm/scripts/gvm)
## install.golines: Install golines, used to format long lines
.PHONY: install.golines
install.golines:
@$(GO) install github.com/segmentio/golines@latest
## install.go-mod-outdated: Install go-mod-outdated, used to check outdated dependencies
.PHONY: install.go-mod-outdated
install.go-mod-outdated:
@$(GO) install github.com/psampaz/go-mod-outdated@latest
## install.mockgen: Install mockgen, used to generate mock functions
.PHONY: install.mockgen
install.mockgen:
@$(GO) install github.com/golang/mock/mockgen@latest
## install.gotests: Install gotests, used to generate test functions
.PHONY: install.gotests
install.gotests:
@$(GO) install github.com/cweill/gotests/gotests@latest
## install.protoc-gen-go: Install protoc-gen-go, used to generate go source files from protobuf files
.PHONY: install.protoc-gen-go
install.protoc-gen-go:
@$(GO) install github.com/golang/protobuf/protoc-gen-go@latest
## install.cfssl: Install cfssl, used to generate certificates
.PHONY: install.cfssl
install.cfssl:
@$(ROOT_DIR)/scripts/install/install.sh OpenIM::install::install_cfssl
## install.depth: Install depth, used to check dependency tree
.PHONY: install.depth
install.depth:
@$(GO) install github.com/KyleBanks/depth/cmd/depth@latest
## install.go-callvis: Install go-callvis, used to visualize call graph
.PHONY: install.go-callvis
install.go-callvis:
@$(GO) install github.com/ofabry/go-callvis@latest
## install.gothanks: Install gothanks, used to thank go dependencies
.PHONY: install.gothanks
install.gothanks:
@$(GO) install github.com/psampaz/gothanks@latest
## install.richgo: Install richgo
.PHONY: install.richgo
install.richgo:
@$(GO) install github.com/kyoh86/richgo@latest
## install.rts: Install rts
.PHONY: install.rts
install.rts:
@$(GO) install github.com/galeone/rts/cmd/rts@latest
## install.gomobile: Install gomobile
.PHONY: install.gomobile
install.gomobile:
@$(GO) install golang.org/x/mobile/cmd/gomobile@latest
## install.gobind: Install gobind
.PHONY: install.gobind
install.gobind:
@$(GO) install golang.org/x/mobile/cmd/gobind@latest

@ -0,0 +1,144 @@
<h1 align="center" style="border-bottom: none">
<b>
<a href="https://doc.rentsoft.cn/">openim-sdk-core</a><br>
</b>
</h1>
<h3 align="center" style="border-bottom: none">
Used in IOS, Android, PC and other platforms ⭐ <br>
<h3>
<p align=center>
<a href="https://goreportcard.com/report/github.com/openimsdk/openim-sdk-core"><img src="https://goreportcard.com/badge/github.com/openimsdk/openim-sdk-core" alt="A+"></a>
<a href="https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22"><img src="https://img.shields.io/github/issues/OpenIMSDK/Open-IM-Server/good%20first%20issue?logo=%22github%22" alt="good first"></a>
<a href="https://github.com/openimsdk/openim-sdk-core"><img src="https://img.shields.io/github/stars/OpenIMSDK/openim-sdk-core.svg?style=flat&logo=github&colorB=deeppink&label=stars"></a>
<a href="https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg"><img src="https://img.shields.io/badge/Slack-100%2B-blueviolet?logo=slack&amp;logoColor=white"></a>
<a href="https://github.com/openimsdk/openim-sdk-core/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-green"></a>
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Language-Go-blue.svg"></a>
</p>
</p>
<p align="center">
<a href="./README.md"><b>English</b></a>
<a href="./README_zh-CN.md"><b>中文</b></a>
</p>
</p>
----
## 🧩 Awesome features
OpenIM-SDK-core is a core SDK of OpenIM.
1. Manage WebSocket long connections, responsible for creating, closing and reconnecting connections.
2. Encoding and decoding. Encode and decode messages in binary format to achieve cross-language compatibility.
3. Implement basic protocols of OpenIM, such as login, push, etc.
4. Provide an event handling mechanism to convert received messages into corresponding events and pass them to upper layer applications for processing.
5. Cache management. Manage user, group, blacklists, and other cache information.
6. Provide basic IM function APIs such as sending messages, creating groups, etc. Hide the underlying implementation details from the upper layer application.
## Quickstart
> **Note**: You can get started quickly with openim-sdk-core.
<details>
<summary>Work with Makefile</summary>
```bash
❯ make help # show help
❯ make build # build binary
```
</details>
<details>
<summary>Work with actions</summary>
Actions provide handling of PR and issue.
We used the bot @kubbot, It can detect issues in Chinese and translate them to English, and you can interact with it using the command `/comment`.
Comment in an issue:
```bash
❯ /intive
```
</details>
<details>
<summary>Work with Tools</summary>
```bash
❯ make tools
```
</details>
<details>
<summary>Work with Docker</summary>
```bash
$ make deploy
```
</details>
## Contributing & Development
OpenIM Our goal is to build a top-level open source community. We have a set of standards, in the [Community repository](https://github.com/openimsdk/community).
If you'd like to contribute to this openim-sdk-core repository, please read our [contributor documentation](https://github.com/openimsdk/openim-sdk-core/blob/main/CONTRIBUTING.md).
## community meeting
We welcome everyone to join us and contribute to openim-sdk-core, whether you are new to open source or professional. We are committed to promoting an open source culture, so we offer community members neighborhood prizes and reward money in recognition of their contributions. We believe that by working together, we can build a strong community and make valuable open source tools and resources available to more people. So if you are interested in openim-sdk-core, please join our community and start contributing your ideas and skills!
We take notes of each [biweekly meeting](https://github.com/openimsdk/Open-IM-Server/issues/381) in [GitHub discussions](https://github.com/openimsdk/Open-IM-Server/discussions/categories/meeting), and our minutes are written in [Google Docs](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing).
openim-sdk-core maintains a [public roadmap](https://github.com/openimsdk/community/tree/main/roadmaps). It gives a a high-level view of the main priorities for the project, the maturity of different features and projects, and how to influence the project direction.
## about OpenIM
### common
+ https://github.com/openimsdk/automation: OpenIM Automation, cicd, and actions, Robotics.
+ https://github.com/openimsdk/community: Community Management for OpenIM
### OpenIM **Links**
Contains some common parts of the OpenIM community.
+ https://github.com/openimsdk/automation: OpenIM Automation, cicd, and actions, Robotics.
+ https://github.com/openimsdk/openim-sdk-core: The IMSDK implemented by golang can be used in IOS, Android, PC and other platforms.
+ https://github.com/openimsdk/openim-sdk-core: Instant messaging IM server.
+ https://github.com/openimsdk/community: Community Management for OpenIM.
### SDKs
+ [openim-sdk-core](https://github.com/openimsdk/openim-sdk-core): A cross-platform SDK implemented in golang that can be used in IOS, Android, PC, and other platforms.
+ [Open-IM-SDK-iOS](https://github.com/openimsdk/Open-IM-SDK-iOS): An iOS SDK generated based on openim-sdk-core, available for developers to reference.
+ [Open-IM-SDK-Android](https://github.com/openimsdk/Open-IM-SDK-Android): An Android SDK generated based on openim-sdk-core, available for developers to reference.
+ [Open-IM-SDK-Flutter](https://github.com/openimsdk/Open-IM-SDK-Flutter): A Flutter SDK generated based on Open-IM-SDK-iOS and Open-IM-SDK-Android, available for developers to reference.
+ [Open-IM-SDK-Uniapp](https://github.com/openimsdk/Open-IM-SDK-Uniapp): A uni-app SDK generated based on Open-IM-SDK-iOS and Open-IM-SDK-Android, available for developers to reference.
### Demos
+ [Open-IM-iOS-Demo](https://github.com/openimsdk/Open-IM-iOS-Demo): An iOS demo based on Open-IM-SDK-iOS, available for developers to reference.
+ [Open-IM-Android-Demo](https://github.com/openimsdk/Open-IM-Android-Demo): An Android demo based on Open-IM-SDK-Android, available for developers to reference.
+ [Open-IM-Flutter-Demo](https://github.com/openimsdk/Open-IM-Flutter-Demo): A Flutter demo based on Open-IM-SDK-Flutter, available for developers to reference.
## Used By
OpenIM is used by the following companies ,let's write it down in [ADOPTER](https://github.com/openimsdk/community/blob/main/ADOPTERS.md).
Please leave your use cases in the comments [here](https://github.com/openimsdk/Open-IM-Server/issues/379).
## License
[openim-sdk-core](https://github.com/openimsdk/openim-sdk-core) is licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/openimsdk/openim-sdk-core/tree/main/LICENSE) for the full license text.
## Thanks to our contributors!
<a href="https://github.com/openimsdk/openim-sdk-core/graphs/contributors">
<img src="https://contrib.rocks/image?repo=OpenIMSDK/openim-sdk-core" />
</a>

@ -0,0 +1,26 @@
<h1 align="center" style="border-bottom: none">
<b>
<a href="https://doc.rentsoft.cn/">openim-sdk-core</a><br>
</b>
</h1>
<h3 align="center" style="border-bottom: none">
Used in IOS, Android, PC and other platforms ⭐ <br>
<h3>
<p align=center>
<a href="https://goreportcard.com/report/github.com/openimsdk/openim-sdk-core"><img src="https://goreportcard.com/badge/github.com/openimsdk/openim-sdk-core" alt="A+"></a>
<a href="https://github.com/openimsdk/openim-sdk-core/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22"><img src="https://img.shields.io/github/issues/OpenIMSDK/Open-IM-Server/good%20first%20issue?logo=%22github%22" alt="good first"></a>
<a href="https://github.com/openimsdk/openim-sdk-core"><img src="https://img.shields.io/github/stars/OpenIMSDK/openim-sdk-core.svg?style=flat&logo=github&colorB=deeppink&label=stars"></a>
<a href="https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg"><img src="https://img.shields.io/badge/Slack-100%2B-blueviolet?logo=slack&amp;logoColor=white"></a>
<a href="https://github.com/openimsdk/openim-sdk-core/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-green"></a>
<a href="https://golang.org/"><img src="https://img.shields.io/badge/Language-Go-blue.svg"></a>
</p>
</p>
<p align="center">
<a href="./README.md"><b>English</b></a>
<a href="./README_zh-CN.md"><b>中文</b></a>
</p>
</p>

@ -0,0 +1,234 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"encoding/json"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/pkg/network"
"github.com/openimsdk/openim-sdk-core/v3/pkg/server_api_params"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/test"
"github.com/openimsdk/tools/log"
"time"
)
var (
//APIADDR = "http://43.155.69.205:10002"
//WSADDR = "ws://43.155.69.205:10001"
//APIADDR = "https://chat-api-dev.opencord.so"
//WSADDR = "wss://chat-ws-dev.opencord.so"
APIADDR = "http://14.29.168.56:10002"
WSADDR = "ws://14.29.168.56:10001"
//APIADDR = "http://113.108.8.93:10002"
//WSADDR = "ws://113.108.8.93:10001"
REGISTERADDR = APIADDR + "/user_register"
ACCOUNTCHECK = APIADDR + "/manager/account_check"
TOKENADDR = APIADDR + "/auth/user_token"
SECRET = "openIM123"
//SECRET = "4zbF9Y6Fs1QJ0hsmpC3B676txZcCnjcZ"
SENDINTERVAL = 20
)
var ctx context.Context
const PlatformID = 3
type ResToken struct {
Data struct {
ExpiredTime int64 `json:"expiredTime"`
Token string `json:"token"`
Uid string `json:"uid"`
}
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
}
func ggetToken(uid string) string {
url := TOKENADDR
var req server_api_params.UserTokenReq
req.Platform = PlatformID
req.UserID = uid
req.Secret = SECRET
req.OperationID = utils.OperationIDGenerator()
r, err := network.Post2Api(url, req, "a")
if err != nil {
log.ZError(ctx, "Post2Api failed ", errors.New("Post2Api failed "), "operationID", req.OperationID, "url", url, "req", req)
return ""
}
var stcResp ResToken
err = json.Unmarshal(r, &stcResp)
if stcResp.ErrCode != 0 {
log.ZError(ctx, "ErrCode failed ", errors.New("ErrCode failed "), "operationID", req.OperationID,
"errorCode", stcResp.ErrCode, "errMsg", stcResp.ErrMsg, "url", url, "req", req)
return ""
}
log.ZInfo(ctx, "get token: ", "operationID", req.OperationID, "token", stcResp.Data.Token)
return stcResp.Data.Token
}
func gRunGetToken(strMyUid string) string {
var token string
for true {
token = ggetToken(strMyUid)
if token == "" {
time.Sleep(time.Duration(100) * time.Millisecond)
continue
} else {
break
}
}
return token
}
func main() {
uid := "1695766238"
//Gordon
//uid:="1554321956297519104"
//Gordon2
//uid := "1583984945064968192"
//uid := "3734595565"
tokenx := gRunGetToken(uid)
//tokenx := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVSUQiOiI3MDcwMDgxNTMiLCJQbGF0Zm9ybSI6IkFuZHJvaWQiLCJleHAiOjE5NjY0MTJ1XjJZGWj5fB3mqC7p6ytxSarvxZfsABwIjoxNjUxMDU1MDU2fQ.aWvmJ_sQxXmT5nKwiM5QsF9-tfkldzOYZtRD3nrUuko"
test.InOutDoTest(uid, tokenx, WSADDR, APIADDR)
time.Sleep(time.Second * 30)
// test.DoTestSendMsg2("7789", "7788")
//test.DoTestGetAdvancedHistoryMessageList()
//test.DoTestGetSelfUserInfo()
//test.DoTestSendMsg2GroupWithMessage(uid, "1623878302774460418", "2")
//test.DoTestAddMessageReactionExtensions(1,"special handle")
//time.Sleep(time.Second*5)
//test.DoTestAddMessageReactionExtensions(2,"special handle")
//time.Sleep(time.Second*5)
//test.DoTestGetMessageListReactionExtensions("special handle")
//test.DoTestSetAppBadge()
//test.DoTestSearchLocalMessages()
//test.DoTestGetAdvancedHistoryMessageList()
println("start")
//test.DoTestGetUserInDepartment()
//test.DoTestGetDepartmentMemberAndSubDepartment()
//test.DoTestDeleteAllMsgFromLocalAndSvr()
// test.DoTestGetDepartmentMemberAndSubDepartment()
//test.DotestUploadFile()
//test.DotestMinio()
//test.DotestSearchFriends()
//if *senderNum == 0 {
// test.RegisterAccounts(*onlineNum)
// return
//}
//
//test.OnlineTest(*onlineNum)
////test.TestSendCostTime()
//test.ReliabilityTest(*singleSenderMsgNum, *intervalTime, 10, *senderNum)
//test.DoTestSearchLocalMessages()
//println("start")
//test.DoTestSendImageMsg(strMyUidx, "17726378428")
//test.DoTestSearchGroups()
//test.DoTestGetHistoryMessage("")
//test.DoTestGetHistoryMessageReverse("")
//test.DoTestInviteInGroup()
//test.DoTestCancel()
//test.DoTestSendMsg2(strMyUidx, friendID)
//test.DoTestGetAllConversation()
//test.DoTestGetOneConversation("17726378428")
//test.DoTestGetConversations(`["single_17726378428"]`)
//test.DoTestGetConversationListSplit()
//test.DoTestGetConversationRecvMessageOpt(`["single_17726378428"]`)
//set batch
//test.DoTestSetConversationRecvMessageOpt([]string{"single_17726378428"}, constant.NotReceiveMessage)
//set one
////set batch
//test.DoTestSetConversationRecvMessageOpt([]string{"single_17726378428"}, constant.ReceiveMessage)
////set one
//test.DoTestSetConversationPinned("single_17726378428", false)
//test.DoTestSetOneConversationRecvMessageOpt("single_17726378428", constant.NotReceiveMessage)
//test.DoTestSetOneConversationPrivateChat("single_17726378428", false)
//test.DoTestReject()
//test.DoTestAccept()
//test.DoTestMarkGroupMessageAsRead()
//test.DoTestGetGroupHistoryMessage()
//test.DoTestGetHistoryMessage("17396220460")
time.Sleep(250000 * time.Millisecond)
//b := utils.GetCurrentTimestampBySecond()
i := 0
for {
//test.DoTestSendMsg2Group(strMyUidx, "42c9f515cb84ee0e82b3f3ce71eb14d6", i)
i++
time.Sleep(250 * time.Millisecond)
//if i == 100 {
// break
//}
//log.Warn("", "10 * time.Millisecond ###################waiting... msg: ", i)
}
//
//log.Warn("", "cost time: ", utils.GetCurrentTimestampBySecond()-b)
//return
//i = 0
//for {
// //test.DoTestSendMsg2Group(strMyUidx, "42c9f515cb84ee0e82b3f3ce71eb14d6", i)
// i++
// time.Sleep(1000 * time.Millisecond)
// if i == 10 {
// break
// }
// log.Warn("", "1000 * time.Millisecond ###################waiting... msg: ", i)
//}
//
//i = 0
//for {
// test.DoTestSendMsg2Group(strMyUidx, "42c9f515cb84ee0e82b3f3ce71eb14d6", i)
// i++
// time.Sleep(10000 * time.Millisecond)
// log.Warn("", "10000 * time.Millisecond ###################waiting... msg: ", i)
//}
//reliabilityTest()
// test.PressTest(testClientNum, intervalSleep, imIP)
}
//
//funcation main() {
// testClientNum := 100
// intervalSleep := 2
// imIP := "43.128.5.63"
//
// msgNum := 1000
// test.ReliabilityTest(msgNum, intervalSleep, imIP)
// for i := 0; i < 6; i++ {
// test.Msgwg.Wait()
// }
//
// for {
//
// if test.CheckReliabilityResult() {
// log.Warn("CheckReliabilityResult ok, again")
//
// } else {
// log.Warn("CheckReliabilityResult failed , wait.... ")
// }
//
// time.Sleep(time.Duration(10) * time.Second)
// }
//
//}
//funcation printCallerNameAndLine() string {
// pc, _, line, _ := runtime.Caller(2)
// return runtime.FuncForPC(pc).Name() + "()@" + strconv.Itoa(line) + ": "
//}
// myuid, maxuid, msgnum

@ -0,0 +1,61 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/test"
"time"
)
func main() {
APIADDR := "http://59.36.173.89:10002"
WSADDR := "ws://59.36.173.89:10001"
REGISTERADDR := APIADDR + "/user_register"
ACCOUNTCHECK := APIADDR + "/manager/account_check"
TOKENADDR := APIADDR + "/auth/user_token"
SECRET := "openIM123"
SENDINTERVAL := 20
test.REGISTERADDR = REGISTERADDR
test.TOKENADDR = TOKENADDR
test.SECRET = SECRET
test.SENDINTERVAL = SENDINTERVAL
test.WSADDR = WSADDR
test.ACCOUNTCHECK = ACCOUNTCHECK
strMyUidx := "5284951719"
tokenx := test.RunGetToken(strMyUidx)
fmt.Println(tokenx)
test.InOutDoTest(strMyUidx, tokenx, WSADDR, APIADDR)
time.Sleep(time.Second * 10)
// test.DoTestGetUsersInfo()
// test.DoTestSetMsgDestructTime("sg_1012596513")
// test.DoTestRevoke()
// test.DotestDeleteFriend("8303492153")
// test.TestMarkGroupMessageAsRead()
// test.DoTestRevoke()
// time.Sleep(time.Second * 5)
// test.DoTestAddToBlackList("9169012630")
// test.DoTestDeleteFromBlackList("9169012630")
// test.DotestDeleteFriend("9169012630")
// test.DoTestSetConversationPinned("si_2456093263_9169012630", true)
// test.DoTestSetOneConversationRecvMessageOpt("si_2456093263_9169012630", 2)
// test.DoTestGetConversationRecvMessageOpt("si_2456093263_9169012630")
// test.DoTestDeleteConversationMsgFromLocalAndSvr("sg_537415520")
for {
time.Sleep(10000 * time.Millisecond)
}
}

@ -0,0 +1,25 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
func main() {
// var onlineNum *int //Number of online users
// onlineNum = flag.Int("on", 10, "online num")
// flag.Parse()
// log.Warn("", "online test start, online num: ", *onlineNum)
// test.OnlineTest(*onlineNum)
// log.Warn("", "online test finish, online num: ", *onlineNum)
// select {}
}

@ -0,0 +1,67 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
)
func main() {
//filePath, err := filepath.Abs(".\test.go")
//if err != nil {
// panic(err)
//}
// 解析Go文件
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, "D:\\Goland\\workspace\\Open-IM-SDK-Core\\main\\test.go", nil, parser.AllErrors)
if err != nil {
panic(err)
}
//myImporter := importer.Default()
// 创建类型检查器
//conf := types.Config{Importer: myImporter}
info := &types.Info{
Defs: make(map[*ast.Ident]types.Object),
}
//// 类型检查
//_, err = conf.Check("", fset, []*ast.File{node}, info)
//if err != nil {
// panic(err)
//}
// 遍历文件中所有函数
fn := func(pkg *types.Package) string {
return pkg.Name()
}
for _, decl := range node.Decls {
if f, ok := decl.(*ast.FuncDecl); ok {
// 打印函数名
fmt.Println("Function Name: ", f.Name.Name)
// 打印参数名和类型
for _, param := range f.Type.Params.List {
for _, name := range param.Names {
obj := info.ObjectOf(name)
typ := obj.Type()
fmt.Printf("Parameter Name: %s, Type: %s\n", name.Name, types.TypeString(typ, fn))
}
}
}
}
}

@ -0,0 +1,38 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"errors"
"flag"
"github.com/openimsdk/openim-sdk-core/v3/test"
"github.com/openimsdk/tools/log"
)
func main() {
var senderNum *int //Number of users sending messages
var singleSenderMsgNum *int //Number of single user send messages
var intervalTime *int //Sending time interval, in millisecond
senderNum = flag.Int("sn", 100, "sender num")
singleSenderMsgNum = flag.Int("mn", 1000, "single sender msg num")
intervalTime = flag.Int("t", 0, "interval time mill second")
flag.Parse()
// test.InitMgr(*senderNum)
log.ZInfo(ctx, "logLevel", uint32(test.LogLevel))
log.ZWarn(ctx, "press test begin ", errors.New(""), "sender num", *senderNum, " single sender msg num", *singleSenderMsgNum, " send msg total num ", *senderNum**singleSenderMsgNum)
test.PressTest(*singleSenderMsgNum, *intervalTime, *senderNum)
select {}
}

@ -0,0 +1,38 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"errors"
"flag"
"github.com/openimsdk/openim-sdk-core/v3/test"
"github.com/openimsdk/tools/log"
)
func main() {
var senderNum *int //Number of users sending messages
var singleSenderMsgNum *int //Number of single user send messages
var intervalTime *int //Sending time interval, in millisecond
senderNum = flag.Int("sn", 200, "sender num")
singleSenderMsgNum = flag.Int("mn", 100, "single sender msg num")
intervalTime = flag.Int("t", 10, "interval time mill second")
flag.Parse()
test.InitMgr(*senderNum)
log.ZInfo(ctx, "logName", test.LogName, "logLevel", uint32(test.LogLevel))
log.ZWarn(ctx, "reliability test start ", errors.New(""), "sender num", *senderNum, " single sender msg num", *singleSenderMsgNum, " send msg total num ", *senderNum**singleSenderMsgNum)
test.ReliabilityTest(*singleSenderMsgNum, *intervalTime, 10, *senderNum)
}

@ -0,0 +1,192 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"errors"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/test"
"github.com/openimsdk/tools/log"
"time"
)
var allDB []*db.DataBase
//funcation TestDB(loginUserID string) {
// operationID := utils.OperationIDGenerator()
// dbUser, err := db.NewDataBase(loginUserID, "/data/test/Open-IM-Server/db/sdk/", operationID)
// if err != nil {
// log.Error(operationID, "NewDataBase failed ", err.Error(), loginUserID)
// return
// }
// conversationList, err := dbUser.GetAllConversationList()
// if err != nil {
// log.Error(operationID, "GetAllConversationList failed ", err.Error())
// }
// log.Info(operationID, "GetAllConversationList len: ", len(conversationList))
//
// groupIDList, err := dbUser.GetJoinedGroupList()
// if err != nil {
// log.Error(operationID, "GetJoinedGroupList failed ", err.Error())
// }
// log.Info(operationID, "GetJoinedGroupList len: ", len(groupIDList))
//
// groupMemberList, err := dbUser.GetAllGroupMemberList()
// if err != nil {
// log.Error(operationID, "GetAllGroupMemberList failed ", err.Error())
// }
// log.Info(operationID, "GetAllGroupMemberList len: ", len(groupMemberList))
// //GetAllMessageForTest
// msgList, err := dbUser.GetAllMessageForTest()
// if err != nil {
// log.Error(operationID, "GetAllMessageForTest failed ", err.Error())
// }
// log.Info(operationID, "GetAllMessageForTest len: ", len(msgList))
// allDB = append(allDB, dbUser)
//
// dbUser.CloseDB(operationID)
// log.Info(operationID, "close db finished ")
//
//}
func main() {
//var userIDList []string
//f, err := os.Open("/data/test/Open-IM-Server/db/sdk")
//if err != nil {
// log.Error("", "open failed ", err.Error())
// return
//}
//files, err := f.Readdir(-1)
//f.Close()
//if err != nil {
// log.Error("", "Readdir failed ", err.Error())
// return
//}
//
//for _, file := range files {
// begin := strings.Index(file.Name(), "OpenIM_v2_")
// end := strings.Index(file.Name(), ".db")
// userID := file.Name()[begin+len("OpenIM_v2_") : end]
// // OpenIM_v2_3380999461.db
// log.Info("", "file name: ", file.Name(), userID)
// TestDB(userID)
//}
//log.Info("", "files: ", len(allDB))
////for _, v := range allDB {
//// v.CloseDB("aa")
////}
//
//log.Info("", "gc begin ")
//runtime.GC()
//log.Info("", "gc end ")
//time.Sleep(100000 * time.Second)
//return
strMyUidx := "3370431052"
tokenx := test.RunGetToken(strMyUidx)
//tokenx := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVSUQiOiI3MDcwMDgxNTMiLCJQbGF0Zm9ybSI6IkFuZHJvaWQiLCJleHAiOjE5NjY0MTJ1XjJZGWj5fB3mqC7p6ytxSarvxZfsABwIjoxNjUxMDU1MDU2fQ.aWvmJ_sQxXmT5nKwiM5QsF9-tfkldzOYZtRD3nrUuko"
//go funcation() {
// time.Sleep(2 * time.Second)
// test.InOutLogou()
//}()
test.InOutDoTest(strMyUidx, tokenx, test.WSADDR, test.APIADDR)
// test.InOutDoTest(strMyUidx, tokenx, test.WSADDR, test.APIADDR)
// time.Sleep(5 * time.Second)
// test.SetListenerAndLogin(strMyUidx, tokenx)
//test.DoTestSetGroupMemberInfo("1104164664", "3188816039", "set ex")
// test.DotestGetGroupMemberList()
//time.Sleep(100000 * time.Second)
// test.DoTestCreateGroup()
// test.DoTestJoinGroup()
// test.DoTestGetGroupsInfo()
// test.DoTestDeleteAllMsgFromLocalAndSvr()
// println("token ", tokenx)
time.Sleep(100000 * time.Second)
b := utils.GetCurrentTimestampBySecond()
i := 0
for {
test.DoTestSendMsg2c2c(strMyUidx, "3380999461", i)
i++
time.Sleep(100 * time.Millisecond)
if i == 10000 {
break
}
log.ZWarn(ctx, "", errors.New(""), "10 * time.Millisecond ###################waiting... msg: ", i)
}
//log.Warn("", "cost time: ", utils.GetCurrentTimestampBySecond()-b)
time.Sleep(100000 * time.Second)
return
i = 0
for {
test.DoTestSendMsg2Group(strMyUidx, "42c9f515cb84ee0e82b3f3ce71eb14d6", i)
i++
time.Sleep(1000 * time.Millisecond)
if i == 10 {
break
}
log.ZWarn(ctx, "", errors.New(""), "1000 * time.Millisecond ###################waiting... msg: ", i)
}
i = 0
for {
test.DoTestSendMsg2Group(strMyUidx, "42c9f515cb84ee0e82b3f3ce71eb14d6", i)
i++
time.Sleep(10000 * time.Millisecond)
log.ZWarn(ctx, "", errors.New(""), "10000 * time.Millisecond ###################waiting... msg: ", i)
}
//reliabilityTest()
// test.PressTest(testClientNum, intervalSleep, imIP)
}
//
//funcation main() {
// testClientNum := 100
// intervalSleep := 2
// imIP := "43.128.5.63"
//
// msgNum := 1000
// test.ReliabilityTest(msgNum, intervalSleep, imIP)
// for i := 0; i < 6; i++ {
// test.Msgwg.Wait()
// }
//
// for {
//
// if test.CheckReliabilityResult() {
// log.Warn("CheckReliabilityResult ok, again")
//
// } else {
// log.Warn("CheckReliabilityResult failed , wait.... ")
// }
//
// time.Sleep(time.Duration(10) * time.Second)
// }
//
//}
//funcation printCallerNameAndLine() string {
// pc, _, line, _ := runtime.Caller(2)
// return runtime.FuncForPC(pc).Name() + "()@" + strconv.Itoa(line) + ": "
//}
// myuid, maxuid, msgnum

@ -0,0 +1,74 @@
docs/.generated_docs
docs/guide/en-US/cmd/openim/openim_color.md
docs/guide/en-US/cmd/openim/openim_completion.md
docs/guide/en-US/cmd/openim/openim_info.md
docs/guide/en-US/cmd/openim/openim_jwt.md
docs/guide/en-US/cmd/openim/openim_jwt_show.md
docs/guide/en-US/cmd/openim/openim_jwt_sign.md
docs/guide/en-US/cmd/openim/openim_jwt_verify.md
docs/guide/en-US/cmd/openim/openim_new.md
docs/guide/en-US/cmd/openim/openim_options.md
docs/guide/en-US/cmd/openim/openim_policy.md
docs/guide/en-US/cmd/openim/openim_policy_create.md
docs/guide/en-US/cmd/openim/openim_policy_delete.md
docs/guide/en-US/cmd/openim/openim_policy_get.md
docs/guide/en-US/cmd/openim/openim_policy_list.md
docs/guide/en-US/cmd/openim/openim_policy_update.md
docs/guide/en-US/cmd/openim/openim_secret.md
docs/guide/en-US/cmd/openim/openim_secret_create.md
docs/guide/en-US/cmd/openim/openim_secret_delete.md
docs/guide/en-US/cmd/openim/openim_secret_get.md
docs/guide/en-US/cmd/openim/openim_secret_list.md
docs/guide/en-US/cmd/openim/openim_secret_update.md
docs/guide/en-US/cmd/openim/openim_set.md
docs/guide/en-US/cmd/openim/openim-rpc-user.md
docs/guide/en-US/cmd/openim/openim-rpc-user_create.md
docs/guide/en-US/cmd/openim/openim-rpc-user_delete.md
docs/guide/en-US/cmd/openim/openim-rpc-user_get.md
docs/guide/en-US/cmd/openim/openim-rpc-user_list.md
docs/guide/en-US/cmd/openim/openim-rpc-user_update.md
docs/guide/en-US/cmd/openim/openim_validate.md
docs/guide/en-US/cmd/openim/openim_version.md
docs/guide/en-US/yaml/openim/openim.yaml
docs/guide/en-US/yaml/openim/openim_color.yaml
docs/guide/en-US/yaml/openim/openim_completion.yaml
docs/guide/en-US/yaml/openim/openim_info.yaml
docs/guide/en-US/yaml/openim/openim_jwt.yaml
docs/guide/en-US/yaml/openim/openim_new.yaml
docs/guide/en-US/yaml/openim/openim_options.yaml
docs/guide/en-US/yaml/openim/openim_policy.yaml
docs/guide/en-US/yaml/openim/openim_secret.yaml
docs/guide/en-US/yaml/openim/openim_set.yaml
docs/guide/en-US/yaml/openim/openim-rpc-user.yaml
docs/guide/en-US/yaml/openim/openim_validate.yaml
docs/guide/en-US/yaml/openim/openim_version.yaml
docs/man/man1/openim-completion.1
docs/man/man1/openim-info.1
docs/man/man1/openim-jwt-show.1
docs/man/man1/openim-jwt-sign.1
docs/man/man1/openim-jwt-verify.1
docs/man/man1/openim-jwt.1
docs/man/man1/openim-new.1
docs/man/man1/openim-options.1
docs/man/man1/openim-policy-create.1
docs/man/man1/openim-policy-delete.1
docs/man/man1/openim-policy-get.1
docs/man/man1/openim-policy-list.1
docs/man/man1/openim-policy-update.1
docs/man/man1/openim-policy.1
docs/man/man1/openim-secret-create.1
docs/man/man1/openim-secret-delete.1
docs/man/man1/openim-secret-get.1
docs/man/man1/openim-secret-list.1
docs/man/man1/openim-secret-update.1
docs/man/man1/openim-secret.1
docs/man/man1/openim-set.1
docs/man/man1/openim-user-create.1
docs/man/man1/openim-user-delete.1
docs/man/man1/openim-user-get.1
docs/man/man1/openim-user-list.1
docs/man/man1/openim-user-update.1
docs/man/man1/openim-user.1
docs/man/man1/openim-validate.1
docs/man/man1/openim-version.1
docs/man/man1/openim.1

@ -0,0 +1 @@
* @openimsdk/go-code-review

@ -0,0 +1,129 @@
# Continuous Integration and Automation
Every 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).
- [Continuous Integration and Automation](#continuous-integration-and-automation)
- [CI Platforms](#ci-platforms)
- [GitHub Actions](#github-actions)
- [Running locally](#running-locally)
## CI Platforms
Currently, there are two different platforms involved in running the CI processes:
- GitHub actions
- Drone pipelines on CNCF infrastructure
### GitHub Actions
All the existing GitHub Actions are defined as YAML files under the `.github/workflows` directory. These can be grouped into:
- **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.
- **Repository automation**. Currently, it only covers issues and epic grooming.
Everything runs on GitHub's provided runners; thus, the tests are limited to run in `x86_64` architectures.
## Running locally
A 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:
**User Makefile:**
```bash
root@PS2023EVRHNCXG:~/workspaces/openim/openim-sdk-core# make help 😊
Usage: make <TARGETS> <OPTIONS> ...
Targets:
all Run tidy, gen, add-copyright, format, lint, cover, build 🚀
build Build binaries by default 🛠
multiarch Build binaries for multiple platforms. See option PLATFORMS. 🌍
tidy tidy go.mod ✨
vendor vendor go.mod 📦
style code style -> fmt,vet,lint 💅
fmt Run go fmt against code. ✨
vet Run go vet against code. ✅
lint Check syntax and styling of go sources. ✔
format Gofmt (reformat) package sources (exclude vendor dir if existed). 🔄
test Run unit test. 🧪
cover Run unit test and get test coverage. 📊
updates Check for updates to go.mod dependencies 🆕
imports task to automatically handle import packages in Go files using goimports tool 📥
clean Remove all files that are created by building. 🗑
image Build docker images for host arch. 🐳
image.multiarch Build docker images for multiple platforms. See option PLATFORMS. 🌍🐳
push Build docker images for host arch and push images to registry. 📤🐳
push.multiarch Build docker images for multiple platforms and push images to registry. 🌍📤🐳
tools Install dependent tools. 🧰
gen Generate all necessary files. 🧩
swagger Generate swagger document. 📖
serve-swagger Serve swagger spec and docs. 🚀📚
verify-copyright Verify the license headers for all files. ✅
add-copyright Add copyright ensure source code files have license headers. 📄
release release the project 🎉
help Show this help info. ℹ
help-all Show all help details info. ℹ📚
Options:
DEBUG Whether or not to generate debug symbols. Default is 0. ❓
BINS Binaries to build. Default is all binaries under cmd. 🛠
This option is available when using: make {build}(.multiarch) 🧰
Example: make build BINS="openim-api openim_cms_api".
PLATFORMS Platform to build for. Default is linux_arm64 and linux_amd64. 🌍
This option is available when using: make {build}.multiarch 🌍
Example: make multiarch PLATFORMS="linux_s390x linux_mips64
linux_mips64le darwin_amd64 windows_amd64 linux_amd64 linux_arm64".
V Set to 1 enable verbose build. Default is 0. 📝
```
How to Use Makefile to Help Contributors Build Projects Quickly 😊
The `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.
Here's a breakdown of the targets and options provided by the Makefile:
**Targets 😃**
1. `all`: This target runs multiple tasks like `tidy`, `gen`, `add-copyright`, `format`, `lint`, `cover`, and `build`. It ensures comprehensive project building.
2. `build`: The primary target that compiles binaries by default. It is particularly useful for creating the necessary executable files.
3. `multiarch`: A target that builds binaries for multiple platforms. Contributors can specify the desired platforms using the `PLATFORMS` option.
4. `tidy`: This target cleans up the `go.mod` file, ensuring its consistency.
5. `vendor`: A target that updates the project dependencies based on the `go.mod` file.
6. `style`: Checks the code style using tools like `fmt`, `vet`, and `lint`. It ensures a consistent coding style throughout the project.
7. `fmt`: Formats the code using the `go fmt` command, ensuring proper indentation and formatting.
8. `vet`: Runs the `go vet` command to identify common errors in the code.
9. `lint`: Validates the syntax and styling of Go source files using a linter.
10. `format`: Reformats the package sources using `gofmt`. It excludes the vendor directory if it exists.
11. `test`: Executes unit tests to ensure the functionality and stability of the code.
12. `cover`: Performs unit tests and calculates the test coverage of the code.
13. `updates`: Checks for updates to the project's dependencies specified in the `go.mod` file.
14. `imports`: Automatically handles import packages in Go files using the `goimports` tool.
15. `clean`: Removes all files generated during the build process, effectively cleaning up the project directory.
16. `image`: Builds Docker images for the host architecture.
17. `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.
18. `push`: Builds Docker images for the host architecture and pushes them to a registry.
19. `push.multiarch`: Builds Docker images for multiple platforms and pushes them to a registry. Contributors can specify the desired platforms using the `PLATFORMS` option.
20. `tools`: Installs the necessary tools or dependencies required by the project.
21. `gen`: Generates all the required files automatically.
22. `swagger`: Generates the swagger document for the project.
23. `serve-swagger`: Serves the swagger specification and documentation.
24. `verify-copyright`: Verifies the license headers for all project files.
25. `add-copyright`: Adds copyright headers to the source code files.
26. `release`: Releases the project, presumably for distribution.
27. `help`: Displays information about available targets and options.
28. `help-all`: Shows detailed information about all available targets and options.
**Options 😄**
1. `DEBUG`: A boolean option that determines whether or not to generate debug symbols. The default value is 0 (false).
2. `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.
3. `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.
4. `V`: A boolean option that enables verbose build output when set to 1 (true). The default value is 0 (false).
With these targets and options in place, contributors can efficiently build projects using the Makefile. Happy coding! 🚀😊

@ -0,0 +1,38 @@
# Code conventions
- [Code conventions](#code-conventions)
- [POSIX shell](#posix-shell)
- [Go](#go)
- [Directory and file conventions](#directory-and-file-conventions)
- [Testing conventions](#testing-conventions)
## POSIX shell
- [Style guide](https://google.github.io/styleguide/shell.xml)
## Go
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- [Effective Go](https://golang.org/doc/effective_go.html)
- Know and avoid [Go landmines](https://gist.github.com/lavalamp/4bd23295a9f32706a48f)
- Comment your code.
- [Go's commenting conventions](http://blog.golang.org/godoc-documenting-go-code)
- If reviewers ask questions about why the code is the way it is, that's a sign that comments might be helpful.
- Command-line flags should use dashes, not underscores
- Naming
- Please consider package name when selecting an interface name, and avoid redundancy. For example, `storage.Interface` is better than `storage.StorageInterface`.
- Do not use uppercase characters, underscores, or dashes in package names.
- Please consider parent directory name when choosing a package name. For example, `pkg/controllers/autoscaler/foo.go` should say `package autoscaler` not `package autoscalercontroller`.
- Unless there's a good reason, the `package foo` line should match the name of the directory in which the `.go` file exists.
- Importers can use a different name if they need to disambiguate.
## Directory and file conventions
- Avoid general utility packages. Packages called "util" are suspect. Instead, derive a name that describes your desired function. For example, the utility functions dealing with waiting for operations are in the `wait` package and include functionality like `Poll`. The full name is `wait.Poll`.
- All filenames should be lowercase.
- All source files and directories should use underscores, not dashes.
- Package directories should generally avoid using separators as much as possible. When package names are multiple words, they usually should be in nested subdirectories.
## Testing conventions
Please refer to [TESTING.md](../../tests/TESTING.md) document.

@ -0,0 +1,80 @@
# Development Guide
Since 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.
- [Development Guide](#development-guide)
- [Non-Linux environment prerequisites](#non-linux-environment-prerequisites)
- [Windows Setup](#windows-setup)
- [macOS Setup](#macos-setup)
- [Installing Required Software](#installing-required-software)
- [Go](#go)
- [Docker](#docker)
- [Vagrant](#vagrant)
- [Cloning, Building and Testing OpenIM](#cloning-building-and-testing-openim)
- [Dependency management](#dependency-management)
## Non-Linux environment prerequisites
All 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](../../Vagrantfile) to use them.
Either way, if one still wants to build and test OpenIM on non-Linux environments, specific setups are to be followed.
### Windows Setup
To 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.
### macOS Setup
The 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.
First, install the GNU utils:
```sh
brew install coreutils findutils gawk gnu-sed gnu-tar grep make
```
Then update the shell init script (i.e. `.bashrc`) to prepend the GNU Utils to the `$PATH` variable
```sh
GNUBINS="$(find /usr/local/opt -type d -follow -name gnubin -print)"
for bindir in ${GNUBINS[@]}; do
PATH=$bindir:$PATH
done
export PATH
```
## Installing Required Software
### Go
It 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.
| OpenIM | requires Go |
|----------------|-------------|
| 2.24 - 3.00 | 1.15 + |
| 3.30 + | 1.18 + |
### Docker
OpenIM 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.
### Vagrant
As described in the [Testing documentation](../../tests/TESTING.md), 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:
- [VirtualBox](https://www.virtualbox.org/)
- [libvirt](https://libvirt.org/) and the [vagrant-libvirt plugin](https://github.com/vagrant-libvirt/vagrant-libvirt#installation)
## Cloning, Building and Testing OpenIM
These topics already have been addressed on their respective documents:
- [Git Workflow](./git-workflow.md)
- [Building](../../BUILDING.md)
- [Testing](../../tests/TESTING.md)
## Dependency management
OpenIM uses [go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.

@ -0,0 +1,102 @@
# Git workflows
This document is an overview of OpenIM git workflow. It includes conventions, tips, and how to maintain good repository hygiene.
- [Git workflows](#git-workflows)
- [Branching model](#branching-model)
- [Branch naming conventions](#branch-naming-conventions)
- [Backport policy](#backport-policy)
- [Git operations](#git-operations)
- [Setting up](#setting-up)
- [Branching out](#branching-out)
- [Keeping local branches in sync](#keeping-local-branches-in-sync)
- [Pushing changes](#pushing-changes)
## Branching model
OpenIM 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.
### Branch naming conventions
Every 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/openim-sdk-core/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.
```text
main -------------------------------------------. (OpenIM 3.10)
release-3.0.0 \---------------|---------------. (OpenIM 3.00)
release-2.4.0 \---------------. (OpenIM 2.40)
```
### Backport policy
All 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.
## Git operations
There are everyday tasks related to git that every contributor needs to perform, and this section elaborates on them.
### Setting up
Creating a OpenIM fork, cloning it, and setting its upstream remote can be summarized on:
1. Visit <https://github.com/openimsdk/openim-sdk-core>
2. Click the `Fork` button (top right) to establish a cloud-based fork
3. Clone fork to local storage
4. Add to your fork OpenIM remote as upstream
Once cloned, in code it would look this way:
```sh
## Clone fork to local storage
export user="your github profile name"
git clone https://github.com/$user/OpenIM.git
# or: git clone git@github.com:$user/OpenIM.git
## Add OpenIM as upstream to your fork
cd OpenIM
git remote add upstream https://github.com/openimsdk/openim-sdk-core.git
# or: git remote add upstream git@github.com:OpenIMSDK/openim-sdk-core.git
## Ensure to never push to upstream directly
git remote set-url --push upstream no_push
## Confirm that your remotes make sense:
git remote -v
```
### Branching out
Every time one wants to work on a new OpenIM feature, we do:
1. Get local main branch up to date
2. Create a new branch from the main one (i.e.: myfeature branch )
In code it would look this way:
```sh
## Get local main up to date
# Assuming the OpenIM clone is the current working directory
git fetch upstream
git checkout main
git rebase upstream/main
## Create a new branch from main
git checkout -b myfeature
```
### Keeping local branches in sync
Either 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:
```sh
git fetch upstream
git rebase upstream/main
```
It 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`.
### Pushing changes
For commit messages and signatures please refer to the [CONTRIBUTING.md](../../CONTRIBUTING.md) document.
Nobody 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.

@ -0,0 +1,43 @@
module github.com/openimsdk/openim-sdk-core/v3
go 1.21
require (
github.com/golang/protobuf v1.5.4
github.com/gorilla/websocket v1.4.2
github.com/jinzhu/copier v0.4.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
github.com/pkg/errors v0.9.1
google.golang.org/protobuf v1.33.0 // indirect
gorm.io/driver/sqlite v1.5.5
nhooyr.io/websocket v1.8.10
)
require golang.org/x/net v0.22.0
require (
github.com/google/go-cmp v0.6.0
github.com/openimsdk/protocol v0.0.69-alpha.16
github.com/openimsdk/tools v0.0.49-alpha.12
github.com/patrickmn/go-cache v2.1.0+incompatible
go.etcd.io/etcd/api/v3 v3.5.13
golang.org/x/image v0.15.0
gorm.io/gorm v1.25.10
)
require (
github.com/coreos/go-semver v0.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/grpc v1.62.1 // indirect
)
//replace github.com/openimsdk/protocol => /Users/chao/Desktop/project/protocol

@ -0,0 +1,77 @@
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/openimsdk/protocol v0.0.69-alpha.16 h1:ciSqm2rjBdpScpkQm3wPjAFv0YbIRp8MITRkDZWVv6c=
github.com/openimsdk/protocol v0.0.69-alpha.16/go.mod h1:OZQA9FR55lseYoN2Ql1XAHYKHJGu7OMNkUbuekrKCM8=
github.com/openimsdk/tools v0.0.49-alpha.12 h1:vsr63W1kHW1dEw9yelMhmr72WmsrjKfs2vXww3upfWI=
github.com/openimsdk/tools v0.0.49-alpha.12/go.mod h1:g7mkHXYUPi0/8aAX8VPMHpnb3hqdV69Jph+bXOGvvNM=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4=
go.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=

@ -0,0 +1,49 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package business
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
type Business struct {
listener func() open_im_sdk_callback.OnCustomBusinessListener
db db_interface.DataBase
}
func NewBusiness(db db_interface.DataBase) *Business {
return &Business{
db: db,
}
}
func (b *Business) DoNotification(ctx context.Context, msg *sdkws.MsgData) {
var n sdk_struct.NotificationElem
err := utils.JsonStringToStruct(string(msg.Content), &n)
if err != nil {
log.ZError(ctx, "unmarshal failed", err, "msg", msg)
return
}
b.listener().OnRecvCustomBusinessMessage(n.Detail)
}

@ -0,0 +1,23 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package business
import (
"github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback"
)
func (w *Business) SetListener(listener func() open_im_sdk_callback.OnCustomBusinessListener) {
w.listener = listener
}

@ -0,0 +1,72 @@
package cache
import "sync"
// Cache is a Generic sync.Map structure.
type Cache[K comparable, V any] struct {
m sync.Map
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{}
}
// Load returns the value stored in the map for a key, or nil if no value is present.
func (c *Cache[K, V]) Load(key K) (value V, ok bool) {
rawValue, ok := c.m.Load(key)
if !ok {
return
}
return rawValue.(V), ok
}
// Store sets the value for a key.
func (c *Cache[K, V]) Store(key K, value V) {
c.m.Store(key, value)
}
// StoreAll sets all value by f's key.
func (c *Cache[K, V]) StoreAll(f func(value V) K, values []V) {
for _, v := range values {
c.m.Store(f(v), v)
}
}
// LoadOrStore returns the existing value for the key if present.
func (c *Cache[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
rawValue, loaded := c.m.LoadOrStore(key, value)
return rawValue.(V), loaded
}
// Delete deletes the value for a key.
func (c *Cache[K, V]) Delete(key K) {
c.m.Delete(key)
}
// DeleteAll deletes all values.
func (c *Cache[K, V]) DeleteAll() {
c.m.Range(func(key, value interface{}) bool {
c.m.Delete(key)
return true
})
}
// RangeAll returns all values in the map.
func (c *Cache[K, V]) RangeAll() (values []V) {
c.m.Range(func(rawKey, rawValue interface{}) bool {
values = append(values, rawValue.(V))
return true
})
return values
}
// RangeCon returns values in the map that satisfy condition f.
func (c *Cache[K, V]) RangeCon(f func(key K, value V) bool) (values []V) {
c.m.Range(func(rawKey, rawValue interface{}) bool {
if f(rawKey.(K), rawValue.(V)) {
values = append(values, rawValue.(V))
}
return true
})
return values
}

@ -0,0 +1,33 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/golang/protobuf/proto"
"github.com/openimsdk/protocol/sdkws"
)
func UnmarshalTips(msg *sdkws.MsgData, detail proto.Message) error {
var tips sdkws.TipsComm
if err := proto.Unmarshal(msg.Content, &tips); err != nil {
return utils.Wrap(err, "")
}
if err := proto.Unmarshal(tips.Detail, detail); err != nil {
return utils.Wrap(err, "")
}
return nil
}

@ -0,0 +1,33 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"bytes"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)
type ObjectStorage interface {
UploadImage(filePath string, onProgressFun func(int)) (string, string, error)
UploadSound(filePath string, onProgressFun func(int)) (string, string, error)
UploadFile(filePath string, onProgressFun func(int)) (string, string, error)
UploadVideo(videoPath, snapshotPath string, onProgressFun func(int)) (string, string, string, string, error)
UploadImageByBuffer(buffer *bytes.Buffer, size int64, imageType string, onProgressFun func(int)) (string, string, error)
UploadSoundByBuffer(buffer *bytes.Buffer, size int64, fileType string, onProgressFun func(int)) (string, string, error)
UploadFileByBuffer(buffer *bytes.Buffer, size int64, fileType string, onProgressFun func(int)) (string, string, error)
UploadVideoByBuffer(videoBuffer, snapshotBuffer *bytes.Buffer, videoSize, snapshotSize int64, videoType, snapshotType string, onProgressFun func(int)) (string, string, string, string, error)
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,714 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"encoding/json"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/utils/datautil"
"reflect"
"runtime"
"time"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (c *Conversation) Work(c2v common.Cmd2Value) {
log.ZDebug(c2v.Ctx, "NotificationCmd start", "cmd", c2v.Cmd, "value", c2v.Value)
defer log.ZDebug(c2v.Ctx, "NotificationCmd end", "cmd", c2v.Cmd, "value", c2v.Value)
switch c2v.Cmd {
case constant.CmdNewMsgCome:
c.doMsgNew(c2v)
case constant.CmdSuperGroupMsgCome:
case constant.CmdUpdateConversation:
c.doUpdateConversation(c2v)
case constant.CmdUpdateMessage:
c.doUpdateMessage(c2v)
case constant.CmSyncReactionExtensions:
case constant.CmdNotification:
c.doNotificationNew(c2v)
}
}
func (c *Conversation) doNotificationNew(c2v common.Cmd2Value) {
ctx := c2v.Ctx
allMsg := c2v.Value.(sdk_struct.CmdNewMsgComeToConversation).Msgs
syncFlag := c2v.Value.(sdk_struct.CmdNewMsgComeToConversation).SyncFlag
switch syncFlag {
case constant.MsgSyncBegin:
c.startTime = time.Now()
c.ConversationListener().OnSyncServerStart()
if err := c.SyncAllConversationHashReadSeqs(ctx); err != nil {
log.ZError(ctx, "SyncConversationHashReadSeqs err", err)
}
//clear SubscriptionStatusMap
c.user.OnlineStatusCache.DeleteAll()
for _, syncFunc := range []func(c context.Context) error{
c.user.SyncLoginUserInfo,
c.friend.SyncAllBlackList, c.friend.SyncAllFriendApplication, c.friend.SyncAllSelfFriendApplication,
c.group.SyncAllAdminGroupApplication, c.group.SyncAllSelfGroupApplication, c.user.SyncAllCommand,
} {
go func(syncFunc func(c context.Context) error) {
_ = syncFunc(ctx)
}(syncFunc)
}
syncFunctions := []func(c context.Context) error{
c.group.SyncAllJoinedGroupsAndMembers, c.friend.IncrSyncFriends,
}
for _, syncFunc := range syncFunctions {
funcName := runtime.FuncForPC(reflect.ValueOf(syncFunc).Pointer()).Name()
startTime := time.Now()
err := syncFunc(ctx)
duration := time.Since(startTime)
if err != nil {
log.ZWarn(ctx, fmt.Sprintf("%s sync err", funcName), err, "duration", duration)
} else {
log.ZDebug(ctx, fmt.Sprintf("%s completed successfully", funcName), "duration", duration)
}
}
case constant.MsgSyncFailed:
c.ConversationListener().OnSyncServerFailed()
case constant.MsgSyncEnd:
log.ZDebug(ctx, "MsgSyncEnd", "time", time.Since(c.startTime).Milliseconds())
defer c.ConversationListener().OnSyncServerFinish()
go c.SyncAllConversations(ctx)
}
for conversationID, msgs := range allMsg {
log.ZDebug(ctx, "notification handling", "conversationID", conversationID, "msgs", msgs)
if len(msgs.Msgs) != 0 {
lastMsg := msgs.Msgs[len(msgs.Msgs)-1]
log.ZDebug(ctx, "SetNotificationSeq", "conversationID", conversationID, "seq", lastMsg.Seq)
if lastMsg.Seq != 0 {
if err := c.db.SetNotificationSeq(ctx, conversationID, lastMsg.Seq); err != nil {
log.ZError(ctx, "SetNotificationSeq err", err, "conversationID", conversationID, "lastMsg", lastMsg)
}
}
}
for _, v := range msgs.Msgs {
switch {
case v.ContentType == constant.ConversationChangeNotification:
c.DoConversationChangedNotification(ctx, v)
case v.ContentType == constant.ConversationPrivateChatNotification:
c.DoConversationIsPrivateChangedNotification(ctx, v)
case v.ContentType == constant.ConversationUnreadNotification:
var tips sdkws.ConversationHasReadTips
_ = json.Unmarshal(v.Content, &tips)
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: tips.ConversationID, Action: constant.UnreadCountSetZero}})
c.db.DeleteConversationUnreadMessageList(ctx, tips.ConversationID, tips.UnreadCountTime)
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{tips.ConversationID}}})
continue
case v.ContentType == constant.BusinessNotification:
c.business.DoNotification(ctx, v)
continue
case v.ContentType == constant.RevokeNotification:
c.doRevokeMsg(ctx, v)
case v.ContentType == constant.ClearConversationNotification:
c.doClearConversations(ctx, v)
case v.ContentType == constant.DeleteMsgsNotification:
c.doDeleteMsgs(ctx, v)
case v.ContentType == constant.HasReadReceipt:
c.doReadDrawing(ctx, v)
}
switch v.SessionType {
case constant.SingleChatType:
if v.ContentType > constant.FriendNotificationBegin && v.ContentType < constant.FriendNotificationEnd {
c.friend.DoNotification(ctx, v)
} else if v.ContentType > constant.UserNotificationBegin && v.ContentType < constant.UserNotificationEnd {
c.user.DoNotification(ctx, v)
} else if datautil.Contain(v.ContentType, constant.GroupApplicationRejectedNotification, constant.GroupApplicationAcceptedNotification, constant.JoinGroupApplicationNotification) {
c.group.DoNotification(ctx, v)
} else if v.ContentType > constant.SignalingNotificationBegin && v.ContentType < constant.SignalingNotificationEnd {
continue
}
case constant.GroupChatType, constant.SuperGroupChatType:
if v.ContentType > constant.GroupNotificationBegin && v.ContentType < constant.GroupNotificationEnd {
c.group.DoNotification(ctx, v)
} else if v.ContentType > constant.SignalingNotificationBegin && v.ContentType < constant.SignalingNotificationEnd {
continue
}
}
}
}
}
func (c *Conversation) doDeleteConversation(c2v common.Cmd2Value) {
node := c2v.Value.(common.DeleteConNode)
ctx := c2v.Ctx
// Mark messages related to this conversation for deletion
err := c.db.UpdateMessageStatusBySourceID(context.Background(), node.SourceID, constant.MsgStatusHasDeleted, int32(node.SessionType))
if err != nil {
log.ZError(ctx, "setMessageStatusBySourceID", err)
return
}
// Reset the session information, empty session
err = c.db.ResetConversation(ctx, node.ConversationID)
if err != nil {
log.ZError(ctx, "ResetConversation err:", err)
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{"", constant.TotalUnreadMessageChanged, ""}})
}
func (c *Conversation) getConversationLatestMsgClientID(latestMsg string) string {
msg := &sdk_struct.MsgStruct{}
if err := json.Unmarshal([]byte(latestMsg), msg); err != nil {
log.ZError(context.Background(), "getConversationLatestMsgClientID", err, "latestMsg", latestMsg)
}
return msg.ClientMsgID
}
func (c *Conversation) doUpdateConversation(c2v common.Cmd2Value) {
ctx := c2v.Ctx
node := c2v.Value.(common.UpdateConNode)
switch node.Action {
case constant.AddConOrUpLatMsg:
var list []*model_struct.LocalConversation
lc := node.Args.(model_struct.LocalConversation)
oc, err := c.db.GetConversation(ctx, lc.ConversationID)
if err == nil {
// log.Info("this is old conversation", *oc)
if lc.LatestMsgSendTime >= oc.LatestMsgSendTime || c.getConversationLatestMsgClientID(lc.LatestMsg) == c.getConversationLatestMsgClientID(oc.LatestMsg) { // The session update of asynchronous messages is subject to the latest sending time
err := c.db.UpdateColumnsConversation(ctx, node.ConID, map[string]interface{}{"latest_msg_send_time": lc.LatestMsgSendTime, "latest_msg": lc.LatestMsg})
if err != nil {
log.ZError(ctx, "updateConversationLatestMsgModel", err, "conversationID", node.ConID)
} else {
oc.LatestMsgSendTime = lc.LatestMsgSendTime
oc.LatestMsg = lc.LatestMsg
list = append(list, oc)
c.ConversationListener().OnConversationChanged(utils.StructToJsonString(list))
}
}
} else {
// log.Info("this is new conversation", lc)
err4 := c.db.InsertConversation(ctx, &lc)
if err4 != nil {
// log.Error("internal", "insert new conversation err:", err4.Error())
} else {
list = append(list, &lc)
c.ConversationListener().OnNewConversation(utils.StructToJsonString(list))
}
}
case constant.UnreadCountSetZero:
if err := c.db.UpdateColumnsConversation(ctx, node.ConID, map[string]interface{}{"unread_count": 0}); err != nil {
log.ZError(ctx, "updateConversationUnreadCountModel err", err, "conversationID", node.ConID)
} else {
totalUnreadCount, err := c.db.GetTotalUnreadMsgCountDB(ctx)
if err == nil {
c.ConversationListener().OnTotalUnreadMessageCountChanged(totalUnreadCount)
} else {
log.ZError(ctx, "getTotalUnreadMsgCountDB err", err)
}
}
// case ConChange:
// err, list := u.getAllConversationListModel()
// if err != nil {
// sdkLog("getAllConversationListModel database err:", err.Error())
// } else {
// if list == nil {
// u.ConversationListenerx.OnConversationChanged(structToJsonString([]ConversationStruct{}))
// } else {
// u.ConversationListenerx.OnConversationChanged(structToJsonString(list))
//
// }
// }
case constant.IncrUnread:
err := c.db.IncrConversationUnreadCount(ctx, node.ConID)
if err != nil {
// log.Error("internal", "incrConversationUnreadCount database err:", err.Error())
return
}
case constant.TotalUnreadMessageChanged:
totalUnreadCount, err := c.db.GetTotalUnreadMsgCountDB(ctx)
if err != nil {
// log.Error("internal", "TotalUnreadMessageChanged database err:", err.Error())
} else {
c.ConversationListener().OnTotalUnreadMessageCountChanged(totalUnreadCount)
}
case constant.UpdateConFaceUrlAndNickName:
var lc model_struct.LocalConversation
st := node.Args.(common.SourceIDAndSessionType)
log.ZInfo(ctx, "UpdateConFaceUrlAndNickName", "st", st)
switch st.SessionType {
case constant.SingleChatType:
lc.UserID = st.SourceID
lc.ConversationID = c.getConversationIDBySessionType(st.SourceID, constant.SingleChatType)
lc.ConversationType = constant.SingleChatType
case constant.SuperGroupChatType:
conversationID, conversationType, err := c.getConversationTypeByGroupID(ctx, st.SourceID)
if err != nil {
return
}
lc.GroupID = st.SourceID
lc.ConversationID = conversationID
lc.ConversationType = conversationType
case constant.NotificationChatType:
lc.UserID = st.SourceID
lc.ConversationID = c.getConversationIDBySessionType(st.SourceID, constant.NotificationChatType)
lc.ConversationType = constant.NotificationChatType
default:
log.ZError(ctx, "not support sessionType", nil, "sessionType", st.SessionType)
return
}
lc.ShowName = st.Nickname
lc.FaceURL = st.FaceURL
err := c.db.UpdateConversation(ctx, &lc)
if err != nil {
// log.Error("internal", "setConversationFaceUrlAndNickName database err:", err.Error())
return
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: lc.ConversationID, Action: constant.ConChange, Args: []string{lc.ConversationID}}})
case constant.UpdateLatestMessageChange:
conversationID := node.ConID
var latestMsg sdk_struct.MsgStruct
l, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
log.ZError(ctx, "getConversationLatestMsgModel err", err, "conversationID", conversationID)
} else {
err := json.Unmarshal([]byte(l.LatestMsg), &latestMsg)
if err != nil {
log.ZError(ctx, "latestMsg,Unmarshal err", err)
} else {
latestMsg.IsRead = true
newLatestMessage := utils.StructToJsonString(latestMsg)
err = c.db.UpdateColumnsConversation(ctx, node.ConID, map[string]interface{}{"latest_msg_send_time": latestMsg.SendTime, "latest_msg": newLatestMessage})
if err != nil {
log.ZError(ctx, "updateConversationLatestMsgModel err", err)
}
}
}
case constant.ConChange:
conversationIDs := node.Args.([]string)
conversations, err := c.db.GetMultipleConversationDB(ctx, conversationIDs)
if err != nil {
log.ZError(ctx, "getMultipleConversationModel err", err)
} else {
var newCList []*model_struct.LocalConversation
for _, v := range conversations {
if v.LatestMsgSendTime != 0 {
newCList = append(newCList, v)
}
}
c.ConversationListener().OnConversationChanged(utils.StructToJsonStringDefault(newCList))
}
case constant.NewCon:
cidList := node.Args.([]string)
cLists, err := c.db.GetMultipleConversationDB(ctx, cidList)
if err != nil {
// log.Error("internal", "getMultipleConversationModel err :", err.Error())
} else {
if cLists != nil {
// log.Info("internal", "getMultipleConversationModel success :", cLists)
c.ConversationListener().OnNewConversation(utils.StructToJsonString(cLists))
}
}
case constant.ConChangeDirect:
cidList := node.Args.(string)
c.ConversationListener().OnConversationChanged(cidList)
case constant.NewConDirect:
cidList := node.Args.(string)
// log.Debug("internal", "NewConversation", cidList)
c.ConversationListener().OnNewConversation(cidList)
case constant.ConversationLatestMsgHasRead:
hasReadMsgList := node.Args.(map[string][]string)
var result []*model_struct.LocalConversation
var latestMsg sdk_struct.MsgStruct
var lc model_struct.LocalConversation
for conversationID, msgIDList := range hasReadMsgList {
LocalConversation, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
// log.Error("internal", "get conversation err", err.Error(), conversationID)
continue
}
err = utils.JsonStringToStruct(LocalConversation.LatestMsg, &latestMsg)
if err != nil {
// log.Error("internal", "JsonStringToStruct err", err.Error(), conversationID)
continue
}
if utils.IsContain(latestMsg.ClientMsgID, msgIDList) {
latestMsg.IsRead = true
lc.ConversationID = conversationID
lc.LatestMsg = utils.StructToJsonString(latestMsg)
LocalConversation.LatestMsg = utils.StructToJsonString(latestMsg)
err := c.db.UpdateConversation(ctx, &lc)
if err != nil {
// log.Error("internal", "UpdateConversation database err:", err.Error())
continue
} else {
result = append(result, LocalConversation)
}
}
}
if result != nil {
// log.Info("internal", "getMultipleConversationModel success :", result)
c.ConversationListener().OnNewConversation(utils.StructToJsonString(result))
}
case constant.SyncConversation:
}
}
func (c *Conversation) doUpdateMessage(c2v common.Cmd2Value) {
node := c2v.Value.(common.UpdateMessageNode)
ctx := c2v.Ctx
switch node.Action {
case constant.UpdateMsgFaceUrlAndNickName:
args := node.Args.(common.UpdateMessageInfo)
switch args.SessionType {
case constant.SingleChatType:
if args.UserID == c.loginUserID {
conversationIDList, err := c.db.GetAllSingleConversationIDList(ctx)
if err != nil {
log.ZError(ctx, "GetAllSingleConversationIDList err", err)
return
} else {
log.ZDebug(ctx, "get single conversationID list", "conversationIDList", conversationIDList)
for _, conversationID := range conversationIDList {
err := c.db.UpdateMsgSenderFaceURLAndSenderNickname(ctx, conversationID, args.UserID, args.FaceURL, args.Nickname)
if err != nil {
log.ZError(ctx, "UpdateMsgSenderFaceURLAndSenderNickname err", err)
continue
}
}
}
} else {
conversationID := c.getConversationIDBySessionType(args.UserID, constant.SingleChatType)
err := c.db.UpdateMsgSenderFaceURLAndSenderNickname(ctx, conversationID, args.UserID, args.FaceURL, args.Nickname)
if err != nil {
log.ZError(ctx, "UpdateMsgSenderFaceURLAndSenderNickname err", err)
}
}
case constant.SuperGroupChatType:
conversationID := c.getConversationIDBySessionType(args.GroupID, constant.SuperGroupChatType)
err := c.db.UpdateMsgSenderFaceURLAndSenderNickname(ctx, conversationID, args.UserID, args.FaceURL, args.Nickname)
if err != nil {
log.ZError(ctx, "UpdateMsgSenderFaceURLAndSenderNickname err", err)
}
case constant.NotificationChatType:
conversationID := c.getConversationIDBySessionType(args.UserID, constant.NotificationChatType)
err := c.db.UpdateMsgSenderFaceURLAndSenderNickname(ctx, conversationID, args.UserID, args.FaceURL, args.Nickname)
if err != nil {
log.ZError(ctx, "UpdateMsgSenderFaceURLAndSenderNickname err", err)
}
default:
log.ZError(ctx, "not support sessionType", nil, "args", args)
return
}
}
}
// funcation (c *Conversation) doSyncReactionExtensions(c2v common.Cmd2Value) {
// if c.ConversationListener == nil {
// // log.Error("internal", "not set conversationListener")
// return
// }
// node := c2v.Value.(common.SyncReactionExtensionsNode)
// ctx := mcontext.NewCtx(node.OperationID)
// switch node.Action {
// case constant.SyncMessageListReactionExtensions:
// args := node.Args.(syncReactionExtensionParams)
// // log.Error(node.OperationID, "come SyncMessageListReactionExtensions", args)
// var reqList []server_api_params.OperateMessageListReactionExtensionsReq
// for _, v := range args.MessageList {
// var temp server_api_params.OperateMessageListReactionExtensionsReq
// temp.ClientMsgID = v.ClientMsgID
// temp.MsgFirstModifyTime = v.MsgFirstModifyTime
// reqList = append(reqList, temp)
// }
// var apiReq server_api_params.GetMessageListReactionExtensionsReq
// apiReq.SourceID = args.SourceID
// apiReq.TypeKeyList = args.TypeKeyList
// apiReq.SessionType = args.SessionType
// apiReq.MessageReactionKeyList = reqList
// apiReq.IsExternalExtensions = args.IsExternalExtension
// apiReq.OperationID = node.OperationID
// apiResp, err := util.CallApi[server_api_params.GetMessageListReactionExtensionsResp](ctx, constant.GetMessageListReactionExtensionsRouter, &apiReq)
// if err != nil {
// // log.NewError(node.OperationID, utils.GetSelfFuncName(), "getMessageListReactionExtensions err:", err.Error())
// return
// }
// // for _, result := range apiResp {
// // log.Warn(node.OperationID, "api return reslut is:", result.ClientMsgID, result.ReactionExtensionList)
// // }
// onLocal := funcation(data []*model_struct.LocalChatLogReactionExtensions) []*server_api_params.SingleMessageExtensionResult {
// var result []*server_api_params.SingleMessageExtensionResult
// for _, v := range data {
// temp := new(server_api_params.SingleMessageExtensionResult)
// tempMap := make(map[string]*sdkws.KeyValue)
// _ = json.Unmarshal(v.LocalReactionExtensions, &tempMap)
// if len(args.TypeKeyList) != 0 {
// for s, _ := range tempMap {
// if !utils.IsContain(s, args.TypeKeyList) {
// delete(tempMap, s)
// }
// }
// }
//
// temp.ReactionExtensionList = tempMap
// temp.ClientMsgID = v.ClientMsgID
// result = append(result, temp)
// }
// return result
// }(args.ExtendMessageList)
// var onServer []*server_api_params.SingleMessageExtensionResult
// for _, v := range *apiResp {
// if v.ErrCode == 0 {
// onServer = append(onServer, v)
// }
// }
// aInBNot, _, sameA, _ := common.CheckReactionExtensionsDiff(onServer, onLocal)
// for _, v := range aInBNot {
// // log.Error(node.OperationID, "come InsertMessageReactionExtension", args, v.ClientMsgID)
// if len(v.ReactionExtensionList) > 0 {
// temp := model_struct.LocalChatLogReactionExtensions{ClientMsgID: v.ClientMsgID, LocalReactionExtensions: []byte(utils.StructToJsonString(v.ReactionExtensionList))}
// err := c.db.InsertMessageReactionExtension(ctx, &temp)
// if err != nil {
// // log.Error(node.OperationID, "InsertMessageReactionExtension err:", err.Error())
// continue
// }
// }
// var changedKv []*sdkws.KeyValue
// for _, value := range v.ReactionExtensionList {
// changedKv = append(changedKv, value)
// }
// if len(changedKv) > 0 {
// c.msgListener.OnRecvMessageExtensionsChanged(v.ClientMsgID, utils.StructToJsonString(changedKv))
// }
// }
// // for _, result := range sameA {
// // log.ZWarn(ctx, "result", result.ReactionExtensionList, result.ClientMsgID)
// // }
// for _, v := range sameA {
// // log.Error(node.OperationID, "come sameA", v.ClientMsgID, v.ReactionExtensionList)
// tempMap := make(map[string]*sdkws.KeyValue)
// for _, extensions := range args.ExtendMessageList {
// if v.ClientMsgID == extensions.ClientMsgID {
// _ = json.Unmarshal(extensions.LocalReactionExtensions, &tempMap)
// break
// }
// }
// if len(v.ReactionExtensionList) == 0 {
// err := c.db.DeleteMessageReactionExtension(ctx, v.ClientMsgID)
// if err != nil {
// // log.Error(node.OperationID, "DeleteMessageReactionExtension err:", err.Error())
// continue
// }
// var deleteKeyList []string
// for key, _ := range tempMap {
// deleteKeyList = append(deleteKeyList, key)
// }
// if len(deleteKeyList) > 0 {
// c.msgListener.OnRecvMessageExtensionsDeleted(v.ClientMsgID, utils.StructToJsonString(deleteKeyList))
// }
// } else {
// deleteKeyList, changedKv := funcation(local, server map[string]*sdkws.KeyValue) ([]string, []*sdkws.KeyValue) {
// var deleteKeyList []string
// var changedKv []*sdkws.KeyValue
// for k, v := range local {
// ia, ok := server[k]
// if ok {
// //服务器不同的kv
// if ia.Value != v.Value {
// changedKv = append(changedKv, ia)
// }
// } else {
// //服务器已经没有kv
// deleteKeyList = append(deleteKeyList, k)
// }
// }
// //从服务器新增的kv
// for k, v := range server {
// _, ok := local[k]
// if !ok {
// changedKv = append(changedKv, v)
//
// }
// }
// return deleteKeyList, changedKv
// }(tempMap, v.ReactionExtensionList)
// extendMsg := model_struct.LocalChatLogReactionExtensions{ClientMsgID: v.ClientMsgID, LocalReactionExtensions: []byte(utils.StructToJsonString(v.ReactionExtensionList))}
// err = c.db.UpdateMessageReactionExtension(ctx, &extendMsg)
// if err != nil {
// // log.Error(node.OperationID, "UpdateMessageReactionExtension err:", err.Error())
// continue
// }
// if len(deleteKeyList) > 0 {
// c.msgListener.OnRecvMessageExtensionsDeleted(v.ClientMsgID, utils.StructToJsonString(deleteKeyList))
// }
// if len(changedKv) > 0 {
// c.msgListener.OnRecvMessageExtensionsChanged(v.ClientMsgID, utils.StructToJsonString(changedKv))
// }
// }
// //err := c.db.GetAndUpdateMessageReactionExtension(v.ClientMsgID, v.ReactionExtensionList)
// //if err != nil {
// // log.Error(node.OperationID, "GetAndUpdateMessageReactionExtension err:", err.Error())
// // continue
// //}
// //var changedKv []*server_api_params.KeyValue
// //for _, value := range v.ReactionExtensionList {
// // changedKv = append(changedKv, value)
// //}
// //if len(changedKv) > 0 {
// // c.msgListener.OnRecvMessageExtensionsChanged(v.ClientMsgID, utils.StructToJsonString(changedKv))
// //}
// }
// case constant.SyncMessageListTypeKeyInfo:
// messageList := node.Args.([]*sdk_struct.MsgStruct)
// var sourceID string
// var sessionType int32
// var reqList []server_api_params.OperateMessageListReactionExtensionsReq
// var temp server_api_params.OperateMessageListReactionExtensionsReq
// for _, v := range messageList {
// //todo syncMessage must sync
// message, err := c.db.GetMessage(ctx, "", v.ClientMsgID)
// if err != nil {
// // log.Error(node.OperationID, "GetMessageController err:", err.Error(), *v)
// continue
// }
// temp.ClientMsgID = message.ClientMsgID
// temp.MsgFirstModifyTime = message.MsgFirstModifyTime
// reqList = append(reqList, temp)
// switch message.SessionType {
// case constant.SingleChatType:
// sourceID = message.SendID + message.RecvID
// case constant.NotificationChatType:
// sourceID = message.RecvID
// case constant.GroupChatType, constant.SuperGroupChatType:
// sourceID = message.RecvID
// }
// sessionType = message.SessionType
// }
// var apiReq server_api_params.GetMessageListReactionExtensionsReq
// apiReq.SourceID = sourceID
// apiReq.SessionType = sessionType
// apiReq.MessageReactionKeyList = reqList
// apiReq.OperationID = node.OperationID
// //var apiResp server_api_params.GetMessageListReactionExtensionsResp
//
// apiResp, err := util.CallApi[server_api_params.GetMessageListReactionExtensionsResp](ctx, constant.GetMessageListReactionExtensionsRouter, &apiReq)
// if err != nil {
// // log.Error(node.OperationID, "GetMessageListReactionExtensions from server err:", err.Error(), apiReq)
// return
// }
// var messageChangedList []*messageKvList
// for _, v := range *apiResp {
// if v.ErrCode == 0 {
// var changedKv []*sdkws.KeyValue
// var prefixTypeKey []string
// extendMsg, _ := c.db.GetMessageReactionExtension(ctx, v.ClientMsgID)
// localKV := make(map[string]*sdkws.KeyValue)
// _ = json.Unmarshal(extendMsg.LocalReactionExtensions, &localKV)
// for typeKey, value := range v.ReactionExtensionList {
// oldValue, ok := localKV[typeKey]
// if ok {
// if !cmp.Equal(value, oldValue) {
// localKV[typeKey] = value
// prefixTypeKey = append(prefixTypeKey, getPrefixTypeKey(typeKey))
// changedKv = append(changedKv, value)
// }
// } else {
// localKV[typeKey] = value
// prefixTypeKey = append(prefixTypeKey, getPrefixTypeKey(typeKey))
// changedKv = append(changedKv, value)
//
// }
//
// }
// extendMsg.LocalReactionExtensions = []byte(utils.StructToJsonString(localKV))
// _ = c.db.UpdateMessageReactionExtension(ctx, extendMsg)
// if len(changedKv) > 0 {
// c.msgListener.OnRecvMessageExtensionsChanged(extendMsg.ClientMsgID, utils.StructToJsonString(changedKv))
// }
// prefixTypeKey = utils.RemoveRepeatedStringInList(prefixTypeKey)
// if len(prefixTypeKey) > 0 && c.msgKvListener != nil {
// var result []*sdk.SingleTypeKeyInfoSum
// oneMessageChanged := new(messageKvList)
// oneMessageChanged.ClientMsgID = extendMsg.ClientMsgID
// for _, v := range prefixTypeKey {
// singleResult := new(sdk.SingleTypeKeyInfoSum)
// singleResult.TypeKey = v
// for typeKey, value := range localKV {
// if strings.HasPrefix(typeKey, v) {
// singleTypeKeyInfo := new(sdk.SingleTypeKeyInfo)
// err := json.Unmarshal([]byte(value.Value), singleTypeKeyInfo)
// if err != nil {
// continue
// }
// if _, ok := singleTypeKeyInfo.InfoList[c.loginUserID]; ok {
// singleResult.IsContainSelf = true
// }
// for _, info := range singleTypeKeyInfo.InfoList {
// v := *info
// singleResult.InfoList = append(singleResult.InfoList, &v)
// }
// singleResult.Counter += singleTypeKeyInfo.Counter
// }
// }
// result = append(result, singleResult)
// }
// oneMessageChanged.ChangedKvList = result
// messageChangedList = append(messageChangedList, oneMessageChanged)
// }
// }
// }
// if len(messageChangedList) > 0 && c.msgKvListener != nil {
// c.msgKvListener.OnMessageKvInfoChanged(utils.StructToJsonString(messageChangedList))
// }
//
// }
//
// }
func (c *Conversation) DoConversationChangedNotification(ctx context.Context, msg *sdkws.MsgData) {
//var notification sdkws.ConversationChangedNotification
tips := &sdkws.ConversationUpdateTips{}
if err := utils.UnmarshalNotificationElem(msg.Content, tips); err != nil {
log.ZError(ctx, "UnmarshalNotificationElem err", err, "msg", msg)
return
}
c.SyncConversations(ctx, tips.ConversationIDList)
}
func (c *Conversation) DoConversationIsPrivateChangedNotification(ctx context.Context, msg *sdkws.MsgData) {
tips := &sdkws.ConversationSetPrivateTips{}
if err := utils.UnmarshalNotificationElem(msg.Content, tips); err != nil {
log.ZError(ctx, "UnmarshalNotificationElem err", err, "msg", msg)
return
}
c.SyncConversations(ctx, []string{tips.ConversationID})
}

@ -0,0 +1,57 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
pbConversation "github.com/openimsdk/protocol/conversation"
)
func ServerConversationToLocal(conversation *pbConversation.Conversation) *model_struct.LocalConversation {
return &model_struct.LocalConversation{
ConversationID: conversation.ConversationID,
ConversationType: conversation.ConversationType,
UserID: conversation.UserID,
GroupID: conversation.GroupID,
RecvMsgOpt: conversation.RecvMsgOpt,
GroupAtType: conversation.GroupAtType,
IsPinned: conversation.IsPinned,
BurnDuration: conversation.BurnDuration,
IsPrivateChat: conversation.IsPrivateChat,
AttachedInfo: conversation.AttachedInfo,
Ex: conversation.Ex,
MsgDestructTime: conversation.MsgDestructTime,
IsMsgDestruct: conversation.IsMsgDestruct,
}
}
func LocalConversationToServer(conversation *model_struct.LocalConversation) *pbConversation.Conversation {
return &pbConversation.Conversation{
ConversationID: conversation.ConversationID,
ConversationType: conversation.ConversationType,
UserID: conversation.UserID,
GroupID: conversation.GroupID,
RecvMsgOpt: conversation.RecvMsgOpt,
GroupAtType: conversation.GroupAtType,
IsPinned: conversation.IsPinned,
BurnDuration: conversation.BurnDuration,
IsPrivateChat: conversation.IsPrivateChat,
AttachedInfo: conversation.AttachedInfo,
MsgDestructTime: conversation.MsgDestructTime,
Ex: conversation.Ex,
IsMsgDestruct: conversation.IsMsgDestruct,
}
}

@ -0,0 +1,492 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"os"
"path/filepath"
"strings"
)
func (c *Conversation) CreateTextMessage(ctx context.Context, text string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Text)
if err != nil {
return nil, err
}
s.TextElem = &sdk_struct.TextElem{Content: text}
return &s, nil
}
func (c *Conversation) CreateAdvancedTextMessage(ctx context.Context, text string, messageEntities []*sdk_struct.MessageEntity) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.AdvancedText)
if err != nil {
return nil, err
}
s.AdvancedTextElem = &sdk_struct.AdvancedTextElem{
Text: text,
MessageEntityList: messageEntities,
}
return &s, nil
}
func (c *Conversation) CreateTextAtMessage(ctx context.Context, text string, userIDList []string, usersInfo []*sdk_struct.AtInfo, qs *sdk_struct.MsgStruct) (*sdk_struct.MsgStruct, error) {
if text == "" {
return nil, errors.New("text can not be empty")
}
if len(userIDList) > 10 {
return nil, sdkerrs.ErrArgs
}
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.AtText)
if err != nil {
return nil, err
}
//Avoid nested references
if qs != nil {
if qs.ContentType == constant.Quote {
qs.ContentType = constant.Text
qs.TextElem = &sdk_struct.TextElem{Content: qs.QuoteElem.Text}
}
}
s.AtTextElem = &sdk_struct.AtTextElem{
Text: text,
AtUserList: userIDList,
AtUsersInfo: usersInfo,
QuoteMessage: qs,
}
return &s, nil
}
func (c *Conversation) CreateLocationMessage(ctx context.Context, description string, longitude, latitude float64) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Location)
if err != nil {
return nil, err
}
s.LocationElem = &sdk_struct.LocationElem{
Description: description,
Longitude: longitude,
Latitude: latitude,
}
return &s, nil
}
func (c *Conversation) CreateCustomMessage(ctx context.Context, data, extension string, description string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Custom)
if err != nil {
return nil, err
}
s.CustomElem = &sdk_struct.CustomElem{
Data: data,
Extension: extension,
Description: description,
}
return &s, nil
}
func (c *Conversation) CreateQuoteMessage(ctx context.Context, text string, qs *sdk_struct.MsgStruct) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Quote)
if err != nil {
return nil, err
}
//Avoid nested references
if qs.ContentType == constant.Quote {
qs.ContentType = constant.Text
qs.TextElem = &sdk_struct.TextElem{Content: qs.QuoteElem.Text}
}
s.QuoteElem = &sdk_struct.QuoteElem{
Text: text,
QuoteMessage: qs,
}
return &s, nil
}
func (c *Conversation) CreateAdvancedQuoteMessage(ctx context.Context, text string, qs *sdk_struct.MsgStruct, messageEntities []*sdk_struct.MessageEntity) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Quote)
if err != nil {
return nil, err
}
//Avoid nested references
if qs.ContentType == constant.Quote {
//qs.Content = qs.QuoteElem.Text
qs.ContentType = constant.Text
qs.TextElem = &sdk_struct.TextElem{Content: qs.QuoteElem.Text}
}
s.QuoteElem = &sdk_struct.QuoteElem{
Text: text,
QuoteMessage: qs,
MessageEntityList: messageEntities,
}
return &s, nil
}
func (c *Conversation) CreateCardMessage(ctx context.Context, card *sdk_struct.CardElem) (*sdk_struct.MsgStruct,
error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Card)
if err != nil {
return nil, err
}
s.CardElem = card
return &s, nil
}
func (c *Conversation) CreateVideoMessageFromFullPath(ctx context.Context, videoFullPath string, videoType string,
duration int64, snapshotFullPath string) (*sdk_struct.MsgStruct, error) {
dstFile := utils.FileTmpPath(videoFullPath, c.DataDir) //a->b
written, err := utils.CopyFile(videoFullPath, dstFile)
if err != nil {
//log.Error("internal", "open file failed: ", err, videoFullPath)
return nil, err
}
log.ZDebug(ctx, "videoFullPath dstFile", "videoFullPath", videoFullPath,
"dstFile", dstFile, "written", written)
dstFile = utils.FileTmpPath(snapshotFullPath, c.DataDir) //a->b
sWritten, err := utils.CopyFile(snapshotFullPath, dstFile)
if err != nil {
//log.Error("internal", "open file failed: ", err, snapshotFullPath)
return nil, err
}
log.ZDebug(ctx, "snapshotFullPath dstFile", "snapshotFullPath", snapshotFullPath,
"dstFile", dstFile, "sWritten", sWritten)
s := sdk_struct.MsgStruct{}
err = c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Video)
if err != nil {
return nil, err
}
s.VideoElem = &sdk_struct.VideoElem{
VideoPath: videoFullPath,
VideoType: videoType,
Duration: duration,
}
if snapshotFullPath == "" {
s.VideoElem.SnapshotPath = ""
} else {
s.VideoElem.SnapshotPath = snapshotFullPath
}
fi, err := os.Stat(s.VideoElem.VideoPath)
if err != nil {
//log.Error("internal", "get file Attributes error", err.Error())
return nil, err
}
s.VideoElem.VideoSize = fi.Size()
if snapshotFullPath != "" {
imageInfo, err := getImageInfo(s.VideoElem.SnapshotPath)
if err != nil {
log.ZError(ctx, "getImageInfo err:", err, "snapshotFullPath", snapshotFullPath)
return nil, err
}
s.VideoElem.SnapshotHeight = imageInfo.Height
s.VideoElem.SnapshotWidth = imageInfo.Width
s.VideoElem.SnapshotSize = imageInfo.Size
}
return &s, nil
}
func (c *Conversation) CreateFileMessageFromFullPath(ctx context.Context, fileFullPath string, fileName string) (*sdk_struct.MsgStruct, error) {
dstFile := utils.FileTmpPath(fileFullPath, c.DataDir)
_, err := utils.CopyFile(fileFullPath, dstFile)
if err != nil {
//log.Error("internal", "open file failed: ", err.Error(), fileFullPath)
return nil, err
}
s := sdk_struct.MsgStruct{}
err = c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.File)
if err != nil {
return nil, err
}
fi, err := os.Stat(fileFullPath)
if err != nil {
//log.Error("internal", "get file Attributes error", err.Error())
return nil, err
}
s.FileElem = &sdk_struct.FileElem{
FilePath: fileFullPath,
FileName: fileName,
FileSize: fi.Size(),
}
return &s, nil
}
func (c *Conversation) CreateImageMessageFromFullPath(ctx context.Context, imageFullPath string) (*sdk_struct.MsgStruct, error) {
dstFile := utils.FileTmpPath(imageFullPath, c.DataDir) //a->b
_, err := utils.CopyFile(imageFullPath, dstFile)
if err != nil {
//log.Error(operationID, "open file failed: ", err, imageFullPath)
return nil, err
}
s := sdk_struct.MsgStruct{}
err = c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Picture)
if err != nil {
return nil, err
}
imageInfo, err := getImageInfo(imageFullPath)
if err != nil {
//log.Error(operationID, "getImageInfo err:", err.Error())
return nil, err
}
s.PictureElem = &sdk_struct.PictureElem{
SourcePath: imageFullPath,
SourcePicture: &sdk_struct.PictureBaseInfo{
Width: imageInfo.Width,
Height: imageInfo.Height,
Type: imageInfo.Type,
},
}
return &s, nil
}
func (c *Conversation) CreateSoundMessageFromFullPath(ctx context.Context, soundPath string, duration int64) (*sdk_struct.MsgStruct, error) {
dstFile := utils.FileTmpPath(soundPath, c.DataDir) //a->b
_, err := utils.CopyFile(soundPath, dstFile)
if err != nil {
//log.Error("internal", "open file failed: ", err, soundPath)
return nil, err
}
s := sdk_struct.MsgStruct{}
err = c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Sound)
if err != nil {
return nil, err
}
fi, err := os.Stat(soundPath)
if err != nil {
//log.Error("internal", "getSoundInfo err:", err.Error(), s.SoundElem.SoundPath)
return nil, err
}
s.SoundElem = &sdk_struct.SoundElem{
SoundPath: soundPath,
Duration: duration,
DataSize: fi.Size(),
SoundType: strings.Replace(filepath.Ext(fi.Name()), ".", "", 1),
}
return &s, nil
}
func (c *Conversation) CreateImageMessage(ctx context.Context, imagePath string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Picture)
if err != nil {
return nil, err
}
path := c.DataDir + imagePath
//path := imagePath
imageInfo, err := getImageInfo(path)
if err != nil {
//log.Error("internal", "get imageInfo err", err.Error())
return nil, err
}
s.PictureElem = &sdk_struct.PictureElem{
SourcePath: path,
SourcePicture: &sdk_struct.PictureBaseInfo{
Width: imageInfo.Width,
Height: imageInfo.Height,
Type: imageInfo.Type,
},
}
return &s, nil
}
func (c *Conversation) CreateImageMessageByURL(ctx context.Context, sourcePath string, sourcePicture, bigPicture, snapshotPicture sdk_struct.PictureBaseInfo) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Picture)
if err != nil {
return nil, err
}
s.PictureElem = &sdk_struct.PictureElem{
SourcePath: sourcePath,
SourcePicture: &sourcePicture,
BigPicture: &bigPicture,
SnapshotPicture: &snapshotPicture,
}
return &s, nil
}
func (c *Conversation) CreateSoundMessageByURL(ctx context.Context, soundElem *sdk_struct.SoundBaseInfo) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Sound)
if err != nil {
return nil, err
}
s.SoundElem = &sdk_struct.SoundElem{
UUID: soundElem.UUID,
SoundPath: soundElem.SoundPath,
SourceURL: soundElem.SourceURL,
DataSize: soundElem.DataSize,
Duration: soundElem.Duration,
SoundType: soundElem.SoundType,
}
return &s, nil
}
func (c *Conversation) CreateSoundMessage(ctx context.Context, soundPath string, duration int64) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Sound)
if err != nil {
return nil, err
}
path := c.DataDir + soundPath
fi, err := os.Stat(path)
if err != nil {
//log.Error("internal", "get sound info err", err.Error())
return nil, err
}
s.SoundElem = &sdk_struct.SoundElem{
SoundPath: path,
Duration: duration,
DataSize: fi.Size(),
}
if typ := strings.Replace(filepath.Ext(fi.Name()), ".", "", 1); typ != "" {
s.SoundElem.SoundType = "audio/" + strings.ToLower(typ)
}
return &s, nil
}
func (c *Conversation) CreateVideoMessageByURL(ctx context.Context, videoElem sdk_struct.VideoBaseInfo) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Video)
if err != nil {
return nil, err
}
s.VideoElem = &sdk_struct.VideoElem{
VideoPath: videoElem.VideoPath,
VideoUUID: videoElem.VideoUUID,
VideoURL: videoElem.VideoURL,
VideoType: videoElem.VideoType,
VideoSize: videoElem.VideoSize,
Duration: videoElem.Duration,
SnapshotPath: videoElem.SnapshotPath,
SnapshotUUID: videoElem.SnapshotUUID,
SnapshotSize: videoElem.SnapshotSize,
SnapshotURL: videoElem.SnapshotURL,
SnapshotWidth: videoElem.SnapshotWidth,
SnapshotHeight: videoElem.SnapshotHeight,
SnapshotType: videoElem.SnapshotType,
}
return &s, nil
}
func (c *Conversation) CreateVideoMessage(ctx context.Context, videoPath string, videoType string, duration int64, snapshotPath string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Video)
if err != nil {
return nil, err
}
s.VideoElem = &sdk_struct.VideoElem{}
s.VideoElem.VideoPath = c.DataDir + videoPath
s.VideoElem.VideoType = videoType
s.VideoElem.Duration = duration
if snapshotPath == "" {
s.VideoElem.SnapshotPath = ""
} else {
s.VideoElem.SnapshotPath = c.DataDir + snapshotPath
}
fi, err := os.Stat(s.VideoElem.VideoPath)
if err != nil {
log.ZDebug(ctx, "get video file error", "videoPath", videoPath, "snapshotPath", snapshotPath)
return nil, err
}
s.VideoElem.VideoSize = fi.Size()
if snapshotPath != "" {
imageInfo, err := getImageInfo(s.VideoElem.SnapshotPath)
if err != nil {
//log.Error("internal", "get snapshot info ", err.Error())
return nil, err
}
s.VideoElem.SnapshotHeight = imageInfo.Height
s.VideoElem.SnapshotWidth = imageInfo.Width
s.VideoElem.SnapshotSize = imageInfo.Size
}
return &s, nil
}
func (c *Conversation) CreateFileMessageByURL(ctx context.Context, fileElem sdk_struct.FileBaseInfo) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.File)
if err != nil {
return nil, err
}
s.FileElem = &sdk_struct.FileElem{
FilePath: fileElem.FilePath,
UUID: fileElem.UUID,
SourceURL: fileElem.SourceURL,
FileName: fileElem.FileName,
FileSize: fileElem.FileSize,
FileType: fileElem.FileType,
}
return &s, nil
}
func (c *Conversation) CreateFileMessage(ctx context.Context, filePath string, fileName string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{FileElem: &sdk_struct.FileElem{}}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.File)
if err != nil {
return nil, err
}
s.FileElem.FilePath = c.DataDir + filePath
s.FileElem.FileName = fileName
fi, err := os.Stat(s.FileElem.FilePath)
if err != nil {
//log.Error("internal", "get file message err", err.Error())
return nil, err
}
s.FileElem.FileSize = fi.Size()
s.Content = utils.StructToJsonString(s.FileElem)
return &s, nil
}
func (c *Conversation) CreateMergerMessage(ctx context.Context, messages []*sdk_struct.MsgStruct, title string, summaries []string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{MergeElem: &sdk_struct.MergeElem{}}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Merger)
if err != nil {
return nil, err
}
s.MergeElem.AbstractList = summaries
s.MergeElem.Title = title
s.MergeElem.MultiMessage = messages
s.Content = utils.StructToJsonString(s.MergeElem)
return &s, nil
}
func (c *Conversation) CreateFaceMessage(ctx context.Context, index int, data string) (*sdk_struct.MsgStruct, error) {
s := sdk_struct.MsgStruct{FaceElem: &sdk_struct.FaceElem{}}
err := c.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Face)
if err != nil {
return nil, err
}
s.FaceElem.Data = data
s.FaceElem.Index = index
s.Content = utils.StructToJsonString(s.FaceElem)
return &s, nil
}
func (c *Conversation) CreateForwardMessage(ctx context.Context, s *sdk_struct.MsgStruct) (*sdk_struct.MsgStruct, error) {
if s.Status != constant.MsgStatusSendSuccess {
log.ZError(ctx, "only send success message can be Forward",
errors.New("only send success message can be Forward"))
return nil, errors.New("only send success message can be Forward")
}
err := c.initBasicInfo(ctx, s, constant.UserMsgType, s.ContentType)
if err != nil {
return nil, err
}
//Forward message seq is set to 0
s.Seq = 0
s.Status = constant.MsgStatusSendSuccess
return s, nil
}

@ -0,0 +1,245 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/jinzhu/copier"
pbMsg "github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
// Delete the local and server
// Delete the local, do not change the server data
// To delete the server, you need to change the local message status to delete
func (c *Conversation) clearConversationFromLocalAndSvr(ctx context.Context, conversationID string, f func(ctx context.Context, conversationID string) error) error {
_, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
// Use conversationID to remove conversations and messages from the server first
err = c.clearConversationMsgFromSvr(ctx, conversationID)
if err != nil {
return err
}
if err := c.clearConversationAndDeleteAllMsg(ctx, conversationID, false, f); err != nil {
return err
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{conversationID}}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
return nil
}
func (c *Conversation) clearConversationAndDeleteAllMsg(ctx context.Context, conversationID string, markDelete bool, f func(ctx context.Context, conversationID string) error) error {
err := c.getConversationMaxSeqAndSetHasRead(ctx, conversationID)
if err != nil {
return err
}
if markDelete {
err = c.db.MarkDeleteConversationAllMessages(ctx, conversationID)
} else {
err = c.db.DeleteConversationAllMessages(ctx, conversationID)
}
if err != nil {
return err
}
log.ZDebug(ctx, "reset conversation", "conversationID", conversationID)
err = f(ctx, conversationID)
if err != nil {
return err
}
return nil
}
// To delete session information, delete the server first, and then invoke the interface.
// The client receives a callback to delete all local information.
func (c *Conversation) clearConversationMsgFromSvr(ctx context.Context, conversationID string) error {
var apiReq pbMsg.ClearConversationsMsgReq
apiReq.UserID = c.loginUserID
apiReq.ConversationIDs = []string{conversationID}
return util.ApiPost(ctx, constant.ClearConversationMsgRouter, &apiReq, nil)
}
// Delete all messages
func (c *Conversation) deleteAllMsgFromLocalAndSvr(ctx context.Context) error {
// Delete the server first (high error rate), then delete it.
err := c.deleteAllMessageFromSvr(ctx)
if err != nil {
return err
}
err = c.deleteAllMsgFromLocal(ctx, false)
if err != nil {
return err
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
return nil
}
// Delete all server messages
func (c *Conversation) deleteAllMessageFromSvr(ctx context.Context) error {
var apiReq pbMsg.UserClearAllMsgReq
apiReq.UserID = c.loginUserID
err := util.ApiPost(ctx, constant.ClearAllMsgRouter, &apiReq, nil)
if err != nil {
return err
}
return nil
}
// Delete all messages from the local
func (c *Conversation) deleteAllMsgFromLocal(ctx context.Context, markDelete bool) error {
conversations, err := c.db.GetAllConversationListDB(ctx)
if err != nil {
return err
}
var successCids []string
log.ZDebug(ctx, "deleteAllMsgFromLocal", "conversations", conversations, "markDelete", markDelete)
for _, v := range conversations {
if err := c.clearConversationAndDeleteAllMsg(ctx, v.ConversationID, markDelete, c.db.ClearConversation); err != nil {
log.ZError(ctx, "clearConversation err", err, "conversationID", v.ConversationID)
continue
}
successCids = append(successCids, v.ConversationID)
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: successCids}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
return nil
}
// Delete a message from the local
func (c *Conversation) deleteMessage(ctx context.Context, conversationID string, clientMsgID string) error {
if err := c.deleteMessageFromSvr(ctx, conversationID, clientMsgID); err != nil {
return err
}
return c.deleteMessageFromLocal(ctx, conversationID, clientMsgID)
}
// The user deletes part of the message from the server
func (c *Conversation) deleteMessageFromSvr(ctx context.Context, conversationID string, clientMsgID string) error {
_, err := c.db.GetMessage(ctx, conversationID, clientMsgID)
if err != nil {
return err
}
localMessage, err := c.db.GetMessage(ctx, conversationID, clientMsgID)
if err != nil {
return err
}
if localMessage.Status == constant.MsgStatusSendFailed {
log.ZInfo(ctx, "delete msg status is send failed, do not need delete", "msg", localMessage)
return nil
}
if localMessage.Seq == 0 {
log.ZInfo(ctx, "delete msg seq is 0, try again", "msg", localMessage)
return sdkerrs.ErrMsgHasNoSeq
}
var apiReq pbMsg.DeleteMsgsReq
apiReq.UserID = c.loginUserID
apiReq.Seqs = []int64{localMessage.Seq}
apiReq.ConversationID = conversationID
return util.ApiPost(ctx, constant.DeleteMsgsRouter, &apiReq, nil)
}
// Delete messages from local
func (c *Conversation) deleteMessageFromLocal(ctx context.Context, conversationID string, clientMsgID string) error {
s, err := c.db.GetMessage(ctx, conversationID, clientMsgID)
if err != nil {
return err
}
if err := c.db.DeleteConversationMsgs(ctx, conversationID, []string{clientMsgID}); err != nil {
return err
}
if !s.IsRead && s.SendID != c.loginUserID {
if err := c.db.DecrConversationUnreadCount(ctx, conversationID, 1); err != nil {
return err
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: conversationID, Action: constant.ConChange, Args: []string{conversationID}}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
}
conversation, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
var latestMsg sdk_struct.MsgStruct
utils.JsonStringToStruct(conversation.LatestMsg, &latestMsg)
if latestMsg.ClientMsgID == clientMsgID {
log.ZDebug(ctx, "latesetMsg deleted", "seq", latestMsg.Seq, "clientMsgID", latestMsg.ClientMsgID)
msgs, err := c.db.GetMessageListNoTime(ctx, conversationID, 1, false)
if err != nil {
return err
}
latestMsgSendTime := latestMsg.SendTime
latestMsgStr := ""
if len(msgs) > 0 {
copier.Copy(&latestMsg, msgs[0])
err := c.msgConvert(&latestMsg)
if err != nil {
log.ZError(ctx, "parsing data error", err, latestMsg)
}
latestMsgStr = utils.StructToJsonString(latestMsg)
latestMsgSendTime = latestMsg.SendTime
}
if err := c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"latest_msg": latestMsgStr, "latest_msg_send_time": latestMsgSendTime}); err != nil {
return err
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{conversationID}}})
}
c.msgListener().OnMsgDeleted(utils.StructToJsonString(s))
return nil
}
func (c *Conversation) doDeleteMsgs(ctx context.Context, msg *sdkws.MsgData) {
tips := sdkws.DeleteMsgsTips{}
utils.UnmarshalNotificationElem(msg.Content, &tips)
log.ZDebug(ctx, "doDeleteMsgs", "seqs", tips.Seqs)
for _, v := range tips.Seqs {
msg, err := c.db.GetMessageBySeq(ctx, tips.ConversationID, v)
if err != nil {
log.ZError(ctx, "GetMessageBySeq err", err, "conversationID", tips.ConversationID, "seq", v)
continue
}
var s sdk_struct.MsgStruct
copier.Copy(&s, msg)
err = c.msgConvert(&s)
if err != nil {
log.ZError(ctx, "parsing data error", err, "msg", msg)
}
if err := c.deleteMessageFromLocal(ctx, tips.ConversationID, msg.ClientMsgID); err != nil {
log.ZError(ctx, "deleteMessageFromLocal err", err, "conversationID", tips.ConversationID, "seq", v)
}
}
}
func (c *Conversation) doClearConversations(ctx context.Context, msg *sdkws.MsgData) {
tips := sdkws.ClearConversationTips{}
utils.UnmarshalNotificationElem(msg.Content, &tips)
log.ZDebug(ctx, "doClearConversations", "tips", tips)
for _, v := range tips.ConversationIDs {
if err := c.clearConversationAndDeleteAllMsg(ctx, v, false, c.db.ClearConversation); err != nil {
log.ZError(ctx, "clearConversation err", err, "conversationID", v)
}
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: tips.ConversationIDs}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
}

@ -0,0 +1,217 @@
package conversation_msg
import (
"context"
"encoding/json"
"github.com/jinzhu/copier"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"github.com/patrickmn/go-cache"
"time"
)
const (
_ int = iota
stateCodeSuccess
stateCodeEnd
)
const (
inputStatesSendTime = time.Second * 10 // input status sending interval time
inputStatesTimeout = inputStatesSendTime + inputStatesSendTime/2 // input status timeout
inputStatesMsgTimeout = inputStatesSendTime / 2 // message sending timeout
)
func newTyping(c *Conversation) *typing {
e := &typing{
conv: c,
send: cache.New(inputStatesSendTime, inputStatesTimeout),
state: cache.New(inputStatesTimeout, inputStatesTimeout),
}
e.platformIDs = make([]int32, 0, len(constant.PlatformID2Name))
e.platformIDSet = make(map[int32]struct{})
for id := range constant.PlatformID2Name {
e.platformIDSet[int32(id)] = struct{}{}
e.platformIDs = append(e.platformIDs, int32(id))
}
datautil.Sort(e.platformIDs, true)
e.state.OnEvicted(func(key string, val interface{}) {
var data inputStatesKey
if err := json.Unmarshal([]byte(key), &data); err != nil {
return
}
e.changes(data.ConversationID, data.UserID)
})
return e
}
type typing struct {
send *cache.Cache
state *cache.Cache
conv *Conversation
platformIDs []int32
platformIDSet map[int32]struct{}
}
func (e *typing) ChangeInputStates(ctx context.Context, conversationID string, focus bool) error {
if conversationID == "" {
return errs.ErrArgs.WrapMsg("conversationID can't be empty")
}
conversation, err := e.conv.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
key := conversation.ConversationID
if focus {
if val, ok := e.send.Get(key); ok {
if val.(int) == stateCodeSuccess {
log.ZDebug(ctx, "typing stateCodeSuccess", "conversationID", conversationID, "focus", focus)
return nil
}
}
e.send.SetDefault(key, stateCodeSuccess)
} else {
if val, ok := e.send.Get(key); ok {
if val.(int) == stateCodeEnd {
log.ZDebug(ctx, "typing stateCodeEnd", "conversationID", conversationID, "focus", focus)
return nil
}
e.send.SetDefault(key, stateCodeEnd)
} else {
log.ZDebug(ctx, "typing send not found", "conversationID", conversationID, "focus", focus)
return nil
}
}
ctx, cancel := context.WithTimeout(ctx, inputStatesMsgTimeout)
defer cancel()
if err := e.sendMsg(ctx, conversation, focus); err != nil {
e.send.Delete(key)
return err
}
return nil
}
func (e *typing) sendMsg(ctx context.Context, conversation *model_struct.LocalConversation, focus bool) error {
s := sdk_struct.MsgStruct{}
err := e.conv.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Typing)
if err != nil {
return err
}
s.RecvID = conversation.UserID
s.GroupID = conversation.GroupID
s.SessionType = conversation.ConversationType
var typingElem sdk_struct.TypingElem
if focus {
typingElem.MsgTips = "yes"
} else {
typingElem.MsgTips = "no"
}
s.Content = utils.StructToJsonString(typingElem)
options := make(map[string]bool, 6)
utils.SetSwitchFromOptions(options, constant.IsHistory, false)
utils.SetSwitchFromOptions(options, constant.IsPersistent, false)
utils.SetSwitchFromOptions(options, constant.IsSenderSync, false)
utils.SetSwitchFromOptions(options, constant.IsConversationUpdate, false)
utils.SetSwitchFromOptions(options, constant.IsSenderConversationUpdate, false)
utils.SetSwitchFromOptions(options, constant.IsUnreadCount, false)
utils.SetSwitchFromOptions(options, constant.IsOfflinePush, false)
var wsMsgData sdkws.MsgData
copier.Copy(&wsMsgData, s)
wsMsgData.Content = []byte(s.Content)
wsMsgData.CreateTime = s.CreateTime
wsMsgData.Options = options
var sendMsgResp sdkws.UserSendMsgResp
err = e.conv.LongConnMgr.SendReqWaitResp(ctx, &wsMsgData, constant.SendMsg, &sendMsgResp)
if err != nil {
log.ZError(ctx, "typing msg to server failed", err, "message", s)
return err
}
return nil
}
type inputStatesKey struct {
ConversationID string `json:"cid,omitempty"`
UserID string `json:"uid,omitempty"`
PlatformID int32 `json:"pid,omitempty"`
}
func (e *typing) getStateKey(conversationID string, userID string, platformID int32) string {
data, err := json.Marshal(inputStatesKey{ConversationID: conversationID, UserID: userID, PlatformID: platformID})
if err != nil {
panic(err)
}
return string(data)
}
func (e *typing) onNewMsg(ctx context.Context, msg *sdkws.MsgData) {
var enteringElem sdk_struct.TypingElem
if err := json.Unmarshal(msg.Content, &enteringElem); err != nil {
log.ZError(ctx, "typing onNewMsg Unmarshal failed", err, "message", msg)
return
}
if msg.SendID == e.conv.loginUserID {
return
}
if _, ok := e.platformIDSet[msg.SenderPlatformID]; !ok {
return
}
now := time.Now().UnixMilli()
expirationTimestamp := msg.SendTime + int64(inputStatesSendTime/time.Millisecond)
if msg.SendTime > now || expirationTimestamp <= now {
return
}
var sourceID string
if msg.GroupID == "" {
sourceID = msg.SendID
} else {
sourceID = msg.GroupID
}
conversationID := e.conv.getConversationIDBySessionType(sourceID, int(msg.SessionType))
key := e.getStateKey(conversationID, msg.SendID, msg.SenderPlatformID)
if enteringElem.MsgTips == "yes" {
d := time.Duration(expirationTimestamp-now) * time.Millisecond
if v, t, ok := e.state.GetWithExpiration(key); ok {
if t.UnixMilli() >= expirationTimestamp {
return
}
e.state.Set(key, v, d)
} else {
e.state.Set(key, struct{}{}, d)
e.changes(conversationID, msg.SendID)
}
} else {
if _, ok := e.state.Get(key); ok {
e.state.Delete(key)
}
}
}
type InputStatesChangedData struct {
ConversationID string `json:"conversationID"`
UserID string `json:"userID"`
PlatformIDs []int32 `json:"platformIDs"`
}
func (e *typing) changes(conversationID string, userID string) {
data := InputStatesChangedData{ConversationID: conversationID, UserID: userID, PlatformIDs: e.GetInputStates(conversationID, userID)}
e.conv.ConversationListener().OnConversationUserInputStatusChanged(utils.StructToJsonString(data))
}
func (e *typing) GetInputStates(conversationID string, userID string) []int32 {
platformIDs := make([]int32, 0, 1)
for _, platformID := range e.platformIDs {
key := e.getStateKey(conversationID, userID, platformID)
if _, ok := e.state.Get(key); ok {
platformIDs = append(platformIDs, platformID)
}
}
return platformIDs
}

@ -0,0 +1,32 @@
package conversation_msg
import (
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/errs"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
)
func getImageInfo(filePath string) (*sdk_struct.ImageInfo, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, errs.WrapMsg(err, "image file open err")
}
defer file.Close()
info, err := file.Stat()
if err != nil {
return nil, err
}
img, format, err := image.Decode(file)
if err != nil {
return nil, errs.WrapMsg(err, "image file decode err")
}
size := img.Bounds().Max
return &sdk_struct.ImageInfo{Width: int32(size.X), Height: int32(size.Y), Type: "image/" + format, Size: info.Size()}, nil
}

@ -0,0 +1,52 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import "sync"
type MaxSeqRecorder struct {
seqs map[string]int64
lock sync.RWMutex
}
func NewMaxSeqRecorder() MaxSeqRecorder {
m := make(map[string]int64)
return MaxSeqRecorder{seqs: m}
}
func (m *MaxSeqRecorder) Get(conversationID string) int64 {
m.lock.RLock()
defer m.lock.RUnlock()
return m.seqs[conversationID]
}
func (m *MaxSeqRecorder) Set(conversationID string, seq int64) {
m.lock.Lock()
defer m.lock.Unlock()
m.seqs[conversationID] = seq
}
func (m *MaxSeqRecorder) Incr(conversationID string, num int64) {
m.lock.Lock()
defer m.lock.Unlock()
m.seqs[conversationID] += num
}
func (m *MaxSeqRecorder) IsNewMsg(conversationID string, seq int64) bool {
m.lock.RLock()
defer m.lock.RUnlock()
currentSeq := m.seqs[conversationID]
return seq > currentSeq
}

@ -0,0 +1,347 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
sdk "github.com/openimsdk/openim-sdk-core/v3/pkg/sdk_params_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/protocol/sdkws"
)
// 检测其内部连续性,如果不连续,则向前补齐,获取这一组消息的最大最小seq,以及需要补齐的seq列表长度
func (c *Conversation) messageBlocksInternalContinuityCheck(ctx context.Context, conversationID string, notStartTime, isReverse bool, count int,
startTime int64, list *[]*model_struct.LocalChatLog, messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) (max, min int64, length int) {
var lostSeqListLength int
maxSeq, minSeq, haveSeqList := c.getMaxAndMinHaveSeqList(*list)
log.ZDebug(ctx, "getMaxAndMinHaveSeqList is:", "maxSeq", maxSeq, "minSeq", minSeq, "haveSeqList", haveSeqList)
if maxSeq != 0 && minSeq != 0 {
successiveSeqList := func(max, min int64) (seqList []int64) {
for i := min; i <= max; i++ {
seqList = append(seqList, i)
}
return seqList
}(maxSeq, minSeq)
lostSeqList := utils.DifferenceSubset(successiveSeqList, haveSeqList)
lostSeqListLength = len(lostSeqList)
log.ZDebug(ctx, "get lost seqList is :", "maxSeq", maxSeq, "minSeq", minSeq, "lostSeqList", lostSeqList, "length:", lostSeqListLength)
if lostSeqListLength > 0 {
var pullSeqList []int64
if lostSeqListLength <= constant.PullMsgNumForReadDiffusion {
pullSeqList = lostSeqList
} else {
pullSeqList = lostSeqList[lostSeqListLength-constant.PullMsgNumForReadDiffusion : lostSeqListLength]
}
c.pullMessageAndReGetHistoryMessages(ctx, conversationID, pullSeqList, notStartTime, isReverse, count, startTime, list, messageListCallback)
}
}
return maxSeq, minSeq, lostSeqListLength
}
// 检测消息块之间的连续性,如果不连续,则向前补齐,返回块之间是否连续,bool
func (c *Conversation) messageBlocksBetweenContinuityCheck(ctx context.Context, lastMinSeq, maxSeq int64, conversationID string,
notStartTime, isReverse bool, count int, startTime int64, list *[]*model_struct.LocalChatLog, messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) bool {
if lastMinSeq != 0 {
log.ZDebug(ctx, "get lost LastMinSeq is :", "lastMinSeq", lastMinSeq, "thisMaxSeq", maxSeq)
if maxSeq != 0 {
if maxSeq+1 != lastMinSeq {
startSeq := int64(lastMinSeq) - constant.PullMsgNumForReadDiffusion
if startSeq <= maxSeq {
startSeq = int64(maxSeq) + 1
}
successiveSeqList := func(max, min int64) (seqList []int64) {
for i := min; i <= max; i++ {
seqList = append(seqList, i)
}
return seqList
}(lastMinSeq-1, startSeq)
log.ZDebug(ctx, "get lost successiveSeqList is :", "successiveSeqList", successiveSeqList, "length:", len(successiveSeqList))
if len(successiveSeqList) > 0 {
c.pullMessageAndReGetHistoryMessages(ctx, conversationID, successiveSeqList, notStartTime, isReverse, count, startTime, list, messageListCallback)
}
} else {
return true
}
} else {
return true
}
} else {
return true
}
return false
}
// 根据最小seq向前补齐消息,由服务器告诉拉取消息结果是否到底,如果网络,则向前补齐,获取这一组消息的最大最小seq,以及需要补齐的seq列表长度
func (c *Conversation) messageBlocksEndContinuityCheck(ctx context.Context, minSeq int64, conversationID string, notStartTime,
isReverse bool, count int, startTime int64, list *[]*model_struct.LocalChatLog, messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) {
if minSeq != 0 {
seqList := func(seq int64) (seqList []int64) {
startSeq := seq - constant.PullMsgNumForReadDiffusion
if startSeq <= 0 {
startSeq = 1
}
log.ZDebug(ctx, "pull start is ", "start seq", startSeq)
for i := startSeq; i < seq; i++ {
seqList = append(seqList, i)
}
return seqList
}(minSeq)
log.ZDebug(ctx, "pull seqList is ", "seqList", seqList, "len", len(seqList))
if len(seqList) > 0 {
c.pullMessageAndReGetHistoryMessages(ctx, conversationID, seqList, notStartTime, isReverse, count, startTime, list, messageListCallback)
}
} else {
//local don't have messages,本地无消息,但是服务器最大消息不为0
seqList := []int64{0, 0}
c.pullMessageAndReGetHistoryMessages(ctx, conversationID, seqList, notStartTime, isReverse, count, startTime, list, messageListCallback)
}
}
func (c *Conversation) getMaxAndMinHaveSeqList(messages []*model_struct.LocalChatLog) (max, min int64, seqList []int64) {
for i := 0; i < len(messages); i++ {
if messages[i].Seq != 0 {
seqList = append(seqList, messages[i].Seq)
}
if messages[i].Seq != 0 && min == 0 && max == 0 {
min = messages[i].Seq
max = messages[i].Seq
}
if messages[i].Seq < min && messages[i].Seq != 0 {
min = messages[i].Seq
}
if messages[i].Seq > max {
max = messages[i].Seq
}
}
return max, min, seqList
}
// 1、保证单次拉取消息量低于sdk单次从服务器拉取量
// 2、块中连续性检测
// 3、块之间连续性检测
func (c *Conversation) pullMessageAndReGetHistoryMessages(ctx context.Context, conversationID string, seqList []int64,
notStartTime, isReverse bool, count int, startTime int64, list *[]*model_struct.LocalChatLog,
messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) {
existedSeqList, err := c.db.GetAlreadyExistSeqList(ctx, conversationID, seqList)
if err != nil {
log.ZError(ctx, "GetAlreadyExistSeqList err", err, "conversationID", conversationID,
"seqList", seqList)
return
}
if len(existedSeqList) == len(seqList) {
log.ZDebug(ctx, "do not pull message", "seqList", seqList, "existedSeqList", existedSeqList)
return
}
newSeqList := utils.DifferenceSubset(seqList, existedSeqList)
if len(newSeqList) == 0 {
log.ZDebug(ctx, "do not pull message", "seqList", seqList, "existedSeqList", existedSeqList,
"newSeqList", newSeqList)
return
}
var pullMsgResp sdkws.PullMessageBySeqsResp
var pullMsgReq sdkws.PullMessageBySeqsReq
pullMsgReq.UserID = c.loginUserID
var seqRange sdkws.SeqRange
seqRange.ConversationID = conversationID
seqRange.Begin = newSeqList[0]
seqRange.End = newSeqList[len(newSeqList)-1]
seqRange.Num = int64(len(newSeqList))
pullMsgReq.SeqRanges = append(pullMsgReq.SeqRanges, &seqRange)
log.ZDebug(ctx, "conversation pull message, ", "req", pullMsgReq)
if notStartTime && !c.LongConnMgr.IsConnected() {
return
}
err = c.SendReqWaitResp(ctx, &pullMsgReq, constant.PullMsgBySeqList, &pullMsgResp)
if err != nil {
errHandle(newSeqList, list, err, messageListCallback)
log.ZDebug(ctx, "pullmsg SendReqWaitResp failed", err, "req")
} else {
log.ZDebug(ctx, "syncMsgFromServerSplit pull msg", "resp", pullMsgResp)
if pullMsgResp.Msgs == nil {
log.ZWarn(ctx, "syncMsgFromServerSplit pull msg is null", errors.New("pull message is null"),
"req", pullMsgReq)
return
}
if v, ok := pullMsgResp.Msgs[conversationID]; ok {
c.pullMessageIntoTable(ctx, pullMsgResp.Msgs, conversationID)
messageListCallback.IsEnd = v.IsEnd
if notStartTime {
*list, err = c.db.GetMessageListNoTime(ctx, conversationID, count, isReverse)
} else {
*list, err = c.db.GetMessageList(ctx, conversationID, count, startTime, isReverse)
}
}
}
}
func errHandle(seqList []int64, list *[]*model_struct.LocalChatLog, err error, messageListCallback *sdk.GetAdvancedHistoryMessageListCallback) {
messageListCallback.ErrCode = 100
messageListCallback.ErrMsg = err.Error()
var result []*model_struct.LocalChatLog
needPullMaxSeq := seqList[len(seqList)-1]
for _, chatLog := range *list {
if chatLog.Seq == 0 || chatLog.Seq > needPullMaxSeq {
temp := chatLog
result = append(result, temp)
} else {
if chatLog.Seq <= needPullMaxSeq {
break
}
}
}
*list = result
}
func (c *Conversation) pullMessageIntoTable(ctx context.Context, pullMsgData map[string]*sdkws.PullMsgs, conversationID string) {
insertMsg := make(map[string][]*model_struct.LocalChatLog, 20)
updateMsg := make(map[string][]*model_struct.LocalChatLog, 30)
var insertMessage, selfInsertMessage, othersInsertMessage []*model_struct.LocalChatLog
var updateMessage []*model_struct.LocalChatLog
var exceptionMsg []*model_struct.LocalErrChatLog
log.ZDebug(ctx, "do Msg come here, len: ", "msg length", len(pullMsgData))
for conversationID, msgs := range pullMsgData {
for _, v := range msgs.Msgs {
log.ZDebug(ctx, "msg detail", "msg", v, "conversationID", conversationID)
msg := c.msgDataToLocalChatLog(v)
//When the message has been marked and deleted by the cloud, it is directly inserted locally without any conversation and message update.
if msg.Status == constant.MsgStatusHasDeleted {
insertMessage = append(insertMessage, msg)
continue
}
msg.Status = constant.MsgStatusSendSuccess
// log.Info(operationID, "new msg, seq, ServerMsgID, ClientMsgID", msg.Seq, msg.ServerMsgID, msg.ClientMsgID)
//De-analyze data
if msg.ClientMsgID == "" {
exceptionMsg = append(exceptionMsg, c.msgDataToLocalErrChatLog(msg))
continue
}
if v.SendID == c.loginUserID { //seq
// Messages sent by myself //if sent through this terminal
m, err := c.db.GetMessage(ctx, conversationID, msg.ClientMsgID)
if err == nil {
log.ZInfo(ctx, "have message", "msg", msg)
if m.Seq == 0 {
updateMessage = append(updateMessage, msg)
} else {
exceptionMsg = append(exceptionMsg, c.msgDataToLocalErrChatLog(msg))
}
} else { // send through other terminal
log.ZInfo(ctx, "sync message", "msg", msg)
selfInsertMessage = append(selfInsertMessage, msg)
}
} else { //Sent by others
if oldMessage, err := c.db.GetMessage(ctx, conversationID, msg.ClientMsgID); err != nil { //Deduplication operation
othersInsertMessage = append(othersInsertMessage, msg)
} else {
if oldMessage.Seq == 0 {
updateMessage = append(updateMessage, msg)
}
}
}
insertMsg[conversationID] = append(insertMessage, c.faceURLAndNicknameHandle(ctx, selfInsertMessage, othersInsertMessage, conversationID)...)
updateMsg[conversationID] = updateMessage
}
//update message
if err6 := c.messageController.BatchUpdateMessageList(ctx, updateMsg); err6 != nil {
log.ZError(ctx, "sync seq normal message err :", err6)
}
b3 := utils.GetCurrentTimestampByMill()
//Normal message storage
_ = c.messageController.BatchInsertMessageList(ctx, insertMsg)
b4 := utils.GetCurrentTimestampByMill()
log.ZDebug(ctx, "BatchInsertMessageListController, ", "cost time", b4-b3)
//Exception message storage
for _, v := range exceptionMsg {
log.ZWarn(ctx, "exceptionMsg show: ", nil, "msg", *v)
}
}
}
// 拉取的消息都需要经过块内部连续性检测以及块和上一块之间的连续性检测不连续则补,补齐的过程中如果出现任何异常只给seq从大到小到断层
// 拉取消息不满量,获取服务器中该群最大seq以及用户对于此群最小seq,本地该群的最小seq,如果本地的不为0并且小于等于服务器最小的,说明已经到底部
// 如果本地的为0,可以理解为初始化的时候,数据还未同步,或者异常情况,如果服务器最大seq-服务器最小seq>=0说明还未到底部,否则到底部
func (c *Conversation) faceURLAndNicknameHandle(ctx context.Context, self, others []*model_struct.LocalChatLog, conversationID string) []*model_struct.LocalChatLog {
lc, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return append(self, others...)
}
switch lc.ConversationType {
case constant.SingleChatType:
c.singleHandle(ctx, self, others, lc)
case constant.SuperGroupChatType:
c.groupHandle(ctx, self, others, lc)
}
return append(self, others...)
}
func (c *Conversation) singleHandle(ctx context.Context, self, others []*model_struct.LocalChatLog, lc *model_struct.LocalConversation) {
userInfo, err := c.db.GetLoginUser(ctx, c.loginUserID)
if err == nil {
for _, chatLog := range self {
chatLog.SenderFaceURL = userInfo.FaceURL
chatLog.SenderNickname = userInfo.Nickname
}
}
if lc.FaceURL != "" && lc.ShowName != "" {
for _, chatLog := range others {
chatLog.SenderFaceURL = lc.FaceURL
chatLog.SenderNickname = lc.ShowName
}
}
}
func (c *Conversation) groupHandle(ctx context.Context, self, others []*model_struct.LocalChatLog, lc *model_struct.LocalConversation) {
allMessage := append(self, others...)
localGroupMemberInfo, err := c.group.GetSpecifiedGroupMembersInfo(ctx, lc.GroupID, datautil.Slice(allMessage, func(e *model_struct.LocalChatLog) string {
return e.SendID
}))
if err != nil {
log.ZError(ctx, "get group member info err", err)
return
}
groupMap := datautil.SliceToMap(localGroupMemberInfo, func(e *model_struct.LocalGroupMember) string {
return e.UserID
})
for _, chatLog := range allMessage {
if g, ok := groupMap[chatLog.SendID]; ok {
if g.FaceURL != "" && g.Nickname != "" {
chatLog.SenderFaceURL = g.FaceURL
chatLog.SenderNickname = g.Nickname
}
}
}
}

@ -0,0 +1,124 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"encoding/json"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
type MessageController struct {
db db_interface.DataBase
ch chan common.Cmd2Value
}
func NewMessageController(db db_interface.DataBase, ch chan common.Cmd2Value) *MessageController {
return &MessageController{db: db, ch: ch}
}
func (m *MessageController) BatchUpdateMessageList(ctx context.Context, updateMsg map[string][]*model_struct.LocalChatLog) error {
if updateMsg == nil {
return nil
}
for conversationID, messages := range updateMsg {
conversation, err := m.db.GetConversation(ctx, conversationID)
if err != nil {
log.ZError(ctx, "GetConversation err", err, "conversationID", conversationID)
continue
}
latestMsg := &sdk_struct.MsgStruct{}
if err := json.Unmarshal([]byte(conversation.LatestMsg), latestMsg); err != nil {
log.ZError(ctx, "Unmarshal err", err, "conversationID",
conversationID, "latestMsg", conversation.LatestMsg)
continue
}
for _, v := range messages {
v1 := new(model_struct.LocalChatLog)
v1.ClientMsgID = v.ClientMsgID
v1.Seq = v.Seq
v1.Status = v.Status
v1.RecvID = v.RecvID
v1.SessionType = v.SessionType
v1.ServerMsgID = v.ServerMsgID
v1.SendTime = v.SendTime
err := m.db.UpdateMessage(ctx, conversationID, v1)
if err != nil {
return utils.Wrap(err, "BatchUpdateMessageList failed")
}
if latestMsg.ClientMsgID == v.ClientMsgID {
latestMsg.ServerMsgID = v.ServerMsgID
latestMsg.Seq = v.Seq
latestMsg.SendTime = v.SendTime
conversation.LatestMsg = utils.StructToJsonString(latestMsg)
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversation.ConversationID,
Action: constant.AddConOrUpLatMsg, Args: *conversation}, m.ch)
}
}
}
return nil
}
func (m *MessageController) BatchInsertMessageList(ctx context.Context, insertMsg map[string][]*model_struct.LocalChatLog) error {
if insertMsg == nil {
return nil
}
for conversationID, messages := range insertMsg {
if len(messages) == 0 {
continue
}
err := m.db.BatchInsertMessageList(ctx, conversationID, messages)
if err != nil {
log.ZError(ctx, "insert GetMessage detail err:", err, "conversationID", conversationID, "messages", messages)
for _, v := range messages {
e := m.db.InsertMessage(ctx, conversationID, v)
if e != nil {
log.ZError(ctx, "InsertMessage err", err, "conversationID", conversationID, "message", v)
}
}
}
}
return nil
}
func (c *Conversation) PullMessageBySeqs(ctx context.Context, seqs []*sdkws.SeqRange) (*sdkws.PullMessageBySeqsResp, error) {
return util.CallApi[sdkws.PullMessageBySeqsResp](ctx, constant.PullUserMsgBySeqRouter, sdkws.PullMessageBySeqsReq{UserID: c.loginUserID, SeqRanges: seqs})
}
func (m *MessageController) SearchMessageByContentTypeAndKeyword(ctx context.Context, contentType []int, keywordList []string,
keywordListMatchType int, startTime, endTime int64) (result []*model_struct.LocalChatLog, err error) {
var list []*model_struct.LocalChatLog
conversationIDList, err := m.db.GetAllConversationIDList(ctx)
for _, v := range conversationIDList {
sList, err := m.db.SearchMessageByContentTypeAndKeyword(ctx, contentType, v, keywordList, keywordListMatchType, startTime, endTime)
if err != nil {
// TODO: log.Error(operationID, "search message in group err", err.Error(), v)
continue
}
list = append(list, sList...)
}
return list, nil
}

@ -0,0 +1,102 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"encoding/json"
"github.com/openimsdk/openim-sdk-core/v3/internal/file"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/log"
)
func NewUploadFileCallback(ctx context.Context, progress func(progress int), msg *sdk_struct.MsgStruct, conversationID string, db db_interface.DataBase) file.UploadFileCallback {
if msg.AttachedInfoElem == nil {
msg.AttachedInfoElem = &sdk_struct.AttachedInfoElem{}
}
if msg.AttachedInfoElem.Progress == nil {
msg.AttachedInfoElem.Progress = &sdk_struct.UploadProgress{}
}
return &msgUploadFileCallback{ctx: ctx, progress: progress, msg: msg, db: db, conversationID: conversationID}
}
type msgUploadFileCallback struct {
ctx context.Context
db db_interface.DataBase
msg *sdk_struct.MsgStruct
conversationID string
value int
progress func(progress int)
}
func (c *msgUploadFileCallback) Open(size int64) {
}
func (c *msgUploadFileCallback) PartSize(partSize int64, num int) {
}
func (c *msgUploadFileCallback) HashPartProgress(index int, size int64, partHash string) {
}
func (c *msgUploadFileCallback) HashPartComplete(partsHash string, fileHash string) {
}
func (c *msgUploadFileCallback) UploadID(uploadID string) {
c.msg.AttachedInfoElem.Progress.UploadID = uploadID
data, err := json.Marshal(c.msg.AttachedInfoElem)
if err != nil {
panic(err)
}
if err := c.db.UpdateColumnsMessage(c.ctx, c.conversationID, c.msg.ClientMsgID, map[string]any{"attached_info": string(data)}); err != nil {
log.ZError(c.ctx, "update PutProgress message attached info failed", err)
}
}
func (c *msgUploadFileCallback) UploadPartComplete(index int, partSize int64, partHash string) {
}
func (c *msgUploadFileCallback) UploadComplete(fileSize int64, streamSize int64, storageSize int64) {
c.msg.AttachedInfoElem.Progress.Save = storageSize
c.msg.AttachedInfoElem.Progress.Current = streamSize
c.msg.AttachedInfoElem.Progress.Total = fileSize
data, err := json.Marshal(c.msg.AttachedInfoElem)
if err != nil {
panic(err)
}
if err := c.db.UpdateColumnsMessage(c.ctx, c.conversationID, c.msg.ClientMsgID, map[string]any{"attached_info": string(data)}); err != nil {
log.ZError(c.ctx, "update PutProgress message attached info failed", err)
}
value := int(float64(streamSize) / float64(fileSize) * 100)
if c.value < value {
c.value = value
c.progress(value)
}
}
func (c *msgUploadFileCallback) Complete(size int64, url string, typ int) {
if c.value != 100 {
c.progress(100)
}
c.msg.AttachedInfoElem.Progress = nil
data, err := json.Marshal(c.msg.AttachedInfoElem)
if err != nil {
panic(err)
}
if err := c.db.UpdateColumnsMessage(c.ctx, c.conversationID, c.msg.ClientMsgID, map[string]any{"attached_info": string(data)}); err != nil {
log.ZError(c.ctx, "update PutComplete message attached info failed", err)
}
}

@ -0,0 +1,281 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"encoding/json"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/utils/datautil"
pbMsg "github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (c *Conversation) markMsgAsRead2Svr(ctx context.Context, conversationID string, seqs []int64) error {
req := &pbMsg.MarkMsgsAsReadReq{UserID: c.loginUserID, ConversationID: conversationID, Seqs: seqs}
return util.ApiPost(ctx, constant.MarkMsgsAsReadRouter, req, nil)
}
func (c *Conversation) markConversationAsReadSvr(ctx context.Context, conversationID string, hasReadSeq int64, seqs []int64) error {
req := &pbMsg.MarkConversationAsReadReq{UserID: c.loginUserID, ConversationID: conversationID, HasReadSeq: hasReadSeq, Seqs: seqs}
return util.ApiPost(ctx, constant.MarkConversationAsRead, req, nil)
}
func (c *Conversation) setConversationHasReadSeq(ctx context.Context, conversationID string, hasReadSeq int64) error {
req := &pbMsg.SetConversationHasReadSeqReq{UserID: c.loginUserID, ConversationID: conversationID, HasReadSeq: hasReadSeq}
return util.ApiPost(ctx, constant.SetConversationHasReadSeq, req, nil)
}
func (c *Conversation) getConversationMaxSeqAndSetHasRead(ctx context.Context, conversationID string) error {
maxSeq, err := c.db.GetConversationNormalMsgSeq(ctx, conversationID)
if err != nil {
return err
}
if maxSeq == 0 {
return nil
}
if err := c.setConversationHasReadSeq(ctx, conversationID, maxSeq); err != nil {
return err
}
if err := c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"has_read_seq": maxSeq}); err != nil {
return err
}
return nil
}
// mark a conversation's all message as read
func (c *Conversation) markConversationMessageAsRead(ctx context.Context, conversationID string) error {
conversation, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
if conversation.UnreadCount == 0 {
return sdkerrs.ErrUnreadCount
}
// get the maximum sequence number of messages in the table that are not sent by oneself
peerUserMaxSeq, err := c.db.GetConversationPeerNormalMsgSeq(ctx, conversationID)
if err != nil {
return err
}
// get the maximum sequence number of messages in the table
maxSeq, err := c.db.GetConversationNormalMsgSeq(ctx, conversationID)
if err != nil {
return err
}
switch conversation.ConversationType {
case constant.SingleChatType:
msgs, err := c.db.GetUnreadMessage(ctx, conversationID)
if err != nil {
return err
}
log.ZDebug(ctx, "get unread message", "msgs", len(msgs))
msgIDs, seqs := c.getAsReadMsgMapAndList(ctx, msgs)
if len(seqs) == 0 {
log.ZWarn(ctx, "seqs is empty", nil, "conversationID", conversationID)
return nil
}
log.ZDebug(ctx, "markConversationMessageAsRead", "conversationID", conversationID, "seqs",
seqs, "peerUserMaxSeq", peerUserMaxSeq, "maxSeq", maxSeq)
if err := c.markConversationAsReadSvr(ctx, conversationID, maxSeq, seqs); err != nil {
return err
}
_, err = c.db.MarkConversationMessageAsReadDB(ctx, conversationID, msgIDs)
if err != nil {
log.ZWarn(ctx, "MarkConversationMessageAsRead err", err, "conversationID", conversationID, "msgIDs", msgIDs)
}
case constant.SuperGroupChatType, constant.NotificationChatType:
log.ZDebug(ctx, "markConversationMessageAsRead", "conversationID", conversationID, "peerUserMaxSeq", peerUserMaxSeq, "maxSeq", maxSeq)
if err := c.markConversationAsReadSvr(ctx, conversationID, maxSeq, nil); err != nil {
return err
}
}
if err := c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"unread_count": 0}); err != nil {
log.ZError(ctx, "UpdateColumnsConversation err", err, "conversationID", conversationID)
}
log.ZDebug(ctx, "update columns sucess")
c.unreadChangeTrigger(ctx, conversationID, peerUserMaxSeq == maxSeq)
return nil
}
// mark a conversation's message as read by seqs
func (c *Conversation) markMessagesAsReadByMsgID(ctx context.Context, conversationID string, msgIDs []string) error {
_, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
msgs, err := c.db.GetMessagesByClientMsgIDs(ctx, conversationID, msgIDs)
if err != nil {
return err
}
if len(msgs) == 0 {
return nil
}
var hasReadSeq = msgs[0].Seq
maxSeq, err := c.db.GetConversationNormalMsgSeq(ctx, conversationID)
if err != nil {
return err
}
markAsReadMsgIDs, seqs := c.getAsReadMsgMapAndList(ctx, msgs)
log.ZDebug(ctx, "msgs len", "markAsReadMsgIDs", len(markAsReadMsgIDs), "seqs", seqs)
if len(seqs) == 0 {
log.ZWarn(ctx, "seqs is empty", nil, "conversationID", conversationID)
return nil
}
if err := c.markMsgAsRead2Svr(ctx, conversationID, seqs); err != nil {
return err
}
decrCount, err := c.db.MarkConversationMessageAsReadDB(ctx, conversationID, markAsReadMsgIDs)
if err != nil {
return err
}
if err := c.db.DecrConversationUnreadCount(ctx, conversationID, decrCount); err != nil {
log.ZError(ctx, "decrConversationUnreadCount err", err, "conversationID", conversationID,
"decrCount", decrCount)
}
c.unreadChangeTrigger(ctx, conversationID, hasReadSeq == maxSeq && msgs[0].SendID != c.loginUserID)
return nil
}
func (c *Conversation) getAsReadMsgMapAndList(ctx context.Context,
msgs []*model_struct.LocalChatLog) (asReadMsgIDs []string, seqs []int64) {
for _, msg := range msgs {
if !msg.IsRead && msg.SendID != c.loginUserID {
if msg.Seq == 0 {
log.ZWarn(ctx, "exception seq", errors.New("exception message "), "msg", msg)
} else {
asReadMsgIDs = append(asReadMsgIDs, msg.ClientMsgID)
seqs = append(seqs, msg.Seq)
}
} else {
log.ZWarn(ctx, "msg can't marked as read", nil, "msg", msg)
}
}
return
}
func (c *Conversation) unreadChangeTrigger(ctx context.Context, conversationID string, latestMsgIsRead bool) {
if latestMsgIsRead {
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: conversationID,
Action: constant.UpdateLatestMessageChange, Args: []string{conversationID}}, Ctx: ctx})
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: conversationID,
Action: constant.ConChange, Args: []string{conversationID}}, Ctx: ctx})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged},
Ctx: ctx})
}
func (c *Conversation) doUnreadCount(ctx context.Context, conversation *model_struct.LocalConversation, hasReadSeq int64, seqs []int64) {
if conversation.ConversationType == constant.SingleChatType {
if len(seqs) != 0 {
_, err := c.db.MarkConversationMessageAsReadBySeqs(ctx, conversation.ConversationID, seqs)
if err != nil {
log.ZWarn(ctx, "MarkConversationMessageAsReadBySeqs err", err, "conversationID", conversation.ConversationID, "seqs", seqs)
}
} else {
log.ZWarn(ctx, "seqs is empty", nil, "conversationID", conversation.ConversationID, "hasReadSeq", hasReadSeq)
}
if hasReadSeq > conversation.HasReadSeq {
decrUnreadCount := hasReadSeq - conversation.HasReadSeq
if err := c.db.DecrConversationUnreadCount(ctx, conversation.ConversationID, decrUnreadCount); err != nil {
log.ZError(ctx, "DecrConversationUnreadCount err", err, "conversationID", conversation.ConversationID, "decrUnreadCount", decrUnreadCount)
}
if err := c.db.UpdateColumnsConversation(ctx, conversation.ConversationID, map[string]interface{}{"has_read_seq": hasReadSeq}); err != nil {
log.ZError(ctx, "UpdateColumnsConversation err", err, "conversationID", conversation.ConversationID)
}
}
latestMsg := &sdk_struct.MsgStruct{}
if err := json.Unmarshal([]byte(conversation.LatestMsg), latestMsg); err != nil {
log.ZError(ctx, "Unmarshal err", err, "conversationID", conversation.ConversationID, "latestMsg", conversation.LatestMsg)
}
if (!latestMsg.IsRead) && datautil.Contain(latestMsg.Seq, seqs...) {
latestMsg.IsRead = true
conversation.LatestMsg = utils.StructToJsonString(&latestMsg)
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversation.ConversationID, Action: constant.AddConOrUpLatMsg, Args: *conversation}, c.GetCh())
}
} else {
if err := c.db.UpdateColumnsConversation(ctx, conversation.ConversationID, map[string]interface{}{"unread_count": 0}); err != nil {
log.ZError(ctx, "UpdateColumnsConversation err", err, "conversationID", conversation.ConversationID)
}
}
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: conversation.ConversationID, Action: constant.ConChange, Args: []string{conversation.ConversationID}}})
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}})
}
func (c *Conversation) doReadDrawing(ctx context.Context, msg *sdkws.MsgData) {
tips := &sdkws.MarkAsReadTips{}
err := utils.UnmarshalNotificationElem(msg.Content, tips)
if err != nil {
log.ZWarn(ctx, "UnmarshalNotificationElem err", err, "msg", msg)
return
}
log.ZDebug(ctx, "do readDrawing", "tips", tips)
conversation, err := c.db.GetConversation(ctx, tips.ConversationID)
if err != nil {
log.ZError(ctx, "GetConversation err", err, "conversationID", tips.ConversationID)
return
}
if tips.MarkAsReadUserID != c.loginUserID {
if len(tips.Seqs) == 0 {
return
}
messages, err := c.db.GetMessagesBySeqs(ctx, tips.ConversationID, tips.Seqs)
if err != nil {
log.ZError(ctx, "GetMessagesBySeqs err", err, "conversationID", tips.ConversationID, "seqs", tips.Seqs)
return
}
if conversation.ConversationType == constant.SingleChatType {
latestMsg := &sdk_struct.MsgStruct{}
if err := json.Unmarshal([]byte(conversation.LatestMsg), latestMsg); err != nil {
log.ZError(ctx, "Unmarshal err", err, "conversationID", tips.ConversationID, "latestMsg", conversation.LatestMsg)
}
var successMsgIDs []string
for _, message := range messages {
attachInfo := sdk_struct.AttachedInfoElem{}
_ = utils.JsonStringToStruct(message.AttachedInfo, &attachInfo)
attachInfo.HasReadTime = msg.SendTime
message.AttachedInfo = utils.StructToJsonString(attachInfo)
message.IsRead = true
if err = c.db.UpdateMessage(ctx, tips.ConversationID, message); err != nil {
log.ZError(ctx, "UpdateMessage err", err, "conversationID", tips.ConversationID, "message", message)
} else {
if latestMsg.ClientMsgID == message.ClientMsgID {
latestMsg.IsRead = message.IsRead
conversation.LatestMsg = utils.StructToJsonString(latestMsg)
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{ConID: conversation.ConversationID, Action: constant.AddConOrUpLatMsg, Args: *conversation}, c.GetCh())
}
successMsgIDs = append(successMsgIDs, message.ClientMsgID)
}
}
var messageReceiptResp = []*sdk_struct.MessageReceipt{{UserID: tips.MarkAsReadUserID, MsgIDList: successMsgIDs,
SessionType: conversation.ConversationType, ReadTime: msg.SendTime}}
c.msgListener().OnRecvC2CReadReceipt(utils.StructToJsonString(messageReceiptResp))
}
} else {
c.doUnreadCount(ctx, conversation, tips.HasReadSeq, tips.Seqs)
}
}

@ -0,0 +1,203 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/tools/utils/timeutil"
"github.com/jinzhu/copier"
pbMsg "github.com/openimsdk/protocol/msg"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (c *Conversation) doRevokeMsg(ctx context.Context, msg *sdkws.MsgData) {
var tips sdkws.RevokeMsgTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
log.ZError(ctx, "unmarshal failed", err, "msg", msg)
return
}
log.ZDebug(ctx, "do revokeMessage", "tips", &tips)
c.revokeMessage(ctx, &tips)
}
func (c *Conversation) revokeMessage(ctx context.Context, tips *sdkws.RevokeMsgTips) {
revokedMsg, err := c.db.GetMessageBySeq(ctx, tips.ConversationID, tips.Seq)
if err != nil {
log.ZError(ctx, "GetMessageBySeq failed", err, "tips", &tips)
return
}
var revokerRole int32
var revokerNickname string
if tips.IsAdminRevoke || tips.SesstionType == constant.SingleChatType {
_, userName, err := c.getUserNameAndFaceURL(ctx, tips.RevokerUserID)
if err != nil {
log.ZError(ctx, "GetUserNameAndFaceURL failed", err, "tips", &tips)
} else {
log.ZDebug(ctx, "revoker user name", "userName", userName)
}
revokerNickname = userName
} else if tips.SesstionType == constant.SuperGroupChatType {
conversation, err := c.db.GetConversation(ctx, tips.ConversationID)
if err != nil {
log.ZError(ctx, "GetConversation failed", err, "conversationID", tips.ConversationID)
return
}
groupMember, err := c.db.GetGroupMemberInfoByGroupIDUserID(ctx, conversation.GroupID, tips.RevokerUserID)
if err != nil {
log.ZError(ctx, "GetGroupMemberInfoByGroupIDUserID failed", err, "tips", &tips)
} else {
log.ZDebug(ctx, "revoker member name", "groupMember", groupMember)
revokerRole = groupMember.RoleLevel
revokerNickname = groupMember.Nickname
}
}
m := sdk_struct.MessageRevoked{
RevokerID: tips.RevokerUserID,
RevokerRole: revokerRole,
ClientMsgID: revokedMsg.ClientMsgID,
RevokerNickname: revokerNickname,
RevokeTime: tips.RevokeTime,
SourceMessageSendTime: revokedMsg.SendTime,
SourceMessageSendID: revokedMsg.SendID,
SourceMessageSenderNickname: revokedMsg.SenderNickname,
SessionType: tips.SesstionType,
Seq: tips.Seq,
Ex: revokedMsg.Ex,
IsAdminRevoke: tips.IsAdminRevoke,
}
// log.ZDebug(ctx, "callback revokeMessage", "m", m)
var n sdk_struct.NotificationElem
n.Detail = utils.StructToJsonString(m)
if err := c.db.UpdateMessageBySeq(ctx, tips.ConversationID, &model_struct.LocalChatLog{Seq: tips.Seq,
Content: utils.StructToJsonString(n), ContentType: constant.RevokeNotification}); err != nil {
log.ZError(ctx, "UpdateMessageBySeq failed", err, "tips", &tips)
return
}
conversation, err := c.db.GetConversation(ctx, tips.ConversationID)
if err != nil {
log.ZError(ctx, "GetConversation failed", err, "tips", &tips)
return
}
var latestMsg sdk_struct.MsgStruct
utils.JsonStringToStruct(conversation.LatestMsg, &latestMsg)
log.ZDebug(ctx, "latestMsg", "latestMsg", &latestMsg, "seq", tips.Seq)
if latestMsg.Seq <= tips.Seq {
var newLatesetMsg sdk_struct.MsgStruct
msgs, err := c.db.GetMessageListNoTime(ctx, tips.ConversationID, 1, false)
if err != nil || len(msgs) == 0 {
log.ZError(ctx, "GetMessageListNoTime failed", err, "tips", &tips)
return
}
log.ZDebug(ctx, "latestMsg is revoked", "seq", tips.Seq, "msg", msgs[0])
copier.Copy(&newLatesetMsg, msgs[0])
err = c.msgConvert(&newLatesetMsg)
if err != nil {
log.ZError(ctx, "parsing data error", err, latestMsg)
} else {
log.ZDebug(ctx, "revoke update conversatoin", "msg", utils.StructToJsonString(newLatesetMsg))
if err := c.db.UpdateColumnsConversation(ctx, tips.ConversationID, map[string]interface{}{"latest_msg": utils.StructToJsonString(newLatesetMsg),
"latest_msg_send_time": newLatesetMsg.SendTime}); err != nil {
log.ZError(ctx, "UpdateColumnsConversation failed", err, "newLatesetMsg", newLatesetMsg)
} else {
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{Action: constant.ConChange, Args: []string{tips.ConversationID}}})
}
}
}
c.msgListener().OnNewRecvMessageRevoked(utils.StructToJsonString(m))
msgList, err := c.db.SearchAllMessageByContentType(ctx, conversation.ConversationID, constant.Quote)
if err != nil {
log.ZError(ctx, "SearchAllMessageByContentType failed", err, "tips", &tips)
return
}
for _, v := range msgList {
c.quoteMsgRevokeHandle(ctx, tips.ConversationID, v, m)
}
}
func (c *Conversation) quoteMsgRevokeHandle(ctx context.Context, conversationID string, v *model_struct.LocalChatLog, revokedMsg sdk_struct.MessageRevoked) {
s := sdk_struct.MsgStruct{}
_ = utils.JsonStringToStruct(v.Content, &s.QuoteElem)
if s.QuoteElem.QuoteMessage == nil {
return
}
if s.QuoteElem.QuoteMessage.ClientMsgID != revokedMsg.ClientMsgID {
return
}
s.QuoteElem.QuoteMessage.Content = utils.StructToJsonString(revokedMsg)
s.QuoteElem.QuoteMessage.ContentType = constant.RevokeNotification
v.Content = utils.StructToJsonString(s.QuoteElem)
if err := c.db.UpdateMessageBySeq(ctx, conversationID, v); err != nil {
log.ZError(ctx, "UpdateMessage failed", err, "v", v)
}
}
func (c *Conversation) revokeOneMessage(ctx context.Context, conversationID, clientMsgID string) error {
conversation, err := c.db.GetConversation(ctx, conversationID)
if err != nil {
return err
}
message, err := c.db.GetMessage(ctx, conversationID, clientMsgID)
if err != nil {
return err
}
if message.Status != constant.MsgStatusSendSuccess {
return errors.New("only send success message can be revoked")
}
switch conversation.ConversationType {
case constant.SingleChatType:
if message.SendID != c.loginUserID {
return errors.New("only send by yourself message can be revoked")
}
case constant.SuperGroupChatType:
if message.SendID != c.loginUserID {
groupAdmins, err := c.db.GetGroupMemberOwnerAndAdminDB(ctx, conversation.GroupID)
if err != nil {
return err
}
var isAdmin bool
for _, member := range groupAdmins {
if member.UserID == c.loginUserID {
isAdmin = true
break
}
}
if !isAdmin {
return errors.New("only group admin can revoke message")
}
}
}
if err := util.ApiPost(ctx, constant.RevokeMsgRouter, pbMsg.RevokeMsgReq{ConversationID: conversationID, Seq: message.Seq, UserID: c.loginUserID}, nil); err != nil {
return err
}
c.revokeMessage(ctx, &sdkws.RevokeMsgTips{
ConversationID: conversationID,
Seq: message.Seq,
RevokerUserID: c.loginUserID,
RevokeTime: timeutil.GetCurrentTimestampBySecond(),
SesstionType: conversation.ConversationType,
ClientMsgID: clientMsgID,
})
return nil
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,148 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package conversation_msg
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/syncer"
"github.com/openimsdk/tools/utils/datautil"
"time"
"github.com/openimsdk/tools/log"
)
func (c *Conversation) SyncConversationsAndTriggerCallback(ctx context.Context, conversationsOnServer []*model_struct.LocalConversation) error {
conversationsOnLocal, err := c.db.GetAllConversations(ctx)
if err != nil {
return err
}
if err := c.batchAddFaceURLAndName(ctx, conversationsOnServer...); err != nil {
return err
}
if err = c.conversationSyncer.Sync(ctx, conversationsOnServer, conversationsOnLocal, func(ctx context.Context, state int, server, local *model_struct.LocalConversation) error {
if state == syncer.Update || state == syncer.Insert {
c.doUpdateConversation(common.Cmd2Value{Value: common.UpdateConNode{ConID: server.ConversationID, Action: constant.ConChange, Args: []string{server.ConversationID}}})
}
return nil
}, true); err != nil {
return err
}
return nil
}
func (c *Conversation) SyncConversations(ctx context.Context, conversationIDs []string) error {
conversationsOnServer, err := c.getServerConversationsByIDs(ctx, conversationIDs)
if err != nil {
return err
}
return c.SyncConversationsAndTriggerCallback(ctx, conversationsOnServer)
}
func (c *Conversation) SyncAllConversations(ctx context.Context) error {
ccTime := time.Now()
conversationsOnServer, err := c.getServerConversationList(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "get server cost time", "cost time", time.Since(ccTime), "conversation on server", conversationsOnServer)
return c.SyncConversationsAndTriggerCallback(ctx, conversationsOnServer)
}
func (c *Conversation) SyncAllConversationHashReadSeqs(ctx context.Context) error {
log.ZDebug(ctx, "start SyncConversationHashReadSeqs")
seqs, err := c.getServerHasReadAndMaxSeqs(ctx)
if err != nil {
return err
}
if len(seqs) == 0 {
return nil
}
var conversationChangedIDs []string
var conversationIDsNeedSync []string
conversationsOnLocal, err := c.db.GetAllConversations(ctx)
if err != nil {
log.ZWarn(ctx, "get all conversations err", err)
return err
}
conversationsOnLocalMap := datautil.SliceToMap(conversationsOnLocal, func(e *model_struct.LocalConversation) string {
return e.ConversationID
})
for conversationID, v := range seqs {
var unreadCount int32
c.maxSeqRecorder.Set(conversationID, v.MaxSeq)
if v.MaxSeq-v.HasReadSeq < 0 {
unreadCount = 0
log.ZWarn(ctx, "unread count is less than 0", nil, "conversationID",
conversationID, "maxSeq", v.MaxSeq, "hasReadSeq", v.HasReadSeq)
} else {
unreadCount = int32(v.MaxSeq - v.HasReadSeq)
}
if conversation, ok := conversationsOnLocalMap[conversationID]; ok {
if conversation.UnreadCount != unreadCount || conversation.HasReadSeq != v.HasReadSeq {
if err := c.db.UpdateColumnsConversation(ctx, conversationID, map[string]interface{}{"unread_count": unreadCount, "has_read_seq": v.HasReadSeq}); err != nil {
log.ZWarn(ctx, "UpdateColumnsConversation err", err, "conversationID", conversationID)
continue
}
conversationChangedIDs = append(conversationChangedIDs, conversationID)
}
} else {
conversationIDsNeedSync = append(conversationIDsNeedSync, conversationID)
}
}
if len(conversationIDsNeedSync) > 0 {
conversationsOnServer, err := c.getServerConversationsByIDs(ctx, conversationIDsNeedSync)
if err != nil {
log.ZWarn(ctx, "getServerConversationsByIDs err", err, "conversationIDs", conversationIDsNeedSync)
return err
}
if err := c.batchAddFaceURLAndName(ctx, conversationsOnServer...); err != nil {
log.ZWarn(ctx, "batchAddFaceURLAndName err", err, "conversationsOnServer", conversationsOnServer)
return err
}
for _, conversation := range conversationsOnServer {
var unreadCount int32
v, ok := seqs[conversation.ConversationID]
if !ok {
continue
}
if v.MaxSeq-v.HasReadSeq < 0 {
unreadCount = 0
log.ZWarn(ctx, "unread count is less than 0", nil, "server seq", v, "conversation", conversation)
} else {
unreadCount = int32(v.MaxSeq - v.HasReadSeq)
}
conversation.UnreadCount = unreadCount
conversation.HasReadSeq = v.HasReadSeq
}
err = c.db.BatchInsertConversationList(ctx, conversationsOnServer)
if err != nil {
log.ZWarn(ctx, "BatchInsertConversationList err", err, "conversationsOnServer", conversationsOnServer)
}
}
log.ZDebug(ctx, "update conversations", "conversations", conversationChangedIDs)
if len(conversationChangedIDs) > 0 {
common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.ConChange, Args: conversationChangedIDs}, c.GetCh())
common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.TotalUnreadMessageChanged}, c.GetCh())
}
return nil
}

@ -0,0 +1,89 @@
// Copyright © 2023 OpenIM open source community. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
func NewBitmap(size int) *Bitmap {
data := make([]uint64, (size+63)/64)
return &Bitmap{data: data, size: size}
}
func ParseBitmap(p []byte, size int) *Bitmap {
data := make([]uint64, len(p)/8)
for i := range data {
data[i] = uint64(p[i*8])<<56 |
uint64(p[i*8+1])<<48 |
uint64(p[i*8+2])<<40 |
uint64(p[i*8+3])<<32 |
uint64(p[i*8+4])<<24 |
uint64(p[i*8+5])<<16 |
uint64(p[i*8+6])<<8 |
uint64(p[i*8+7])
}
return &Bitmap{
data: data,
size: size,
}
}
type Bitmap struct {
data []uint64
size int
}
func (b *Bitmap) Set(index int) {
if index < 0 || index >= b.size {
panic("out of range")
}
wordIndex := index / 64
bitIndex := uint(index % 64)
b.data[wordIndex] |= 1 << bitIndex
}
func (b *Bitmap) Clear(index int) {
if index < 0 || index >= b.size {
panic("out of range")
}
wordIndex := index / 64
bitIndex := uint(index % 64)
b.data[wordIndex] &= ^(1 << bitIndex)
}
func (b *Bitmap) Get(index int) bool {
if index < 0 || index >= b.size {
panic("out of range")
}
wordIndex := index / 64
bitIndex := uint(index % 64)
return (b.data[wordIndex] & (1 << bitIndex)) != 0
}
func (b *Bitmap) Size() int {
return b.size
}
func (b *Bitmap) Serialize() []byte {
p := make([]byte, len(b.data)*8)
for i, word := range b.data {
p[i*8] = byte(word >> 56)
p[i*8+1] = byte(word >> 48)
p[i*8+2] = byte(word >> 40)
p[i*8+3] = byte(word >> 32)
p[i*8+4] = byte(word >> 24)
p[i*8+5] = byte(word >> 16)
p[i*8+6] = byte(word >> 8)
p[i*8+7] = byte(word)
}
return p
}

@ -0,0 +1,62 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import "fmt"
type UploadFileCallback interface {
Open(size int64) // 文件打开的大小
PartSize(partSize int64, num int) // 分片大小,数量
HashPartProgress(index int, size int64, partHash string) // 每块分片的hash值
HashPartComplete(partsHash string, fileHash string) // 分块完成,服务端标记hash和文件最终hash
UploadID(uploadID string) // 上传ID
UploadPartComplete(index int, partSize int64, partHash string) // 上传分片进度
UploadComplete(fileSize int64, streamSize int64, storageSize int64) // 整体进度
Complete(size int64, url string, typ int) // 上传完成
}
type emptyUploadCallback struct{}
func (e emptyUploadCallback) Open(size int64) {
fmt.Println("Callback Open:", size)
}
func (e emptyUploadCallback) PartSize(partSize int64, num int) {
fmt.Println("Callback PartSize:", partSize, num)
}
func (e emptyUploadCallback) HashPartProgress(index int, size int64, partHash string) {
//fmt.Println("Callback HashPartProgress:", index, size, partHash)
}
func (e emptyUploadCallback) HashPartComplete(partsHash string, fileHash string) {
fmt.Println("Callback HashPartComplete:", partsHash, fileHash)
}
func (e emptyUploadCallback) UploadID(uploadID string) {
fmt.Println("Callback UploadID:", uploadID)
}
func (e emptyUploadCallback) UploadPartComplete(index int, partSize int64, partHash string) {
fmt.Println("Callback UploadPartComplete:", index, partSize, partHash)
}
func (e emptyUploadCallback) UploadComplete(fileSize int64, streamSize int64, storageSize int64) {
fmt.Println("Callback UploadComplete:", fileSize, streamSize, storageSize)
}
func (e emptyUploadCallback) Complete(size int64, url string, typ int) {
fmt.Println("Callback Complete:", size, url, typ)
}

@ -0,0 +1,24 @@
// Copyright © 2023 OpenIM open source community. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import "io"
type ReadFile interface {
io.Reader
io.Closer
Size() int64
StartSeek(whence int) error
}

@ -0,0 +1,73 @@
// Copyright © 2023 OpenIM open source community. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !js
package file
import (
"bufio"
"io"
"os"
)
const readBufferSize = 1024 * 1024 * 5 // 5mb
func Open(req *UploadFileReq) (ReadFile, error) {
file, err := os.Open(req.Filepath)
if err != nil {
return nil, err
}
info, err := file.Stat()
if err != nil {
_ = file.Close()
return nil, err
}
df := &defaultFile{
file: file,
info: info,
}
df.resetReaderBuffer()
return df, nil
}
type defaultFile struct {
file *os.File
info os.FileInfo
reader io.Reader
}
func (d *defaultFile) resetReaderBuffer() {
d.reader = bufio.NewReaderSize(d.file, readBufferSize)
}
func (d *defaultFile) Read(p []byte) (n int, err error) {
return d.reader.Read(p)
}
func (d *defaultFile) Close() error {
return d.file.Close()
}
func (d *defaultFile) StartSeek(whence int) error {
if _, err := d.file.Seek(io.SeekStart, whence); err != nil {
return err
}
d.resetReaderBuffer()
return nil
}
func (d *defaultFile) Size() int64 {
return d.info.Size()
}

@ -0,0 +1,156 @@
// Copyright © 2023 OpenIM open source community. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build js && wasm
// +build js,wasm
package file
import (
"bufio"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/wasm/exec"
"io"
"syscall/js"
)
const readBufferSize = 1024 * 1024 * 5 // 5mb
func Open(req *UploadFileReq) (ReadFile, error) {
file := newJsCallFile(req.Uuid)
size, err := file.Open()
if err != nil {
return nil, err
}
jf := &jsFile{
size: size,
file: file,
}
jf.resetReaderBuffer()
return jf, nil
}
type jsFile struct {
size int64
file *jsCallFile
whence int
reader io.Reader
}
func (j *jsFile) resetReaderBuffer() {
j.reader = bufio.NewReaderSize(&reader{fn: j.read}, readBufferSize)
}
func (j *jsFile) read(p []byte) (n int, err error) {
length := len(p)
if length == 0 {
return 0, errors.New("read buffer is empty")
}
if j.whence >= int(j.size) {
return 0, io.EOF
}
if j.whence+length > int(j.size) {
length = int(j.size) - j.whence
}
data, err := j.file.Read(int64(j.whence), int64(length))
if err != nil {
return 0, err
}
if len(data) > len(p) {
return 0, errors.New("js read data > length")
}
j.whence += len(data)
copy(p, data)
return len(data), nil
}
func (j *jsFile) Read(p []byte) (n int, err error) {
return j.reader.Read(p)
}
func (j *jsFile) Close() error {
return j.file.Close()
}
func (j *jsFile) Size() int64 {
return j.size
}
func (j *jsFile) StartSeek(whence int) error {
if whence < 0 || whence > int(j.size) {
return errors.New("seek whence is out of range")
}
j.whence = whence
j.resetReaderBuffer()
return nil
}
type reader struct {
fn func(p []byte) (n int, err error)
}
func (r *reader) Read(p []byte) (n int, err error) {
return r.fn(p)
}
type jsCallFile struct {
uuid string
}
func newJsCallFile(uuid string) *jsCallFile {
return &jsCallFile{uuid: uuid}
}
func (j *jsCallFile) Open() (int64, error) {
return WasmOpen(j.uuid)
}
func (j *jsCallFile) Read(offset int64, length int64) ([]byte, error) {
return WasmRead(j.uuid, offset, length)
}
func (j *jsCallFile) Close() error {
return WasmClose(j.uuid)
}
func WasmOpen(uuid string) (int64, error) {
result, err := exec.Exec(uuid)
if err != nil {
return 0, err
}
if v, ok := result.(float64); ok {
size := int64(v)
if size < 0 {
return 0, errors.New("file size < 0")
}
return size, nil
}
return 0, exec.ErrType
}
func WasmRead(uuid string, offset int64, length int64) ([]byte, error) {
result, err := exec.Exec(uuid, offset, length)
if err != nil {
return nil, err
} else {
if v, ok := result.(js.Value); ok {
return exec.ExtractArrayBuffer(v), nil
} else {
return nil, exec.ErrType
}
}
}
func WasmClose(uuid string) error {
_, err := exec.Exec(uuid)
return err
}

@ -0,0 +1,40 @@
package file
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/pkg/ccontext"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"path/filepath"
"testing"
)
func TestUpload(t *testing.T) {
conf := &ccontext.GlobalConfig{
UserID: `4931176757`,
Token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOiI0OTMxMTc2NzU3IiwiUGxhdGZvcm1JRCI6MSwiZXhwIjoxNzA3MTE0MjIyLCJuYmYiOjE2OTkzMzc5MjIsImlhdCI6MTY5OTMzODIyMn0.AyNvrMGEdXD5rkvn7ZLHCNs-lNbDCb2otn97yLXia5Y`,
IMConfig: sdk_struct.IMConfig{
ApiAddr: `http://203.56.175.233:10002`,
},
}
ctx := ccontext.WithInfo(context.WithValue(context.Background(), "operationID", "OP123456"), conf)
f := NewFile(nil, conf.UserID)
//fp := `C:\Users\openIM\Desktop\微信截图_20231025170714.png`
//fp := `C:\Users\openIM\Desktop\my_image (2).tar`
//fp := `C:\Users\openIM\Desktop\1234.zip`
//fp := `C:\Users\openIM\Desktop\openIM.wasm`
//fp := `C:\Users\openIM\Desktop\ubuntu.7z`
//fp := `C:\Users\openIM\Desktop\log2023-10-31.log`
fp := `C:\Users\openIM\Desktop\protoc.zip`
resp, err := f.UploadFile(ctx, &UploadFileReq{
Filepath: fp,
Name: filepath.Base(fp),
Cause: "test",
}, nil)
if err != nil {
t.Fatal("failed", err)
}
t.Log("success", resp.URL)
}

@ -0,0 +1,43 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"crypto/md5"
"encoding/hex"
"hash"
"io"
)
func NewMd5Reader(r io.Reader) *Md5Reader {
return &Md5Reader{h: md5.New(), r: r}
}
type Md5Reader struct {
h hash.Hash
r io.Reader
}
func (r *Md5Reader) Read(p []byte) (n int, err error) {
n, err = r.r.Read(p)
if err == nil && n > 0 {
r.h.Write(p[:n])
}
return
}
func (r *Md5Reader) Md5() string {
return hex.EncodeToString(r.h.Sum(nil))
}

@ -0,0 +1,44 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"io"
)
func NewProgressReader(r io.Reader, fn func(current int64)) io.Reader {
if r == nil || fn == nil {
return r
}
return &Reader{
r: r,
fn: fn,
}
}
type Reader struct {
r io.Reader
read int64
fn func(current int64)
}
func (r *Reader) Read(p []byte) (n int, err error) {
n, err = r.r.Read(p)
if err == nil && n > 0 {
r.read += int64(n)
r.fn(r.read)
}
return n, err
}

@ -0,0 +1,576 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/tools/errs"
"io"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/openimsdk/protocol/third"
"github.com/openimsdk/tools/log"
)
type UploadFileReq struct {
Filepath string `json:"filepath"`
Name string `json:"name"`
ContentType string `json:"contentType"`
Cause string `json:"cause"`
Uuid string `json:"uuid"`
}
type UploadFileResp struct {
URL string `json:"url"`
}
type partInfo struct {
ContentType string
PartSize int64
PartNum int
FileMd5 string
PartMd5 string
PartSizes []int64
PartMd5s []string
}
func NewFile(database db_interface.DataBase, loginUserID string) *File {
return &File{database: database, loginUserID: loginUserID, confLock: &sync.Mutex{}, mapLocker: &sync.Mutex{}, uploading: make(map[string]*lockInfo)}
}
type File struct {
database db_interface.DataBase
loginUserID string
confLock sync.Locker
partLimit *third.PartLimitResp
mapLocker sync.Locker
uploading map[string]*lockInfo
}
type lockInfo struct {
count int32
locker sync.Locker
}
func (f *File) lockHash(hash string) {
f.mapLocker.Lock()
locker, ok := f.uploading[hash]
if !ok {
locker = &lockInfo{count: 0, locker: &sync.Mutex{}}
f.uploading[hash] = locker
}
atomic.AddInt32(&locker.count, 1)
f.mapLocker.Unlock()
locker.locker.Lock()
}
func (f *File) unlockHash(hash string) {
f.mapLocker.Lock()
locker, ok := f.uploading[hash]
if !ok {
f.mapLocker.Unlock()
return
}
if atomic.AddInt32(&locker.count, -1) == 0 {
delete(f.uploading, hash)
}
f.mapLocker.Unlock()
locker.locker.Unlock()
}
func (f *File) UploadFile(ctx context.Context, req *UploadFileReq, cb UploadFileCallback) (*UploadFileResp, error) {
if cb == nil {
cb = emptyUploadCallback{}
}
if req.Name == "" {
return nil, errors.New("name is empty")
}
if req.Name[0] == '/' {
req.Name = req.Name[1:]
}
if prefix := f.loginUserID + "/"; !strings.HasPrefix(req.Name, prefix) {
req.Name = prefix + req.Name
}
file, err := Open(req)
if err != nil {
return nil, err
}
defer file.Close()
fileSize := file.Size()
cb.Open(fileSize)
info, err := f.getPartInfo(ctx, file, fileSize, cb)
if err != nil {
return nil, err
}
if req.ContentType == "" {
req.ContentType = info.ContentType
}
partSize := info.PartSize
partSizes := info.PartSizes
partMd5s := info.PartMd5s
partMd5Val := info.PartMd5
if err := file.StartSeek(0); err != nil {
return nil, err
}
f.lockHash(partMd5Val)
defer f.unlockHash(partMd5Val)
maxParts := 20
if maxParts > len(partSizes) {
maxParts = len(partSizes)
}
uploadInfo, err := f.getUpload(ctx, &third.InitiateMultipartUploadReq{
Hash: partMd5Val,
Size: fileSize,
PartSize: partSize,
MaxParts: int32(maxParts), // 一次性获取签名数量
Cause: req.Cause,
Name: req.Name,
ContentType: req.ContentType,
})
if err != nil {
return nil, err
}
if uploadInfo.Resp.Upload == nil {
cb.Complete(fileSize, uploadInfo.Resp.Url, 0)
return &UploadFileResp{
URL: uploadInfo.Resp.Url,
}, nil
}
if uploadInfo.Resp.Upload.PartSize != partSize {
f.cleanPartLimit()
return nil, fmt.Errorf("part fileSize not match, expect %d, got %d", partSize, uploadInfo.Resp.Upload.PartSize)
}
cb.UploadID(uploadInfo.Resp.Upload.UploadID)
uploadedSize := fileSize
for i := 0; i < len(partSizes); i++ {
if !uploadInfo.Bitmap.Get(i) {
uploadedSize -= partSizes[i]
}
}
continueUpload := uploadedSize > 0
for i, currentPartSize := range partSizes {
partNumber := int32(i + 1)
md5Reader := NewMd5Reader(io.LimitReader(file, currentPartSize))
if uploadInfo.Bitmap.Get(i) {
if _, err := io.Copy(io.Discard, md5Reader); err != nil {
return nil, err
}
} else {
reader := NewProgressReader(md5Reader, func(current int64) {
cb.UploadComplete(fileSize, uploadedSize+current, uploadedSize)
})
urlval, header, err := uploadInfo.GetPartSign(ctx, partNumber)
if err != nil {
return nil, err
}
if err := f.doPut(ctx, http.DefaultClient, urlval, header, reader, currentPartSize); err != nil {
log.ZError(ctx, "doPut", err, "partMd5Val", partMd5Val, "name", req.Name, "partNumber", partNumber)
return nil, err
}
uploadedSize += currentPartSize
if uploadInfo.DBInfo != nil && uploadInfo.Bitmap != nil {
uploadInfo.Bitmap.Set(i)
uploadInfo.DBInfo.UploadInfo = base64.StdEncoding.EncodeToString(uploadInfo.Bitmap.Serialize())
if err := f.database.UpdateUpload(ctx, uploadInfo.DBInfo); err != nil {
log.ZError(ctx, "SetUploadPartPush", err, "partMd5Val", partMd5Val, "name", req.Name, "partNumber", partNumber)
}
}
}
md5val := md5Reader.Md5()
if md5val != partMd5s[i] {
return nil, fmt.Errorf("upload part %d failed, md5 not match, expect %s, got %s", i, partMd5s[i], md5val)
}
cb.UploadPartComplete(i, currentPartSize, partMd5s[i])
log.ZDebug(ctx, "upload part success", "partMd5Val", md5val, "name", req.Name, "partNumber", partNumber)
}
log.ZDebug(ctx, "upload all part success", "partHash", partMd5Val, "name", req.Name)
resp, err := f.completeMultipartUpload(ctx, &third.CompleteMultipartUploadReq{
UploadID: uploadInfo.Resp.Upload.UploadID,
Parts: partMd5s,
Name: req.Name,
ContentType: req.ContentType,
Cause: req.Cause,
})
if err != nil {
return nil, err
}
typ := 1
if continueUpload {
typ++
}
cb.Complete(fileSize, resp.Url, typ)
if uploadInfo.DBInfo != nil {
if err := f.database.DeleteUpload(ctx, info.PartMd5); err != nil {
log.ZError(ctx, "DeleteUpload", err, "partMd5Val", info.PartMd5, "name", req.Name)
}
}
return &UploadFileResp{
URL: resp.Url,
}, nil
}
func (f *File) cleanPartLimit() {
f.confLock.Lock()
defer f.confLock.Unlock()
f.partLimit = nil
}
func (f *File) initiateMultipartUploadResp(ctx context.Context, req *third.InitiateMultipartUploadReq) (*third.InitiateMultipartUploadResp, error) {
return util.CallApi[third.InitiateMultipartUploadResp](ctx, constant.ObjectInitiateMultipartUpload, req)
}
func (f *File) authSign(ctx context.Context, req *third.AuthSignReq) (*third.AuthSignResp, error) {
if len(req.PartNumbers) == 0 {
return nil, errs.ErrArgs.WrapMsg("partNumbers is empty")
}
return util.CallApi[third.AuthSignResp](ctx, constant.ObjectAuthSign, req)
}
func (f *File) completeMultipartUpload(ctx context.Context, req *third.CompleteMultipartUploadReq) (*third.CompleteMultipartUploadResp, error) {
return util.CallApi[third.CompleteMultipartUploadResp](ctx, constant.ObjectCompleteMultipartUpload, req)
}
func (f *File) getPartNum(fileSize int64, partSize int64) int {
partNum := fileSize / partSize
if fileSize%partSize != 0 {
partNum++
}
return int(partNum)
}
func (f *File) partSize(ctx context.Context, size int64) (int64, error) {
f.confLock.Lock()
defer f.confLock.Unlock()
if f.partLimit == nil {
resp, err := util.CallApi[third.PartLimitResp](ctx, constant.ObjectPartLimit, &third.PartLimitReq{})
if err != nil {
return 0, err
}
f.partLimit = resp
}
if size <= 0 {
return 0, errors.New("size must be greater than 0")
}
if size > f.partLimit.MaxPartSize*int64(f.partLimit.MaxNumSize) {
return 0, fmt.Errorf("size must be less than %db", f.partLimit.MaxPartSize*int64(f.partLimit.MaxNumSize))
}
if size <= f.partLimit.MinPartSize*int64(f.partLimit.MaxNumSize) {
return f.partLimit.MinPartSize, nil
}
partSize := size / int64(f.partLimit.MaxNumSize)
if size%int64(f.partLimit.MaxNumSize) != 0 {
partSize++
}
return partSize, nil
}
func (f *File) accessURL(ctx context.Context, req *third.AccessURLReq) (*third.AccessURLResp, error) {
return util.CallApi[third.AccessURLResp](ctx, constant.ObjectAccessURL, req)
}
func (f *File) doHttpReq(req *http.Request) ([]byte, *http.Response, error) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
return data, resp, nil
}
func (f *File) partMD5(parts []string) string {
s := strings.Join(parts, ",")
md5Sum := md5.Sum([]byte(s))
return hex.EncodeToString(md5Sum[:])
}
type AuthSignParts struct {
Sign *third.SignPart
Times []time.Time
}
type UploadInfo struct {
PartNum int
Bitmap *Bitmap
DBInfo *model_struct.LocalUpload
Resp *third.InitiateMultipartUploadResp
//Signs *AuthSignParts
CreateTime time.Time
BatchSignNum int32
f *File
}
func (u *UploadInfo) getIndex(partNumber int32) int {
if u.Resp.Upload.Sign == nil {
return -1
} else {
if u.CreateTime.IsZero() {
return -1
} else {
if time.Since(u.CreateTime) > time.Minute {
return -1
}
}
}
for i, part := range u.Resp.Upload.Sign.Parts {
if part.PartNumber == partNumber {
return i
}
}
return -1
}
func (u *UploadInfo) buildRequest(i int) (*url.URL, http.Header, error) {
sign := u.Resp.Upload.Sign
part := sign.Parts[i]
rawURL := sign.Url
if part.Url != "" {
rawURL = part.Url
}
urlval, err := url.Parse(rawURL)
if err != nil {
return nil, nil, err
}
if len(sign.Query)+len(part.Query) > 0 {
query := urlval.Query()
for i := range sign.Query {
v := sign.Query[i]
query[v.Key] = v.Values
}
for i := range part.Query {
v := part.Query[i]
query[v.Key] = v.Values
}
urlval.RawQuery = query.Encode()
}
header := make(http.Header)
for i := range sign.Header {
v := sign.Header[i]
header[v.Key] = v.Values
}
for i := range part.Header {
v := part.Header[i]
header[v.Key] = v.Values
}
return urlval, header, nil
}
func (u *UploadInfo) GetPartSign(ctx context.Context, partNumber int32) (*url.URL, http.Header, error) {
if partNumber < 1 || int(partNumber) > u.PartNum {
return nil, nil, errors.New("invalid partNumber")
}
if index := u.getIndex(partNumber); index >= 0 {
return u.buildRequest(index)
}
partNumbers := make([]int32, 0, u.BatchSignNum)
for i := int32(0); i < u.BatchSignNum; i++ {
if int(partNumber+i) > u.PartNum {
break
}
partNumbers = append(partNumbers, partNumber+i)
}
authSignResp, err := u.f.authSign(ctx, &third.AuthSignReq{
UploadID: u.Resp.Upload.UploadID,
PartNumbers: partNumbers,
})
if err != nil {
return nil, nil, err
}
u.Resp.Upload.Sign.Url = authSignResp.Url
u.Resp.Upload.Sign.Query = authSignResp.Query
u.Resp.Upload.Sign.Header = authSignResp.Header
u.Resp.Upload.Sign.Parts = authSignResp.Parts
u.CreateTime = time.Now()
index := u.getIndex(partNumber)
if index < 0 {
return nil, nil, errs.ErrInternalServer.WrapMsg("server part sign invalid")
}
return u.buildRequest(index)
}
func (f *File) getUpload(ctx context.Context, req *third.InitiateMultipartUploadReq) (*UploadInfo, error) {
partNum := f.getPartNum(req.Size, req.PartSize)
var bitmap *Bitmap
if f.database != nil {
dbUpload, err := f.database.GetUpload(ctx, req.Hash)
if err == nil {
bitmapBytes, err := base64.StdEncoding.DecodeString(dbUpload.UploadInfo)
if err != nil || len(bitmapBytes) == 0 || partNum <= 1 || dbUpload.ExpireTime-3600*1000 < time.Now().UnixMilli() {
if err := f.database.DeleteUpload(ctx, req.Hash); err != nil {
return nil, err
}
dbUpload = nil
}
if dbUpload == nil {
bitmap = NewBitmap(partNum)
} else {
bitmap = ParseBitmap(bitmapBytes, partNum)
}
tUpInfo := &third.UploadInfo{
PartSize: req.PartSize,
Sign: &third.AuthSignParts{},
}
if dbUpload != nil {
tUpInfo.UploadID = dbUpload.UploadID
tUpInfo.ExpireTime = dbUpload.ExpireTime
}
return &UploadInfo{
PartNum: partNum,
Bitmap: bitmap,
DBInfo: dbUpload,
Resp: &third.InitiateMultipartUploadResp{
Upload: tUpInfo,
},
BatchSignNum: req.MaxParts,
f: f,
}, nil
}
log.ZError(ctx, "get upload db", err, "pratsMd5", req.Hash)
}
resp, err := f.initiateMultipartUploadResp(ctx, req)
if err != nil {
return nil, err
}
if resp.Upload == nil {
return &UploadInfo{
Resp: resp,
}, nil
}
bitmap = NewBitmap(partNum)
var dbUpload *model_struct.LocalUpload
if f.database != nil {
dbUpload = &model_struct.LocalUpload{
PartHash: req.Hash,
UploadID: resp.Upload.UploadID,
UploadInfo: base64.StdEncoding.EncodeToString(bitmap.Serialize()),
ExpireTime: resp.Upload.ExpireTime,
CreateTime: time.Now().UnixMilli(),
}
if err := f.database.InsertUpload(ctx, dbUpload); err != nil {
log.ZError(ctx, "insert upload db", err, "pratsHash", req.Hash, "name", req.Name)
}
}
if req.MaxParts >= 0 && len(resp.Upload.Sign.Parts) != int(req.MaxParts) {
resp.Upload.Sign.Parts = nil
}
return &UploadInfo{
PartNum: partNum,
Bitmap: bitmap,
DBInfo: dbUpload,
Resp: resp,
CreateTime: time.Now(),
BatchSignNum: req.MaxParts,
f: f,
}, nil
}
func (f *File) doPut(ctx context.Context, client *http.Client, url *url.URL, header http.Header, reader io.Reader, size int64) error {
rawURL := url.String()
req, err := http.NewRequestWithContext(ctx, http.MethodPut, rawURL, reader)
if err != nil {
return err
}
for key := range header {
req.Header[key] = header[key]
}
req.ContentLength = size
log.ZDebug(ctx, "do put req", "url", rawURL, "contentLength", size, "header", req.Header)
resp, err := client.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
log.ZDebug(ctx, "do put resp status", "url", rawURL, "status", resp.Status, "contentLength", resp.ContentLength, "header", resp.Header)
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
log.ZDebug(ctx, "do put resp body", "url", rawURL, "body", string(body))
if resp.StatusCode/200 != 1 {
return fmt.Errorf("PUT %s failed, status code %d, body %s", rawURL, resp.StatusCode, string(body))
}
return nil
}
func (f *File) getPartInfo(ctx context.Context, r io.Reader, fileSize int64, cb UploadFileCallback) (*partInfo, error) {
partSize, err := f.partSize(ctx, fileSize)
if err != nil {
return nil, err
}
partNum := int(fileSize / partSize)
if fileSize%partSize != 0 {
partNum++
}
cb.PartSize(partSize, partNum)
partSizes := make([]int64, partNum)
for i := 0; i < partNum; i++ {
partSizes[i] = partSize
}
partSizes[partNum-1] = fileSize - partSize*(int64(partNum)-1)
partMd5s := make([]string, partNum)
buf := make([]byte, 1024*8)
fileMd5 := md5.New()
var contentType string
for i := 0; i < partNum; i++ {
h := md5.New()
r := io.LimitReader(r, partSize)
for {
if n, err := r.Read(buf); err == nil {
if contentType == "" {
contentType = http.DetectContentType(buf[:n])
}
h.Write(buf[:n])
fileMd5.Write(buf[:n])
} else if err == io.EOF {
break
} else {
return nil, err
}
}
partMd5s[i] = hex.EncodeToString(h.Sum(nil))
cb.HashPartProgress(i, partSizes[i], partMd5s[i])
}
partMd5Val := f.partMD5(partMd5s)
fileMd5val := hex.EncodeToString(fileMd5.Sum(nil))
cb.HashPartComplete(f.partMD5(partMd5s), hex.EncodeToString(fileMd5.Sum(nil)))
return &partInfo{
ContentType: contentType,
PartSize: partSize,
PartNum: partNum,
FileMd5: fileMd5val,
PartMd5: partMd5Val,
PartSizes: partSizes,
PartMd5s: partMd5s,
}, nil
}

@ -0,0 +1,71 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package friend
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/sdkws"
)
func ServerFriendRequestToLocalFriendRequest(info *sdkws.FriendRequest) *model_struct.LocalFriendRequest {
return &model_struct.LocalFriendRequest{
FromUserID: info.FromUserID,
FromNickname: info.FromNickname,
FromFaceURL: info.FromFaceURL,
//FromGender: info.FromGender,
ToUserID: info.ToUserID,
ToNickname: info.ToNickname,
ToFaceURL: info.ToFaceURL,
//ToGender: info.ToGender,
HandleResult: info.HandleResult,
ReqMsg: info.ReqMsg,
CreateTime: info.CreateTime,
HandlerUserID: info.HandlerUserID,
HandleMsg: info.HandleMsg,
HandleTime: info.HandleTime,
Ex: info.Ex,
//AttachedInfo: info.AttachedInfo,
}
}
func ServerFriendToLocalFriend(info *sdkws.FriendInfo) *model_struct.LocalFriend {
return &model_struct.LocalFriend{
OwnerUserID: info.OwnerUserID,
FriendUserID: info.FriendUser.UserID,
Remark: info.Remark,
CreateTime: info.CreateTime,
AddSource: info.AddSource,
OperatorUserID: info.OperatorUserID,
Nickname: info.FriendUser.Nickname,
FaceURL: info.FriendUser.FaceURL,
Ex: info.Ex,
//AttachedInfo: info.FriendUser.AttachedInfo,
IsPinned: info.IsPinned,
}
}
func ServerBlackToLocalBlack(info *sdkws.BlackInfo) *model_struct.LocalBlack {
return &model_struct.LocalBlack{
OwnerUserID: info.OwnerUserID,
BlockUserID: info.BlackUserInfo.UserID,
CreateTime: info.CreateTime,
AddSource: info.AddSource,
OperatorUserID: info.OperatorUserID,
Nickname: info.BlackUserInfo.Nickname,
FaceURL: info.BlackUserInfo.FaceURL,
Ex: info.Ex,
//AttachedInfo: info.FriendUser.AttachedInfo,
}
}

@ -0,0 +1,199 @@
// Copyright 2021 OpenIM Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package friend
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/user"
"github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/page"
"github.com/openimsdk/openim-sdk-core/v3/pkg/syncer"
friend "github.com/openimsdk/protocol/relation"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func NewFriend(loginUserID string, db db_interface.DataBase, user *user.User, conversationCh chan common.Cmd2Value) *Friend {
f := &Friend{loginUserID: loginUserID, db: db, user: user, conversationCh: conversationCh}
f.initSyncer()
return f
}
type Friend struct {
friendListener open_im_sdk_callback.OnFriendshipListenerSdk
loginUserID string
db db_interface.DataBase
user *user.User
friendSyncer *syncer.Syncer[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string]
blockSyncer *syncer.Syncer[*model_struct.LocalBlack, syncer.NoResp, [2]string]
requestRecvSyncer *syncer.Syncer[*model_struct.LocalFriendRequest, syncer.NoResp, [2]string]
requestSendSyncer *syncer.Syncer[*model_struct.LocalFriendRequest, syncer.NoResp, [2]string]
conversationCh chan common.Cmd2Value
listenerForService open_im_sdk_callback.OnListenerForService
}
func (f *Friend) initSyncer() {
f.friendSyncer = syncer.New2[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](
syncer.WithInsert[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, value *model_struct.LocalFriend) error {
return f.db.InsertFriend(ctx, value)
}),
syncer.WithDelete[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, value *model_struct.LocalFriend) error {
return f.db.DeleteFriendDB(ctx, value.FriendUserID)
}),
syncer.WithUpdate[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, server, local *model_struct.LocalFriend) error {
return f.db.UpdateFriend(ctx, server)
}),
syncer.WithUUID[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(value *model_struct.LocalFriend) [2]string {
return [...]string{value.OwnerUserID, value.FriendUserID}
}),
syncer.WithNotice[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, state int, server, local *model_struct.LocalFriend) error {
switch state {
case syncer.Insert:
f.friendListener.OnFriendAdded(*server)
case syncer.Delete:
log.ZDebug(ctx, "syncer OnFriendDeleted", "local", local)
f.friendListener.OnFriendDeleted(*local)
case syncer.Update:
f.friendListener.OnFriendInfoChanged(*server)
if local.Nickname != server.Nickname || local.FaceURL != server.FaceURL || local.Remark != server.Remark {
if server.Remark != "" {
server.Nickname = server.Remark
}
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{
Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{
SourceID: server.FriendUserID,
SessionType: constant.SingleChatType,
FaceURL: server.FaceURL,
Nickname: server.Nickname,
},
}, f.conversationCh)
_ = common.TriggerCmdUpdateMessage(ctx, common.UpdateMessageNode{
Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{
SessionType: constant.SingleChatType,
UserID: server.FriendUserID,
FaceURL: server.FaceURL,
Nickname: server.Nickname,
},
}, f.conversationCh)
}
}
return nil
}),
syncer.WithBatchInsert[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, values []*model_struct.LocalFriend) error {
log.ZDebug(ctx, "BatchInsertFriend", "length", len(values))
return f.db.BatchInsertFriend(ctx, values)
}),
syncer.WithDeleteAll[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(ctx context.Context, _ string) error {
return f.db.DeleteAllFriend(ctx)
}),
syncer.WithBatchPageReq[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(entityID string) page.PageReq {
return &friend.GetPaginationFriendsReq{UserID: entityID,
Pagination: &sdkws.RequestPagination{ShowNumber: 100}}
}),
syncer.WithBatchPageRespConvertFunc[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](func(resp *friend.GetPaginationFriendsResp) []*model_struct.LocalFriend {
return datautil.Batch(ServerFriendToLocalFriend, resp.FriendsInfo)
}),
syncer.WithReqApiRouter[*model_struct.LocalFriend, friend.GetPaginationFriendsResp, [2]string](constant.GetFriendListRouter),
)
f.blockSyncer = syncer.New[*model_struct.LocalBlack, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalBlack) error {
return f.db.InsertBlack(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalBlack) error {
return f.db.DeleteBlack(ctx, value.BlockUserID)
}, func(ctx context.Context, server *model_struct.LocalBlack, local *model_struct.LocalBlack) error {
return f.db.UpdateBlack(ctx, server)
}, func(value *model_struct.LocalBlack) [2]string {
return [...]string{value.OwnerUserID, value.BlockUserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalBlack) error {
switch state {
case syncer.Insert:
f.friendListener.OnBlackAdded(*server)
case syncer.Delete:
f.friendListener.OnBlackDeleted(*local)
}
return nil
})
f.requestRecvSyncer = syncer.New[*model_struct.LocalFriendRequest, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalFriendRequest) error {
return f.db.InsertFriendRequest(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalFriendRequest) error {
return f.db.DeleteFriendRequestBothUserID(ctx, value.FromUserID, value.ToUserID)
}, func(ctx context.Context, server *model_struct.LocalFriendRequest, local *model_struct.LocalFriendRequest) error {
return f.db.UpdateFriendRequest(ctx, server)
}, func(value *model_struct.LocalFriendRequest) [2]string {
return [...]string{value.FromUserID, value.ToUserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalFriendRequest) error {
switch state {
case syncer.Insert:
f.friendListener.OnFriendApplicationAdded(*server)
case syncer.Delete:
f.friendListener.OnFriendApplicationDeleted(*local)
case syncer.Update:
switch server.HandleResult {
case constant.FriendResponseAgree:
f.friendListener.OnFriendApplicationAccepted(*server)
case constant.FriendResponseRefuse:
f.friendListener.OnFriendApplicationRejected(*server)
case constant.FriendResponseDefault:
f.friendListener.OnFriendApplicationAdded(*server)
}
}
return nil
})
f.requestSendSyncer = syncer.New[*model_struct.LocalFriendRequest, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalFriendRequest) error {
return f.db.InsertFriendRequest(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalFriendRequest) error {
return f.db.DeleteFriendRequestBothUserID(ctx, value.FromUserID, value.ToUserID)
}, func(ctx context.Context, server *model_struct.LocalFriendRequest, local *model_struct.LocalFriendRequest) error {
return f.db.UpdateFriendRequest(ctx, server)
}, func(value *model_struct.LocalFriendRequest) [2]string {
return [...]string{value.FromUserID, value.ToUserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalFriendRequest) error {
switch state {
case syncer.Insert:
f.friendListener.OnFriendApplicationAdded(*server)
case syncer.Delete:
f.friendListener.OnFriendApplicationDeleted(*local)
case syncer.Update:
switch server.HandleResult {
case constant.FriendResponseAgree:
f.friendListener.OnFriendApplicationAccepted(*server)
case constant.FriendResponseRefuse:
f.friendListener.OnFriendApplicationRejected(*server)
}
}
return nil
})
}
func (f *Friend) Db() db_interface.DataBase {
return f.db
}
func (f *Friend) SetListener(listener func() open_im_sdk_callback.OnFriendshipListener) {
f.friendListener = open_im_sdk_callback.NewOnFriendshipListenerSdk(listener)
}
func (f *Friend) SetListenerForService(listener open_im_sdk_callback.OnListenerForService) {
f.listenerForService = listener
}

@ -0,0 +1,33 @@
package friend
import (
"crypto/md5"
"encoding/binary"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/constant"
"github.com/openimsdk/tools/utils/datautil"
"strconv"
"strings"
)
func (f *Friend) CalculateHash(friends []*model_struct.LocalFriend) uint64 {
datautil.SortAny(friends, func(a, b *model_struct.LocalFriend) bool {
return a.CreateTime > b.CreateTime
})
if len(friends) > constant.MaxSyncPullNumber {
friends = friends[:constant.MaxSyncPullNumber]
}
hashStr := strings.Join(datautil.Slice(friends, func(f *model_struct.LocalFriend) string {
return strings.Join([]string{
f.FriendUserID,
f.Remark,
strconv.FormatInt(f.CreateTime, 10),
strconv.Itoa(int(f.AddSource)),
f.OperatorUserID,
f.Ex,
strconv.FormatBool(f.IsPinned),
}, ",")
}), ";")
sum := md5.Sum([]byte(hashStr))
return binary.BigEndian.Uint64(sum[:])
}

@ -0,0 +1,136 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package friend
import (
"context"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (f *Friend) DoNotification(ctx context.Context, msg *sdkws.MsgData) {
go func() {
if err := f.doNotification(ctx, msg); err != nil {
log.ZError(ctx, "doNotification error", err, "msg", msg)
}
}()
}
func (f *Friend) doNotification(ctx context.Context, msg *sdkws.MsgData) error {
switch msg.ContentType {
case constant.FriendApplicationNotification:
tips := sdkws.FriendApplicationTips{}
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
return f.SyncBothFriendRequest(ctx,
tips.FromToUserID.FromUserID, tips.FromToUserID.ToUserID)
case constant.FriendApplicationApprovedNotification:
var tips sdkws.FriendApplicationApprovedTips
err := utils.UnmarshalNotificationElem(msg.Content, &tips)
if err != nil {
return err
}
if tips.FromToUserID.FromUserID == f.loginUserID {
err = f.SyncFriends(ctx, []string{tips.FromToUserID.ToUserID})
} else if tips.FromToUserID.ToUserID == f.loginUserID {
err = f.SyncFriends(ctx, []string{tips.FromToUserID.FromUserID})
}
if err != nil {
return err
}
return f.SyncBothFriendRequest(ctx, tips.FromToUserID.FromUserID, tips.FromToUserID.ToUserID)
case constant.FriendApplicationRejectedNotification:
var tips sdkws.FriendApplicationRejectedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
return f.SyncBothFriendRequest(ctx, tips.FromToUserID.FromUserID, tips.FromToUserID.ToUserID)
case constant.FriendAddedNotification:
var tips sdkws.FriendAddedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.Friend != nil && tips.Friend.FriendUser != nil {
if tips.Friend.FriendUser.UserID == f.loginUserID {
return f.SyncFriends(ctx, []string{tips.Friend.OwnerUserID})
} else if tips.Friend.OwnerUserID == f.loginUserID {
return f.SyncFriends(ctx, []string{tips.Friend.FriendUser.UserID})
}
}
case constant.FriendDeletedNotification:
var tips sdkws.FriendDeletedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID != nil {
if tips.FromToUserID.FromUserID == f.loginUserID {
return f.deleteFriend(ctx, tips.FromToUserID.ToUserID)
}
}
case constant.FriendRemarkSetNotification:
var tips sdkws.FriendInfoChangedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID != nil {
if tips.FromToUserID.FromUserID == f.loginUserID {
return f.SyncFriends(ctx, []string{tips.FromToUserID.ToUserID})
}
}
case constant.FriendInfoUpdatedNotification:
var tips sdkws.UserInfoUpdatedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.UserID != f.loginUserID {
return f.SyncFriends(ctx, []string{tips.UserID})
}
case constant.BlackAddedNotification:
var tips sdkws.BlackAddedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID.FromUserID == f.loginUserID {
return f.SyncAllBlackList(ctx)
}
case constant.BlackDeletedNotification:
var tips sdkws.BlackDeletedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID.FromUserID == f.loginUserID {
return f.SyncAllBlackList(ctx)
}
case constant.FriendsInfoUpdateNotification:
var tips sdkws.FriendsInfoUpdateTips
if err := utils.UnmarshalNotificationElem(msg.Content, &tips); err != nil {
return err
}
if tips.FromToUserID.ToUserID == f.loginUserID {
return f.SyncFriends(ctx, tips.FriendIDs)
}
default:
return fmt.Errorf("type failed %d", msg.ContentType)
}
return nil
}

@ -0,0 +1,344 @@
// Copyright 2021 OpenIM Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package friend
import (
"context"
friend "github.com/openimsdk/protocol/relation"
"github.com/openimsdk/protocol/wrapperspb"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/datafetcher"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
sdk "github.com/openimsdk/openim-sdk-core/v3/pkg/sdk_params_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/server_api_params"
"github.com/openimsdk/tools/log"
)
func (f *Friend) GetSpecifiedFriendsInfo(ctx context.Context, friendUserIDList []string) ([]*server_api_params.FullUserInfo, error) {
datafetcher := datafetcher.NewDataFetcher(
f.db,
f.friendListTableName(),
f.loginUserID,
func(localFriend *model_struct.LocalFriend) string {
return localFriend.FriendUserID
},
func(ctx context.Context, values []*model_struct.LocalFriend) error {
return f.db.BatchInsertFriend(ctx, values)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalFriend, error) {
return f.db.GetFriendInfoList(ctx, userIDs)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalFriend, error) {
serverFriend, err := f.GetDesignatedFriends(ctx, userIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerFriendToLocalFriend, serverFriend), nil
},
)
localFriendList, err := datafetcher.FetchMissingAndFillLocal(ctx, friendUserIDList)
if err != nil {
return nil, err
}
log.ZDebug(ctx, "GetDesignatedFriendsInfo", "localFriendList", localFriendList)
blackList, err := f.db.GetBlackInfoList(ctx, friendUserIDList)
if err != nil {
return nil, err
}
log.ZDebug(ctx, "GetDesignatedFriendsInfo", "blackList", blackList)
m := make(map[string]*model_struct.LocalBlack)
for i, black := range blackList {
m[black.BlockUserID] = blackList[i]
}
res := make([]*server_api_params.FullUserInfo, 0, len(localFriendList))
for _, localFriend := range localFriendList {
res = append(res, &server_api_params.FullUserInfo{
PublicInfo: nil,
FriendInfo: localFriend,
BlackInfo: m[localFriend.FriendUserID],
})
}
return res, nil
}
func (f *Friend) AddFriend(ctx context.Context, userIDReqMsg *friend.ApplyToAddFriendReq) error {
if userIDReqMsg.FromUserID == "" {
userIDReqMsg.FromUserID = f.loginUserID
}
if err := util.ApiPost(ctx, constant.AddFriendRouter, userIDReqMsg, nil); err != nil {
return err
}
return f.SyncAllFriendApplication(ctx)
}
func (f *Friend) GetFriendApplicationListAsRecipient(ctx context.Context) ([]*model_struct.LocalFriendRequest, error) {
return f.db.GetRecvFriendApplication(ctx)
}
func (f *Friend) GetFriendApplicationListAsApplicant(ctx context.Context) ([]*model_struct.LocalFriendRequest, error) {
return f.db.GetSendFriendApplication(ctx)
}
func (f *Friend) AcceptFriendApplication(ctx context.Context, userIDHandleMsg *sdk.ProcessFriendApplicationParams) error {
return f.RespondFriendApply(ctx, &friend.RespondFriendApplyReq{FromUserID: userIDHandleMsg.ToUserID, ToUserID: f.loginUserID, HandleResult: constant.FriendResponseAgree, HandleMsg: userIDHandleMsg.HandleMsg})
}
func (f *Friend) RefuseFriendApplication(ctx context.Context, userIDHandleMsg *sdk.ProcessFriendApplicationParams) error {
return f.RespondFriendApply(ctx, &friend.RespondFriendApplyReq{FromUserID: userIDHandleMsg.ToUserID, ToUserID: f.loginUserID, HandleResult: constant.FriendResponseRefuse, HandleMsg: userIDHandleMsg.HandleMsg})
}
func (f *Friend) RespondFriendApply(ctx context.Context, req *friend.RespondFriendApplyReq) error {
if req.ToUserID == "" {
req.ToUserID = f.loginUserID
}
if err := util.ApiPost(ctx, constant.AddFriendResponse, req, nil); err != nil {
return err
}
if req.HandleResult == constant.FriendResponseAgree {
_ = f.SyncFriends(ctx, []string{req.FromUserID})
}
_ = f.SyncAllFriendApplication(ctx)
return nil
// return f.SyncFriendApplication(ctx)
}
func (f *Friend) CheckFriend(ctx context.Context, friendUserIDList []string) ([]*server_api_params.UserIDResult, error) {
friendList, err := f.db.GetFriendInfoList(ctx, friendUserIDList)
if err != nil {
return nil, err
}
blackList, err := f.db.GetBlackInfoList(ctx, friendUserIDList)
if err != nil {
return nil, err
}
res := make([]*server_api_params.UserIDResult, 0, len(friendUserIDList))
for _, v := range friendUserIDList {
var r server_api_params.UserIDResult
isBlack := false
isFriend := false
for _, b := range blackList {
if v == b.BlockUserID {
isBlack = true
break
}
}
for _, f := range friendList {
if v == f.FriendUserID {
isFriend = true
break
}
}
r.UserID = v
if isFriend && !isBlack {
r.Result = 1
} else {
r.Result = 0
}
res = append(res, &r)
}
return res, nil
}
func (f *Friend) DeleteFriend(ctx context.Context, friendUserID string) error {
if err := util.ApiPost(ctx, constant.DeleteFriendRouter, &friend.DeleteFriendReq{OwnerUserID: f.loginUserID, FriendUserID: friendUserID}, nil); err != nil {
return err
}
return f.deleteFriend(ctx, friendUserID)
}
func (f *Friend) GetFriendList(ctx context.Context) ([]*server_api_params.FullUserInfo, error) {
localFriendList, err := f.db.GetAllFriendList(ctx)
if err != nil {
return nil, err
}
localBlackList, err := f.db.GetBlackListDB(ctx)
if err != nil {
return nil, err
}
m := make(map[string]*model_struct.LocalBlack)
for i, black := range localBlackList {
m[black.BlockUserID] = localBlackList[i]
}
res := make([]*server_api_params.FullUserInfo, 0, len(localFriendList))
for _, localFriend := range localFriendList {
res = append(res, &server_api_params.FullUserInfo{
PublicInfo: nil,
FriendInfo: localFriend,
BlackInfo: m[localFriend.FriendUserID],
})
}
return res, nil
}
func (f *Friend) GetFriendListPage(ctx context.Context, offset, count int32) ([]*server_api_params.FullUserInfo, error) {
dataFetcher := datafetcher.NewDataFetcher(
f.db,
f.friendListTableName(),
f.loginUserID,
func(localFriend *model_struct.LocalFriend) string {
return localFriend.FriendUserID
},
func(ctx context.Context, values []*model_struct.LocalFriend) error {
return f.db.BatchInsertFriend(ctx, values)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalFriend, error) {
return f.db.GetFriendInfoList(ctx, userIDs)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalFriend, error) {
serverFriend, err := f.GetDesignatedFriends(ctx, userIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerFriendToLocalFriend, serverFriend), nil
},
)
localFriendList, err := dataFetcher.FetchWithPagination(ctx, int(offset), int(count))
if err != nil {
return nil, err
}
// don't need extra handle. only full pull.
localBlackList, err := f.db.GetBlackListDB(ctx)
if err != nil {
return nil, err
}
m := make(map[string]*model_struct.LocalBlack)
for i, black := range localBlackList {
m[black.BlockUserID] = localBlackList[i]
}
res := make([]*server_api_params.FullUserInfo, 0, len(localFriendList))
for _, localFriend := range localFriendList {
res = append(res, &server_api_params.FullUserInfo{
PublicInfo: nil,
FriendInfo: localFriend,
BlackInfo: m[localFriend.FriendUserID],
})
}
return res, nil
}
func (f *Friend) SearchFriends(ctx context.Context, param *sdk.SearchFriendsParam) ([]*sdk.SearchFriendItem, error) {
if len(param.KeywordList) == 0 || (!param.IsSearchNickname && !param.IsSearchUserID && !param.IsSearchRemark) {
return nil, sdkerrs.ErrArgs.WrapMsg("keyword is null or search field all false")
}
localFriendList, err := f.db.SearchFriendList(ctx, param.KeywordList[0], param.IsSearchUserID, param.IsSearchNickname, param.IsSearchRemark)
if err != nil {
return nil, err
}
localBlackList, err := f.db.GetBlackListDB(ctx)
if err != nil {
return nil, err
}
m := make(map[string]struct{})
for _, black := range localBlackList {
m[black.BlockUserID] = struct{}{}
}
res := make([]*sdk.SearchFriendItem, 0, len(localFriendList))
for i, localFriend := range localFriendList {
var relationship int
if _, ok := m[localFriend.FriendUserID]; ok {
relationship = constant.BlackRelationship
} else {
relationship = constant.FriendRelationship
}
res = append(res, &sdk.SearchFriendItem{
LocalFriend: *localFriendList[i],
Relationship: relationship,
})
}
return res, nil
}
func (f *Friend) SetFriendRemark(ctx context.Context, userIDRemark *sdk.SetFriendRemarkParams) error {
if err := util.ApiPost(ctx, constant.SetFriendRemark, &friend.SetFriendRemarkReq{OwnerUserID: f.loginUserID, FriendUserID: userIDRemark.ToUserID, Remark: userIDRemark.Remark}, nil); err != nil {
return err
}
return f.SyncFriends(ctx, []string{userIDRemark.ToUserID})
}
func (f *Friend) PinFriends(ctx context.Context, friends *sdk.SetFriendPinParams) error {
if err := util.ApiPost(ctx, constant.UpdateFriends, &friend.UpdateFriendsReq{OwnerUserID: f.loginUserID, FriendUserIDs: friends.ToUserIDs, IsPinned: friends.IsPinned}, nil); err != nil {
return err
}
return f.SyncFriends(ctx, friends.ToUserIDs)
}
func (f *Friend) AddBlack(ctx context.Context, blackUserID string, ex string) error {
if err := util.ApiPost(ctx, constant.AddBlackRouter, &friend.AddBlackReq{OwnerUserID: f.loginUserID, BlackUserID: blackUserID, Ex: ex}, nil); err != nil {
return err
}
return f.SyncAllBlackList(ctx)
}
func (f *Friend) RemoveBlack(ctx context.Context, blackUserID string) error {
if err := util.ApiPost(ctx, constant.RemoveBlackRouter, &friend.RemoveBlackReq{OwnerUserID: f.loginUserID, BlackUserID: blackUserID}, nil); err != nil {
return err
}
return f.SyncAllBlackList(ctx)
}
func (f *Friend) GetBlackList(ctx context.Context) ([]*model_struct.LocalBlack, error) {
return f.db.GetBlackListDB(ctx)
}
func (f *Friend) SetFriendsEx(ctx context.Context, friendIDs []string, ex string) error {
if err := util.ApiPost(ctx, constant.UpdateFriends, &friend.UpdateFriendsReq{OwnerUserID: f.loginUserID, FriendUserIDs: friendIDs, Ex: &wrapperspb.StringValue{
Value: ex,
}}, nil); err != nil {
return err
}
// Check if the specified ID is a friend
friendResults, err := f.CheckFriend(ctx, friendIDs)
if err != nil {
return errs.WrapMsg(err, "Error checking friend status")
}
// Determine if friendID is indeed a friend
// Iterate over each friendID
for _, friendID := range friendIDs {
isFriend := false
// Check if this friendID is in the friendResults
for _, result := range friendResults {
if result.UserID == friendID && result.Result == 1 { // Assuming result 1 means they are friends
isFriend = true
break
}
}
// If this friendID is not a friend, return an error
if !isFriend {
return errs.ErrRecordNotFound.WrapMsg("Not friend")
}
}
// If the code reaches here, all friendIDs are confirmed as friends
// Update friend information if they are friends
updateErr := f.db.UpdateColumnsFriend(ctx, friendIDs, map[string]interface{}{"Ex": ex})
if updateErr != nil {
return errs.WrapMsg(updateErr, "Error updating friend information")
}
return nil
}

@ -0,0 +1,180 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package friend
import (
"context"
"fmt"
"github.com/openimsdk/tools/utils/datautil"
"time"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
friend "github.com/openimsdk/protocol/relation"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (f *Friend) SyncBothFriendRequest(ctx context.Context, fromUserID, toUserID string) error {
var resp friend.GetDesignatedFriendsApplyResp
if err := util.ApiPost(ctx, constant.GetDesignatedFriendsApplyRouter, &friend.GetDesignatedFriendsApplyReq{FromUserID: fromUserID, ToUserID: toUserID}, &resp); err != nil {
return nil
}
localData, err := f.db.GetBothFriendReq(ctx, fromUserID, toUserID)
if err != nil {
return err
}
if toUserID == f.loginUserID {
return f.requestRecvSyncer.Sync(ctx, datautil.Batch(ServerFriendRequestToLocalFriendRequest, resp.FriendRequests), localData, nil)
} else if fromUserID == f.loginUserID {
return f.requestSendSyncer.Sync(ctx, datautil.Batch(ServerFriendRequestToLocalFriendRequest, resp.FriendRequests), localData, nil)
}
return nil
}
// send
func (f *Friend) SyncAllSelfFriendApplication(ctx context.Context) error {
req := &friend.GetPaginationFriendsApplyFromReq{UserID: f.loginUserID, Pagination: &sdkws.RequestPagination{}}
fn := func(resp *friend.GetPaginationFriendsApplyFromResp) []*sdkws.FriendRequest {
return resp.FriendRequests
}
requests, err := util.GetPageAll(ctx, constant.GetSelfFriendApplicationListRouter, req, fn)
if err != nil {
return err
}
localData, err := f.db.GetSendFriendApplication(ctx)
if err != nil {
return err
}
return f.requestSendSyncer.Sync(ctx, datautil.Batch(ServerFriendRequestToLocalFriendRequest, requests), localData, nil)
}
// recv
func (f *Friend) SyncAllFriendApplication(ctx context.Context) error {
req := &friend.GetPaginationFriendsApplyToReq{UserID: f.loginUserID, Pagination: &sdkws.RequestPagination{}}
fn := func(resp *friend.GetPaginationFriendsApplyToResp) []*sdkws.FriendRequest { return resp.FriendRequests }
requests, err := util.GetPageAll(ctx, constant.GetFriendApplicationListRouter, req, fn)
if err != nil {
return err
}
localData, err := f.db.GetRecvFriendApplication(ctx)
if err != nil {
return err
}
return f.requestRecvSyncer.Sync(ctx, datautil.Batch(ServerFriendRequestToLocalFriendRequest, requests), localData, nil)
}
func (f *Friend) SyncAllFriendList(ctx context.Context) error {
t := time.Now()
defer func(start time.Time) {
elapsed := time.Since(start).Milliseconds()
log.ZDebug(ctx, "SyncAllFriendList fn call end", "cost time", fmt.Sprintf("%d ms", elapsed))
}(t)
return f.IncrSyncFriends(ctx)
//req := &friend.GetPaginationFriendsReq{UserID: f.loginUserID, Pagination: &sdkws.RequestPagination{}}
//fn := func(resp *friend.GetPaginationFriendsResp) []*sdkws.FriendInfo { return resp.FriendsInfo }
//friends, err := util.GetPageAll(ctx, constant.GetFriendListRouter, req, fn)
//if err != nil {
// return err
//}
//localData, err := f.db.GetAllFriendList(ctx)
//if err != nil {
// return err
//}
//log.ZDebug(ctx, "sync friend", "data from server", friends, "data from local", localData)
//return f.friendSyncer.Sync(ctx, util.Batch(ServerFriendToLocalFriend, friends), localData, nil)
}
func (f *Friend) deleteFriend(ctx context.Context, friendUserID string) error {
return f.IncrSyncFriends(ctx)
//friends, err := f.db.GetFriendInfoList(ctx, []string{friendUserID})
//if err != nil {
// return err
//}
//if len(friends) == 0 {
// return sdkerrs.ErrUserIDNotFound.WrapMsg("friendUserID not found")
//}
//if err := f.db.DeleteFriendDB(ctx, friendUserID); err != nil {
// return err
//}
//f.friendListener.OnFriendDeleted(*friends[0])
//return nil
}
func (f *Friend) SyncFriends(ctx context.Context, friendIDs []string) error {
return f.IncrSyncFriends(ctx)
//var resp friend.GetDesignatedFriendsResp
//if err := util.ApiPost(ctx, constant.GetDesignatedFriendsRouter, &friend.GetDesignatedFriendsReq{OwnerUserID: f.loginUserID, FriendUserIDs: friendIDs}, &resp); err != nil {
// return err
//}
//localData, err := f.db.GetFriendInfoList(ctx, friendIDs)
//if err != nil {
// return err
//}
//log.ZDebug(ctx, "sync friend", "data from server", resp.FriendsInfo, "data from local", localData)
//return f.friendSyncer.Sync(ctx, util.Batch(ServerFriendToLocalFriend, resp.FriendsInfo), localData, nil)
}
//func (f *Friend) SyncFriendPart(ctx context.Context) error {
// hashResp, err := util.CallApi[friend.GetFriendHashResp](ctx, constant.GetFriendHash, &friend.GetFriendHashReq{UserID: f.loginUserID})
// if err != nil {
// return err
// }
// friends, err := f.db.GetAllFriendList(ctx)
// if err != nil {
// return err
// }
// hashCode := f.CalculateHash(friends)
// log.ZDebug(ctx, "SyncFriendPart", "serverHash", hashResp.Hash, "serverTotal", hashResp.Total, "localHash", hashCode, "localTotal", len(friends))
// if hashCode == hashResp.Hash {
// return nil
// }
// req := &friend.GetPaginationFriendsReq{
// UserID: f.loginUserID,
// Pagination: &sdkws.RequestPagination{PageNumber: pconstant.FirstPageNumber, ShowNumber: pconstant.MaxSyncPullNumber},
// }
// resp, err := util.CallApi[friend.GetPaginationFriendsResp](ctx, constant.GetFriendListRouter, req)
// if err != nil {
// return err
// }
// serverFriends := util.Batch(ServerFriendToLocalFriend, resp.FriendsInfo)
// return f.friendSyncer.Sync(ctx, serverFriends, friends, nil)
//}
func (f *Friend) SyncAllBlackList(ctx context.Context) error {
req := &friend.GetPaginationBlacksReq{UserID: f.loginUserID, Pagination: &sdkws.RequestPagination{}}
fn := func(resp *friend.GetPaginationBlacksResp) []*sdkws.BlackInfo { return resp.Blacks }
serverData, err := util.GetPageAll(ctx, constant.GetBlackListRouter, req, fn)
if err != nil {
return err
}
log.ZDebug(ctx, "black from server", "data", serverData)
localData, err := f.db.GetBlackListDB(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "black from local", "data", localData)
return f.blockSyncer.Sync(ctx, datautil.Batch(ServerBlackToLocalBlack, serverData), localData, nil)
}
func (f *Friend) GetDesignatedFriends(ctx context.Context, friendIDs []string) ([]*sdkws.FriendInfo, error) {
resp := &friend.GetDesignatedFriendsResp{}
if err := util.ApiPost(ctx, constant.GetDesignatedFriendsRouter, &friend.GetDesignatedFriendsReq{OwnerUserID: f.loginUserID, FriendUserIDs: friendIDs}, &resp); err != nil {
return nil, err
}
return resp.FriendsInfo, nil
}

@ -0,0 +1,72 @@
package friend
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/incrversion"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
friend "github.com/openimsdk/protocol/relation"
"github.com/openimsdk/tools/utils/datautil"
)
const (
LocalFriendSyncMaxNum = 1000
)
func (f *Friend) IncrSyncFriends(ctx context.Context) error {
friendSyncer := incrversion.VersionSynchronizer[*model_struct.LocalFriend, *friend.GetIncrementalFriendsResp]{
Ctx: ctx,
DB: f.db,
TableName: f.friendListTableName(),
EntityID: f.loginUserID,
Key: func(localFriend *model_struct.LocalFriend) string {
return localFriend.FriendUserID
},
Local: func() ([]*model_struct.LocalFriend, error) {
return f.db.GetAllFriendList(ctx)
},
Server: func(version *model_struct.LocalVersionSync) (*friend.GetIncrementalFriendsResp, error) {
return util.CallApi[friend.GetIncrementalFriendsResp](ctx, constant.GetIncrementalFriends, &friend.GetIncrementalFriendsReq{
UserID: f.loginUserID,
Version: version.Version,
VersionID: version.VersionID,
})
},
Full: func(resp *friend.GetIncrementalFriendsResp) bool {
return resp.Full
},
Version: func(resp *friend.GetIncrementalFriendsResp) (string, uint64) {
return resp.VersionID, resp.Version
},
Delete: func(resp *friend.GetIncrementalFriendsResp) []string {
return resp.Delete
},
Update: func(resp *friend.GetIncrementalFriendsResp) []*model_struct.LocalFriend {
return datautil.Batch(ServerFriendToLocalFriend, resp.Update)
},
Insert: func(resp *friend.GetIncrementalFriendsResp) []*model_struct.LocalFriend {
return datautil.Batch(ServerFriendToLocalFriend, resp.Insert)
},
Syncer: func(server, local []*model_struct.LocalFriend) error {
return f.friendSyncer.Sync(ctx, server, local, nil)
},
FullSyncer: func(ctx context.Context) error {
return f.friendSyncer.FullSync(ctx, f.loginUserID)
},
FullID: func(ctx context.Context) ([]string, error) {
resp, err := util.CallApi[friend.GetFullFriendUserIDsResp](ctx, constant.GetFullFriendUserIDs, &friend.GetFullFriendUserIDsReq{
UserID: f.loginUserID,
})
if err != nil {
return nil, err
}
return resp.UserIDs, nil
},
}
return friendSyncer.Sync()
}
func (f *Friend) friendListTableName() string {
return model_struct.LocalFriend{}.TableName()
}

@ -0,0 +1,13 @@
package friend
import (
"fmt"
"testing"
)
func Test_main(t *testing.T) {
a := []int{1, 2, 3, 4, 5}
fmt.Println(a[:3])
fmt.Println(a[3:])
fmt.Println(a[2:4])
}

@ -0,0 +1,62 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package full
import (
"context"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/internal/friend"
"github.com/openimsdk/openim-sdk-core/v3/internal/group"
"github.com/openimsdk/openim-sdk-core/v3/internal/user"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
)
type Full struct {
user *user.User
friend *friend.Friend
group *group.Group
ch chan common.Cmd2Value
db db_interface.DataBase
}
func (u *Full) Group() *group.Group {
return u.group
}
func NewFull(user *user.User, friend *friend.Friend, group *group.Group, ch chan common.Cmd2Value,
db db_interface.DataBase) *Full {
return &Full{user: user, friend: friend, group: group, ch: ch, db: db}
}
func (u *Full) GetGroupInfoFromLocal2Svr(ctx context.Context, groupID string, sessionType int32) (*model_struct.LocalGroup, error) {
switch sessionType {
case constant.GroupChatType:
return u.group.GetGroupInfoFromLocal2Svr(ctx, groupID)
case constant.SuperGroupChatType:
return u.GetGroupInfoByGroupID(ctx, groupID)
default:
return nil, fmt.Errorf("sessionType is not support %d", sessionType)
}
}
func (u *Full) GetReadDiffusionGroupIDList(ctx context.Context) ([]string, error) {
g, err := u.group.GetJoinedDiffusionGroupIDListFromSvr(ctx)
if err != nil {
return nil, err
}
return g, err
}

@ -0,0 +1,29 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package full
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
)
func (u *Full) GetGroupInfoByGroupID(ctx context.Context, groupID string) (*model_struct.LocalGroup, error) {
g2, err := u.group.GetGroupInfoFromLocal2Svr(ctx, groupID)
return g2, err
}
func (u *Full) GetGroupsInfo(ctx context.Context, groupIDs ...string) (map[string]*model_struct.LocalGroup, error) {
return u.group.GetGroupsInfoFromLocal2Svr(ctx, groupIDs...)
}

@ -0,0 +1,199 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package full
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
api "github.com/openimsdk/openim-sdk-core/v3/pkg/server_api_params"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (u *Full) GetUsersInfo(ctx context.Context, userIDs []string) ([]*api.FullUserInfo, error) {
friendList, err := u.db.GetFriendInfoList(ctx, userIDs)
if err != nil {
return nil, err
}
blackList, err := u.db.GetBlackInfoList(ctx, userIDs)
if err != nil {
return nil, err
}
users, err := u.user.GetServerUserInfo(ctx, userIDs)
if err != nil {
return nil, err
}
friendMap := make(map[string]*model_struct.LocalFriend)
for i, f := range friendList {
friendMap[f.FriendUserID] = friendList[i]
}
blackMap := make(map[string]*model_struct.LocalBlack)
for i, b := range blackList {
blackMap[b.BlockUserID] = blackList[i]
}
userMap := make(map[string]*api.PublicUser)
for _, info := range users {
userMap[info.UserID] = &api.PublicUser{
UserID: info.UserID,
Nickname: info.Nickname,
FaceURL: info.FaceURL,
Ex: info.Ex,
CreateTime: info.CreateTime,
}
}
res := make([]*api.FullUserInfo, 0, len(users))
for _, userID := range userIDs {
info, ok := userMap[userID]
if !ok {
continue
}
res = append(res, &api.FullUserInfo{
PublicInfo: info,
FriendInfo: friendMap[userID],
BlackInfo: blackMap[userID],
})
// update single conversation
conversation, err := u.db.GetConversationByUserID(ctx, userID)
if err != nil {
log.ZWarn(ctx, "GetConversationByUserID failed", err, "userID", userID)
} else {
if _, ok := friendMap[userID]; ok {
continue
}
log.ZDebug(ctx, "GetConversationByUserID", "conversation", conversation)
if conversation.ShowName != info.Nickname || conversation.FaceURL != info.FaceURL {
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{SourceID: userID, SessionType: conversation.ConversationType, FaceURL: info.FaceURL, Nickname: info.Nickname}}, u.ch)
_ = common.TriggerCmdUpdateMessage(ctx, common.UpdateMessageNode{Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{SessionType: conversation.ConversationType, UserID: userID, FaceURL: info.FaceURL, Nickname: info.Nickname}}, u.ch)
}
}
}
return res, nil
}
func (u *Full) GetUsersInfoWithCache(ctx context.Context, userIDs []string, groupID string) ([]*api.FullUserInfoWithCache, error) {
friendList, err := u.db.GetFriendInfoList(ctx, userIDs)
if err != nil {
return nil, err
}
blackList, err := u.db.GetBlackInfoList(ctx, userIDs)
if err != nil {
return nil, err
}
users, err := u.user.GetServerUserInfo(ctx, userIDs)
if err == nil {
var strangers []*model_struct.LocalStranger
for _, val := range users {
strangerTemp := &model_struct.LocalStranger{
UserID: val.UserID,
Nickname: val.Nickname,
FaceURL: val.FaceURL,
CreateTime: val.CreateTime,
AppMangerLevel: val.AppMangerLevel,
Ex: val.Ex,
AttachedInfo: val.Ex,
GlobalRecvMsgOpt: val.GlobalRecvMsgOpt,
}
strangers = append(strangers, strangerTemp)
}
err := u.db.SetStrangerInfo(ctx, strangers)
if err != nil {
return nil, err
}
} else {
strangerList, err := u.db.GetStrangerInfo(ctx, userIDs)
if err != nil {
return nil, err
}
for _, val := range strangerList {
userTemp := &sdkws.UserInfo{
UserID: val.UserID,
Nickname: val.Nickname,
FaceURL: val.FaceURL,
Ex: val.Ex,
CreateTime: val.CreateTime,
AppMangerLevel: val.AppMangerLevel,
GlobalRecvMsgOpt: val.GlobalRecvMsgOpt,
}
users = append(users, userTemp)
}
}
var groupMemberList []*model_struct.LocalGroupMember
if groupID != "" {
groupMemberList, err = u.db.GetGroupSomeMemberInfo(ctx, groupID, userIDs)
if err != nil {
return nil, err
}
}
friendMap := make(map[string]*model_struct.LocalFriend)
for i, f := range friendList {
friendMap[f.FriendUserID] = friendList[i]
}
blackMap := make(map[string]*model_struct.LocalBlack)
for i, b := range blackList {
blackMap[b.BlockUserID] = blackList[i]
}
groupMemberMap := make(map[string]*model_struct.LocalGroupMember)
for i, b := range groupMemberList {
groupMemberMap[b.UserID] = groupMemberList[i]
}
userMap := make(map[string]*api.PublicUser)
for _, info := range users {
userMap[info.UserID] = &api.PublicUser{
UserID: info.UserID,
Nickname: info.Nickname,
FaceURL: info.FaceURL,
Ex: info.Ex,
CreateTime: info.CreateTime,
}
}
res := make([]*api.FullUserInfoWithCache, 0, len(users))
for _, userID := range userIDs {
info, ok := userMap[userID]
if !ok {
continue
}
res = append(res, &api.FullUserInfoWithCache{
PublicInfo: info,
FriendInfo: friendMap[userID],
BlackInfo: blackMap[userID],
GroupMemberInfo: groupMemberMap[userID],
})
// update single conversation
conversation, err := u.db.GetConversationByUserID(ctx, userID)
if err != nil {
log.ZWarn(ctx, "GetConversationByUserID failed", err, "userID", userID)
} else {
if _, ok := friendMap[userID]; ok {
continue
}
log.ZDebug(ctx, "GetConversationByUserID", "conversation", conversation)
if conversation.ShowName != info.Nickname || conversation.FaceURL != info.FaceURL {
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{SourceID: userID, SessionType: conversation.ConversationType, FaceURL: info.FaceURL, Nickname: info.Nickname}}, u.ch)
_ = common.TriggerCmdUpdateMessage(ctx, common.UpdateMessageNode{Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{SessionType: conversation.ConversationType, UserID: userID, FaceURL: info.FaceURL, Nickname: info.Nickname}}, u.ch)
}
}
}
return res, nil
}

@ -0,0 +1,97 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package group
import (
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/sdkws"
)
func ServerGroupToLocalGroup(info *sdkws.GroupInfo) *model_struct.LocalGroup {
return &model_struct.LocalGroup{
GroupID: info.GroupID,
GroupName: info.GroupName,
Notification: info.Notification,
Introduction: info.Introduction,
FaceURL: info.FaceURL,
CreateTime: info.CreateTime,
Status: info.Status,
CreatorUserID: info.CreatorUserID,
GroupType: info.GroupType,
OwnerUserID: info.OwnerUserID,
MemberCount: int32(info.MemberCount),
Ex: info.Ex,
NeedVerification: info.NeedVerification,
LookMemberInfo: info.LookMemberInfo,
ApplyMemberFriend: info.ApplyMemberFriend,
NotificationUpdateTime: info.NotificationUpdateTime,
NotificationUserID: info.NotificationUserID,
//AttachedInfo: info.AttachedInfo, // TODO
}
}
func ServerGroupMemberToLocalGroupMember(info *sdkws.GroupMemberFullInfo) *model_struct.LocalGroupMember {
return &model_struct.LocalGroupMember{
GroupID: info.GroupID,
UserID: info.UserID,
Nickname: info.Nickname,
FaceURL: info.FaceURL,
RoleLevel: info.RoleLevel,
JoinTime: info.JoinTime,
JoinSource: info.JoinSource,
InviterUserID: info.InviterUserID,
MuteEndTime: info.MuteEndTime,
OperatorUserID: info.OperatorUserID,
Ex: info.Ex,
//AttachedInfo: info.AttachedInfo, // todo
}
}
func ServerGroupRequestToLocalGroupRequest(info *sdkws.GroupRequest) *model_struct.LocalGroupRequest {
return &model_struct.LocalGroupRequest{
GroupID: info.GroupInfo.GroupID,
GroupName: info.GroupInfo.GroupName,
Notification: info.GroupInfo.Notification,
Introduction: info.GroupInfo.Introduction,
GroupFaceURL: info.GroupInfo.FaceURL,
CreateTime: info.GroupInfo.CreateTime,
Status: info.GroupInfo.Status,
CreatorUserID: info.GroupInfo.CreatorUserID,
GroupType: info.GroupInfo.GroupType,
OwnerUserID: info.GroupInfo.OwnerUserID,
MemberCount: int32(info.GroupInfo.MemberCount),
UserID: info.UserInfo.UserID,
Nickname: info.UserInfo.Nickname,
UserFaceURL: info.UserInfo.FaceURL,
//Gender: info.UserInfo.Gender,
HandleResult: info.HandleResult,
ReqMsg: info.ReqMsg,
HandledMsg: info.HandleMsg,
ReqTime: info.ReqTime,
HandleUserID: info.HandleUserID,
HandledTime: info.HandleTime,
Ex: info.Ex,
//AttachedInfo: info.AttachedInfo,
JoinSource: info.JoinSource,
InviterUserID: info.InviterUserID,
}
}
func ServerGroupRequestToLocalAdminGroupRequest(info *sdkws.GroupRequest) *model_struct.LocalAdminGroupRequest {
return &model_struct.LocalAdminGroupRequest{
LocalGroupRequest: *ServerGroupRequestToLocalGroupRequest(info),
}
}

@ -0,0 +1,343 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package group
import (
"context"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/page"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/syncer"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/protocol/group"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
)
func NewGroup(loginUserID string, db db_interface.DataBase,
conversationCh chan common.Cmd2Value) *Group {
g := &Group{
loginUserID: loginUserID,
db: db,
conversationCh: conversationCh,
}
g.initSyncer()
return g
}
// //utils.GetCurrentTimestampByMill()
type Group struct {
listener func() open_im_sdk_callback.OnGroupListener
loginUserID string
db db_interface.DataBase
groupSyncer *syncer.Syncer[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string]
groupMemberSyncer *syncer.Syncer[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string]
groupRequestSyncer *syncer.Syncer[*model_struct.LocalGroupRequest, syncer.NoResp, [2]string]
groupAdminRequestSyncer *syncer.Syncer[*model_struct.LocalAdminGroupRequest, syncer.NoResp, [2]string]
joinedSuperGroupCh chan common.Cmd2Value
heartbeatCmdCh chan common.Cmd2Value
conversationCh chan common.Cmd2Value
// memberSyncMutex sync.RWMutex
listenerForService open_im_sdk_callback.OnListenerForService
}
func (g *Group) initSyncer() {
g.groupSyncer = syncer.New2[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](
syncer.WithInsert[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, value *model_struct.LocalGroup) error {
return g.db.InsertGroup(ctx, value)
}),
syncer.WithDelete[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, value *model_struct.LocalGroup) error {
if err := g.db.DeleteGroupAllMembers(ctx, value.GroupID); err != nil {
return err
}
if err := g.db.DeleteVersionSync(ctx, g.groupAndMemberVersionTableName(), value.GroupID); err != nil {
return err
}
return g.db.DeleteGroup(ctx, value.GroupID)
}),
syncer.WithUpdate[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, server, local *model_struct.LocalGroup) error {
log.ZInfo(ctx, "groupSyncer trigger update function", "groupID", server.GroupID, "server", server, "local", local)
return g.db.UpdateGroup(ctx, server)
}),
syncer.WithUUID[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(value *model_struct.LocalGroup) string {
return value.GroupID
}),
syncer.WithNotice[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, state int, server, local *model_struct.LocalGroup) error {
switch state {
case syncer.Insert:
// when a user kicked to the group and invited to the group again, group info maybe updated,
// so conversation info need to be updated
g.listener().OnJoinedGroupAdded(utils.StructToJsonString(server))
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{
Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{
SourceID: server.GroupID, SessionType: constant.SuperGroupChatType,
FaceURL: server.FaceURL, Nickname: server.GroupName,
},
}, g.conversationCh)
case syncer.Delete:
g.listener().OnJoinedGroupDeleted(utils.StructToJsonString(local))
case syncer.Update:
log.ZInfo(ctx, "groupSyncer trigger update", "groupID",
server.GroupID, "data", server, "isDismissed", server.Status == constant.GroupStatusDismissed)
if server.Status == constant.GroupStatusDismissed {
if err := g.db.DeleteGroupAllMembers(ctx, server.GroupID); err != nil {
log.ZError(ctx, "delete group all members failed", err)
}
g.listener().OnGroupDismissed(utils.StructToJsonString(server))
} else {
g.listener().OnGroupInfoChanged(utils.StructToJsonString(server))
if server.GroupName != local.GroupName || local.FaceURL != server.FaceURL {
_ = common.TriggerCmdUpdateConversation(ctx, common.UpdateConNode{
Action: constant.UpdateConFaceUrlAndNickName,
Args: common.SourceIDAndSessionType{
SourceID: server.GroupID, SessionType: constant.SuperGroupChatType,
FaceURL: server.FaceURL, Nickname: server.GroupName,
},
}, g.conversationCh)
}
}
}
return nil
}),
syncer.WithBatchInsert[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, values []*model_struct.LocalGroup) error {
return g.db.BatchInsertGroup(ctx, values)
}),
syncer.WithDeleteAll[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(ctx context.Context, _ string) error {
return g.db.DeleteAllGroup(ctx)
}),
syncer.WithBatchPageReq[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(entityID string) page.PageReq {
return &group.GetJoinedGroupListReq{FromUserID: entityID,
Pagination: &sdkws.RequestPagination{ShowNumber: 100}}
}),
syncer.WithBatchPageRespConvertFunc[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](func(resp *group.GetJoinedGroupListResp) []*model_struct.LocalGroup {
return datautil.Batch(ServerGroupToLocalGroup, resp.Groups)
}),
syncer.WithReqApiRouter[*model_struct.LocalGroup, group.GetJoinedGroupListResp, string](constant.GetJoinedGroupListRouter),
)
g.groupMemberSyncer = syncer.New2[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](
syncer.WithInsert[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, value *model_struct.LocalGroupMember) error {
return g.db.InsertGroupMember(ctx, value)
}),
syncer.WithDelete[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, value *model_struct.LocalGroupMember) error {
return g.db.DeleteGroupMember(ctx, value.GroupID, value.UserID)
}),
syncer.WithUpdate[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, server, local *model_struct.LocalGroupMember) error {
return g.db.UpdateGroupMember(ctx, server)
}),
syncer.WithUUID[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(value *model_struct.LocalGroupMember) [2]string {
return [...]string{value.GroupID, value.UserID}
}),
syncer.WithNotice[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, state int, server, local *model_struct.LocalGroupMember) error {
switch state {
case syncer.Insert:
g.listener().OnGroupMemberAdded(utils.StructToJsonString(server))
// When a user is kicked and invited to the group again, group member info will be updated.
_ = common.TriggerCmdUpdateMessage(ctx,
common.UpdateMessageNode{
Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{
SessionType: constant.SuperGroupChatType, UserID: server.UserID, FaceURL: server.FaceURL,
Nickname: server.Nickname, GroupID: server.GroupID,
},
}, g.conversationCh)
case syncer.Delete:
g.listener().OnGroupMemberDeleted(utils.StructToJsonString(local))
case syncer.Update:
g.listener().OnGroupMemberInfoChanged(utils.StructToJsonString(server))
if server.Nickname != local.Nickname || server.FaceURL != local.FaceURL {
_ = common.TriggerCmdUpdateMessage(ctx,
common.UpdateMessageNode{
Action: constant.UpdateMsgFaceUrlAndNickName,
Args: common.UpdateMessageInfo{
SessionType: constant.SuperGroupChatType, UserID: server.UserID, FaceURL: server.FaceURL,
Nickname: server.Nickname, GroupID: server.GroupID,
},
}, g.conversationCh)
}
}
return nil
}),
syncer.WithBatchInsert[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, values []*model_struct.LocalGroupMember) error {
return g.db.BatchInsertGroupMember(ctx, values)
}),
syncer.WithDeleteAll[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(ctx context.Context, groupID string) error {
return g.db.DeleteGroupAllMembers(ctx, groupID)
}),
syncer.WithBatchPageReq[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(entityID string) page.PageReq {
return &group.GetGroupMemberListReq{GroupID: entityID, Pagination: &sdkws.RequestPagination{ShowNumber: 100}}
}),
syncer.WithBatchPageRespConvertFunc[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](func(resp *group.GetGroupMemberListResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Members)
}),
syncer.WithReqApiRouter[*model_struct.LocalGroupMember, group.GetGroupMemberListResp, [2]string](constant.GetGroupMemberListRouter),
)
g.groupRequestSyncer = syncer.New[*model_struct.LocalGroupRequest, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalGroupRequest) error {
return g.db.InsertGroupRequest(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalGroupRequest) error {
return g.db.DeleteGroupRequest(ctx, value.GroupID, value.UserID)
}, func(ctx context.Context, server, local *model_struct.LocalGroupRequest) error {
return g.db.UpdateGroupRequest(ctx, server)
}, func(value *model_struct.LocalGroupRequest) [2]string {
return [...]string{value.GroupID, value.UserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalGroupRequest) error {
switch state {
case syncer.Insert:
g.listener().OnGroupApplicationAdded(utils.StructToJsonString(server))
case syncer.Update:
switch server.HandleResult {
case constant.FriendResponseAgree:
g.listener().OnGroupApplicationAccepted(utils.StructToJsonString(server))
case constant.FriendResponseRefuse:
g.listener().OnGroupApplicationRejected(utils.StructToJsonString(server))
default:
g.listener().OnGroupApplicationAdded(utils.StructToJsonString(server))
}
}
return nil
})
g.groupAdminRequestSyncer = syncer.New[*model_struct.LocalAdminGroupRequest, syncer.NoResp, [2]string](func(ctx context.Context, value *model_struct.LocalAdminGroupRequest) error {
return g.db.InsertAdminGroupRequest(ctx, value)
}, func(ctx context.Context, value *model_struct.LocalAdminGroupRequest) error {
return g.db.DeleteAdminGroupRequest(ctx, value.GroupID, value.UserID)
}, func(ctx context.Context, server, local *model_struct.LocalAdminGroupRequest) error {
return g.db.UpdateAdminGroupRequest(ctx, server)
}, func(value *model_struct.LocalAdminGroupRequest) [2]string {
return [...]string{value.GroupID, value.UserID}
}, nil, func(ctx context.Context, state int, server, local *model_struct.LocalAdminGroupRequest) error {
switch state {
case syncer.Insert:
g.listener().OnGroupApplicationAdded(utils.StructToJsonString(server))
case syncer.Update:
switch server.HandleResult {
case constant.FriendResponseAgree:
g.listener().OnGroupApplicationAccepted(utils.StructToJsonString(server))
case constant.FriendResponseRefuse:
g.listener().OnGroupApplicationRejected(utils.StructToJsonString(server))
default:
g.listener().OnGroupApplicationAdded(utils.StructToJsonString(server))
}
}
return nil
})
}
func (g *Group) SetGroupListener(listener func() open_im_sdk_callback.OnGroupListener) {
g.listener = listener
}
func (g *Group) SetListenerForService(listener open_im_sdk_callback.OnListenerForService) {
g.listenerForService = listener
}
func (g *Group) GetGroupOwnerIDAndAdminIDList(ctx context.Context, groupID string) (ownerID string, adminIDList []string, err error) {
localGroup, err := g.db.GetGroupInfoByGroupID(ctx, groupID)
if err != nil {
return "", nil, err
}
adminIDList, err = g.db.GetGroupAdminID(ctx, groupID)
if err != nil {
return "", nil, err
}
return localGroup.OwnerUserID, adminIDList, nil
}
func (g *Group) GetGroupInfoFromLocal2Svr(ctx context.Context, groupID string) (*model_struct.LocalGroup, error) {
localGroup, err := g.db.GetGroupInfoByGroupID(ctx, groupID)
if err == nil {
return localGroup, nil
}
svrGroup, err := g.getGroupsInfoFromSvr(ctx, []string{groupID})
if err != nil {
return nil, err
}
if len(svrGroup) == 0 {
return nil, sdkerrs.ErrGroupIDNotFound.WrapMsg("server not this group")
}
return ServerGroupToLocalGroup(svrGroup[0]), nil
}
func (g *Group) GetGroupsInfoFromLocal2Svr(ctx context.Context, groupIDs ...string) (map[string]*model_struct.LocalGroup, error) {
groupMap := make(map[string]*model_struct.LocalGroup)
if len(groupIDs) == 0 {
return groupMap, nil
}
groups, err := g.db.GetGroups(ctx, groupIDs)
if err != nil {
return nil, err
}
var groupIDsNeedSync []string
localGroupIDs := datautil.Slice(groups, func(group *model_struct.LocalGroup) string {
return group.GroupID
})
for _, groupID := range groupIDs {
if !datautil.Contain(groupID, localGroupIDs...) {
groupIDsNeedSync = append(groupIDsNeedSync, groupID)
}
}
if len(groupIDsNeedSync) > 0 {
svrGroups, err := g.getGroupsInfoFromSvr(ctx, groupIDsNeedSync)
if err != nil {
return nil, err
}
for _, svrGroup := range svrGroups {
groups = append(groups, ServerGroupToLocalGroup(svrGroup))
}
}
for _, group := range groups {
groupMap[group.GroupID] = group
}
return groupMap, nil
}
func (g *Group) getGroupsInfoFromSvr(ctx context.Context, groupIDs []string) ([]*sdkws.GroupInfo, error) {
resp, err := util.CallApi[group.GetGroupsInfoResp](ctx, constant.GetGroupsInfoRouter, &group.GetGroupsInfoReq{GroupIDs: groupIDs})
if err != nil {
return nil, err
}
return resp.GroupInfos, nil
}
func (g *Group) getGroupAbstractInfoFromSvr(ctx context.Context, groupIDs []string) (*group.GetGroupAbstractInfoResp, error) {
return util.CallApi[group.GetGroupAbstractInfoResp](ctx, constant.GetGroupAbstractInfoRouter, &group.GetGroupAbstractInfoReq{GroupIDs: groupIDs})
}
func (g *Group) GetJoinedDiffusionGroupIDListFromSvr(ctx context.Context) ([]string, error) {
groups, err := g.GetServerJoinGroup(ctx)
if err != nil {
return nil, err
}
var groupIDs []string
for _, g := range groups {
if g.GroupType == constant.WorkingGroup {
groupIDs = append(groupIDs, g.GroupID)
}
}
return groupIDs, nil
}

@ -0,0 +1,245 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package group
import (
"context"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
func (g *Group) DoNotification(ctx context.Context, msg *sdkws.MsgData) {
go func() {
if err := g.doNotification(ctx, msg); err != nil {
log.ZError(ctx, "DoGroupNotification failed", err)
}
}()
}
func (g *Group) doNotification(ctx context.Context, msg *sdkws.MsgData) error {
switch msg.ContentType {
case constant.GroupCreatedNotification: // 1501
var detail sdkws.GroupCreatedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, detail.Group.GroupID)
case constant.GroupInfoSetNotification: // 1502
var detail sdkws.GroupInfoSetTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
nil, nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.JoinGroupApplicationNotification: // 1503
var detail sdkws.JoinGroupApplicationTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if detail.Applicant.UserID == g.loginUserID {
return g.SyncSelfGroupApplications(ctx, detail.Group.GroupID)
} else {
return g.SyncAdminGroupApplications(ctx, detail.Group.GroupID)
}
case constant.MemberQuitNotification: // 1504
var detail sdkws.MemberQuitTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if detail.QuitUser.UserID == g.loginUserID {
return g.IncrSyncJoinGroup(ctx)
} else {
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, []*sdkws.GroupMemberFullInfo{detail.QuitUser},
nil, nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
}
case constant.GroupApplicationAcceptedNotification: // 1505
var detail sdkws.GroupApplicationAcceptedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
switch detail.ReceiverAs {
case 0:
return g.SyncAllSelfGroupApplication(ctx)
case 1:
return g.SyncAdminGroupApplications(ctx, detail.Group.GroupID)
default:
return errs.New(fmt.Sprintf("GroupApplicationAcceptedNotification ReceiverAs unknown %d", detail.ReceiverAs)).Wrap()
}
case constant.GroupApplicationRejectedNotification: // 1506
var detail sdkws.GroupApplicationRejectedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
switch detail.ReceiverAs {
case 0:
return g.SyncAllSelfGroupApplication(ctx)
case 1:
return g.SyncAdminGroupApplications(ctx, detail.Group.GroupID)
default:
return errs.New(fmt.Sprintf("GroupApplicationRejectedNotification ReceiverAs unknown %d", detail.ReceiverAs)).Wrap()
}
case constant.GroupOwnerTransferredNotification: // 1507
var detail sdkws.GroupOwnerTransferredTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if detail.Group == nil {
return errs.New(fmt.Sprintf("group is nil, groupID: %s", detail.Group.GroupID)).Wrap()
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.NewGroupOwner, detail.OldGroupOwnerInfo}, nil,
detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.MemberKickedNotification: // 1508
var detail sdkws.MemberKickedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
var self bool
for _, info := range detail.KickedUserList {
if info.UserID == g.loginUserID {
self = true
break
}
}
if self {
return g.IncrSyncJoinGroup(ctx)
} else {
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, detail.KickedUserList, nil,
nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
}
case constant.MemberInvitedNotification: // 1509
var detail sdkws.MemberInvitedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
userIDMap := datautil.SliceSetAny(detail.InvitedUserList, func(e *sdkws.GroupMemberFullInfo) string {
return e.UserID
})
//自己也是被邀请的一员
if _, ok := userIDMap[g.loginUserID]; ok {
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, detail.Group.GroupID)
} else {
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
detail.InvitedUserList, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
}
case constant.MemberEnterNotification: // 1510
var detail sdkws.MemberEnterTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
if detail.EntrantUser.UserID == g.loginUserID {
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, detail.Group.GroupID)
} else {
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
[]*sdkws.GroupMemberFullInfo{detail.EntrantUser}, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
}
case constant.GroupDismissedNotification: // 1511
var detail sdkws.GroupDismissedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
g.listener().OnGroupDismissed(utils.StructToJsonString(detail.Group))
return g.IncrSyncJoinGroup(ctx)
case constant.GroupMemberMutedNotification: // 1512
var detail sdkws.GroupMemberMutedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.MutedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMemberCancelMutedNotification: // 1513
var detail sdkws.GroupMemberCancelMutedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.MutedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMutedNotification: // 1514
var detail sdkws.GroupMutedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupCancelMutedNotification: // 1515
var detail sdkws.GroupCancelMutedTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMemberInfoSetNotification: // 1516
var detail sdkws.GroupMemberInfoSetTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.ChangedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMemberSetToAdminNotification: // 1517
var detail sdkws.GroupMemberInfoSetTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.ChangedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupMemberSetToOrdinaryUserNotification: // 1518
var detail sdkws.GroupMemberInfoSetTips
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
[]*sdkws.GroupMemberFullInfo{detail.ChangedUser}, nil, nil,
detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupInfoSetAnnouncementNotification: // 1519
var detail sdkws.GroupInfoSetAnnouncementTips //
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil, nil,
nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
case constant.GroupInfoSetNameNotification: // 1520
var detail sdkws.GroupInfoSetNameTips //
if err := utils.UnmarshalNotificationElem(msg.Content, &detail); err != nil {
return err
}
return g.onlineSyncGroupAndMember(ctx, detail.Group.GroupID, nil,
nil, nil, detail.Group, detail.GroupMemberVersion, detail.GroupMemberVersionID)
default:
return errs.New("unknown tips type", "contentType", msg.ContentType).Wrap()
}
}

@ -0,0 +1,383 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package group
import (
"context"
"time"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/openim-sdk-core/v3/pkg/datafetcher"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdk_params_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/protocol/group"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/protocol/wrapperspb"
)
func (g *Group) CreateGroup(ctx context.Context, req *group.CreateGroupReq) (*sdkws.GroupInfo, error) {
if req.OwnerUserID == "" {
req.OwnerUserID = g.loginUserID
}
if req.GroupInfo.GroupType != constant.WorkingGroup {
return nil, sdkerrs.ErrGroupType
}
req.GroupInfo.CreatorUserID = g.loginUserID
resp, err := util.CallApi[group.CreateGroupResp](ctx, constant.CreateGroupRouter, req)
if err != nil {
return nil, err
}
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return nil, err
}
if err := g.IncrSyncGroupAndMember(ctx, resp.GroupInfo.GroupID); err != nil {
return nil, err
}
return resp.GroupInfo, nil
}
func (g *Group) JoinGroup(ctx context.Context, groupID, reqMsg string, joinSource int32, ex string) error {
if err := util.ApiPost(ctx, constant.JoinGroupRouter, &group.JoinGroupReq{GroupID: groupID, ReqMessage: reqMsg, JoinSource: joinSource, InviterUserID: g.loginUserID, Ex: ex}, nil); err != nil {
return err
}
if err := g.SyncSelfGroupApplications(ctx, groupID); err != nil {
return err
}
return nil
}
func (g *Group) QuitGroup(ctx context.Context, groupID string) error {
if err := util.ApiPost(ctx, constant.QuitGroupRouter, &group.QuitGroupReq{GroupID: groupID}, nil); err != nil {
return err
}
return nil
}
func (g *Group) DismissGroup(ctx context.Context, groupID string) error {
if err := util.ApiPost(ctx, constant.DismissGroupRouter, &group.DismissGroupReq{GroupID: groupID}, nil); err != nil {
return err
}
return nil
}
func (g *Group) SetGroupApplyMemberFriend(ctx context.Context, groupID string, rule int32) error {
return g.SetGroupInfo(ctx, &sdkws.GroupInfoForSet{GroupID: groupID, ApplyMemberFriend: wrapperspb.Int32(rule)})
}
func (g *Group) SetGroupLookMemberInfo(ctx context.Context, groupID string, rule int32) error {
return g.SetGroupInfo(ctx, &sdkws.GroupInfoForSet{GroupID: groupID, LookMemberInfo: wrapperspb.Int32(rule)})
}
func (g *Group) SetGroupVerification(ctx context.Context, groupID string, verification int32) error {
return g.SetGroupInfo(ctx, &sdkws.GroupInfoForSet{GroupID: groupID, NeedVerification: wrapperspb.Int32(verification)})
}
func (g *Group) ChangeGroupMute(ctx context.Context, groupID string, isMute bool) (err error) {
if isMute {
err = util.ApiPost(ctx, constant.MuteGroupRouter, &group.MuteGroupReq{GroupID: groupID}, nil)
} else {
err = util.ApiPost(ctx, constant.CancelMuteGroupRouter, &group.CancelMuteGroupReq{GroupID: groupID}, nil)
}
if err != nil {
return err
}
if err := g.IncrSyncGroupAndMember(ctx, groupID); err != nil {
return err
}
return nil
}
func (g *Group) ChangeGroupMemberMute(ctx context.Context, groupID, userID string, mutedSeconds int) (err error) {
if mutedSeconds == 0 {
err = util.ApiPost(ctx, constant.CancelMuteGroupMemberRouter, &group.CancelMuteGroupMemberReq{GroupID: groupID, UserID: userID}, nil)
} else {
err = util.ApiPost(ctx, constant.MuteGroupMemberRouter, &group.MuteGroupMemberReq{GroupID: groupID, UserID: userID, MutedSeconds: uint32(mutedSeconds)}, nil)
}
if err != nil {
return err
}
return nil
}
func (g *Group) TransferGroupOwner(ctx context.Context, groupID, newOwnerUserID string) error {
if err := util.ApiPost(ctx, constant.TransferGroupRouter, &group.TransferGroupOwnerReq{GroupID: groupID, OldOwnerUserID: g.loginUserID, NewOwnerUserID: newOwnerUserID}, nil); err != nil {
return err
}
if err := g.IncrSyncGroupAndMember(ctx, groupID); err != nil {
return err
}
return nil
}
func (g *Group) KickGroupMember(ctx context.Context, groupID string, reason string, userIDList []string) error {
if err := util.ApiPost(ctx, constant.KickGroupMemberRouter, &group.KickGroupMemberReq{GroupID: groupID, KickedUserIDs: userIDList, Reason: reason}, nil); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, groupID)
}
func (g *Group) SetGroupInfo(ctx context.Context, groupInfo *sdkws.GroupInfoForSet) error {
if err := util.ApiPost(ctx, constant.SetGroupInfoRouter, &group.SetGroupInfoReq{GroupInfoForSet: groupInfo}, nil); err != nil {
return err
}
return g.IncrSyncJoinGroup(ctx)
}
func (g *Group) SetGroupMemberInfo(ctx context.Context, groupMemberInfo *group.SetGroupMemberInfo) error {
if err := util.ApiPost(ctx, constant.SetGroupMemberInfoRouter, &group.SetGroupMemberInfoReq{Members: []*group.SetGroupMemberInfo{groupMemberInfo}}, nil); err != nil {
return err
}
return g.IncrSyncGroupAndMember(ctx, groupMemberInfo.GroupID)
}
func (g *Group) SetGroupMemberRoleLevel(ctx context.Context, groupID, userID string, roleLevel int) error {
return g.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfo{GroupID: groupID, UserID: userID, RoleLevel: wrapperspb.Int32(int32(roleLevel))})
}
func (g *Group) SetGroupMemberNickname(ctx context.Context, groupID, userID string, groupMemberNickname string) error {
return g.SetGroupMemberInfo(ctx, &group.SetGroupMemberInfo{GroupID: groupID, UserID: userID, Nickname: wrapperspb.String(groupMemberNickname)})
}
func (g *Group) GetJoinedGroupList(ctx context.Context) ([]*model_struct.LocalGroup, error) {
return g.db.GetJoinedGroupListDB(ctx)
}
func (g *Group) GetJoinedGroupListPage(ctx context.Context, offset, count int32) ([]*model_struct.LocalGroup, error) {
dataFetcher := datafetcher.NewDataFetcher(
g.db,
g.groupTableName(),
g.loginUserID,
func(localGroup *model_struct.LocalGroup) string {
return localGroup.GroupID
},
func(ctx context.Context, values []*model_struct.LocalGroup) error {
return g.db.BatchInsertGroup(ctx, values)
},
func(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
return g.db.GetGroups(ctx, groupIDs)
},
func(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
serverGroupInfo, err := g.getGroupsInfoFromSvr(ctx, groupIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerGroupToLocalGroup, serverGroupInfo), nil
},
)
return dataFetcher.FetchWithPagination(ctx, int(offset), int(count))
}
func (g *Group) GetSpecifiedGroupsInfo(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
dataFetcher := datafetcher.NewDataFetcher(
g.db,
g.groupTableName(),
g.loginUserID,
func(localGroup *model_struct.LocalGroup) string {
return localGroup.GroupID
},
func(ctx context.Context, values []*model_struct.LocalGroup) error {
return g.db.BatchInsertGroup(ctx, values)
},
func(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
return g.db.GetGroups(ctx, groupIDs)
},
func(ctx context.Context, groupIDs []string) ([]*model_struct.LocalGroup, error) {
serverGroupInfo, err := g.getGroupsInfoFromSvr(ctx, groupIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerGroupToLocalGroup, serverGroupInfo), nil
},
)
return dataFetcher.FetchMissingAndFillLocal(ctx, groupIDs)
}
func (g *Group) SearchGroups(ctx context.Context, param sdk_params_callback.SearchGroupsParam) ([]*model_struct.LocalGroup, error) {
if len(param.KeywordList) == 0 || (!param.IsSearchGroupName && !param.IsSearchGroupID) {
return nil, sdkerrs.ErrArgs.WrapMsg("keyword is null or search field all false")
}
groups, err := g.db.GetAllGroupInfoByGroupIDOrGroupName(ctx, param.KeywordList[0], param.IsSearchGroupID, param.IsSearchGroupName) // todo param.KeywordList[0]
if err != nil {
return nil, err
}
return groups, nil
}
// funcation (g *Group) SetGroupInfo(ctx context.Context, groupInfo *sdk_params_callback.SetGroupInfoParam, groupID string) error {
// return g.SetGroupInfo(ctx, &sdkws.GroupInfoForSet{
// GroupID: groupID,
// GroupName: groupInfo.GroupName,
// Notification: groupInfo.Notification,
// Introduction: groupInfo.Introduction,
// FaceURL: groupInfo.FaceURL,
// Ex: groupInfo.Ex,
// NeedVerification: wrapperspb.Int32Ptr(groupInfo.NeedVerification),
// })
// }
func (g *Group) GetGroupMemberOwnerAndAdmin(ctx context.Context, groupID string) ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupMemberOwnerAndAdminDB(ctx, groupID)
}
func (g *Group) GetGroupMemberListByJoinTimeFilter(ctx context.Context, groupID string, offset, count int32, joinTimeBegin, joinTimeEnd int64, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
if joinTimeEnd == 0 {
joinTimeEnd = time.Now().UnixMilli()
}
return g.db.GetGroupMemberListSplitByJoinTimeFilter(ctx, groupID, int(offset), int(count), joinTimeBegin, joinTimeEnd, userIDs)
}
func (g *Group) GetSpecifiedGroupMembersInfo(ctx context.Context, groupID string, userIDList []string) ([]*model_struct.LocalGroupMember, error) {
dataFetcher := datafetcher.NewDataFetcher(
g.db,
g.groupAndMemberVersionTableName(),
groupID,
func(localGroupMember *model_struct.LocalGroupMember) string {
return localGroupMember.UserID
},
func(ctx context.Context, values []*model_struct.LocalGroupMember) error {
return g.db.BatchInsertGroupMember(ctx, values)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupSomeMemberInfo(ctx, groupID, userIDList)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
serverGroupMember, err := g.GetDesignatedGroupMembers(ctx, groupID, userIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerGroupMemberToLocalGroupMember, serverGroupMember), nil
},
)
return dataFetcher.FetchMissingAndFillLocal(ctx, userIDList)
// return g.db.GetGroupSomeMemberInfo(ctx, groupID, userIDList)
}
func (g *Group) GetGroupMemberList(ctx context.Context, groupID string, filter, offset, count int32) ([]*model_struct.LocalGroupMember, error) {
dataFetcher := datafetcher.NewDataFetcher(
g.db,
g.groupAndMemberVersionTableName(),
groupID,
func(localGroupMember *model_struct.LocalGroupMember) string {
return localGroupMember.UserID
},
func(ctx context.Context, values []*model_struct.LocalGroupMember) error {
return g.db.BatchInsertGroupMember(ctx, values)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupMemberListByUserIDs(ctx, groupID, filter, userIDs)
},
func(ctx context.Context, userIDs []string) ([]*model_struct.LocalGroupMember, error) {
serverGroupMember, err := g.GetDesignatedGroupMembers(ctx, groupID, userIDs)
if err != nil {
return nil, err
}
return datautil.Batch(ServerGroupMemberToLocalGroupMember, serverGroupMember), nil
},
)
return dataFetcher.FetchWithPagination(ctx, int(offset), int(count))
}
func (g *Group) GetGroupApplicationListAsRecipient(ctx context.Context) ([]*model_struct.LocalAdminGroupRequest, error) {
return g.db.GetAdminGroupApplication(ctx)
}
func (g *Group) GetGroupApplicationListAsApplicant(ctx context.Context) ([]*model_struct.LocalGroupRequest, error) {
return g.db.GetSendGroupApplication(ctx)
}
func (g *Group) SearchGroupMembers(ctx context.Context, searchParam *sdk_params_callback.SearchGroupMembersParam) ([]*model_struct.LocalGroupMember, error) {
return g.db.SearchGroupMembersDB(ctx, searchParam.KeywordList[0], searchParam.GroupID, searchParam.IsSearchMemberNickname, searchParam.IsSearchUserID, searchParam.Offset, searchParam.Count)
}
func (g *Group) IsJoinGroup(ctx context.Context, groupID string) (bool, error) {
groupList, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return false, err
}
for _, localGroup := range groupList {
if localGroup.GroupID == groupID {
return true, nil
}
}
return false, nil
}
func (g *Group) InviteUserToGroup(ctx context.Context, groupID, reason string, userIDList []string) error {
if err := util.ApiPost(ctx, constant.InviteUserToGroupRouter, &group.InviteUserToGroupReq{GroupID: groupID, Reason: reason, InvitedUserIDs: userIDList}, nil); err != nil {
return err
}
if err := g.IncrSyncGroupAndMember(ctx, groupID); err != nil {
return err
}
return nil
}
func (g *Group) AcceptGroupApplication(ctx context.Context, groupID, fromUserID, handleMsg string) error {
return g.HandlerGroupApplication(ctx, &group.GroupApplicationResponseReq{GroupID: groupID, FromUserID: fromUserID, HandledMsg: handleMsg, HandleResult: constant.GroupResponseAgree})
}
func (g *Group) RefuseGroupApplication(ctx context.Context, groupID, fromUserID, handleMsg string) error {
return g.HandlerGroupApplication(ctx, &group.GroupApplicationResponseReq{GroupID: groupID, FromUserID: fromUserID, HandledMsg: handleMsg, HandleResult: constant.GroupResponseRefuse})
}
func (g *Group) HandlerGroupApplication(ctx context.Context, req *group.GroupApplicationResponseReq) error {
if err := util.ApiPost(ctx, constant.AcceptGroupApplicationRouter, req, nil); err != nil {
return err
}
// SyncAdminGroupApplication todo
return nil
}
//func (g *Group) SearchGroupMembersV2(ctx context.Context, req *group.SearchGroupMemberReq) ([]*model_struct.LocalGroupMember, error) {
// if err := req.Check(); err != nil {
// return nil, err
// }
// info, err := g.db.GetGroupInfoByGroupID(ctx, req.GroupID)
// if err != nil {
// return nil, err
// }
// if info.MemberCount <= pconstant.MaxSyncPullNumber {
// return g.db.SearchGroupMembersDB(ctx, req.Keyword, req.GroupID, true, false,
// int((req.Pagination.PageNumber-1)*req.Pagination.ShowNumber), int(req.Pagination.ShowNumber))
// }
// resp, err := util.CallApi[group.SearchGroupMemberResp](ctx, constant.SearchGroupMember, req)
// if err != nil {
// return nil, err
// }
// return datautil.Slice(resp.Members, g.pbGroupMemberToLocal), nil
//}
func (g *Group) pbGroupMemberToLocal(pb *sdkws.GroupMemberFullInfo) *model_struct.LocalGroupMember {
return &model_struct.LocalGroupMember{
GroupID: pb.GroupID,
UserID: pb.UserID,
Nickname: pb.Nickname,
FaceURL: pb.FaceURL,
RoleLevel: pb.RoleLevel,
JoinTime: pb.JoinTime,
JoinSource: pb.JoinSource,
InviterUserID: pb.InviterUserID,
MuteEndTime: pb.MuteEndTime,
OperatorUserID: pb.OperatorUserID,
Ex: pb.Ex,
// AttachedInfo: pb.AttachedInfo,
}
}

@ -0,0 +1,310 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package group
import (
"context"
"crypto/md5"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/protocol/group"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"time"
)
func (g *Group) getGroupHash(members []*model_struct.LocalGroupMember) uint64 {
userIDs := datautil.Slice(members, func(member *model_struct.LocalGroupMember) string {
return member.UserID
})
datautil.Sort(userIDs, true)
memberMap := make(map[string]*sdkws.GroupMemberFullInfo)
for _, member := range members {
memberMap[member.UserID] = &sdkws.GroupMemberFullInfo{
GroupID: member.GroupID,
UserID: member.UserID,
RoleLevel: member.RoleLevel,
JoinTime: member.JoinTime,
Nickname: member.Nickname,
FaceURL: member.FaceURL,
AppMangerLevel: 0,
JoinSource: member.JoinSource,
OperatorUserID: member.OperatorUserID,
Ex: member.Ex,
MuteEndTime: member.MuteEndTime,
InviterUserID: member.InviterUserID,
}
}
res := make([]*sdkws.GroupMemberFullInfo, 0, len(members))
for _, userID := range userIDs {
res = append(res, memberMap[userID])
}
val, _ := json.Marshal(res)
sum := md5.Sum(val)
return binary.BigEndian.Uint64(sum[:])
}
func (g *Group) SyncAllGroupMember(ctx context.Context, groupID string) error {
absInfo, err := g.GetGroupAbstractInfo(ctx, groupID)
if err != nil {
return err
}
localData, err := g.db.GetGroupMemberListSplit(ctx, groupID, 0, 0, 9999999)
if err != nil {
return err
}
hashCode := g.getGroupHash(localData)
if len(localData) == int(absInfo.GroupMemberNumber) && hashCode == absInfo.GroupMemberListHash {
log.ZDebug(ctx, "SyncAllGroupMember no change in personnel", "groupID", groupID, "hashCode", hashCode, "absInfo.GroupMemberListHash", absInfo.GroupMemberListHash)
return nil
}
members, err := g.GetServerGroupMembers(ctx, groupID)
if err != nil {
return err
}
return g.syncGroupMembers(ctx, groupID, members, localData)
}
func (g *Group) SyncAllGroupMember2(ctx context.Context, groupID string) error {
return g.IncrSyncGroupAndMember(ctx, groupID)
}
func (g *Group) syncGroupMembers(ctx context.Context, groupID string, members []*sdkws.GroupMemberFullInfo, localData []*model_struct.LocalGroupMember) error {
log.ZInfo(ctx, "SyncGroupMember Info", "groupID", groupID, "members", len(members), "localData", len(localData))
err := g.groupMemberSyncer.Sync(ctx, datautil.Batch(ServerGroupMemberToLocalGroupMember, members), localData, nil)
if err != nil {
return err
}
//if len(members) != len(localData) {
log.ZInfo(ctx, "SyncGroupMember Sync Group Member Count", "groupID", groupID, "members", len(members), "localData", len(localData))
gs, err := g.GetSpecifiedGroupsInfo(ctx, []string{groupID})
if err != nil {
return err
}
log.ZInfo(ctx, "SyncGroupMember GetGroupsInfo", "groupID", groupID, "len", len(gs), "gs", gs)
if len(gs) > 0 {
v := gs[0]
count, err := g.db.GetGroupMemberCount(ctx, groupID)
if err != nil {
return err
}
if v.MemberCount != count {
v.MemberCount = count
if v.GroupType == constant.SuperGroupChatType {
if err := g.db.UpdateSuperGroup(ctx, v); err != nil {
//return err
log.ZError(ctx, "SyncGroupMember UpdateSuperGroup", err, "groupID", groupID, "info", v)
}
} else {
if err := g.db.UpdateGroup(ctx, v); err != nil {
log.ZError(ctx, "SyncGroupMember UpdateGroup", err, "groupID", groupID, "info", v)
}
}
data, err := json.Marshal(v)
if err != nil {
return err
}
log.ZInfo(ctx, "SyncGroupMember OnGroupInfoChanged", "groupID", groupID, "data", string(data))
g.listener().OnGroupInfoChanged(string(data))
}
}
//}
return nil
}
func (g *Group) SyncGroupMembers(ctx context.Context, groupID string, userIDs ...string) error {
return g.IncrSyncGroupAndMember(ctx, groupID)
//members, err := g.GetDesignatedGroupMembers(ctx, groupID, userIDs)
//if err != nil {
// return err
//}
//localData, err := g.db.GetGroupSomeMemberInfo(ctx, groupID, userIDs)
//if err != nil {
// return err
//}
//return g.syncGroupMembers(ctx, groupID, members, localData)
}
func (g *Group) SyncGroups(ctx context.Context, groupIDs ...string) error {
return g.IncrSyncJoinGroup(ctx)
//groups, err := g.getGroupsInfoFromSvr(ctx, groupIDs)
//if err != nil {
// return err
//}
//localData, err := g.db.GetGroups(ctx, groupIDs)
//if err != nil {
// return err
//}
//if err := g.groupSyncer.Sync(ctx, util.Batch(ServerGroupToLocalGroup, groups), localData, nil); err != nil {
// return err
//}
//return nil
}
func (g *Group) deleteGroup(ctx context.Context, groupID string) error {
return g.IncrSyncJoinGroup(ctx)
//groupInfo, err := g.db.GetGroupInfoByGroupID(ctx, groupID)
//if err != nil {
// return err
//}
//if err := g.db.DeleteGroup(ctx, groupID); err != nil {
// return err
//}
//g.listener().OnJoinedGroupDeleted(utils.StructToJsonString(groupInfo))
//return nil
}
// func (g *Group) SyncAllJoinedGroupsAndMembers(ctx context.Context) error {
// t := time.Now()
// defer func(start time.Time) {
//
// elapsed := time.Since(start).Milliseconds()
// log.ZDebug(ctx, "SyncAllJoinedGroupsAndMembers fn call end", "cost time", fmt.Sprintf("%d ms", elapsed))
//
// }(t)
// _, err := g.syncAllJoinedGroups(ctx)
// if err != nil {
// return err
// }
// groups, err := g.db.GetJoinedGroupListDB(ctx)
// if err != nil {
// return err
// }
// var wg sync.WaitGroup
// for _, group := range groups {
// wg.Add(1)
// go func(groupID string) {
// defer wg.Done()
// if err := g.SyncAllGroupMember(ctx, groupID); err != nil {
// log.ZError(ctx, "SyncGroupMember failed", err)
// }
// }(group.GroupID)
// }
// wg.Wait()
// return nil
// }
func (g *Group) SyncAllJoinedGroupsAndMembers(ctx context.Context) error {
t := time.Now()
defer func(start time.Time) {
elapsed := time.Since(start).Milliseconds()
log.ZDebug(ctx, "SyncAllJoinedGroupsAndMembers fn call end", "cost time", fmt.Sprintf("%d ms", elapsed))
}(t)
if err := g.IncrSyncJoinGroup(ctx); err != nil {
return err
}
return g.IncrSyncJoinGroupMember(ctx)
}
func (g *Group) syncAllJoinedGroups(ctx context.Context) ([]*sdkws.GroupInfo, error) {
groups, err := g.GetServerJoinGroup(ctx)
if err != nil {
return nil, err
}
localData, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return nil, err
}
if err := g.groupSyncer.Sync(ctx, datautil.Batch(ServerGroupToLocalGroup, groups), localData, nil); err != nil {
return nil, err
}
return groups, nil
}
func (g *Group) SyncAllSelfGroupApplication(ctx context.Context) error {
list, err := g.GetServerSelfGroupApplication(ctx)
if err != nil {
return err
}
localData, err := g.db.GetSendGroupApplication(ctx)
if err != nil {
return err
}
if err := g.groupRequestSyncer.Sync(ctx, datautil.Batch(ServerGroupRequestToLocalGroupRequest, list), localData, nil); err != nil {
return err
}
// todo
return nil
}
func (g *Group) SyncSelfGroupApplications(ctx context.Context, groupIDs ...string) error {
return g.SyncAllSelfGroupApplication(ctx)
}
func (g *Group) SyncAllAdminGroupApplication(ctx context.Context) error {
requests, err := g.GetServerAdminGroupApplicationList(ctx)
if err != nil {
return err
}
localData, err := g.db.GetAdminGroupApplication(ctx)
if err != nil {
return err
}
return g.groupAdminRequestSyncer.Sync(ctx, datautil.Batch(ServerGroupRequestToLocalAdminGroupRequest, requests), localData, nil)
}
func (g *Group) SyncAdminGroupApplications(ctx context.Context, groupIDs ...string) error {
return g.SyncAllAdminGroupApplication(ctx)
}
func (g *Group) GetServerJoinGroup(ctx context.Context) ([]*sdkws.GroupInfo, error) {
fn := func(resp *group.GetJoinedGroupListResp) []*sdkws.GroupInfo { return resp.Groups }
req := &group.GetJoinedGroupListReq{FromUserID: g.loginUserID, Pagination: &sdkws.RequestPagination{}}
return util.GetPageAll(ctx, constant.GetJoinedGroupListRouter, req, fn)
}
func (g *Group) GetServerAdminGroupApplicationList(ctx context.Context) ([]*sdkws.GroupRequest, error) {
fn := func(resp *group.GetGroupApplicationListResp) []*sdkws.GroupRequest { return resp.GroupRequests }
req := &group.GetGroupApplicationListReq{FromUserID: g.loginUserID, Pagination: &sdkws.RequestPagination{}}
return util.GetPageAll(ctx, constant.GetRecvGroupApplicationListRouter, req, fn)
}
func (g *Group) GetServerSelfGroupApplication(ctx context.Context) ([]*sdkws.GroupRequest, error) {
fn := func(resp *group.GetGroupApplicationListResp) []*sdkws.GroupRequest { return resp.GroupRequests }
req := &group.GetUserReqApplicationListReq{UserID: g.loginUserID, Pagination: &sdkws.RequestPagination{}}
return util.GetPageAll(ctx, constant.GetSendGroupApplicationListRouter, req, fn)
}
func (g *Group) GetServerGroupMembers(ctx context.Context, groupID string) ([]*sdkws.GroupMemberFullInfo, error) {
req := &group.GetGroupMemberListReq{GroupID: groupID, Pagination: &sdkws.RequestPagination{}}
fn := func(resp *group.GetGroupMemberListResp) []*sdkws.GroupMemberFullInfo { return resp.Members }
return util.GetPageAll(ctx, constant.GetGroupMemberListRouter, req, fn)
}
func (g *Group) GetDesignatedGroupMembers(ctx context.Context, groupID string, userID []string) ([]*sdkws.GroupMemberFullInfo, error) {
resp := &group.GetGroupMembersInfoResp{}
if err := util.ApiPost(ctx, constant.GetGroupMembersInfoRouter, &group.GetGroupMembersInfoReq{GroupID: groupID, UserIDs: userID}, resp); err != nil {
return nil, err
}
return resp.Members, nil
}
func (g *Group) GetGroupAbstractInfo(ctx context.Context, groupID string) (*group.GroupAbstractInfo, error) {
resp, err := util.CallApi[group.GetGroupAbstractInfoResp](ctx, constant.GetGroupAbstractInfoRouter, &group.GetGroupAbstractInfoReq{GroupIDs: []string{groupID}})
if err != nil {
return nil, err
}
if len(resp.GroupAbstractInfos) == 0 {
return nil, errors.New("group not found")
}
return resp.GroupAbstractInfos[0], nil
}

@ -0,0 +1,347 @@
package group
import (
"context"
"sync"
"gorm.io/gorm"
"github.com/openimsdk/openim-sdk-core/v3/internal/incrversion"
"github.com/openimsdk/openim-sdk-core/v3/internal/util"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
constantpb "github.com/openimsdk/protocol/constant"
"github.com/openimsdk/protocol/group"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
)
type BatchIncrementalReq struct {
UserID string `json:"user_id"`
List []*group.GetIncrementalGroupMemberReq `json:"list"`
}
type BatchIncrementalResp struct {
List map[string]*group.GetIncrementalGroupMemberResp `json:"list"`
}
func (g *Group) getIncrementalGroupMemberBatch(ctx context.Context, groups []*group.GetIncrementalGroupMemberReq) (map[string]*group.GetIncrementalGroupMemberResp, error) {
resp, err := util.CallApi[BatchIncrementalResp](ctx, constant.GetIncrementalGroupMemberBatch, &BatchIncrementalReq{UserID: g.loginUserID, List: groups})
if err != nil {
return nil, err
}
return resp.List, nil
}
func (g *Group) groupAndMemberVersionTableName() string {
return "local_group_entities_version"
}
func (g *Group) groupTableName() string {
return model_struct.LocalGroup{}.TableName()
}
func (g *Group) IncrSyncJoinGroupMember(ctx context.Context) error {
groups, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return err
}
groupIDs := datautil.Slice(groups, func(e *model_struct.LocalGroup) string {
return e.GroupID
})
return g.IncrSyncGroupAndMember(ctx, groupIDs...)
}
func (g *Group) IncrSyncGroupAndMember(ctx context.Context, groupIDs ...string) error {
var wg sync.WaitGroup
if len(groupIDs) == 0 {
return nil
}
const maxSyncNum = constantpb.MaxSyncPullNumber
groupIDSet := datautil.SliceSet(groupIDs)
var groups []*group.GetIncrementalGroupMemberReq
if len(groupIDs) > maxSyncNum {
groups = make([]*group.GetIncrementalGroupMemberReq, 0, maxSyncNum)
} else {
groups = make([]*group.GetIncrementalGroupMemberReq, 0, len(groupIDs))
}
for {
if len(groupIDSet) == 0 {
return nil
}
for groupID := range groupIDSet {
if len(groups) == cap(groups) {
break
}
req := group.GetIncrementalGroupMemberReq{
GroupID: groupID,
}
lvs, err := g.db.GetVersionSync(ctx, g.groupAndMemberVersionTableName(), groupID)
if err == nil {
req.VersionID = lvs.VersionID
req.Version = lvs.Version
} else if errs.Unwrap(err) != gorm.ErrRecordNotFound {
return err
}
groups = append(groups, &req)
}
groupVersion, err := g.getIncrementalGroupMemberBatch(ctx, groups)
if err != nil {
return err
}
groups = groups[:0]
for groupID, resp := range groupVersion {
tempResp := resp
tempGroupID := groupID
wg.Add(1)
go func() error {
if err := g.syncGroupAndMember(ctx, tempGroupID, tempResp); err != nil {
return err
}
wg.Done()
return nil
}()
delete(groupIDSet, tempGroupID)
}
wg.Wait()
num := len(groupIDSet)
_ = num
}
}
func (g *Group) syncGroupAndMember(ctx context.Context, groupID string, resp *group.GetIncrementalGroupMemberResp) error {
groupMemberSyncer := incrversion.VersionSynchronizer[*model_struct.LocalGroupMember, *group.GetIncrementalGroupMemberResp]{
Ctx: ctx,
DB: g.db,
TableName: g.groupAndMemberVersionTableName(),
EntityID: groupID,
Key: func(localGroupMember *model_struct.LocalGroupMember) string {
return localGroupMember.UserID
},
Local: func() ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupMemberListByGroupID(ctx, groupID)
},
ServerVersion: func() *group.GetIncrementalGroupMemberResp {
return resp
},
Full: func(resp *group.GetIncrementalGroupMemberResp) bool {
return resp.Full
},
Version: func(resp *group.GetIncrementalGroupMemberResp) (string, uint64) {
return resp.VersionID, resp.Version
},
Delete: func(resp *group.GetIncrementalGroupMemberResp) []string {
return resp.Delete
},
Update: func(resp *group.GetIncrementalGroupMemberResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Update)
},
Insert: func(resp *group.GetIncrementalGroupMemberResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Insert)
},
ExtraData: func(resp *group.GetIncrementalGroupMemberResp) any {
return resp.Group
},
ExtraDataProcessor: func(ctx context.Context, data any) error {
groupInfo, ok := data.(*sdkws.GroupInfo)
if !ok {
return errs.New("group info type error")
}
if groupInfo == nil {
return nil
}
local, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "group info", "groupInfo", groupInfo)
changes := datautil.Batch(ServerGroupToLocalGroup, []*sdkws.GroupInfo{groupInfo})
kv := datautil.SliceToMapAny(local, func(e *model_struct.LocalGroup) (string, *model_struct.LocalGroup) {
return e.GroupID, e
})
for i, change := range changes {
key := change.GroupID
kv[key] = changes[i]
}
server := datautil.Values(kv)
return g.groupSyncer.Sync(ctx, server, local, nil)
},
Syncer: func(server, local []*model_struct.LocalGroupMember) error {
return g.groupMemberSyncer.Sync(ctx, server, local, nil)
},
FullSyncer: func(ctx context.Context) error {
return g.groupMemberSyncer.FullSync(ctx, groupID)
},
FullID: func(ctx context.Context) ([]string, error) {
resp, err := util.CallApi[group.GetFullGroupMemberUserIDsResp](ctx, constant.GetFullGroupMemberUserIDs, &group.GetFullGroupMemberUserIDsReq{
GroupID: groupID,
})
if err != nil {
return nil, err
}
return resp.UserIDs, nil
},
}
return groupMemberSyncer.Sync()
}
func (g *Group) onlineSyncGroupAndMember(ctx context.Context, groupID string, deleteGroupMembers, updateGroupMembers, insertGroupMembers []*sdkws.GroupMemberFullInfo,
updateGroup *sdkws.GroupInfo, version uint64, versionID string) error {
groupMemberSyncer := incrversion.VersionSynchronizer[*model_struct.LocalGroupMember, *group.GetIncrementalGroupMemberResp]{
Ctx: ctx,
DB: g.db,
TableName: g.groupAndMemberVersionTableName(),
EntityID: groupID,
Key: func(localGroupMember *model_struct.LocalGroupMember) string {
return localGroupMember.UserID
},
Local: func() ([]*model_struct.LocalGroupMember, error) {
return g.db.GetGroupMemberListByGroupID(ctx, groupID)
},
ServerVersion: func() *group.GetIncrementalGroupMemberResp {
return &group.GetIncrementalGroupMemberResp{
Version: version,
VersionID: versionID,
Full: false,
Delete: datautil.Slice(deleteGroupMembers, func(e *sdkws.GroupMemberFullInfo) string {
return e.UserID
}),
Insert: insertGroupMembers,
Update: updateGroupMembers,
Group: updateGroup,
}
},
Server: func(version *model_struct.LocalVersionSync) (*group.GetIncrementalGroupMemberResp, error) {
singleGroupReq := &group.GetIncrementalGroupMemberReq{
GroupID: groupID,
VersionID: version.VersionID,
Version: version.Version,
}
resp, err := util.CallApi[BatchIncrementalResp](ctx, constant.GetIncrementalGroupMemberBatch,
&BatchIncrementalReq{UserID: g.loginUserID, List: []*group.GetIncrementalGroupMemberReq{singleGroupReq}})
if err != nil {
return nil, err
}
if resp.List != nil {
if singleGroupResp, ok := resp.List[groupID]; ok {
return singleGroupResp, nil
}
}
return nil, errs.New("group member version record not found")
},
Full: func(resp *group.GetIncrementalGroupMemberResp) bool {
return resp.Full
},
Version: func(resp *group.GetIncrementalGroupMemberResp) (string, uint64) {
return resp.VersionID, resp.Version
},
Delete: func(resp *group.GetIncrementalGroupMemberResp) []string {
return resp.Delete
},
Update: func(resp *group.GetIncrementalGroupMemberResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Update)
},
Insert: func(resp *group.GetIncrementalGroupMemberResp) []*model_struct.LocalGroupMember {
return datautil.Batch(ServerGroupMemberToLocalGroupMember, resp.Insert)
},
ExtraData: func(resp *group.GetIncrementalGroupMemberResp) any {
return resp.Group
},
ExtraDataProcessor: func(ctx context.Context, data any) error {
groupInfo, ok := data.(*sdkws.GroupInfo)
if !ok {
return errs.New("group info type error")
}
if groupInfo == nil {
return nil
}
local, err := g.db.GetJoinedGroupListDB(ctx)
if err != nil {
return err
}
log.ZDebug(ctx, "group info", "groupInfo", groupInfo)
changes := datautil.Batch(ServerGroupToLocalGroup, []*sdkws.GroupInfo{groupInfo})
kv := datautil.SliceToMapAny(local, func(e *model_struct.LocalGroup) (string, *model_struct.LocalGroup) {
return e.GroupID, e
})
for i, change := range changes {
key := change.GroupID
kv[key] = changes[i]
}
server := datautil.Values(kv)
return g.groupSyncer.Sync(ctx, server, local, nil)
},
Syncer: func(server, local []*model_struct.LocalGroupMember) error {
return g.groupMemberSyncer.Sync(ctx, server, local, nil)
},
FullSyncer: func(ctx context.Context) error {
return g.groupMemberSyncer.FullSync(ctx, groupID)
},
FullID: func(ctx context.Context) ([]string, error) {
resp, err := util.CallApi[group.GetFullGroupMemberUserIDsResp](ctx, constant.GetFullGroupMemberUserIDs, &group.GetFullGroupMemberUserIDsReq{
GroupID: groupID,
})
if err != nil {
return nil, err
}
return resp.UserIDs, nil
},
}
return groupMemberSyncer.CheckVersionSync()
}
func (g *Group) IncrSyncJoinGroup(ctx context.Context) error {
opt := incrversion.VersionSynchronizer[*model_struct.LocalGroup, *group.GetIncrementalJoinGroupResp]{
Ctx: ctx,
DB: g.db,
TableName: g.groupTableName(),
EntityID: g.loginUserID,
Key: func(LocalGroup *model_struct.LocalGroup) string {
return LocalGroup.GroupID
},
Local: func() ([]*model_struct.LocalGroup, error) {
return g.db.GetJoinedGroupListDB(ctx)
},
Server: func(version *model_struct.LocalVersionSync) (*group.GetIncrementalJoinGroupResp, error) {
return util.CallApi[group.GetIncrementalJoinGroupResp](ctx, constant.GetIncrementalJoinGroup, &group.GetIncrementalJoinGroupReq{
UserID: g.loginUserID,
Version: version.Version,
VersionID: version.VersionID,
})
},
Full: func(resp *group.GetIncrementalJoinGroupResp) bool {
return resp.Full
},
Version: func(resp *group.GetIncrementalJoinGroupResp) (string, uint64) {
return resp.VersionID, resp.Version
},
Delete: func(resp *group.GetIncrementalJoinGroupResp) []string {
return resp.Delete
},
Update: func(resp *group.GetIncrementalJoinGroupResp) []*model_struct.LocalGroup {
return datautil.Batch(ServerGroupToLocalGroup, resp.Update)
},
Insert: func(resp *group.GetIncrementalJoinGroupResp) []*model_struct.LocalGroup {
return datautil.Batch(ServerGroupToLocalGroup, resp.Insert)
},
Syncer: func(server, local []*model_struct.LocalGroup) error {
return g.groupSyncer.Sync(ctx, server, local, nil)
},
FullSyncer: func(ctx context.Context) error {
return g.groupSyncer.FullSync(ctx, g.loginUserID)
},
FullID: func(ctx context.Context) ([]string, error) {
resp, err := util.CallApi[group.GetFullJoinGroupIDsResp](ctx, constant.GetFullJoinedGroupIDs, &group.GetFullJoinGroupIDsReq{
UserID: g.loginUserID,
})
if err != nil {
return nil, err
}
return resp.GroupIDs, nil
},
}
return opt.Sync()
}

@ -0,0 +1,265 @@
package incrversion
import (
"context"
"reflect"
"sort"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"gorm.io/gorm"
)
type VersionSynchronizer[V, R any] struct {
Ctx context.Context
DB db_interface.VersionSyncModel
TableName string
EntityID string
Key func(V) string
Local func() ([]V, error)
ServerVersion func() R
Server func(version *model_struct.LocalVersionSync) (R, error)
Full func(resp R) bool
Version func(resp R) (string, uint64)
Delete func(resp R) []string
Update func(resp R) []V
Insert func(resp R) []V
ExtraData func(resp R) any
ExtraDataProcessor func(ctx context.Context, data any) error
Syncer func(server, local []V) error
FullSyncer func(ctx context.Context) error
FullID func(ctx context.Context) ([]string, error)
}
func (o *VersionSynchronizer[V, R]) getVersionInfo() (*model_struct.LocalVersionSync, error) {
versionInfo, err := o.DB.GetVersionSync(o.Ctx, o.TableName, o.EntityID)
if err != nil && errs.Unwrap(err) != gorm.ErrRecordNotFound {
log.ZWarn(o.Ctx, "get version info", err)
return nil, err
}
return versionInfo, nil
}
func (o *VersionSynchronizer[V, R]) updateVersionInfo(lvs *model_struct.LocalVersionSync, resp R) error {
lvs.Table = o.TableName
lvs.EntityID = o.EntityID
lvs.VersionID, lvs.Version = o.Version(resp)
return o.DB.SetVersionSync(o.Ctx, lvs)
}
func judgeInterfaceIsNil(data any) bool {
return reflect.ValueOf(data).Kind() == reflect.Ptr && reflect.ValueOf(data).IsNil()
}
func (o *VersionSynchronizer[V, R]) Sync() error {
var lvs *model_struct.LocalVersionSync
var resp R
var extraData any
if o.ServerVersion == nil {
var err error
lvs, err = o.getVersionInfo()
if err != nil {
return err
}
resp, err = o.Server(lvs)
if err != nil {
return err
}
} else {
var err error
lvs, err = o.getVersionInfo()
if err != nil {
return err
}
resp = o.ServerVersion()
}
delIDs := o.Delete(resp)
changes := o.Update(resp)
insert := o.Insert(resp)
if o.ExtraData != nil {
temp := o.ExtraData(resp)
if !judgeInterfaceIsNil(temp) {
extraData = temp
}
}
if len(delIDs) == 0 && len(changes) == 0 && len(insert) == 0 && !o.Full(resp) && extraData == nil {
log.ZDebug(o.Ctx, "no data to sync", "table", o.TableName, "entityID", o.EntityID)
return nil
}
if o.Full(resp) {
err := o.FullSyncer(o.Ctx)
if err != nil {
return err
}
lvs.UIDList, err = o.FullID(o.Ctx)
if err != nil {
return err
}
} else {
if len(delIDs) > 0 {
lvs.UIDList = DeleteElements(lvs.UIDList, delIDs)
}
if len(insert) > 0 {
lvs.UIDList = append(lvs.UIDList, datautil.Slice(insert, o.Key)...)
}
local, err := o.Local()
if err != nil {
return err
}
kv := datautil.SliceToMapAny(local, func(v V) (string, V) {
return o.Key(v), v
})
changes = append(changes, insert...)
for i, change := range changes {
key := o.Key(change)
kv[key] = changes[i]
}
for _, id := range delIDs {
delete(kv, id)
}
server := datautil.Values(kv)
if err := o.Syncer(server, local); err != nil {
return err
}
if extraData != nil && o.ExtraDataProcessor != nil {
if err := o.ExtraDataProcessor(o.Ctx, extraData); err != nil {
return err
}
}
}
return o.updateVersionInfo(lvs, resp)
}
func (o *VersionSynchronizer[V, R]) CheckVersionSync() error {
lvs, err := o.getVersionInfo()
if err != nil {
return err
}
var extraData any
resp := o.ServerVersion()
delIDs := o.Delete(resp)
changes := o.Update(resp)
insert := o.Insert(resp)
versionID, version := o.Version(resp)
if o.ExtraData != nil {
temp := o.ExtraData(resp)
if !judgeInterfaceIsNil(temp) {
extraData = temp
}
}
if len(delIDs) == 0 && len(changes) == 0 && len(insert) == 0 && !o.Full(resp) && extraData == nil {
log.ZWarn(o.Ctx, "exception no data to sync", errs.New("notification no data"), "table", o.TableName, "entityID", o.EntityID)
return nil
}
log.ZDebug(o.Ctx, "check version sync", "table", o.TableName, "entityID", o.EntityID, "versionID", versionID, "localVersionID", lvs.VersionID, "version", version, "localVersion", lvs.Version)
/// If the version unique ID cannot correspond with the local version,
// it indicates that the data might have been tampered with or an exception has occurred.
//Trigger the complete client-server incremental synchronization.
if versionID != lvs.VersionID {
log.ZDebug(o.Ctx, "version id not match", errs.New("version id not match"), "versionID", versionID, "localVersionID", lvs.VersionID)
o.ServerVersion = nil
return o.Sync()
}
if lvs.Version+1 == version {
if len(delIDs) > 0 {
lvs.UIDList = DeleteElements(lvs.UIDList, delIDs)
}
if len(insert) > 0 {
lvs.UIDList = append(lvs.UIDList, datautil.Slice(insert, o.Key)...)
}
local, err := o.Local()
if err != nil {
return err
}
kv := datautil.SliceToMapAny(local, func(v V) (string, V) {
return o.Key(v), v
})
changes = append(changes, insert...)
for i, change := range changes {
key := o.Key(change)
kv[key] = changes[i]
}
for _, id := range delIDs {
delete(kv, id)
}
server := datautil.Values(kv)
if err := o.Syncer(server, local); err != nil {
return err
}
if extraData != nil && o.ExtraDataProcessor != nil {
if err := o.ExtraDataProcessor(o.Ctx, extraData); err != nil {
return err
}
}
return o.updateVersionInfo(lvs, resp)
} else if version <= lvs.Version {
log.ZWarn(o.Ctx, "version less than local version", errs.New("version less than local version"),
"table", o.TableName, "entityID", o.EntityID, "version", version, "localVersion", lvs.Version)
return nil
} else {
// If the version number has a gap with the local version number,
//it indicates that some pushed data might be missing.
//Trigger the complete client-server incremental synchronization.
o.ServerVersion = nil
return o.Sync()
}
}
// DeleteElements 删除切片中包含在另一个切片中的元素,并保持切片顺序
func DeleteElements[E comparable](es []E, toDelete []E) []E {
// 将要删除的元素存储在哈希集合中
deleteSet := make(map[E]struct{}, len(toDelete))
for _, e := range toDelete {
deleteSet[e] = struct{}{}
}
// 通过一个索引 j 来跟踪新的切片位置
j := 0
for _, e := range es {
if _, found := deleteSet[e]; !found {
es[j] = e
j++
}
}
return es[:j]
}
// DeleteElement 删除切片中的指定元素,并保持切片顺序
func DeleteElement[E comparable](es []E, element E) []E {
j := 0
for _, e := range es {
if e != element {
es[j] = e
j++
}
}
return es[:j]
}
// Slice Converts slice types in batches and sorts the resulting slice using a custom comparator
func Slice[E any, T any](es []E, fn func(e E) T, less func(a, b T) bool) []T {
// 转换切片
v := make([]T, len(es))
for i := 0; i < len(es); i++ {
v[i] = fn(es[i])
}
// 排序切片
sort.Slice(v, func(i, j int) bool {
return less(v[i], v[j])
})
return v
}

@ -0,0 +1,61 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"bytes"
"compress/gzip"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"io"
)
type Compressor interface {
Compress(rawData []byte) ([]byte, error)
DeCompress(compressedData []byte) ([]byte, error)
}
type GzipCompressor struct {
compressProtocol string
}
func NewGzipCompressor() *GzipCompressor {
return &GzipCompressor{compressProtocol: "gzip"}
}
func (g *GzipCompressor) Compress(rawData []byte) ([]byte, error) {
gzipBuffer := bytes.Buffer{}
gz := gzip.NewWriter(&gzipBuffer)
if _, err := gz.Write(rawData); err != nil {
return nil, utils.Wrap(err, "")
}
if err := gz.Close(); err != nil {
return nil, utils.Wrap(err, "")
}
return gzipBuffer.Bytes(), nil
}
func (g *GzipCompressor) DeCompress(compressedData []byte) ([]byte, error) {
buff := bytes.NewBuffer(compressedData)
reader, err := gzip.NewReader(buff)
if err != nil {
return nil, utils.Wrap(err, "NewReader failed")
}
compressedData, err = io.ReadAll(reader)
if err != nil {
return nil, utils.Wrap(err, "ReadAll failed")
}
_ = reader.Close()
return compressedData, nil
}

@ -0,0 +1,39 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
const (
WebSocket = iota
Tcp
)
const (
// MessageText is for UTF-8 encoded text messages like JSON.
MessageText = iota + 1
// MessageBinary is for binary messages like protobufs.
MessageBinary
// CloseMessage denotes a close control message. The optional message
// payload contains a numeric code and text. Use the FormatCloseMessage
// function to format a close message payload.
CloseMessage = 8
// PingMessage denotes a ping control message. The optional message payload
// is UTF-8 encoded text.
PingMessage = 9
// PongMessage denotes a pong control message. The optional message payload
// is UTF-8 encoded text.
PongMessage = 10
)

@ -0,0 +1,52 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"time"
"github.com/openimsdk/protocol/constant"
)
type ConnContext struct {
RemoteAddr string
}
func (c *ConnContext) Deadline() (deadline time.Time, ok bool) {
return
}
func (c *ConnContext) Done() <-chan struct{} {
return nil
}
func (c *ConnContext) Err() error {
return nil
}
func (c *ConnContext) Value(key any) any {
switch key {
case constant.RemoteAddr:
return c.RemoteAddr
default:
return ""
}
}
func newContext(remoteAddr string) *ConnContext {
return &ConnContext{
RemoteAddr: remoteAddr,
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save