Selaa lähdekoodia

init project

master
DengBiao 2 vuotta sitten
commit
b0bb62eb4e
100 muutettua tiedostoa jossa 6406 lisäystä ja 0 poistoa
  1. +8
    -0
      .idea/.gitignore
  2. +9
    -0
      .idea/gim.iml
  3. +8
    -0
      .idea/modules.xml
  4. +6
    -0
      .idea/vcs.xml
  5. +37
    -0
      Dockerfile_connect
  6. +37
    -0
      Dockerfile_logic
  7. +21
    -0
      LICENSE
  8. +100
    -0
      README.md
  9. +16
    -0
      build_docker.sh
  10. +3
    -0
      build_proto.sh
  11. +23
    -0
      chart/.helmignore
  12. +24
    -0
      chart/Chart.yaml
  13. +11
    -0
      chart/templates/configmap/configmap.yaml
  14. +34
    -0
      chart/templates/role/cluster_role.yaml
  15. +53
    -0
      chart/templates/server/connect.yaml
  16. +50
    -0
      chart/templates/server/logic.yaml
  17. +13
    -0
      chart/values.yaml
  18. +43
    -0
      cmd/business/main.go
  19. +60
    -0
      cmd/connect/main.go
  20. +3
    -0
      cmd/connect/run.sh
  21. +57
    -0
      cmd/file/main.go
  22. +50
    -0
      cmd/logic/main.go
  23. +61
    -0
      config/config.go
  24. +3
    -0
      docker/Dockerfile
  25. +2
    -0
      docs/plan.md
  26. +17
    -0
      docs/流程图/心跳.puml
  27. +31
    -0
      docs/流程图/消息单发.puml
  28. +31
    -0
      docs/流程图/消息群发.puml
  29. +15
    -0
      docs/流程图/登录.puml
  30. +16
    -0
      docs/流程图/离线消息同步.puml
  31. +138
    -0
      docs/错误处理.md
  32. +69
    -0
      go.mod
  33. +813
    -0
      go.sum
  34. +46
    -0
      internal/business/api/business_ext.go
  35. +51
    -0
      internal/business/api/business_ext_test.go
  36. +28
    -0
      internal/business/api/business_int.go
  37. +41
    -0
      internal/business/api/business_int_test.go
  38. +20
    -0
      internal/business/app/auth_app.go
  39. +65
    -0
      internal/business/app/user_app.go
  40. +7
    -0
      internal/business/domain/user/model/device.go
  41. +34
    -0
      internal/business/domain/user/model/user.go
  42. +74
    -0
      internal/business/domain/user/repo/auth_cache.go
  43. +23
    -0
      internal/business/domain/user/repo/auth_cache_test.go
  44. +19
    -0
      internal/business/domain/user/repo/auth_repo.go
  45. +51
    -0
      internal/business/domain/user/repo/user_cache.go
  46. +81
    -0
      internal/business/domain/user/repo/user_dao.go
  47. +44
    -0
      internal/business/domain/user/repo/user_dao_test.go
  48. +64
    -0
      internal/business/domain/user/repo/user_repo.go
  49. +86
    -0
      internal/business/domain/user/service/auth.go
  50. +12
    -0
      internal/business/domain/user/service/auth_test.go
  51. +32
    -0
      internal/connect/api.go
  52. +261
    -0
      internal/connect/conn.go
  53. +36
    -0
      internal/connect/conn_manager.go
  54. +72
    -0
      internal/connect/mq.go
  55. +105
    -0
      internal/connect/room.go
  56. +70
    -0
      internal/connect/tcp_server.go
  57. +87
    -0
      internal/connect/ws_server.go
  58. +168
    -0
      internal/logic/api/logic_ext.go
  59. +208
    -0
      internal/logic/api/logic_ext_test.go
  60. +79
    -0
      internal/logic/api/logic_int.go
  61. +128
    -0
      internal/logic/api/logic_int_test.go
  62. +85
    -0
      internal/logic/app/device_app.go
  63. +53
    -0
      internal/logic/app/friend_app.go
  64. +150
    -0
      internal/logic/app/group_app.go
  65. +59
    -0
      internal/logic/app/message_app.go
  66. +21
    -0
      internal/logic/app/room_app.go
  67. +66
    -0
      internal/logic/domain/device/device.go
  68. +68
    -0
      internal/logic/domain/device/device_dao.go
  69. +38
    -0
      internal/logic/domain/device/device_dao_test.go
  70. +76
    -0
      internal/logic/domain/device/device_repo.go
  71. +89
    -0
      internal/logic/domain/device/device_service.go
  72. +46
    -0
      internal/logic/domain/device/user_device_cache.go
  73. +19
    -0
      internal/logic/domain/friend/friend.go
  74. +37
    -0
      internal/logic/domain/friend/friend_repo.go
  75. +23
    -0
      internal/logic/domain/friend/friend_repo_test.go
  76. +160
    -0
      internal/logic/domain/friend/friend_service.go
  77. +364
    -0
      internal/logic/domain/group/model/group.go
  78. +48
    -0
      internal/logic/domain/group/repo/group_cache.go
  79. +35
    -0
      internal/logic/domain/group/repo/group_dao.go
  80. +11
    -0
      internal/logic/domain/group/repo/group_dao_test.go
  81. +70
    -0
      internal/logic/domain/group/repo/group_repo.go
  82. +69
    -0
      internal/logic/domain/group/repo/group_user_repo.go
  83. +24
    -0
      internal/logic/domain/group/repo/group_user_repo_test.go
  84. +83
    -0
      internal/logic/domain/message/model/message.go
  85. +12
    -0
      internal/logic/domain/message/model/sender.go
  86. +41
    -0
      internal/logic/domain/message/repo/device_ack_repo.go
  87. +49
    -0
      internal/logic/domain/message/repo/message_repo.go
  88. +39
    -0
      internal/logic/domain/message/repo/message_repo_test.go
  89. +43
    -0
      internal/logic/domain/message/repo/seq_repo.go
  90. +10
    -0
      internal/logic/domain/message/repo/seq_repo_test.go
  91. +34
    -0
      internal/logic/domain/message/service/device_ack.go
  92. +20
    -0
      internal/logic/domain/message/service/device_ack_test.go
  93. +194
    -0
      internal/logic/domain/message/service/message_service.go
  94. +40
    -0
      internal/logic/domain/message/service/message_service_test.go
  95. +86
    -0
      internal/logic/domain/message/service/push.go
  96. +15
    -0
      internal/logic/domain/message/service/seq.go
  97. +12
    -0
      internal/logic/domain/message/service/seq_test.go
  98. +94
    -0
      internal/logic/domain/room/room_message_repo.go
  99. +21
    -0
      internal/logic/domain/room/room_seq_repo.go
  100. +148
    -0
      internal/logic/domain/room/room_service.go

+ 8
- 0
.idea/.gitignore Näytä tiedosto

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

+ 9
- 0
.idea/gim.iml Näytä tiedosto

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

+ 8
- 0
.idea/modules.xml Näytä tiedosto

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/gim.iml" filepath="$PROJECT_DIR$/.idea/gim.iml" />
</modules>
</component>
</project>

+ 6
- 0
.idea/vcs.xml Näytä tiedosto

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

+ 37
- 0
Dockerfile_connect Näytä tiedosto

@@ -0,0 +1,37 @@
# 多重构建,减少镜像大小
# 构建:使用golang:1.18版本
FROM golang:1.18.4 as build

# 容器环境变量添加,会覆盖默认的变量值
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
ENV TZ="Asia/Shanghai"
# 设置工作区
WORKDIR /go/release

# 把全部文件添加到/go/release目录
ADD . .

# 编译:把main.go编译成可执行的二进制文件,命名为zyos
RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -o zyos cmd/connect/main.go

FROM ubuntu:xenial as prod
LABEL maintainer="dengbiao"
ENV TZ="Asia/Shanghai"

# 时区纠正
RUN rm -f /etc/localtime \
&& ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone

# 在build阶段复制可执行的go二进制文件app
COPY --from=build /go/release/zyos ./zyos

# 启动服务
# 优化内核参数
#RUN sysctl -w net.ipv4.tcp_fin_timeout=15
#RUN sysctl -w net.ipv4.tcp_tw_recycle=1
#RUN sysctl -w net.ipv4.ip_local_port_range=1024 65535
#RUN sysctl -p
#CMD ["bash","-c","sysctl -w net.ipv4.tcp_timestamps=1 && sysctl -w net.ipv4.tcp_tw_reuse=1 && sysctl -w net.ipv4.tcp_tw_timeout=10 && sysctl -w net.ipv4.tcp_fin_timeout=10 && sysctl -w net.ipv4.ip_local_port_range='1024 65535' && ./zyos"]
CMD ["./zyos"]

+ 37
- 0
Dockerfile_logic Näytä tiedosto

@@ -0,0 +1,37 @@
# 多重构建,减少镜像大小
# 构建:使用golang:1.18版本
FROM golang:1.18.4 as build

# 容器环境变量添加,会覆盖默认的变量值
ENV GO111MODULE=on
ENV GOPROXY=https://goproxy.cn,direct
ENV TZ="Asia/Shanghai"
# 设置工作区
WORKDIR /go/release

# 把全部文件添加到/go/release目录
ADD . .

# 编译:把main.go编译成可执行的二进制文件,命名为zyos
RUN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -o zyos cmd/logic/main.go

FROM ubuntu:xenial as prod
LABEL maintainer="dengbiao"
ENV TZ="Asia/Shanghai"

# 时区纠正
RUN rm -f /etc/localtime \
&& ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone

# 在build阶段复制可执行的go二进制文件app
COPY --from=build /go/release/zyos ./zyos

# 启动服务
# 优化内核参数
#RUN sysctl -w net.ipv4.tcp_fin_timeout=15
#RUN sysctl -w net.ipv4.tcp_tw_recycle=1
#RUN sysctl -w net.ipv4.ip_local_port_range=1024 65535
#RUN sysctl -p
#CMD ["bash","-c","sysctl -w net.ipv4.tcp_timestamps=1 && sysctl -w net.ipv4.tcp_tw_reuse=1 && sysctl -w net.ipv4.tcp_tw_timeout=10 && sysctl -w net.ipv4.tcp_fin_timeout=10 && sysctl -w net.ipv4.ip_local_port_range='1024 65535' && ./zyos"]
CMD ["./zyos"]

+ 21
- 0
LICENSE Näytä tiedosto

@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Alber

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 100
- 0
README.md Näytä tiedosto

@@ -0,0 +1,100 @@
### 简要介绍
gim是一个即时通讯服务器,代码全部使用golang完成。主要特性
1.支持tcp,websocket接入
2.离线消息同步
3.单用户多设备同时在线
4.单聊,群聊,以及房间聊天场景
5.支持服务水平扩展
6.使用领域驱动设计
gim可以作为以业务服务器的一个组件,为现有业务服务器提供im的能力,业务服务器
只需要实现business.int.proto协议中定义的GRPC接口,为gim服务提供基本的用户功能即可
### 使用技术:
数据库:MySQL+Redis
通讯框架:GRPC
长连接通讯协议:Protocol Buffers
日志框架:Zap
ORM框架:GORM
### 安装部署
1.首先安装MySQL,Redis
2.创建数据库gim,执行sql/create_table.sql,完成初始化表的创建(数据库包含提供测试的一些初始数据)
3.修改config下配置文件,使之和你本地配置一致,如果没有配置gim_env环境变量,默认会加载config/local_conf.go配置
4.分别切换到cmd的connect,logic,business目录下,执行go run main.go,启动TCP连接层服务器,WebSocket连接层服务器,逻辑层服务器,用户服务器
(注意:connect只能在linux下启动,如果想在其他平台下启动,请安装docker,执行cmd/connect/run.sh)
### 项目目录简介
项目结构遵循 https://github.com/golang-standards/project-layout
```
cmd: 服务启动入口
config: 服务配置
internal: 每个服务私有代码
pkg: 服务共有代码
sql: 项目sql文件
test: 长连接测试脚本
```
### 服务简介
1.connect
维持与客户端的TCP和WebSocket长连接,心跳,以及TCP拆包粘包,消息编解码
2.logic
设备信息,好友信息,群组信息管理,消息转发逻辑
3.business
一个简单的业务服务器服务,可以根据自己的业务需求,进行扩展,但是前提是,你的业务服务器实现了business.int.proto接口
### 客户端接入流程
1.调用LogicExt.RegisterDevice接口,完成设备注册,获取设备ID(device_id),注意,一个设备只需完成一次注册即可,后续如果本地有device_id,就不需要注册了,举个例子,如果是APP第一次安装,就需要调用这个接口,后面即便是换账号登录,也不需要重新注册。
2.调用BusinessExt.SignIn接口,完成账户登录,获取账户登录的token。
3.建立长连接,使用步骤2拿到的token,完成长连接登录。
如果是web端,需要调用建立WebSocket时,如果是APP端,就需要建立TCP长连接。
在完成建立TCP长连接时,第一个包应该是长连接登录包(SignInInput),如果信息无误,客户端就会成功建立长连接。
4.使用长连接发送消息同步包(SyncInput),完成离线消息同步,注意:seq字段是客户端接收到消息的最大同步序列号,如果用户是换设备登录或者第一次登录,seq应该传0。
接下来,用户可以使用LogicExt.SendMessage接口来发送消息,消息接收方可以使用长连接接收到对应的消息。
### 网络模型
TCP的网络层使用linux的epoll实现,相比golang原生,能减少goroutine使用,从而节省系统资源占用
### 单用户多设备支持,离线消息同步
每个用户都会维护一个自增的序列号,当用户A给用户B发送消息是,首先会获取A的最大序列号,设置为这条消息的seq,持久化到用户A的消息列表,
再通过长连接下发到用户A账号登录的所有设备,再获取用户B的最大序列号,设置为这条消息的seq,持久化到用户B的消息列表,再通过长连接下发
到用户B账号登录的所有设备。
假如用户的某个设备不在线,在设备长连接登录时,用本地收到消息的最大序列号,到服务器做消息同步,这样就可以保证离线消息不丢失。
### 读扩散和写扩散
首先解释一下,什么是读扩散,什么是写扩散
#### 读扩散
**简介**:群组成员发送消息时,先建立一个会话,都将这个消息写入这个会话中,同步离线消息时,需要同步这个会话的未同步消息
**优点**:每个消息只需要写入数据库一次就行,减少数据库访问次数,节省数据库空间
**缺点**:一个用户有n个群组,客户端每次同步消息时,要上传n个序列号,服务器要对这n个群组分别做消息同步
#### 写扩散
**简介**:在群组中,每个用户维持一个自己的消息列表,当群组中有人发送消息时,给群组的每个用户的消息列表插入一条消息即可
**优点**:每个用户只需要维护一个序列号和消息列表
**缺点**:一个群组有多少人,就要插入多少条消息,当群组成员很多时,DB的压力会增大
### 消息转发逻辑选型以及特点
#### 群组:
采用写扩散,群组成员信息持久化到数据库保存。支持消息离线同步。
#### 房间:
采用读扩散,会将消息短暂的保存到Redis,长连接登录消息同步不会同步离线消息。
### 核心流程时序图
#### 长连接登录
![登录.png](https://upload-images.jianshu.io/upload_images/5760439-2e54d3c5dd0a44c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#### 离线消息同步
![离线消息同步.png](https://upload-images.jianshu.io/upload_images/5760439-aa513ea0de851e12.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#### 心跳
![心跳.png](https://upload-images.jianshu.io/upload_images/5760439-26d491374da3843b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#### 消息单发
c1.d1和c1.d2分别表示c1用户的两个设备d1和d2,c2.d3和c2.d4同理
![消息单发.png](https://upload-images.jianshu.io/upload_images/5760439-35f1a91c8d7fffa6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#### 群组消息群发
c1,c2.c3表示一个群组中的三个用户
![消息群发.png](https://upload-images.jianshu.io/upload_images/5760439-47a87c45b899b3f9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#### APP
基于Flutter写了一个简单的客户端
GitHub地址:https://github.com/alberliu/fim
APP下载:https://github.com/alberliu/fim/releases/download/v1.2.0/FIM.apk
APP截图:
![登录.png](https://upload-images.jianshu.io/upload_images/5760439-c8c5e61815b34687.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![好友.png](https://upload-images.jianshu.io/upload_images/5760439-9ea6a87711f8e749.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![聊天.png](https://upload-images.jianshu.io/upload_images/5760439-2f1e7da8be247e4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![群组.png](https://upload-images.jianshu.io/upload_images/5760439-beb97223497e2ee9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![我的.png](https://upload-images.jianshu.io/upload_images/5760439-aee324007a1d2eb1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
![消息.png](https://upload-images.jianshu.io/upload_images/5760439-47597c7c5859d515.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
### 联系方式
![my.png](https://upload-images.jianshu.io/upload_images/5760439-484c85f9fbda35d4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
### 赞赏支持
如果觉得项目对你有帮助,请支持一下
![pay.png](https://upload-images.jianshu.io/upload_images/5760439-7aac91bc83c8735f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/310)
### github
https://github.com/alberliu/gim

+ 16
- 0
build_docker.sh Näytä tiedosto

@@ -0,0 +1,16 @@
if [[ $? -ne 0 ]]; then
exit 1
fi

server=$1
cd cmd/$server
# 打包可执行文件
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main
pwd
mv main ../../docker/
cd ../../docker/
pwd
# 构建镜像
docker build -t $1 .

kind load docker-image $server --name kind

+ 3
- 0
build_proto.sh Näytä tiedosto

@@ -0,0 +1,3 @@
cd pkg/proto
protoc --go_out=plugins=grpc:../../../ *.proto
cd ../../

+ 23
- 0
chart/.helmignore Näytä tiedosto

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

+ 24
- 0
chart/Chart.yaml Näytä tiedosto

@@ -0,0 +1,24 @@
apiVersion: v2
name: gim
description: A Helm chart for Kubernetes

# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"

+ 11
- 0
chart/templates/configmap/configmap.yaml Näytä tiedosto

@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: config
data:
# 类属性键;每一个键都映射到一个简单的值,仅仅支持键值对,不支持嵌套
mysql: "root:Fnuo123com@@tcp(119.23.182.117:3306)/gim?charset=utf8&parseTime=true"
redisIP: "120.24.28.6:32572"
redisPassword: ""
pushRoomSubscribeNum: "100"
pushAllSubscribeNum: "100"

+ 34
- 0
chart/templates/role/cluster_role.yaml Näytä tiedosto

@@ -0,0 +1,34 @@
# 为pod中的服务赋予发现服务和读取配置的权限
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-role
rules:
- apiGroups:
- ""
resources:
- pods
- pods/status
- services
- services/status
- endpoints
- endpoints/status
- configmaps
- configmaps/status
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argo-namespaces-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pod-role
subjects:
- kind: ServiceAccount
name: default
namespace: default

+ 53
- 0
chart/templates/server/connect.yaml Näytä tiedosto

@@ -0,0 +1,53 @@
# deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: connect-deployment
namespace: gim
labels:
app: connect
spec:
replicas: 1
selector:
matchLabels:
app: connect
template:
metadata:
labels:
app: connect
spec:
containers:
- name: connect
image: 'registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-logic:202209018-01'
imagePullPolicy: Always
ports:
- containerPort: 8000
- containerPort: 8001
- containerPort: 8002
volumeMounts: # 映射文件为宿主机文件
- mountPath: /log/
name: log
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
volumes:
- name: log
hostPath:
path: /log/
---
# service 配置
apiVersion: v1
kind: Service
metadata:
name: connect
labels:
app: connect # 只有设置label,才能被服务发现找到
spec:
selector:
app: connect
ports:
- protocol: TCP
port: 8000
targetPort: 8000

+ 50
- 0
chart/templates/server/logic.yaml Näytä tiedosto

@@ -0,0 +1,50 @@
# deployment 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: logic-deployment
labels:
app: logic
spec:
replicas: 1
selector:
matchLabels:
app: logic
template:
metadata:
labels:
app: logic
spec:
containers:
- name: logic
image: 'registry.cn-shenzhen.aliyuncs.com/fnuoos-prd/zyos-logic:202209018-01'
imagePullPolicy: Always # 在kind中需要指定,不然会强制到远程拉取镜像,导致部署失败
ports:
- containerPort: 8001
volumeMounts: # 映射文件为宿主机文件
- mountPath: /log/
name: log
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
volumes:
- name: log
hostPath:
path: /log/
---
# service 配置
apiVersion: v1
kind: Service
metadata:
name: logic
labels:
app: logic # 只有设置label,才能被服务发现找到
spec:
selector:
app: logic
ports:
- protocol: TCP
port: 8000
targetPort: 8000

+ 13
- 0
chart/values.yaml Näytä tiedosto

@@ -0,0 +1,13 @@
server:
connect:
name: connect
image: connect
replicas: 1
logic:
name: logic
image: logic
replicas: 1
business:
name: logic
image: logic
replicas: 1

+ 43
- 0
cmd/business/main.go Näytä tiedosto

@@ -0,0 +1,43 @@
package main

import (
"gim/config"
"gim/internal/business/api"
"gim/pkg/interceptor"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/urlwhitelist"
"net"
"os"
"os/signal"
"syscall"

"go.uber.org/zap"
"google.golang.org/grpc"
)

func main() {
server := grpc.NewServer(grpc.UnaryInterceptor(interceptor.NewInterceptor("business_interceptor", urlwhitelist.Business)))

// 监听服务关闭信号,服务平滑重启
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM)
s := <-c
logger.Logger.Info("server stop", zap.Any("signal", s))
server.GracefulStop()
}()

pb.RegisterBusinessIntServer(server, &api.BusinessIntServer{})
pb.RegisterBusinessExtServer(server, &api.BusinessExtServer{})
listen, err := net.Listen("tcp", config.RPCListenAddr)
if err != nil {
panic(err)
}

logger.Logger.Info("rpc服务已经开启")
err = server.Serve(listen)
if err != nil {
logger.Logger.Error("serve error", zap.Error(err))
}
}

+ 60
- 0
cmd/connect/main.go Näytä tiedosto

@@ -0,0 +1,60 @@
package main

import (
"context"
"gim/config"
"gim/internal/connect"
"gim/pkg/interceptor"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"net"
"os"
"os/signal"
"syscall"

"google.golang.org/grpc"

"go.uber.org/zap"
)

func main() {
// 启动TCP长链接服务器
go func() {
connect.StartTCPServer(config.TCPListenAddr)
}()

// 启动WebSocket长链接服务器
go func() {
connect.StartWSServer(config.WSListenAddr)
}()

// 启动服务订阅
connect.StartSubscribe()

server := grpc.NewServer(grpc.UnaryInterceptor(interceptor.NewInterceptor("connect_interceptor", nil)))

// 监听服务关闭信号,服务平滑重启
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM)
s := <-c
logger.Logger.Info("server stop start", zap.Any("signal", s))
_, _ = rpc.GetLogicIntClient().ServerStop(context.TODO(), &pb.ServerStopReq{ConnAddr: config.LocalAddr})
logger.Logger.Info("server stop end")

server.GracefulStop()
}()

pb.RegisterConnectIntServer(server, &connect.ConnIntServer{})
listener, err := net.Listen("tcp", config.RPCListenAddr)
if err != nil {
panic(err)
}

logger.Logger.Info("rpc服务已经开启")
err = server.Serve(listener)
if err != nil {
logger.Logger.Error("serve error", zap.Error(err))
}
}

+ 3
- 0
cmd/connect/run.sh Näytä tiedosto

@@ -0,0 +1,3 @@
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
echo "打包完成"
docker run -v $(pwd)/:/app -p 8080:8080 -p 8081:8081 -p 50100:50100 alpine .//app/main

+ 57
- 0
cmd/file/main.go Näytä tiedosto

@@ -0,0 +1,57 @@
package main

import (
"gim/pkg/logger"
"gim/pkg/util"
"net/http"
"strconv"
"strings"
"time"

"go.uber.org/zap"

"github.com/gin-gonic/gin"
)

const baseUrl = "http://111.229.238.28:8085/file/"

type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}

func main() {
router := gin.Default()
router.Static("/file", "/root/file")

// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// single file
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusOK, Response{Code: 1001, Message: err.Error()})
return
}

filenames := strings.Split(file.Filename, ".")
name := strconv.FormatInt(time.Now().UnixNano(), 10) + "-" + util.RandString(30) + "." + filenames[len(filenames)-1]
filePath := "/root/file/" + name
err = c.SaveUploadedFile(file, filePath)
if err != nil {
c.JSON(http.StatusOK, Response{Code: 1001, Message: err.Error()})
return
}

c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: map[string]string{"url": baseUrl + name},
})
})
err := router.Run(":8085")
if err != nil {
logger.Logger.Error("Run error", zap.Error(err))
}
}

+ 50
- 0
cmd/logic/main.go Näytä tiedosto

@@ -0,0 +1,50 @@
package main

import (
"gim/config"
"gim/internal/logic/api"
"gim/internal/logic/app"
"gim/internal/logic/proxy"
"gim/pkg/interceptor"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/urlwhitelist"
"net"
"os"
"os/signal"
"syscall"

"go.uber.org/zap"
"google.golang.org/grpc"
)

func init() {
proxy.MessageProxy = app.MessageApp
proxy.DeviceProxy = app.DeviceApp
}

func main() {
server := grpc.NewServer(grpc.UnaryInterceptor(interceptor.NewInterceptor("logic_interceptor", urlwhitelist.Logic)))

// 监听服务关闭信号,服务平滑重启
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM)
s := <-c
logger.Logger.Info("server stop", zap.Any("signal", s))
server.GracefulStop()
}()

pb.RegisterLogicIntServer(server, &api.LogicIntServer{})
pb.RegisterLogicExtServer(server, &api.LogicExtServer{})
listen, err := net.Listen("tcp", config.RPCListenAddr)
if err != nil {
panic(err)
}

logger.Logger.Info("rpc服务已经开启")
err = server.Serve(listen)
if err != nil {
logger.Logger.Error("serve error", zap.Error(err))
}
}

+ 61
- 0
config/config.go Näytä tiedosto

@@ -0,0 +1,61 @@
package config

import (
"context"
"gim/pkg/k8sutil"
"gim/pkg/logger"
"os"
"strconv"

"go.uber.org/zap"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
//RPCListenAddr = ":8000"
//TCPListenAddr = ":8080"
//WSListenAddr = ":8001"
RPCListenAddr = ":8000"
TCPListenAddr = ":8001"
WSListenAddr = ":8002"
)

var (
NameSpace string = "default"
MySQL string
RedisIP string
RedisPassword string

LocalAddr string
PushRoomSubscribeNum int
PushAllSubscribeNum int
)

func init() {
k8sClient, err := k8sutil.GetK8sClient()
if err != nil {
panic(err)
}
configmap, err := k8sClient.CoreV1().ConfigMaps(NameSpace).Get(context.TODO(), "config", metav1.GetOptions{})
if err != nil {
panic(err)
}

MySQL = configmap.Data["mysql"]
RedisIP = configmap.Data["redisIP"]
RedisPassword = configmap.Data["redisPassword"]
PushRoomSubscribeNum, _ = strconv.Atoi(configmap.Data["pushRoomSubscribeNum"])
if PushRoomSubscribeNum == 0 {
panic("PushRoomSubscribeNum == 0")
}
PushAllSubscribeNum, _ = strconv.Atoi(configmap.Data["pushAllSubscribeNum"])
if PushRoomSubscribeNum == 0 {
panic("PushAllSubscribeNum == 0")
}

LocalAddr = os.Getenv("POD_IP") + RPCListenAddr

logger.Level = zap.DebugLevel
logger.Target = logger.Console
}

+ 3
- 0
docker/Dockerfile Näytä tiedosto

@@ -0,0 +1,3 @@
FROM scratch as final
COPY main .
CMD ["/main"]

+ 2
- 0
docs/plan.md Näytä tiedosto

@@ -0,0 +1,2 @@
pb编译命令
protoc --go_out=plugins=grpc:../../../ *.proto

+ 17
- 0
docs/流程图/心跳.puml Näytä tiedosto

@@ -0,0 +1,17 @@
@startuml
participant client
participant connect
participant logic

client -> connect: 心跳包请求
connect --> client: 心跳包响应

client -> client: 等待一段时间

client -> connect: 心跳包请求
connect --> client: 心跳包响应


connect -> connect: 连续n次没有收到,释放连接
connect -> logic: 通知设备下线
@enduml

+ 31
- 0
docs/流程图/消息单发.puml Näytä tiedosto

@@ -0,0 +1,31 @@
@startuml
participant c1.d1
participant c1.d2
participant c2.d3
participant c2.d4
participant connect
participant logic

c1.d1 -> logic: c1给c2用户发送消息
logic --> c1.d1 : 返回消息发送成功

logic -> logic: 获取c1用户下一个消息序列号
logic -> logic: 将消息持久化到c1用户的消息列表
logic -> logic: 查询c1用户其他在线设备
logic --> connect: 给设备d2发送消息
connect --> c1.d2: 给设备d2发送消息
c1.d2 ->connect : 消息ack
connect -> logic: 消息ack

logic -> logic: 获取c2用户下一个消息序列号
logic -> logic: 将消息持久化到c2用户的消息列表
logic -> logic: 查询c2用户所有在线设备
logic -> connect: 给设备d3发送消息
connect -> c2.d3: 给设备d3发送消息
c2.d3 ->connect : 消息ack
connect -> logic: 消息ack
logic -> connect: 给设备d4发送消息
connect -> c2.d4: 给设备d4发送消息
c2.d4 ->connect : 消息ack
connect -> logic: 消息ack
@enduml

+ 31
- 0
docs/流程图/消息群发.puml Näytä tiedosto

@@ -0,0 +1,31 @@
@startuml
participant c1
participant c2
participant c3

participant connect
participant logic

c1 -> logic: 发送消息到群组
logic --> c1: 消息发送成功

logic -> logic: 查询群组所有成员

logic -> logic: 将消息持久化到c1的消息列表
logic -> connect: 发送消息给c1的其他在线设备
connect -> c1: 发送消息给c1的其他在线设备
c1 -> connect: 消息ack
connect -> logic: 消息ack

logic -> logic: 将消息持久化到c2的消息列表
logic -> connect: 发送消息给c2的其他在线设备
connect -> c2: 发送消息给c2的其他在线设备
c2 -> connect: 消息ack
connect -> logic: 消息ack

logic -> logic: 将消息持久化到c3的消息列表
logic -> connect: 发送消息给c3的其他在线设备
connect -> c3: 发送消息给c3的其他在线设备
c3 -> connect: 消息ack
connect -> logic: 消息ack
@enduml

+ 15
- 0
docs/流程图/登录.puml Näytä tiedosto

@@ -0,0 +1,15 @@
@startuml
participant client
participant connect
participant logic

client -> connect: 设备鉴权
connect -> logic: 设备鉴权

logic -> logic: 检查token是否合法

logic --> connect: 返回校验结果
connect --> client: 返回校验结果

connect -> connect: 如果鉴权失败,断开连接
@enduml

+ 16
- 0
docs/流程图/离线消息同步.puml Näytä tiedosto

@@ -0,0 +1,16 @@
@startuml
participant client
participant connect
participant logic

client -> connect: 离线消息同步
connect -> logic: 离线消息同步

logic -> logic: 如果seq!=0,同步序列号大于seq的消息,否则,同步用户未收到的消息

logic --> connect: 返回离线消息
connect --> client: 返回离线消息

client -> connect: 消息ack
connect -> logic: 消息ack
@enduml

+ 138
- 0
docs/错误处理.md Näytä tiedosto

@@ -0,0 +1,138 @@
### 错误处理,链路追踪,日志打印
系统中的错误一般可以归类为两种,一种是业务定义的错误,一种就是未知的错误,在业务正式上线的时候,业务定义的错误的属于正常业务逻辑,不需要打印出来,
但是未知的错误,我们就需要打印出来,我们不仅要知道是什么错误,还要知道错误的调用堆栈,所以这里我对GRPC的错误进行了一些封装,使之包含调用堆栈。
```go
func WrapError(err error) error {
if err == nil {
return nil
}

s := &spb.Status{
Code: int32(codes.Unknown),
Message: err.Error(),
Details: []*any.Any{
{
TypeUrl: TypeUrlStack,
Value: util.Str2bytes(stack()),
},
},
}
return status.FromProto(s).Err()
}
// Stack 获取堆栈信息
func stack() string {
var pc = make([]uintptr, 20)
n := runtime.Callers(3, pc)

var build strings.Builder
for i := 0; i < n; i++ {
f := runtime.FuncForPC(pc[i] - 1)
file, line := f.FileLine(pc[i] - 1)
n := strings.Index(file, name)
if n != -1 {
s := fmt.Sprintf(" %s:%d \n", file[n:], line)
build.WriteString(s)
}
}
return build.String()
}
```
这样,不仅可以拿到错误的堆栈,错误的堆栈也可以跨RPC传输,但是,但是这样你只能拿到当前服务的堆栈,却不能拿到调用方的堆栈,就比如说,A服务调用
B服务,当B服务发生错误时,在A服务通过日志打印错误的时候,我们只打印了B服务的调用堆栈,怎样可以把A服务的堆栈打印出来。我们在A服务调用的地方也获取
一次堆栈。
```go
func WrapRPCError(err error) error {
if err == nil {
return nil
}
e, _ := status.FromError(err)
s := &spb.Status{
Code: int32(e.Code()),
Message: e.Message(),
Details: []*any.Any{
{
TypeUrl: TypeUrlStack,
Value: util.Str2bytes(GetErrorStack(e) + " --grpc-- \n" + stack()),
},
},
}
return status.FromProto(s).Err()
}

func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
err := invoker(ctx, method, req, reply, cc, opts...)
return gerrors.WrapRPCError(err)
}

var LogicIntClient pb.LogicIntClient

func InitLogicIntClient(addr string) {
conn, err := grpc.DialContext(context.TODO(), addr, grpc.WithInsecure(), grpc.WithUnaryInterceptor(interceptor))
if err != nil {
logger.Sugar.Error(err)
panic(err)
}

LogicIntClient = pb.NewLogicIntClient(conn)
}
```
像这样,就可以获取完整一次调用堆栈。
错误打印也没有必要在函数返回错误的时候,每次都去打印。因为错误已经包含了堆栈信息
```go
// 错误的方式
if err != nil {
logger.Sugar.Error(err)
return err
}

// 正确的方式
if err != nil {
return err
}
```
然后,我们在上层统一打印就可以
```go
func startServer {
extListen, err := net.Listen("tcp", conf.LogicConf.ClientRPCExtListenAddr)
if err != nil {
panic(err)
}
extServer := grpc.NewServer(grpc.UnaryInterceptor(LogicClientExtInterceptor))
pb.RegisterLogicClientExtServer(extServer, &LogicClientExtServer{})
err = extServer.Serve(extListen)
if err != nil {
panic(err)
}
}

func LogicClientExtInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer logPanic("logic_client_ext_interceptor", ctx, req, info, &err)

resp, err = handler(ctx, req)
logger.Logger.Debug("logic_client_ext_interceptor", zap.Any("info", info), zap.Any("ctx", ctx), zap.Any("req", req),
zap.Any("resp", resp), zap.Error(err))

s, _ := status.FromError(err)
if s.Code() != 0 && s.Code() < 1000 {
md, _ := metadata.FromIncomingContext(ctx)
logger.Logger.Error("logic_client_ext_interceptor", zap.String("method", info.FullMethod), zap.Any("md", md), zap.Any("req", req),
zap.Any("resp", resp), zap.Error(err), zap.String("stack", gerrors.GetErrorStack(s)))
}
return
}
```
这样做的前提就是,在业务代码中透传context,golang不像其他语言,可以在线程本地保存变量,像Java的ThreadLocal,所以只能通过函数参数的形式进行传递,im中,service层函数的第一个参数
都是context,但是dao层和cache层就不需要了,不然,显得代码臃肿。
最后可以在客户端的每次请求添加一个随机的request_id,这样客户端到服务的每次请求都可以串起来了。
```go
func getCtx() context.Context {
token, _ := util.GetToken(1, 2, 3, time.Now().Add(1*time.Hour).Unix(), util.PublicKey)
return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs(
"app_id", "1",
"user_id", "2",
"device_id", "3",
"token", token,
"request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))
}
```

+ 69
- 0
go.mod Näytä tiedosto

@@ -0,0 +1,69 @@
module gim

go 1.17

require (
github.com/alberliu/gn v1.10.0
github.com/gin-gonic/gin v1.7.7
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-sql-driver/mysql v1.6.0
github.com/golang/protobuf v1.5.2
github.com/gorilla/websocket v1.4.2
github.com/jinzhu/gorm v1.9.16
github.com/json-iterator/go v1.1.12
go.uber.org/zap v1.21.0
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf
google.golang.org/grpc v1.42.0
google.golang.org/protobuf v1.28.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
k8s.io/api v0.25.1
k8s.io/apimachinery v0.25.1
k8s.io/client-go v0.25.1
)

require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.70.1 // indirect
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)

+ 813
- 0
go.sum Näytä tiedosto

@@ -0,0 +1,813 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.27/go.mod h1:7l8ybrIdUmGqZMTD0sRtAr8NvbHjfofbf8RSP2q7w7U=
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
github.com/Azure/go-autorest/autorest/adal v0.9.20/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alberliu/gn v1.10.0 h1:BQcfgp1c8wc+AXvutImeJ5RAr7Lh/tc23vELJ3dgh10=
github.com/alberliu/gn v1.10.0/go.mod h1:Rm7O35H784ZsqxJr/3VcNgyHHfI7jf7GFTQ+o/fXAsg=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw=
github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
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.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU=
github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
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.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf h1:PSEM+IQFb9xdsj2CGhfqUTfsZvF8DScCVP1QZb2IiTQ=
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.25.1 h1:yL7du50yc93k17nH/Xe9jujAYrcDkI/i5DL1jPz4E3M=
k8s.io/api v0.25.1/go.mod h1:hh4itDvrWSJsmeUc28rIFNri8MatNAAxJjKcQmhX6TU=
k8s.io/apimachinery v0.25.1 h1:t0XrnmCEHVgJlR2arwO8Awp9ylluDic706WePaYCBTI=
k8s.io/apimachinery v0.25.1/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA=
k8s.io/client-go v0.25.1 h1:uFj4AJKtE1/ckcSKz8IhgAuZTdRXZDKev8g387ndD58=
k8s.io/client-go v0.25.1/go.mod h1:rdFWTLV/uj2C74zGbQzOsmXPUtMAjSf7ajil4iJUNKo=
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ=
k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA=
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU=
k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4=
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

+ 46
- 0
internal/business/api/business_ext.go Näytä tiedosto

@@ -0,0 +1,46 @@
package api

import (
"context"
"gim/internal/business/app"
"gim/pkg/grpclib"
"gim/pkg/pb"
)

type BusinessExtServer struct{}

func (s *BusinessExtServer) SignIn(ctx context.Context, req *pb.SignInReq) (*pb.SignInResp, error) {
isNew, userId, token, err := app.AuthApp.SignIn(ctx, req.PhoneNumber, req.Code, req.DeviceId)
if err != nil {
return nil, err
}
return &pb.SignInResp{
IsNew: isNew,
UserId: userId,
Token: token,
}, nil
}

func (s *BusinessExtServer) GetUser(ctx context.Context, req *pb.GetUserReq) (*pb.GetUserResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

user, err := app.UserApp.Get(ctx, userId)
return &pb.GetUserResp{User: user}, err
}

func (s *BusinessExtServer) UpdateUser(ctx context.Context, req *pb.UpdateUserReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

return new(pb.Empty), app.UserApp.Update(ctx, userId, req)
}

func (s *BusinessExtServer) SearchUser(ctx context.Context, req *pb.SearchUserReq) (*pb.SearchUserResp, error) {
users, err := app.UserApp.Search(ctx, req.Key)
return &pb.SearchUserResp{Users: users}, err
}

+ 51
- 0
internal/business/api/business_ext_test.go Näytä tiedosto

@@ -0,0 +1,51 @@
package api

import (
"context"
"fmt"
"gim/pkg/pb"
"strconv"
"testing"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

func getBusinessExtClient() pb.BusinessExtClient {
conn, err := grpc.Dial("111.229.238.28:50200", grpc.WithInsecure())
if err != nil {
fmt.Println(err)
return nil
}
return pb.NewBusinessExtClient(conn)
}

func getCtx() context.Context {
token := "0"
return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs(
"user_id", "3",
"device_id", "1",
"token", token,
"request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))
}

func TestUserExtServer_SignIn(t *testing.T) {
resp, err := getBusinessExtClient().SignIn(getCtx(), &pb.SignInReq{
PhoneNumber: "11111111111",
Code: "1",
DeviceId: 1,
})
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", resp)
}

func TestUserExtServer_GetUser(t *testing.T) {
resp, err := getBusinessExtClient().GetUser(getCtx(), &pb.GetUserReq{UserId: 1})
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", resp)
}

+ 28
- 0
internal/business/api/business_int.go Näytä tiedosto

@@ -0,0 +1,28 @@
package api

import (
"context"
"gim/internal/business/app"
"gim/pkg/pb"
)

type BusinessIntServer struct{}

func (*BusinessIntServer) Auth(ctx context.Context, req *pb.AuthReq) (*pb.Empty, error) {
return &pb.Empty{}, app.AuthApp.Auth(ctx, req.UserId, req.DeviceId, req.Token)
}

func (*BusinessIntServer) GetUser(ctx context.Context, req *pb.GetUserReq) (*pb.GetUserResp, error) {
user, err := app.UserApp.Get(ctx, req.UserId)
return &pb.GetUserResp{User: user}, err
}

func (*BusinessIntServer) GetUsers(ctx context.Context, req *pb.GetUsersReq) (*pb.GetUsersResp, error) {
var userIds = make([]int64, 0, len(req.UserIds))
for k := range req.UserIds {
userIds = append(userIds, k)
}

users, err := app.UserApp.GetByIds(ctx, userIds)
return &pb.GetUsersResp{Users: users}, err
}

+ 41
- 0
internal/business/api/business_int_test.go Näytä tiedosto

@@ -0,0 +1,41 @@
package api

import (
"fmt"
"gim/pkg/pb"
"testing"

"google.golang.org/grpc"
)

func getBusinessIntClient() pb.BusinessIntClient {
conn, err := grpc.Dial("localhost:50300", grpc.WithInsecure())
if err != nil {
fmt.Println(err)
return nil
}
return pb.NewBusinessIntClient(conn)
}

func TestUserIntServer_Auth(t *testing.T) {
_, err := getBusinessIntClient().Auth(getCtx(), &pb.AuthReq{
UserId: 3,
DeviceId: 1,
Token: "0",
})
fmt.Println(err)
}

func TestUserIntServer_GetUsers(t *testing.T) {
resp, err := getBusinessIntClient().GetUsers(getCtx(), &pb.GetUsersReq{
UserIds: map[int64]int32{1: 0, 2: 0, 3: 0},
})
if err != nil {
fmt.Println(err)
return
}

for k, v := range resp.Users {
fmt.Printf("%+-5v %+v\n", k, v)
}
}

+ 20
- 0
internal/business/app/auth_app.go Näytä tiedosto

@@ -0,0 +1,20 @@
package app

import (
"context"
"gim/internal/business/domain/user/service"
)

type authApp struct{}

var AuthApp = new(authApp)

// SignIn 长连接登录
func (*authApp) SignIn(ctx context.Context, phoneNumber, code string, deviceId int64) (bool, int64, string, error) {
return service.AuthService.SignIn(ctx, phoneNumber, code, deviceId)
}

// Auth 验证用户是否登录
func (*authApp) Auth(ctx context.Context, userId, deviceId int64, token string) error {
return service.AuthService.Auth(ctx, userId, deviceId, token)
}

+ 65
- 0
internal/business/app/user_app.go Näytä tiedosto

@@ -0,0 +1,65 @@
package app

import (
"context"
"gim/internal/business/domain/user/repo"
"gim/pkg/pb"
"time"
)

type userApp struct{}

var UserApp = new(userApp)

func (*userApp) Get(ctx context.Context, userId int64) (*pb.User, error) {
user, err := repo.UserRepo.Get(userId)
return user.ToProto(), err
}

func (*userApp) Update(ctx context.Context, userId int64, req *pb.UpdateUserReq) error {
u, err := repo.UserRepo.Get(userId)
if err != nil {
return err
}
if u == nil {
return nil
}

u.Nickname = req.Nickname
u.Sex = req.Sex
u.AvatarUrl = req.AvatarUrl
u.Extra = req.Extra
u.UpdateTime = time.Now()

err = repo.UserRepo.Save(u)
if err != nil {
return err
}
return nil
}

func (*userApp) GetByIds(ctx context.Context, userIds []int64) (map[int64]*pb.User, error) {
users, err := repo.UserRepo.GetByIds(userIds)
if err != nil {
return nil, err
}

pbUsers := make(map[int64]*pb.User, len(users))
for i := range users {
pbUsers[users[i].Id] = users[i].ToProto()
}
return pbUsers, nil
}

func (*userApp) Search(ctx context.Context, key string) ([]*pb.User, error) {
users, err := repo.UserRepo.Search(key)
if err != nil {
return nil, err
}

pbUsers := make([]*pb.User, len(users))
for i, v := range users {
pbUsers[i] = v.ToProto()
}
return pbUsers, nil
}

+ 7
- 0
internal/business/domain/user/model/device.go Näytä tiedosto

@@ -0,0 +1,7 @@
package model

type Device struct {
Type int32 // 设备类型,1:Android;2:IOS;3:Windows; 4:MacOS;5:Web
Token string // token
Expire int64 // 过期时间
}

+ 34
- 0
internal/business/domain/user/model/user.go Näytä tiedosto

@@ -0,0 +1,34 @@
package model

import (
"gim/pkg/pb"
"time"
)

// User 账户
type User struct {
Id int64 // 用户id
PhoneNumber string // 手机号
Nickname string // 昵称
Sex int32 // 性别,1:男;2:女
AvatarUrl string // 用户头像
Extra string // 附加属性
CreateTime time.Time // 创建时间
UpdateTime time.Time // 更新时间
}

func (u *User) ToProto() *pb.User {
if u == nil {
return nil
}

return &pb.User{
UserId: u.Id,
Nickname: u.Nickname,
Sex: u.Sex,
AvatarUrl: u.AvatarUrl,
Extra: u.Extra,
CreateTime: u.CreateTime.Unix(),
UpdateTime: u.UpdateTime.Unix(),
}
}

+ 74
- 0
internal/business/domain/user/repo/auth_cache.go Näytä tiedosto

@@ -0,0 +1,74 @@
package repo

import (
"encoding/json"
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"gim/pkg/util"
"strconv"

"github.com/go-redis/redis"
)

const (
AuthKey = "auth:"
)

type authCache struct{}

var AuthCache = new(authCache)

func (*authCache) Get(userId, deviceId int64) (*model.Device, error) {
bytes, err := db.RedisCli.HGet(AuthKey+strconv.FormatInt(userId, 10), strconv.FormatInt(deviceId, 10)).Bytes()
if err != nil && err != redis.Nil {
return nil, gerrors.WrapError(err)
}
if err == redis.Nil {
return nil, nil
}

var device model.Device
err = json.Unmarshal(bytes, &device)
if err != nil {
return nil, gerrors.WrapError(err)
}
return &device, nil
}

func (*authCache) Set(userId, deviceId int64, device model.Device) error {
bytes, err := json.Marshal(device)
if err != nil {
return gerrors.WrapError(err)
}

_, err = db.RedisCli.HSet(AuthKey+strconv.FormatInt(userId, 10), strconv.FormatInt(deviceId, 10), bytes).Result()
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

func (*authCache) GetAll(userId int64) (map[int64]model.Device, error) {
result, err := db.RedisCli.HGetAll(AuthKey + strconv.FormatInt(userId, 10)).Result()
if err != nil {
return nil, gerrors.WrapError(err)
}

var devices = make(map[int64]model.Device, len(result))

for k, v := range result {
deviceId, err := strconv.ParseInt(k, 10, 64)
if err != nil {
return nil, gerrors.WrapError(err)
}

var device model.Device
err = json.Unmarshal(util.Str2bytes(v), &device)
if err != nil {
return nil, gerrors.WrapError(err)
}
devices[deviceId] = device
}
return devices, nil
}

+ 23
- 0
internal/business/domain/user/repo/auth_cache_test.go Näytä tiedosto

@@ -0,0 +1,23 @@
package repo

import (
"fmt"
"gim/internal/business/domain/user/model"
"testing"
)

func TestAuthCache_Get(t *testing.T) {
fmt.Println(AuthCache.Get(1, 1))
}

func TestAuthCache_Set(t *testing.T) {
fmt.Println(AuthCache.Set(1, 1, model.Device{
Type: 1,
Token: "111",
Expire: 111,
}))
}

func TestAuthCache_GetAll(t *testing.T) {
fmt.Println(AuthCache.GetAll(1))
}

+ 19
- 0
internal/business/domain/user/repo/auth_repo.go Näytä tiedosto

@@ -0,0 +1,19 @@
package repo

import "gim/internal/business/domain/user/model"

type authRepo struct{}

var AuthRepo = new(authRepo)

func (*authRepo) Get(userId, deviceId int64) (*model.Device, error) {
return AuthCache.Get(userId, deviceId)
}

func (*authRepo) Set(userId, deviceId int64, device model.Device) error {
return AuthCache.Set(userId, deviceId, device)
}

func (*authRepo) GetAll(userId int64) (map[int64]model.Device, error) {
return AuthCache.GetAll(userId)
}

+ 51
- 0
internal/business/domain/user/repo/user_cache.go Näytä tiedosto

@@ -0,0 +1,51 @@
package repo

import (
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"strconv"
"time"

"github.com/go-redis/redis"
)

const (
UserKey = "user:"
UserExpire = 2 * time.Hour
)

type userCache struct{}

var UserCache = new(userCache)

// Get 获取用户缓存
func (c *userCache) Get(userId int64) (*model.User, error) {
var user model.User
err := db.RedisUtil.Get(UserKey+strconv.FormatInt(userId, 10), &user)
if err != nil && err != redis.Nil {
return nil, gerrors.WrapError(err)
}
if err == redis.Nil {
return nil, nil
}
return &user, nil
}

// Set 设置用户缓存
func (c *userCache) Set(user model.User) error {
err := db.RedisUtil.Set(UserKey+strconv.FormatInt(user.Id, 10), user, UserExpire)
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// Del 删除用户缓存
func (c *userCache) Del(userId int64) error {
_, err := db.RedisCli.Del(UserKey + strconv.FormatInt(userId, 10)).Result()
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

+ 81
- 0
internal/business/domain/user/repo/user_dao.go Näytä tiedosto

@@ -0,0 +1,81 @@
package repo

import (
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"time"

"github.com/jinzhu/gorm"
)

type userDao struct{}

var UserDao = new(userDao)

// Add 插入一条用户信息
func (*userDao) Add(user model.User) (int64, error) {
user.CreateTime = time.Now()
user.UpdateTime = time.Now()
err := db.DB.Create(&user).Error
if err != nil {
return 0, gerrors.WrapError(err)
}
return user.Id, nil
}

// Get 获取用户信息
func (*userDao) Get(userId int64) (*model.User, error) {
var user = model.User{Id: userId}
err := db.DB.First(&user).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &user, err
}

// Save 保存
func (*userDao) Save(user *model.User) error {
err := db.DB.Save(user).Error
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// GetByPhoneNumber 根据手机号获取用户信息
func (*userDao) GetByPhoneNumber(phoneNumber string) (*model.User, error) {
var user model.User
err := db.DB.First(&user, "phone_number = ?", phoneNumber).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &user, err
}

// GetByIds 获取用户信息
func (*userDao) GetByIds(userIds []int64) ([]model.User, error) {
var users []model.User
err := db.DB.Find(&users, "id in (?)", userIds).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return users, err
}

// Search 查询用户,这里简单实现,生产环境建议使用ES
func (*userDao) Search(key string) ([]model.User, error) {
var users []model.User
key = "%" + key + "%"
err := db.DB.Where("phone_number like ? or nickname like ?", key, key).Find(&users).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return users, nil
}

+ 44
- 0
internal/business/domain/user/repo/user_dao_test.go Näytä tiedosto

@@ -0,0 +1,44 @@
package repo

import (
"fmt"
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"testing"
)

func init() {
fmt.Println("init db")
db.InitByTest()
}

func TestUserDao_Add(t *testing.T) {
id, err := UserDao.Add(model.User{
PhoneNumber: "18829291351",
Nickname: "Alber",
Sex: 1,
AvatarUrl: "AvatarUrl",
Extra: "Extra",
})
fmt.Printf("%+v\n %+v\n ", id, err)
}

func TestUserDao_Get(t *testing.T) {
user, err := UserDao.Get(1)
fmt.Printf("%+v\n %+v\n ", user, err)
}

func TestUserDao_GetByIds(t *testing.T) {
users, err := UserDao.GetByIds([]int64{1, 2, 3})
fmt.Printf("%+v\n %+v\n ", users, err)
}

func TestUserDao_GetByPhoneNumber(t *testing.T) {
user, err := UserDao.GetByPhoneNumber("18829291351")
fmt.Printf("%+v\n %+v\n ", user, err)
}

func TestUserDao_Search(t *testing.T) {
users, err := UserDao.Search("哈哈哈")
fmt.Printf("%+v\n %+v\n ", users, err)
}

+ 64
- 0
internal/business/domain/user/repo/user_repo.go Näytä tiedosto

@@ -0,0 +1,64 @@
package repo

import (
"gim/internal/business/domain/user/model"
)

type userRepo struct{}

var UserRepo = new(userRepo)

// Get 获取单个用户
func (*userRepo) Get(userId int64) (*model.User, error) {
user, err := UserCache.Get(userId)
if err != nil {
return nil, err
}
if user != nil {
return user, nil
}

user, err = UserDao.Get(userId)
if err != nil {
return nil, err
}

if user != nil {
err = UserCache.Set(*user)
if err != nil {
return nil, err
}
}
return user, err
}

func (*userRepo) GetByPhoneNumber(phoneNumber string) (*model.User, error) {
return UserDao.GetByPhoneNumber(phoneNumber)
}

// GetByIds 获取多个用户
func (*userRepo) GetByIds(userIds []int64) ([]model.User, error) {
return UserDao.GetByIds(userIds)
}

// Search 搜索用户
func (*userRepo) Search(key string) ([]model.User, error) {
return UserDao.Search(key)
}

// Save 保存用户
func (*userRepo) Save(user *model.User) error {
userId := user.Id
err := UserDao.Save(user)
if err != nil {
return err
}

if userId != 0 {
err = UserCache.Del(user.Id)
if err != nil {
return err
}
}
return nil
}

+ 86
- 0
internal/business/domain/user/service/auth.go Näytä tiedosto

@@ -0,0 +1,86 @@
package service

import (
"context"
"gim/internal/business/domain/user/model"
"gim/internal/business/domain/user/repo"
"gim/pkg/gerrors"
"gim/pkg/pb"
"gim/pkg/rpc"
"time"
)

type authService struct{}

var AuthService = new(authService)

// SignIn 登录
func (*authService) SignIn(ctx context.Context, phoneNumber, code string, deviceId int64) (bool, int64, string, error) {
if !Verify(phoneNumber, code) {
return false, 0, "", gerrors.ErrBadCode
}

user, err := repo.UserRepo.GetByPhoneNumber(phoneNumber)
if err != nil {
return false, 0, "", err
}

var isNew = false
if user == nil {
user = &model.User{
PhoneNumber: phoneNumber,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
err := repo.UserRepo.Save(user)
if err != nil {
return false, 0, "", err
}
isNew = true
}

resp, err := rpc.GetLogicIntClient().GetDevice(ctx, &pb.GetDeviceReq{DeviceId: deviceId})
if err != nil {
return false, 0, "", err
}

// 方便测试
token := "0"
//token := util.RandString(40)
err = repo.AuthRepo.Set(user.Id, resp.Device.DeviceId, model.Device{
Type: resp.Device.Type,
Token: token,
Expire: time.Now().AddDate(0, 3, 0).Unix(),
})
if err != nil {
return false, 0, "", err
}

return isNew, user.Id, token, nil
}

func Verify(phoneNumber, code string) bool {
// 假装他成功了
return true
}

// Auth 验证用户是否登录
func (*authService) Auth(ctx context.Context, userId, deviceId int64, token string) error {
device, err := repo.AuthRepo.Get(userId, deviceId)
if err != nil {
return err
}

if device == nil {
return gerrors.ErrUnauthorized
}

if device.Expire < time.Now().Unix() {
return gerrors.ErrUnauthorized
}

if device.Token != token {
return gerrors.ErrUnauthorized
}
return nil
}

+ 12
- 0
internal/business/domain/user/service/auth_test.go Näytä tiedosto

@@ -0,0 +1,12 @@
package service

import (
"testing"
)

func TestAuthService_SignIn(t *testing.T) {
}

func TestAuthService_Auth(t *testing.T) {

}

+ 32
- 0
internal/connect/api.go Näytä tiedosto

@@ -0,0 +1,32 @@
package connect

import (
"context"
"gim/pkg/grpclib"
"gim/pkg/logger"
"gim/pkg/pb"

"go.uber.org/zap"
)

type ConnIntServer struct{}

// DeliverMessage 投递消息
func (s *ConnIntServer) DeliverMessage(ctx context.Context, req *pb.DeliverMessageReq) (*pb.Empty, error) {
resp := &pb.Empty{}

// 获取设备对应的TCP连接
conn := GetConn(req.DeviceId)
if conn == nil {
logger.Logger.Warn("GetConn warn", zap.Int64("device_id", req.DeviceId))
return resp, nil
}

if conn.DeviceId != req.DeviceId {
logger.Logger.Warn("GetConn warn", zap.Int64("device_id", req.DeviceId))
return resp, nil
}

conn.Send(pb.PackageType_PT_MESSAGE, grpclib.GetCtxRequestId(ctx), req.MessageSend, nil)
return resp, nil
}

+ 261
- 0
internal/connect/conn.go Näytä tiedosto

@@ -0,0 +1,261 @@
package connect

import (
"container/list"
"context"
"gim/config"
"gim/pkg/grpclib"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"sync"
"time"

"go.uber.org/zap"

"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"

"github.com/alberliu/gn"
"github.com/gorilla/websocket"
)

const (
CoonTypeTCP int8 = 1 // tcp连接
ConnTypeWS int8 = 2 // websocket连接
)

type Conn struct {
CoonType int8 // 连接类型
TCP *gn.Conn // tcp连接
WSMutex sync.Mutex // WS写锁
WS *websocket.Conn // websocket连接
UserId int64 // 用户ID
DeviceId int64 // 设备ID
RoomId int64 // 订阅的房间ID
Element *list.Element // 链表节点
}

// Write 写入数据
func (c *Conn) Write(bytes []byte) error {
if c.CoonType == CoonTypeTCP {
return c.TCP.WriteWithEncoder(bytes)
} else if c.CoonType == ConnTypeWS {
return c.WriteToWS(bytes)
}
logger.Logger.Error("unknown conn type", zap.Any("conn", c))
return nil
}

// WriteToWS 消息写入WebSocket
func (c *Conn) WriteToWS(bytes []byte) error {
c.WSMutex.Lock()
defer c.WSMutex.Unlock()

err := c.WS.SetWriteDeadline(time.Now().Add(10 * time.Millisecond))
if err != nil {
return err
}
return c.WS.WriteMessage(websocket.BinaryMessage, bytes)
}

// Close 关闭
func (c *Conn) Close() error {
// 取消设备和连接的对应关系
if c.DeviceId != 0 {
DeleteConn(c.DeviceId)
}

// 取消订阅,需要异步出去,防止重复加锁造成死锁
go func() {
SubscribedRoom(c, 0)
}()

if c.DeviceId != 0 {
_, _ = rpc.GetLogicIntClient().Offline(context.TODO(), &pb.OfflineReq{
UserId: c.UserId,
DeviceId: c.DeviceId,
ClientAddr: c.GetAddr(),
})
}

if c.CoonType == CoonTypeTCP {
c.TCP.Close()
} else if c.CoonType == ConnTypeWS {
return c.WS.Close()
}
return nil
}

func (c *Conn) GetAddr() string {
if c.CoonType == CoonTypeTCP {
return c.TCP.GetAddr()
} else if c.CoonType == ConnTypeWS {
return c.WS.RemoteAddr().String()
}
return ""
}

// HandleMessage 消息处理
func (c *Conn) HandleMessage(bytes []byte) {
var input = new(pb.Input)
err := proto.Unmarshal(bytes, input)
if err != nil {
logger.Logger.Error("unmarshal error", zap.Error(err))
return
}
logger.Logger.Debug("HandleMessage", zap.Any("input", input))

// 对未登录的用户进行拦截
if input.Type != pb.PackageType_PT_SIGN_IN && c.UserId == 0 {
// 应该告诉用户没有登录
return
}

switch input.Type {
case pb.PackageType_PT_SIGN_IN:
c.SignIn(input)
case pb.PackageType_PT_SYNC:
c.Sync(input)
case pb.PackageType_PT_HEARTBEAT:
c.Heartbeat(input)
case pb.PackageType_PT_MESSAGE:
c.MessageACK(input)
case pb.PackageType_PT_SUBSCRIBE_ROOM:
c.SubscribedRoom(input)
default:
logger.Logger.Error("handler switch other")
}
}

// Send 下发消息
func (c *Conn) Send(pt pb.PackageType, requestId int64, message proto.Message, err error) {
var output = pb.Output{
Type: pt,
RequestId: requestId,
}

if err != nil {
status, _ := status.FromError(err)
output.Code = int32(status.Code())
output.Message = status.Message()
}

if message != nil {
msgBytes, err := proto.Marshal(message)
if err != nil {
logger.Sugar.Error(err)
return
}
output.Data = msgBytes
}

outputBytes, err := proto.Marshal(&output)
if err != nil {
logger.Sugar.Error(err)
return
}

err = c.Write(outputBytes)
if err != nil {
logger.Sugar.Error(err)
c.Close()
return
}
}

// SignIn 登录
func (c *Conn) SignIn(input *pb.Input) {
var signIn pb.SignInInput
err := proto.Unmarshal(input.Data, &signIn)
if err != nil {
logger.Sugar.Error(err)
return
}

_, err = rpc.GetLogicIntClient().ConnSignIn(grpclib.ContextWithRequestId(context.TODO(), input.RequestId), &pb.ConnSignInReq{
UserId: signIn.UserId,
DeviceId: signIn.DeviceId,
Token: signIn.Token,
ConnAddr: config.LocalAddr,
ClientAddr: c.GetAddr(),
})

c.Send(pb.PackageType_PT_SIGN_IN, input.RequestId, nil, err)
if err != nil {
return
}

c.UserId = signIn.UserId
c.DeviceId = signIn.DeviceId
SetConn(signIn.DeviceId, c)
}

// Sync 消息同步
func (c *Conn) Sync(input *pb.Input) {
var sync pb.SyncInput
err := proto.Unmarshal(input.Data, &sync)
if err != nil {
logger.Sugar.Error(err)
return
}

resp, err := rpc.GetLogicIntClient().Sync(grpclib.ContextWithRequestId(context.TODO(), input.RequestId), &pb.SyncReq{
UserId: c.UserId,
DeviceId: c.DeviceId,
Seq: sync.Seq,
})

var message proto.Message
if err == nil {
message = &pb.SyncOutput{Messages: resp.Messages, HasMore: resp.HasMore}
}
c.Send(pb.PackageType_PT_SYNC, input.RequestId, message, err)
}

// Heartbeat 心跳
func (c *Conn) Heartbeat(input *pb.Input) {
c.Send(pb.PackageType_PT_HEARTBEAT, input.RequestId, nil, nil)

logger.Sugar.Infow("heartbeat", "device_id", c.DeviceId, "user_id", c.UserId)
}

// MessageACK 消息收到回执
func (c *Conn) MessageACK(input *pb.Input) {
var messageACK pb.MessageACK
err := proto.Unmarshal(input.Data, &messageACK)
if err != nil {
logger.Sugar.Error(err)
return
}

_, _ = rpc.GetLogicIntClient().MessageACK(grpclib.ContextWithRequestId(context.TODO(), input.RequestId), &pb.MessageACKReq{
UserId: c.UserId,
DeviceId: c.DeviceId,
DeviceAck: messageACK.DeviceAck,
ReceiveTime: messageACK.ReceiveTime,
})
}

// SubscribedRoom 订阅房间
func (c *Conn) SubscribedRoom(input *pb.Input) {
var subscribeRoom pb.SubscribeRoomInput
err := proto.Unmarshal(input.Data, &subscribeRoom)
if err != nil {
logger.Sugar.Error(err)
return
}

SubscribedRoom(c, subscribeRoom.RoomId)
c.Send(pb.PackageType_PT_SUBSCRIBE_ROOM, input.RequestId, nil, nil)
_, err = rpc.GetLogicIntClient().SubscribeRoom(context.TODO(), &pb.SubscribeRoomReq{
UserId: c.UserId,
DeviceId: c.DeviceId,
RoomId: subscribeRoom.RoomId,
Seq: subscribeRoom.Seq,
ConnAddr: config.LocalAddr,
})
if err != nil {
logger.Logger.Error("SubscribedRoom error", zap.Error(err))
}
}

+ 36
- 0
internal/connect/conn_manager.go Näytä tiedosto

@@ -0,0 +1,36 @@
package connect

import (
"gim/pkg/pb"
"sync"
)

var ConnsManager = sync.Map{}

// SetConn 存储
func SetConn(deviceId int64, conn *Conn) {
ConnsManager.Store(deviceId, conn)
}

// GetConn 获取
func GetConn(deviceId int64) *Conn {
value, ok := ConnsManager.Load(deviceId)
if ok {
return value.(*Conn)
}
return nil
}

// DeleteConn 删除
func DeleteConn(deviceId int64) {
ConnsManager.Delete(deviceId)
}

// PushAll 全服推送
func PushAll(message *pb.MessageSend) {
ConnsManager.Range(func(key, value interface{}) bool {
conn := value.(*Conn)
conn.Send(pb.PackageType_PT_MESSAGE, 0, message, nil)
return true
})
}

+ 72
- 0
internal/connect/mq.go Näytä tiedosto

@@ -0,0 +1,72 @@
package connect

import (
"gim/config"
"gim/pkg/db"
"gim/pkg/logger"
"gim/pkg/mq"
"gim/pkg/pb"
"time"

"github.com/go-redis/redis"

"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)

// StartSubscribe 启动MQ消息处理逻辑
func StartSubscribe() {
pushRoomPriorityChannel := db.RedisCli.Subscribe(mq.PushRoomPriorityTopic).Channel()
pushRoomChannel := db.RedisCli.Subscribe(mq.PushRoomTopic).Channel()
for i := 0; i < config.PushRoomSubscribeNum; i++ {
go handlePushRoomMsg(pushRoomPriorityChannel, pushRoomChannel)
}

pushAllChannel := db.RedisCli.Subscribe(mq.PushAllTopic).Channel()
for i := 0; i < config.PushAllSubscribeNum; i++ {
go handlePushAllMsg(pushAllChannel)
}
}

func handlePushRoomMsg(priorityChannel, channel <-chan *redis.Message) {
for {
select {
case msg := <-priorityChannel:
handlePushRoom([]byte(msg.Payload))
default:
select {
case msg := <-channel:
handlePushRoom([]byte(msg.Payload))
default:
time.Sleep(100 * time.Millisecond)
continue
}
}
}
}

func handlePushAllMsg(channel <-chan *redis.Message) {
for msg := range channel {
handlePushAll([]byte(msg.Payload))
}
}

func handlePushRoom(bytes []byte) {
var msg pb.PushRoomMsg
err := proto.Unmarshal(bytes, &msg)
if err != nil {
logger.Logger.Error("handlePushRoom error", zap.Error(err))
return
}
PushRoom(msg.RoomId, msg.MessageSend)
}

func handlePushAll(bytes []byte) {
var msg pb.PushAllMsg
err := proto.Unmarshal(bytes, &msg)
if err != nil {
logger.Logger.Error("handlePushRoom error", zap.Error(err))
return
}
PushAll(msg.MessageSend)
}

+ 105
- 0
internal/connect/room.go Näytä tiedosto

@@ -0,0 +1,105 @@
package connect

import (
"container/list"
"gim/pkg/pb"
"sync"
)

var RoomsManager sync.Map

// SubscribedRoom 订阅房间
func SubscribedRoom(conn *Conn, roomId int64) {
if roomId == conn.RoomId {
return
}

oldRoomId := conn.RoomId
// 取消订阅
if oldRoomId != 0 {
value, ok := RoomsManager.Load(oldRoomId)
if !ok {
return
}
room := value.(*Room)
room.Unsubscribe(conn)

if room.Conns.Front() == nil {
RoomsManager.Delete(oldRoomId)
}
return
}

// 订阅
if roomId != 0 {
value, ok := RoomsManager.Load(roomId)
var room *Room
if !ok {
room = NewRoom(roomId)
RoomsManager.Store(roomId, room)
} else {
room = value.(*Room)
}
room.Subscribe(conn)
return
}
}

// PushRoom 房间消息推送
func PushRoom(roomId int64, message *pb.MessageSend) {
value, ok := RoomsManager.Load(roomId)
if !ok {
return
}

value.(*Room).Push(message)
}

type Room struct {
RoomId int64 // 房间ID
Conns *list.List // 订阅房间消息的连接
lock sync.RWMutex
}

func NewRoom(roomId int64) *Room {
return &Room{
RoomId: roomId,
Conns: list.New(),
}
}

// Subscribe 订阅房间
func (r *Room) Subscribe(conn *Conn) {
r.lock.Lock()
defer r.lock.Unlock()

conn.Element = r.Conns.PushBack(conn)
conn.RoomId = r.RoomId
}

// Unsubscribe 取消订阅
func (r *Room) Unsubscribe(conn *Conn) {
r.lock.Lock()
defer r.lock.Unlock()

r.Conns.Remove(conn.Element)
conn.Element = nil
conn.RoomId = 0
}

// Push 推送消息到房间
func (r *Room) Push(message *pb.MessageSend) {
r.lock.RLock()
defer r.lock.RUnlock()

element := r.Conns.Front()
for {
conn := element.Value.(*Conn)
conn.Send(pb.PackageType_PT_MESSAGE, 0, message, nil)

element = element.Next()
if element == nil {
break
}
}
}

+ 70
- 0
internal/connect/tcp_server.go Näytä tiedosto

@@ -0,0 +1,70 @@
package connect

import (
"context"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"time"

"go.uber.org/zap"

"github.com/alberliu/gn"
)

var encoder = gn.NewHeaderLenEncoder(2, 1024)

var server *gn.Server

// StartTCPServer 启动TCP服务器
func StartTCPServer(addr string) {
gn.SetLogger(logger.Sugar)

var err error
server, err = gn.NewServer(addr, &handler{},
gn.WithDecoder(gn.NewHeaderLenDecoder(2)),
gn.WithEncoder(gn.NewHeaderLenEncoder(2, 1024)),
gn.WithReadBufferLen(256),
gn.WithTimeout(11*time.Minute),
gn.WithAcceptGNum(10),
gn.WithIOGNum(100))
if err != nil {
logger.Sugar.Error(err)
panic(err)
}

server.Run()
}

type handler struct{}

func (*handler) OnConnect(c *gn.Conn) {
// 初始化连接数据
conn := &Conn{
CoonType: CoonTypeTCP,
TCP: c,
}
c.SetData(conn)
logger.Logger.Debug("connect:", zap.Int32("fd", c.GetFd()), zap.String("addr", c.GetAddr()))
}

func (*handler) OnMessage(c *gn.Conn, bytes []byte) {
conn := c.GetData().(*Conn)
conn.HandleMessage(bytes)
}

func (*handler) OnClose(c *gn.Conn, err error) {
conn := c.GetData().(*Conn)
logger.Logger.Debug("close", zap.String("addr", c.GetAddr()), zap.Int64("user_id", conn.UserId),
zap.Int64("device_id", conn.DeviceId), zap.Error(err))

DeleteConn(conn.DeviceId)

if conn.UserId != 0 {
_, _ = rpc.GetLogicIntClient().Offline(context.TODO(), &pb.OfflineReq{
UserId: conn.UserId,
DeviceId: conn.DeviceId,
ClientAddr: c.GetAddr(),
})
}
}

+ 87
- 0
internal/connect/ws_server.go Näytä tiedosto

@@ -0,0 +1,87 @@
package connect

import (
"gim/pkg/logger"
"gim/pkg/util"
"io"
"net/http"
"strings"
"time"

"go.uber.org/zap"

"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 65536,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

// StartWSServer 启动WebSocket服务器
func StartWSServer(address string) {
http.HandleFunc("/ws", wsHandler)
logger.Logger.Info("websocket server start")
err := http.ListenAndServe(address, nil)
if err != nil {
panic(err)
}
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
wsConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Sugar.Error(err)
return
}

conn := &Conn{
CoonType: ConnTypeWS,
WS: wsConn,
}
DoConn(conn)
}

// DoConn 处理连接
func DoConn(conn *Conn) {
defer util.RecoverPanic()

for {
err := conn.WS.SetReadDeadline(time.Now().Add(12 * time.Minute))
if err != nil {
HandleReadErr(conn, err)
return
}
_, data, err := conn.WS.ReadMessage()
if err != nil {
HandleReadErr(conn, err)
return
}

conn.HandleMessage(data)
}
}

// HandleReadErr 读取conn错误
func HandleReadErr(conn *Conn, err error) {
logger.Logger.Debug("read tcp error:", zap.Int64("user_id", conn.UserId),
zap.Int64("device_id", conn.DeviceId), zap.Error(err))
str := err.Error()
// 服务器主动关闭连接
if strings.HasSuffix(str, "use of closed network connection") {
return
}

conn.Close()
// 客户端主动关闭连接或者异常程序退出
if err == io.EOF {
return
}
// SetReadDeadline 之后,超时返回的错误
if strings.HasSuffix(str, "i/o timeout") {
return
}
}

+ 168
- 0
internal/logic/api/logic_ext.go Näytä tiedosto

@@ -0,0 +1,168 @@
package api

import (
"context"
"gim/internal/logic/app"
"gim/pkg/grpclib"
"gim/pkg/pb"
)

type LogicExtServer struct{}

// RegisterDevice 注册设备
func (*LogicExtServer) RegisterDevice(ctx context.Context, in *pb.RegisterDeviceReq) (*pb.RegisterDeviceResp, error) {
deviceId, err := app.DeviceApp.Register(ctx, in)
return &pb.RegisterDeviceResp{DeviceId: deviceId}, err
}

// SendMessage 发送消息
func (*LogicExtServer) SendMessage(ctx context.Context, in *pb.SendMessageReq) (*pb.SendMessageResp, error) {
userId, deviceId, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

sender := pb.Sender{
SenderType: pb.SenderType_ST_USER,
SenderId: userId,
DeviceId: deviceId,
}
seq, err := app.MessageApp.SendMessage(ctx, &sender, in)
if err != nil {
return nil, err
}
return &pb.SendMessageResp{Seq: seq}, nil
}

// PushRoom 推送房间
func (s *LogicExtServer) PushRoom(ctx context.Context, req *pb.PushRoomReq) (*pb.Empty, error) {
userId, deviceId, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
return &pb.Empty{}, app.RoomApp.Push(ctx, &pb.Sender{
SenderType: pb.SenderType_ST_USER,
SenderId: userId,
DeviceId: deviceId,
}, req)
}

func (s *LogicExtServer) AddFriend(ctx context.Context, in *pb.AddFriendReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

err = app.FriendApp.AddFriend(ctx, userId, in.FriendId, in.Remarks, in.Description)
if err != nil {
return nil, err
}

return &pb.Empty{}, nil
}

func (s *LogicExtServer) AgreeAddFriend(ctx context.Context, in *pb.AgreeAddFriendReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

err = app.FriendApp.AgreeAddFriend(ctx, userId, in.UserId, in.Remarks)
if err != nil {
return nil, err
}

return &pb.Empty{}, nil
}

func (s *LogicExtServer) SetFriend(ctx context.Context, req *pb.SetFriendReq) (*pb.SetFriendResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

app.FriendApp.SetFriend(ctx, userId, req)
if err != nil {
return nil, err
}
return &pb.SetFriendResp{}, nil
}

func (s *LogicExtServer) GetFriends(ctx context.Context, in *pb.Empty) (*pb.GetFriendsResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}
friends, err := app.FriendApp.List(ctx, userId)
return &pb.GetFriendsResp{Friends: friends}, err
}

// CreateGroup 创建群组
func (*LogicExtServer) CreateGroup(ctx context.Context, in *pb.CreateGroupReq) (*pb.CreateGroupResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

groupId, err := app.GroupApp.CreateGroup(ctx, userId, in)
return &pb.CreateGroupResp{GroupId: groupId}, err
}

// UpdateGroup 更新群组
func (*LogicExtServer) UpdateGroup(ctx context.Context, in *pb.UpdateGroupReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

return &pb.Empty{}, app.GroupApp.Update(ctx, userId, in)
}

// GetGroup 获取群组信息
func (*LogicExtServer) GetGroup(ctx context.Context, in *pb.GetGroupReq) (*pb.GetGroupResp, error) {
group, err := app.GroupApp.GetGroup(ctx, in.GroupId)
return &pb.GetGroupResp{Group: group}, err
}

// GetGroups 获取用户加入的所有群组
func (*LogicExtServer) GetGroups(ctx context.Context, in *pb.Empty) (*pb.GetGroupsResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

groups, err := app.GroupApp.GetUserGroups(ctx, userId)
return &pb.GetGroupsResp{Groups: groups}, err
}

func (s *LogicExtServer) AddGroupMembers(ctx context.Context, in *pb.AddGroupMembersReq) (*pb.AddGroupMembersResp, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

userIds, err := app.GroupApp.AddMembers(ctx, userId, in.GroupId, in.UserIds)
return &pb.AddGroupMembersResp{UserIds: userIds}, err
}

// UpdateGroupMember 更新群组成员信息
func (*LogicExtServer) UpdateGroupMember(ctx context.Context, in *pb.UpdateGroupMemberReq) (*pb.Empty, error) {
return &pb.Empty{}, app.GroupApp.UpdateMember(ctx, in)
}

// DeleteGroupMember 添加群组成员
func (*LogicExtServer) DeleteGroupMember(ctx context.Context, in *pb.DeleteGroupMemberReq) (*pb.Empty, error) {
userId, _, err := grpclib.GetCtxData(ctx)
if err != nil {
return nil, err
}

err = app.GroupApp.DeleteMember(ctx, in.GroupId, in.UserId, userId)
return &pb.Empty{}, err
}

// GetGroupMembers 获取群组成员信息
func (s *LogicExtServer) GetGroupMembers(ctx context.Context, in *pb.GetGroupMembersReq) (*pb.GetGroupMembersResp, error) {
members, err := app.GroupApp.GetMembers(ctx, in.GroupId)
return &pb.GetGroupMembersResp{Members: members}, err
}

+ 208
- 0
internal/logic/api/logic_ext_test.go Näytä tiedosto

@@ -0,0 +1,208 @@
package api

import (
"context"
"fmt"
"gim/pkg/pb"
"gim/pkg/util"
"strconv"
"testing"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
)

func getLogicExtClient() pb.LogicExtClient {
conn, err := grpc.Dial("111.229.238.28:50000", grpc.WithInsecure())
if err != nil {
fmt.Println(err)
return nil
}
return pb.NewLogicExtClient(conn)
}

// deprecated:

func getCtx() context.Context {
token := "0"
return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs(
"user_id", "2",
"device_id", "1",
"token", token,
"request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))
}

func TestLogicExtServer_RegisterDevice(t *testing.T) {
resp, err := getLogicExtClient().RegisterDevice(context.TODO(),
&pb.RegisterDeviceReq{
Type: 1,
Brand: "huawei",
Model: "huawei P30",
SystemVersion: "1.0.0",
SdkVersion: "1.0.0",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_SendMessage(t *testing.T) {
buf, err := proto.Marshal(&pb.Text{
Text: "hello alber ",
})
if err != nil {
fmt.Println(err)
return
}
resp, err := getLogicExtClient().SendMessage(getCtx(),
&pb.SendMessageReq{
ReceiverType: pb.ReceiverType_RT_USER,
ReceiverId: 1,
ToUserIds: nil,
MessageType: pb.MessageType_MT_TEXT,
MessageContent: buf,
IsPersist: true,
SendTime: util.UnixMilliTime(time.Now()),
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_SendImageMessage(t *testing.T) {
buf, err := proto.Marshal(&pb.Image{
Id: "",
Width: 0,
Height: 0,
Url: "https://img.iplaysoft.com/wp-content/uploads/2019/free-images/free_stock_photo.jpg",
ThumbnailUrl: "",
})
if err != nil {
fmt.Println(err)
return
}
resp, err := getLogicExtClient().SendMessage(getCtx(),
&pb.SendMessageReq{
ReceiverType: pb.ReceiverType_RT_USER,
ReceiverId: 1,
ToUserIds: nil,
MessageType: pb.MessageType_MT_IMAGE,
MessageContent: buf,
IsPersist: true,
SendTime: util.UnixMilliTime(time.Now()),
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_CreateGroup(t *testing.T) {
resp, err := getLogicExtClient().CreateGroup(getCtx(),
&pb.CreateGroupReq{
Name: "10",
Introduction: "10",
Extra: "10",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_UpdateGroup(t *testing.T) {
resp, err := getLogicExtClient().UpdateGroup(getCtx(),
&pb.UpdateGroupReq{
GroupId: 2,
Name: "11",
Introduction: "11",
Extra: "11",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_GetGroup(t *testing.T) {
resp, err := getLogicExtClient().GetGroup(getCtx(),
&pb.GetGroupReq{
GroupId: 2,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_GetUserGroups(t *testing.T) {
resp, err := getLogicExtClient().GetGroups(getCtx(), &pb.Empty{})
if err != nil {
fmt.Println(err)
return
}
// todo 不能获取用户所在的超大群组
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_AddGroupMember(t *testing.T) {
resp, err := getLogicExtClient().AddGroupMembers(getCtx(),
&pb.AddGroupMembersReq{
GroupId: 2,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_UpdateGroupMember(t *testing.T) {
resp, err := getLogicExtClient().UpdateGroupMember(getCtx(),
&pb.UpdateGroupMemberReq{
GroupId: 2,
UserId: 3,
Remarks: "2",
Extra: "2",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_DeleteGroupMember(t *testing.T) {
resp, err := getLogicExtClient().DeleteGroupMember(getCtx(),
&pb.DeleteGroupMemberReq{
GroupId: 10,
UserId: 1,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicExtServer_GetGroupMembers(t *testing.T) {
resp, err := getLogicExtClient().GetGroupMembers(getCtx(),
&pb.GetGroupMembersReq{
GroupId: 2,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

+ 79
- 0
internal/logic/api/logic_int.go Näytä tiedosto

@@ -0,0 +1,79 @@
package api

import (
"context"
"gim/internal/logic/app"
"gim/pkg/logger"
"gim/pkg/pb"
)

type LogicIntServer struct{}

// ConnSignIn 设备登录
func (*LogicIntServer) ConnSignIn(ctx context.Context, req *pb.ConnSignInReq) (*pb.Empty, error) {
return &pb.Empty{},
app.DeviceApp.SignIn(ctx, req.UserId, req.DeviceId, req.Token, req.ConnAddr, req.ClientAddr)
}

// Sync 设备同步消息
func (*LogicIntServer) Sync(ctx context.Context, req *pb.SyncReq) (*pb.SyncResp, error) {
return app.MessageApp.Sync(ctx, req.UserId, req.Seq)
}

// MessageACK 设备收到消息ack
func (*LogicIntServer) MessageACK(ctx context.Context, req *pb.MessageACKReq) (*pb.Empty, error) {
return &pb.Empty{}, app.MessageApp.MessageAck(ctx, req.UserId, req.DeviceId, req.DeviceAck)
}

// Offline 设备离线
func (*LogicIntServer) Offline(ctx context.Context, req *pb.OfflineReq) (*pb.Empty, error) {
return &pb.Empty{}, app.DeviceApp.Offline(ctx, req.DeviceId, req.ClientAddr)
}

func (s *LogicIntServer) SubscribeRoom(ctx context.Context, req *pb.SubscribeRoomReq) (*pb.Empty, error) {
return &pb.Empty{}, app.RoomApp.SubscribeRoom(ctx, req)
}

// SendMessage 发送消息
func (*LogicIntServer) SendMessage(ctx context.Context, req *pb.SendMessageReq) (*pb.SendMessageResp, error) {
sender := pb.Sender{
SenderType: pb.SenderType_ST_BUSINESS,
SenderId: 0,
DeviceId: 0,
}

seq, err := app.MessageApp.SendMessage(ctx, &sender, req)
if err != nil {
return nil, err
}
return &pb.SendMessageResp{Seq: seq}, nil
}

// PushRoom 推送房间
func (s *LogicIntServer) PushRoom(ctx context.Context, req *pb.PushRoomReq) (*pb.Empty, error) {
return &pb.Empty{}, app.RoomApp.Push(ctx, &pb.Sender{
SenderType: pb.SenderType_ST_BUSINESS,
}, req)
}

// PushAll 全服推送
func (s *LogicIntServer) PushAll(ctx context.Context, req *pb.PushAllReq) (*pb.Empty, error) {
return &pb.Empty{}, app.MessageApp.PushAll(ctx, req)
}

// GetDevice 获取设备信息
func (*LogicIntServer) GetDevice(ctx context.Context, req *pb.GetDeviceReq) (*pb.GetDeviceResp, error) {
device, err := app.DeviceApp.GetDevice(ctx, req.DeviceId)
return &pb.GetDeviceResp{Device: device}, err
}

// ServerStop 服务停止
func (s *LogicIntServer) ServerStop(ctx context.Context, in *pb.ServerStopReq) (*pb.Empty, error) {
go func() {
err := app.DeviceApp.ServerStop(ctx, in.ConnAddr)
if err != nil {
logger.Sugar.Error(err)
}
}()
return &pb.Empty{}, nil
}

+ 128
- 0
internal/logic/api/logic_int_test.go Näytä tiedosto

@@ -0,0 +1,128 @@
package api

import (
"context"
"fmt"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/util"
"testing"
"time"

"google.golang.org/protobuf/proto"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

func getLogicIntClient() pb.LogicIntClient {
conn, err := grpc.Dial("111.229.238.28:50000", grpc.WithInsecure())
if err != nil {
logger.Sugar.Error(err)
return nil
}
return pb.NewLogicIntClient(conn)
}

func TestLogicIntServer_SignIn(t *testing.T) {
token := ""

resp, err := getLogicIntClient().ConnSignIn(context.TODO(),
&pb.ConnSignInReq{
DeviceId: 1,
UserId: 1,
Token: token,
ConnAddr: "127.0.0.1:5000",
})
if err != nil {
logger.Sugar.Error(err)
return
}
logger.Sugar.Info(resp)
}

func TestLogicIntServer_Sync(t *testing.T) {
resp, err := getLogicIntClient().Sync(metadata.NewOutgoingContext(context.TODO(), metadata.Pairs("key", "val")),
&pb.SyncReq{
UserId: 1,
DeviceId: 1,
Seq: 0,
})
if err != nil {
logger.Sugar.Error(err)
return
}
logger.Sugar.Info(resp)
}

func TestLogicIntServer_MessageACK(t *testing.T) {
resp, err := getLogicIntClient().MessageACK(metadata.NewOutgoingContext(context.TODO(), metadata.Pairs("key", "val")),
&pb.MessageACKReq{
UserId: 1,
DeviceId: 1,
DeviceAck: 1,
ReceiveTime: 1,
})
if err != nil {
logger.Sugar.Error(err)
return
}
logger.Sugar.Info(resp)
}

func TestLogicIntServer_Offline(t *testing.T) {
resp, err := getLogicIntClient().Offline(metadata.NewOutgoingContext(context.TODO(), metadata.Pairs("key", "val")),
&pb.OfflineReq{
UserId: 1,
DeviceId: 1,
})
if err != nil {
logger.Sugar.Error(err)
return
}
logger.Sugar.Info(resp)
}

func TestLogicIntServer_PushRoom(t *testing.T) {
buf, err := proto.Marshal(&pb.Text{
Text: "hello alber ",
})
if err != nil {
fmt.Println(err)
return
}
resp, err := getLogicIntClient().PushRoom(getCtx(),
&pb.PushRoomReq{
RoomId: 1,
MessageType: pb.MessageType_MT_TEXT,
MessageContent: buf,
SendTime: util.UnixMilliTime(time.Now()),
IsPersist: true,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

func TestLogicIntServer_PushAll(t *testing.T) {
buf, err := proto.Marshal(&pb.Text{
Text: "hello alber ",
})
if err != nil {
fmt.Println(err)
return
}
resp, err := getLogicIntClient().PushAll(getCtx(),
&pb.PushAllReq{
MessageType: pb.MessageType_MT_TEXT,
MessageContent: buf,
SendTime: util.UnixMilliTime(time.Now()),
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\n", resp)
}

+ 85
- 0
internal/logic/app/device_app.go Näytä tiedosto

@@ -0,0 +1,85 @@
package app

import (
"context"
devicedomain "gim/internal/logic/domain/device"
"gim/pkg/gerrors"
"gim/pkg/pb"
)

type deviceApp struct{}

var DeviceApp = new(deviceApp)

// Register 注册设备
func (*deviceApp) Register(ctx context.Context, in *pb.RegisterDeviceReq) (int64, error) {
device := devicedomain.Device{
Type: in.Type,
Brand: in.Brand,
Model: in.Model,
SystemVersion: in.SystemVersion,
SDKVersion: in.SdkVersion,
}

// 判断设备信息是否合法
if !device.IsLegal() {
return 0, gerrors.ErrBadRequest
}

err := devicedomain.DeviceRepo.Save(&device)
if err != nil {
return 0, err
}

return device.Id, nil
}

// SignIn 登录
func (*deviceApp) SignIn(ctx context.Context, userId, deviceId int64, token string, connAddr string, clientAddr string) error {
return devicedomain.DeviceService.SignIn(ctx, userId, deviceId, token, connAddr, clientAddr)
}

// Offline 设备离线
func (*deviceApp) Offline(ctx context.Context, deviceId int64, clientAddr string) error {
device, err := devicedomain.DeviceRepo.Get(deviceId)
if err != nil {
return err
}
if device == nil {
return nil
}

if device.ClientAddr != clientAddr {
return nil
}
device.Status = devicedomain.DeviceOffLine

err = devicedomain.DeviceRepo.Save(device)
if err != nil {
return err
}
return nil
}

// ListOnlineByUserId 获取用户所有在线设备
func (*deviceApp) ListOnlineByUserId(ctx context.Context, userId int64) ([]*pb.Device, error) {
return devicedomain.DeviceService.ListOnlineByUserId(ctx, userId)
}

// GetDevice 获取设备信息
func (*deviceApp) GetDevice(ctx context.Context, deviceId int64) (*pb.Device, error) {
device, err := devicedomain.DeviceRepo.Get(deviceId)
if err != nil {
return nil, err
}
if device == nil {
return nil, gerrors.ErrDeviceNotExist
}

return device.ToProto(), err
}

// ServerStop connect服务停止
func (*deviceApp) ServerStop(ctx context.Context, connAddr string) error {
return devicedomain.DeviceService.ServerStop(ctx, connAddr)
}

+ 53
- 0
internal/logic/app/friend_app.go Näytä tiedosto

@@ -0,0 +1,53 @@
package app

import (
"context"
frienddomain "gim/internal/logic/domain/friend"
"gim/pkg/pb"
"time"
)

type friendApp struct{}

var FriendApp = new(friendApp)

// List 获取好友列表
func (s *friendApp) List(ctx context.Context, userId int64) ([]*pb.Friend, error) {
return frienddomain.FriendService.List(ctx, userId)
}

// AddFriend 添加好友
func (*friendApp) AddFriend(ctx context.Context, userId, friendId int64, remarks, description string) error {
return frienddomain.FriendService.AddFriend(ctx, userId, friendId, remarks, description)
}

// AgreeAddFriend 同意添加好友
func (*friendApp) AgreeAddFriend(ctx context.Context, userId, friendId int64, remarks string) error {
return frienddomain.FriendService.AgreeAddFriend(ctx, userId, friendId, remarks)
}

// SetFriend 设置好友信息
func (*friendApp) SetFriend(ctx context.Context, userId int64, req *pb.SetFriendReq) error {
friend, err := frienddomain.FriendRepo.Get(userId, req.FriendId)
if err != nil {
return err
}
if friend == nil {
return nil
}

friend.Remarks = req.Remarks
friend.Extra = req.Extra
friend.UpdateTime = time.Now()

err = frienddomain.FriendRepo.Save(friend)
if err != nil {
return err
}
return nil
}

// SendToFriend 消息发送至好友
func (*friendApp) SendToFriend(ctx context.Context, sender *pb.Sender, req *pb.SendMessageReq) (int64, error) {
return frienddomain.FriendService.SendToFriend(ctx, sender, req)
}

+ 150
- 0
internal/logic/app/group_app.go Näytä tiedosto

@@ -0,0 +1,150 @@
package app

import (
"context"
"gim/internal/logic/domain/group/model"
"gim/internal/logic/domain/group/repo"
"gim/pkg/pb"
)

type groupApp struct{}

var GroupApp = new(groupApp)

// CreateGroup 创建群组
func (*groupApp) CreateGroup(ctx context.Context, userId int64, in *pb.CreateGroupReq) (int64, error) {
group := model.CreateGroup(userId, in)
err := repo.GroupRepo.Save(group)
if err != nil {
return 0, err
}
return group.Id, nil
}

// GetGroup 获取群组信息
func (*groupApp) GetGroup(ctx context.Context, groupId int64) (*pb.Group, error) {
group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return nil, err
}

return group.ToProto(), nil
}

// GetUserGroups 获取用户加入的群组列表
func (*groupApp) GetUserGroups(ctx context.Context, userId int64) ([]*pb.Group, error) {
groups, err := repo.GroupUserRepo.ListByUserId(userId)
if err != nil {
return nil, err
}

pbGroups := make([]*pb.Group, len(groups))
for i := range groups {
pbGroups[i] = groups[i].ToProto()
}
return pbGroups, nil
}

// Update 更新群组
func (*groupApp) Update(ctx context.Context, userId int64, update *pb.UpdateGroupReq) error {
group, err := repo.GroupRepo.Get(update.GroupId)
if err != nil {
return err
}

err = group.Update(ctx, update)
if err != nil {
return err
}

err = repo.GroupRepo.Save(group)
if err != nil {
return err
}

err = group.PushUpdate(ctx, userId)
if err != nil {
return err
}
return nil
}

// AddMembers 添加群组成员
func (*groupApp) AddMembers(ctx context.Context, userId, groupId int64, userIds []int64) ([]int64, error) {
group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return nil, err
}
existIds, addedIds, err := group.AddMembers(ctx, userIds)
if err != nil {
return nil, err
}
err = repo.GroupRepo.Save(group)
if err != nil {
return nil, err
}

err = group.PushAddMember(ctx, userId, addedIds)
if err != nil {
return nil, err
}
return existIds, nil
}

// UpdateMember 更新群组用户
func (*groupApp) UpdateMember(ctx context.Context, in *pb.UpdateGroupMemberReq) error {
group, err := repo.GroupRepo.Get(in.GroupId)
if err != nil {
return err
}
err = group.UpdateMember(ctx, in)
if err != nil {
return err
}
err = repo.GroupRepo.Save(group)
if err != nil {
return err
}
return nil
}

// DeleteMember 删除群组成员
func (*groupApp) DeleteMember(ctx context.Context, groupId int64, userId int64, optId int64) error {
group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return err
}
err = group.DeleteMember(ctx, userId)
if err != nil {
return err
}
err = repo.GroupRepo.Save(group)
if err != nil {
return err
}

err = group.PushDeleteMember(ctx, optId, userId)
if err != nil {
return err
}
return nil
}

// GetMembers 获取群组成员
func (*groupApp) GetMembers(ctx context.Context, groupId int64) ([]*pb.GroupMember, error) {
group, err := repo.GroupRepo.Get(groupId)
if err != nil {
return nil, err
}
return group.GetMembers(ctx)
}

// SendMessage 获取群组成员
func (*groupApp) SendMessage(ctx context.Context, sender *pb.Sender, req *pb.SendMessageReq) (int64, error) {
group, err := repo.GroupRepo.Get(req.ReceiverId)
if err != nil {
return 0, err
}

return group.SendMessage(ctx, sender, req)
}

+ 59
- 0
internal/logic/app/message_app.go Näytä tiedosto

@@ -0,0 +1,59 @@
package app

import (
"context"
"gim/internal/logic/domain/message/service"
"gim/pkg/pb"

"google.golang.org/protobuf/proto"
)

type messageApp struct{}

var MessageApp = new(messageApp)

// SendToUser 发送消息给用户
func (*messageApp) SendToUser(ctx context.Context, sender *pb.Sender, toUserId int64, req *pb.SendMessageReq) (int64, error) {
return service.MessageService.SendToUser(ctx, sender, toUserId, req)
}

// PushToUser 推送消息给用户
func (*messageApp) PushToUser(ctx context.Context, userId int64, code pb.PushCode, message proto.Message, isPersist bool) error {
return service.PushService.PushToUser(ctx, userId, code, message, isPersist)
}

// PushAll 全服推送
func (*messageApp) PushAll(ctx context.Context, req *pb.PushAllReq) error {
return service.PushService.PushAll(ctx, req)
}

// Sync 消息同步
func (*messageApp) Sync(ctx context.Context, userId, seq int64) (*pb.SyncResp, error) {
return service.MessageService.Sync(ctx, userId, seq)
}

// MessageAck 收到消息回执
func (*messageApp) MessageAck(ctx context.Context, userId, deviceId, ack int64) error {
return service.DeviceAckService.Update(ctx, userId, deviceId, ack)
}

// SendMessage 发送消息
func (s *messageApp) SendMessage(ctx context.Context, sender *pb.Sender, req *pb.SendMessageReq) (int64, error) {
// 如果发送者是用户,需要补充用户的信息
service.MessageService.AddSenderInfo(sender)

switch req.ReceiverType {
// 消息接收者为用户
case pb.ReceiverType_RT_USER:
// 发送者为用户
if sender.SenderType == pb.SenderType_ST_USER {
return FriendApp.SendToFriend(ctx, sender, req)
} else {
return s.SendToUser(ctx, sender, req.ReceiverId, req)
}
// 消息接收者是群组
case pb.ReceiverType_RT_GROUP:
return GroupApp.SendMessage(ctx, sender, req)
}
return 0, nil
}

+ 21
- 0
internal/logic/app/room_app.go Näytä tiedosto

@@ -0,0 +1,21 @@
package app

import (
"context"
"gim/internal/logic/domain/room"
"gim/pkg/pb"
)

type roomApp struct{}

var RoomApp = new(roomApp)

// Push 推送房间消息
func (s *roomApp) Push(ctx context.Context, sender *pb.Sender, req *pb.PushRoomReq) error {
return room.RoomService.Push(ctx, sender, req)
}

// SubscribeRoom 订阅房间
func (s *roomApp) SubscribeRoom(ctx context.Context, req *pb.SubscribeRoomReq) error {
return room.RoomService.SubscribeRoom(ctx, req)
}

+ 66
- 0
internal/logic/domain/device/device.go Näytä tiedosto

@@ -0,0 +1,66 @@
package device

import (
"gim/pkg/pb"
"time"
)

const (
DeviceOnLine = 1 // 设备在线
DeviceOffLine = 0 // 设备离线
)

// Device 设备
type Device struct {
Id int64 // 设备id
UserId int64 // 用户id
Type int32 // 设备类型,1:Android;2:IOS;3:Windows; 4:MacOS;5:Web
Brand string // 手机厂商
Model string // 机型
SystemVersion string // 系统版本
SDKVersion string // SDK版本
Status int32 // 在线状态,0:离线;1:在线
ConnAddr string // 连接层服务层地址
ClientAddr string // 客户端地址
CreateTime time.Time // 创建时间
UpdateTime time.Time // 更新时间
}

func (d *Device) ToProto() *pb.Device {
return &pb.Device{
DeviceId: d.Id,
UserId: d.UserId,
Type: d.Type,
Brand: d.Brand,
Model: d.Model,
SystemVersion: d.SystemVersion,
SdkVersion: d.SDKVersion,
Status: d.Status,
ConnAddr: d.ConnAddr,
ClientAddr: d.ClientAddr,
CreateTime: d.CreateTime.Unix(),
UpdateTime: d.UpdateTime.Unix(),
}
}

func (d *Device) IsLegal() bool {
if d.Type == 0 || d.Brand == "" || d.Model == "" ||
d.SystemVersion == "" || d.SDKVersion == "" {
return false
}
return true
}

func (d *Device) Online(userId int64, connAddr string, clientAddr string) {
d.UserId = userId
d.ConnAddr = connAddr
d.ClientAddr = clientAddr
d.Status = DeviceOnLine
}

func (d *Device) Offline(userId int64, connAddr string, clientAddr string) {
d.UserId = userId
d.ConnAddr = connAddr
d.ClientAddr = clientAddr
d.Status = DeviceOnLine
}

+ 68
- 0
internal/logic/domain/device/device_dao.go Näytä tiedosto

@@ -0,0 +1,68 @@
package device

import (
"gim/internal/business/domain/user/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"time"

"github.com/jinzhu/gorm"
)

type deviceDao struct{}

var DeviceDao = new(deviceDao)

// Save 插入一条设备信息
func (*deviceDao) Save(device *Device) error {
device.CreateTime = time.Now()
device.UpdateTime = time.Now()
err := db.DB.Save(&device).Error
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// Get 获取设备
func (*deviceDao) Get(deviceId int64) (*Device, error) {
var device = Device{Id: deviceId}
err := db.DB.First(&device).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &device, nil
}

// ListOnlineByUserId 查询用户所有的在线设备
func (*deviceDao) ListOnlineByUserId(userId int64) ([]Device, error) {
var devices []Device
err := db.DB.Find(&devices, "user_id = ? and status = ?", userId, DeviceOnLine).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return devices, nil
}

// ListOnlineByConnAddr 查询用户所有的在线设备
func (*deviceDao) ListOnlineByConnAddr(connAddr string) ([]Device, error) {
var devices []Device
err := db.DB.Find(&devices, "conn_addr = ? and status = ?", connAddr, DeviceOnLine).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return devices, nil
}

// UpdateStatus 更新在线状态
func (*deviceDao) UpdateStatus(deviceId int64, connAddr string, status int) (int64, error) {
db := db.DB.Model(&model.Device{}).Where("id = ? and conn_addr = ?", deviceId, connAddr).
Update("status", status)
if db.Error != nil {
return 0, gerrors.WrapError(db.Error)
}
return db.RowsAffected, nil
}

+ 38
- 0
internal/logic/domain/device/device_dao_test.go Näytä tiedosto

@@ -0,0 +1,38 @@
package device

import (
"fmt"
"gim/pkg/db"
"testing"
)

func init() {
fmt.Println("start")
db.InitByTest()
}

func TestDeviceDao_Add(t *testing.T) {
device := Device{
UserId: 1,
Type: 1,
Brand: "huawei",
Model: "huawei P10",
SystemVersion: "8.0.0",
SDKVersion: "1.0.0",
Status: 1,
}
err := DeviceDao.Save(&device)
fmt.Println(err)
fmt.Println(device)
}

func TestDeviceDao_Get(t *testing.T) {
device, err := DeviceDao.Get(1)
fmt.Printf("%+v\n %+v\n", device, err)
}

func TestDeviceDao_ListOnlineByUserId(t *testing.T) {
devices, err := DeviceDao.ListOnlineByUserId(1)
fmt.Println(err)
fmt.Printf("%+v \n", devices)
}

+ 76
- 0
internal/logic/domain/device/device_repo.go Näytä tiedosto

@@ -0,0 +1,76 @@
package device

type deviceRepo struct{}

var DeviceRepo = new(deviceRepo)

// Get 获取设备
func (*deviceRepo) Get(deviceId int64) (*Device, error) {
device, err := DeviceDao.Get(deviceId)
if err != nil {
return nil, err
}
return device, nil
}

// Save 保存设备信息
func (*deviceRepo) Save(device *Device) error {
err := DeviceDao.Save(device)
if err != nil {
return err
}

if device.UserId != 0 {
err = UserDeviceCache.Del(device.UserId)
if err != nil {
return err
}
}
return nil
}

// ListOnlineByUserId 获取用户的所有在线设备
func (*deviceRepo) ListOnlineByUserId(userId int64) ([]Device, error) {
devices, err := UserDeviceCache.Get(userId)
if err != nil {
return nil, err
}

if devices != nil {
return devices, nil
}

devices, err = DeviceDao.ListOnlineByUserId(userId)
if err != nil {
return nil, err
}

err = UserDeviceCache.Set(userId, devices)
if err != nil {
return nil, err
}

return devices, nil
}

// ListOnlineByConnAddr 查询用户所有的在线设备
func (*deviceRepo) ListOnlineByConnAddr(connAddr string) ([]Device, error) {
return DeviceDao.ListOnlineByConnAddr(connAddr)
}

// UpdateStatusOffline 更新设备为离线状态
func (*deviceRepo) UpdateStatusOffline(device Device) error {
affected, err := DeviceDao.UpdateStatus(device.Id, device.ConnAddr, DeviceOffLine)
if err != nil {
return err
}

if affected == 1 && device.UserId != 0 {
err = UserDeviceCache.Del(device.UserId)
if err != nil {
return err
}
}

return nil
}

+ 89
- 0
internal/logic/domain/device/device_service.go Näytä tiedosto

@@ -0,0 +1,89 @@
package device

import (
"context"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"time"

"go.uber.org/zap"
)

type deviceService struct{}

var DeviceService = new(deviceService)

// Register 注册设备
func (*deviceService) Register(ctx context.Context, device *Device) error {
err := DeviceDao.Save(device)
if err != nil {
return err
}

return nil
}

// SignIn 长连接登录
func (*deviceService) SignIn(ctx context.Context, userId, deviceId int64, token string, connAddr string, clientAddr string) error {
_, err := rpc.GetBusinessIntClient().Auth(ctx, &pb.AuthReq{UserId: userId, DeviceId: deviceId, Token: token})
if err != nil {
return err
}

// 标记用户在设备上登录
device, err := DeviceRepo.Get(deviceId)
if err != nil {
return err
}
if device == nil {
return nil
}

device.Online(userId, connAddr, clientAddr)

err = DeviceRepo.Save(device)
if err != nil {
return err
}
return nil
}

// Auth 权限验证
func (*deviceService) Auth(ctx context.Context, userId, deviceId int64, token string) error {
_, err := rpc.GetBusinessIntClient().Auth(ctx, &pb.AuthReq{UserId: userId, DeviceId: deviceId, Token: token})
if err != nil {
return err
}
return nil
}

func (*deviceService) ListOnlineByUserId(ctx context.Context, userId int64) ([]*pb.Device, error) {
devices, err := DeviceRepo.ListOnlineByUserId(userId)
if err != nil {
return nil, err
}
pbDevices := make([]*pb.Device, len(devices))
for i := range devices {
pbDevices[i] = devices[i].ToProto()
}
return pbDevices, nil
}

// ServerStop connect服务停止,需要将连接在这台connect上的设备标记为下线
func (*deviceService) ServerStop(ctx context.Context, connAddr string) error {
devices, err := DeviceRepo.ListOnlineByConnAddr(connAddr)
if err != nil {
return err
}

for i := range devices {
// 因为是异步修改设备转台,要避免设备重连,导致状态不一致
err = DeviceRepo.UpdateStatusOffline(devices[i])
if err != nil {
logger.Logger.Error("DeviceRepo.Save error", zap.Any("device", devices[i]), zap.Error(err))
}
time.Sleep(2 * time.Millisecond)
}
return nil
}

+ 46
- 0
internal/logic/domain/device/user_device_cache.go Näytä tiedosto

@@ -0,0 +1,46 @@
package device

import (
"gim/pkg/db"
"gim/pkg/gerrors"
"strconv"
"time"

"github.com/go-redis/redis"
)

const (
UserDeviceKey = "user_device:"
UserDeviceExpire = 2 * time.Hour
)

type userDeviceCache struct{}

var UserDeviceCache = new(userDeviceCache)

// Get 获取指定用户的所有在线设备
func (c *userDeviceCache) Get(userId int64) ([]Device, error) {
var devices []Device
err := db.RedisUtil.Get(UserDeviceKey+strconv.FormatInt(userId, 10), &devices)
if err != nil && err != redis.Nil {
return nil, gerrors.WrapError(err)
}

if err == redis.Nil {
return nil, nil
}
return devices, nil
}

// Set 将指定用户的所有在线设备存入缓存
func (c *userDeviceCache) Set(userId int64, devices []Device) error {
err := db.RedisUtil.Set(UserDeviceKey+strconv.FormatInt(userId, 10), devices, UserDeviceExpire)
return gerrors.WrapError(err)
}

// Del 删除用户的在线设备列表
func (c *userDeviceCache) Del(userId int64) error {
key := UserDeviceKey + strconv.FormatInt(userId, 10)
_, err := db.RedisCli.Del(key).Result()
return gerrors.WrapError(err)
}

+ 19
- 0
internal/logic/domain/friend/friend.go Näytä tiedosto

@@ -0,0 +1,19 @@
package friend

import "time"

const (
FriendStatusApply = 0 // 申请
FriendStatusAgree = 1 // 同意
)

type Friend struct {
Id int64
UserId int64
FriendId int64
Remarks string
Extra string
Status int
CreateTime time.Time
UpdateTime time.Time
}

+ 37
- 0
internal/logic/domain/friend/friend_repo.go Näytä tiedosto

@@ -0,0 +1,37 @@
package friend

import (
"gim/pkg/db"
"gim/pkg/gerrors"

"github.com/jinzhu/gorm"
)

type friendRepo struct{}

var FriendRepo = new(friendRepo)

// Get 获取好友
func (*friendRepo) Get(userId, friendId int64) (*Friend, error) {
friend := Friend{}
err := db.DB.First(&friend, "user_id = ? and friend_id = ?", userId, friendId).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &friend, nil
}

// Save 添加好友
func (*friendRepo) Save(friend *Friend) error {
return gerrors.WrapError(db.DB.Save(&friend).Error)
}

// List 获取好友列表
func (*friendRepo) List(userId int64, status int) ([]Friend, error) {
var friends []Friend
err := db.DB.Where("user_id = ? and status = ?", userId, status).Find(&friends).Error
return friends, gerrors.WrapError(err)
}

+ 23
- 0
internal/logic/domain/friend/friend_repo_test.go Näytä tiedosto

@@ -0,0 +1,23 @@
package friend

import (
"fmt"
"testing"
)

func Test_friendDao_Get(t *testing.T) {
friend, err := FriendRepo.Get(1, 2)
fmt.Printf("%+v \n %+v \n", friend, err)
}

func Test_friendDao_Save(t *testing.T) {
fmt.Println(FriendRepo.Save(&Friend{
UserId: 1,
FriendId: 2,
}))
}

func Test_friendDao_List(t *testing.T) {
friends, err := FriendRepo.List(1, FriendStatusAgree)
fmt.Printf("%+v \n %+v \n", friends, err)
}

+ 160
- 0
internal/logic/domain/friend/friend_service.go Näytä tiedosto

@@ -0,0 +1,160 @@
package friend

import (
"context"
"gim/internal/logic/proxy"
"gim/pkg/gerrors"
"gim/pkg/pb"
"gim/pkg/rpc"
"time"
)

type friendService struct{}

var FriendService = new(friendService)

// List 获取好友列表
func (s *friendService) List(ctx context.Context, userId int64) ([]*pb.Friend, error) {
friends, err := FriendRepo.List(userId, FriendStatusAgree)
if err != nil {
return nil, err
}

userIds := make(map[int64]int32, len(friends))
for i := range friends {
userIds[friends[i].FriendId] = 0
}
resp, err := rpc.GetBusinessIntClient().GetUsers(ctx, &pb.GetUsersReq{UserIds: userIds})
if err != nil {
return nil, err
}

var infos = make([]*pb.Friend, len(friends))
for i := range friends {
friend := pb.Friend{
UserId: friends[i].FriendId,
Remarks: friends[i].Remarks,
Extra: friends[i].Extra,
}

user, ok := resp.Users[friends[i].FriendId]
if ok {
friend.Nickname = user.Nickname
friend.Sex = user.Sex
friend.AvatarUrl = user.AvatarUrl
friend.UserExtra = user.Extra
}
infos[i] = &friend
}

return infos, nil
}

// AddFriend 添加好友
func (*friendService) AddFriend(ctx context.Context, userId, friendId int64, remarks, description string) error {
friend, err := FriendRepo.Get(userId, friendId)
if err != nil {
return err
}
if friend != nil {
if friend.Status == FriendStatusApply {
return nil
}
if friend.Status == FriendStatusAgree {
return gerrors.ErrAlreadyIsFriend
}
}

now := time.Now()
err = FriendRepo.Save(&Friend{
UserId: userId,
FriendId: friendId,
Remarks: remarks,
Status: FriendStatusApply,
CreateTime: now,
UpdateTime: now,
})
if err != nil {
return err
}

resp, err := rpc.GetBusinessIntClient().GetUser(ctx, &pb.GetUserReq{UserId: userId})
if err != nil {
return err
}

err = proxy.MessageProxy.PushToUser(ctx, friendId, pb.PushCode_PC_ADD_FRIEND, &pb.AddFriendPush{
FriendId: userId,
Nickname: resp.User.Nickname,
AvatarUrl: resp.User.AvatarUrl,
Description: description,
}, true)
if err != nil {
return err
}
return nil
}

// AgreeAddFriend 同意添加好友
func (*friendService) AgreeAddFriend(ctx context.Context, userId, friendId int64, remarks string) error {
friend, err := FriendRepo.Get(friendId, userId)
if err != nil {
return err
}
if friend == nil {
return gerrors.ErrBadRequest
}
if friend.Status == FriendStatusAgree {
return nil
}
friend.Status = FriendStatusAgree
err = FriendRepo.Save(friend)
if err != nil {
return err
}

now := time.Now()
err = FriendRepo.Save(&Friend{
UserId: userId,
FriendId: friendId,
Remarks: remarks,
Status: FriendStatusAgree,
CreateTime: now,
UpdateTime: now,
})
if err != nil {
return err
}

resp, err := rpc.GetBusinessIntClient().GetUser(ctx, &pb.GetUserReq{UserId: userId})
if err != nil {
return err
}

err = proxy.MessageProxy.PushToUser(ctx, friendId, pb.PushCode_PC_AGREE_ADD_FRIEND, &pb.AgreeAddFriendPush{
FriendId: userId,
Nickname: resp.User.Nickname,
AvatarUrl: resp.User.AvatarUrl,
}, true)
if err != nil {
return err
}
return nil
}

// SendToFriend 消息发送至好友
func (*friendService) SendToFriend(ctx context.Context, sender *pb.Sender, req *pb.SendMessageReq) (int64, error) {
// 发给发送者
seq, err := proxy.MessageProxy.SendToUser(ctx, sender, sender.SenderId, req)
if err != nil {
return 0, err
}

// 发给接收者
_, err = proxy.MessageProxy.SendToUser(ctx, sender, req.ReceiverId, req)
if err != nil {
return 0, err
}

return seq, nil
}

+ 364
- 0
internal/logic/domain/group/model/group.go Näytä tiedosto

@@ -0,0 +1,364 @@
package model

import (
"context"
"gim/internal/logic/proxy"
"gim/pkg/gerrors"
"gim/pkg/grpclib"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"gim/pkg/util"
"time"

"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)

const (
UpdateTypeUpdate = 1
UpdateTypeDelete = 2
)

// Group 群组
type Group struct {
Id int64 // 群组id
Name string // 组名
AvatarUrl string // 头像
Introduction string // 群简介
UserNum int32 // 群组人数
Extra string // 附加字段
CreateTime time.Time // 创建时间
UpdateTime time.Time // 更新时间
Members []GroupUser `gorm:"-"` // 群组成员
}

type GroupUser struct {
Id int64 // 自增主键
GroupId int64 // 群组id
UserId int64 // 用户id
MemberType int // 群组类型
Remarks string // 备注
Extra string // 附加属性
Status int // 状态
CreateTime time.Time // 创建时间
UpdateTime time.Time // 更新时间
UpdateType int `gorm:"-"` // 更新类型
}

func (g *Group) ToProto() *pb.Group {
if g == nil {
return nil
}

return &pb.Group{
GroupId: g.Id,
Name: g.Name,
AvatarUrl: g.AvatarUrl,
Introduction: g.Introduction,
UserMum: g.UserNum,
Extra: g.Extra,
CreateTime: g.CreateTime.Unix(),
UpdateTime: g.UpdateTime.Unix(),
}
}

func CreateGroup(userId int64, in *pb.CreateGroupReq) *Group {
now := time.Now()
group := &Group{
Name: in.Name,
AvatarUrl: in.AvatarUrl,
Introduction: in.Introduction,
Extra: in.Extra,
Members: make([]GroupUser, 0, len(in.MemberIds)+1),
CreateTime: now,
UpdateTime: now,
}

// 创建者添加为管理员
group.Members = append(group.Members, GroupUser{
GroupId: group.Id,
UserId: userId,
MemberType: int(pb.MemberType_GMT_ADMIN),
CreateTime: now,
UpdateTime: now,
UpdateType: UpdateTypeUpdate,
})

// 其让人添加为成员
for i := range in.MemberIds {
group.Members = append(group.Members, GroupUser{
GroupId: group.Id,
UserId: in.MemberIds[i],
MemberType: int(pb.MemberType_GMT_MEMBER),
CreateTime: now,
UpdateTime: now,
UpdateType: UpdateTypeUpdate,
})
}
return group
}

func (g *Group) Update(ctx context.Context, in *pb.UpdateGroupReq) error {
g.Name = in.Name
g.AvatarUrl = in.AvatarUrl
g.Introduction = in.Introduction
g.Extra = in.Extra
return nil
}

func (g *Group) PushUpdate(ctx context.Context, userId int64) error {
userResp, err := rpc.GetBusinessIntClient().GetUser(ctx, &pb.GetUserReq{UserId: userId})
if err != nil {
return err
}
err = g.PushMessage(ctx, pb.PushCode_PC_UPDATE_GROUP, &pb.UpdateGroupPush{
OptId: userId,
OptName: userResp.User.Nickname,
Name: g.Name,
AvatarUrl: g.AvatarUrl,
Introduction: g.Introduction,
Extra: g.Extra,
}, true)
if err != nil {
return err
}
return nil
}

// SendMessage 消息发送至群组
func (g *Group) SendMessage(ctx context.Context, sender *pb.Sender, req *pb.SendMessageReq) (int64, error) {
if sender.SenderType == pb.SenderType_ST_USER && !g.IsMember(sender.SenderId) {
logger.Sugar.Error(ctx, sender.SenderId, req.ReceiverId, "不在群组内")
return 0, gerrors.ErrNotInGroup
}

// 如果发送者是用户,将消息发送给发送者,获取用户seq
var userSeq int64
var err error
if sender.SenderType == pb.SenderType_ST_USER {
userSeq, err = proxy.MessageProxy.SendToUser(ctx, sender, sender.SenderId, req)
if err != nil {
return 0, err
}
}

go func() {
defer util.RecoverPanic()
// 将消息发送给群组用户,使用写扩散
for _, user := range g.Members {
// 前面已经发送过,这里不需要再发送
if sender.SenderType == pb.SenderType_ST_USER && user.UserId == sender.SenderId {
continue
}
_, err := proxy.MessageProxy.SendToUser(grpclib.NewAndCopyRequestId(ctx), sender, user.UserId, req)
if err != nil {
return
}
}
}()

return userSeq, nil
}

func (g *Group) IsMember(userId int64) bool {
for i := range g.Members {
if g.Members[i].UserId == userId {
return true
}
}
return false
}

// PushMessage 向群组推送消息
func (g *Group) PushMessage(ctx context.Context, code pb.PushCode, message proto.Message, isPersist bool) error {
logger.Logger.Debug("push_to_group",
zap.Int64("request_id", grpclib.GetCtxRequestId(ctx)),
zap.Int64("group_id", g.Id),
zap.Int32("code", int32(code)),
zap.Any("message", message))

messageBuf, err := proto.Marshal(message)
if err != nil {
return gerrors.WrapError(err)
}

commandBuf, err := proto.Marshal(&pb.Command{Code: int32(code), Data: messageBuf})
if err != nil {
return gerrors.WrapError(err)
}

_, err = g.SendMessage(ctx,
&pb.Sender{
SenderType: pb.SenderType_ST_SYSTEM,
SenderId: 0,
},
&pb.SendMessageReq{
ReceiverType: pb.ReceiverType_RT_GROUP,
ReceiverId: g.Id,
ToUserIds: nil,
MessageType: pb.MessageType_MT_COMMAND,
MessageContent: commandBuf,
SendTime: util.UnixMilliTime(time.Now()),
IsPersist: isPersist,
},
)
if err != nil {
return err
}
return nil
}

// GetMembers 获取群组用户
func (g *Group) GetMembers(ctx context.Context) ([]*pb.GroupMember, error) {
members := g.Members
userIds := make(map[int64]int32, len(members))
for i := range members {
userIds[members[i].UserId] = 0
}
resp, err := rpc.GetBusinessIntClient().GetUsers(ctx, &pb.GetUsersReq{UserIds: userIds})
if err != nil {
return nil, err
}

var infos = make([]*pb.GroupMember, len(members))
for i := range members {
member := pb.GroupMember{
UserId: members[i].UserId,
MemberType: pb.MemberType(members[i].MemberType),
Remarks: members[i].Remarks,
Extra: members[i].Extra,
}

user, ok := resp.Users[members[i].UserId]
if ok {
member.Nickname = user.Nickname
member.Sex = user.Sex
member.AvatarUrl = user.AvatarUrl
member.UserExtra = user.Extra
}
infos[i] = &member
}

return infos, nil
}

// AddMembers 给群组添加用户
func (g *Group) AddMembers(ctx context.Context, userIds []int64) ([]int64, []int64, error) {
var existIds []int64
var addedIds []int64

now := time.Now()
for i, userId := range userIds {
if g.IsMember(userId) {
existIds = append(existIds, userIds[i])
continue
}

g.Members = append(g.Members, GroupUser{
GroupId: g.Id,
UserId: userIds[i],
MemberType: int(pb.MemberType_GMT_MEMBER),
CreateTime: now,
UpdateTime: now,
UpdateType: UpdateTypeUpdate,
})
addedIds = append(addedIds, userIds[i])
}

g.UserNum += int32(len(addedIds))

return existIds, addedIds, nil
}

func (g *Group) PushAddMember(ctx context.Context, optUserId int64, addedIds []int64) error {
var addIdMap = make(map[int64]int32, len(addedIds))
for i := range addedIds {
addIdMap[addedIds[i]] = 0
}

addIdMap[optUserId] = 0
usersResp, err := rpc.GetBusinessIntClient().GetUsers(ctx, &pb.GetUsersReq{UserIds: addIdMap})
if err != nil {
return err
}
var members []*pb.GroupMember
for k, _ := range addIdMap {
member, ok := usersResp.Users[k]
if !ok {
continue
}

members = append(members, &pb.GroupMember{
UserId: member.UserId,
Nickname: member.Nickname,
Sex: member.Sex,
AvatarUrl: member.AvatarUrl,
UserExtra: member.Extra,
Remarks: "",
Extra: "",
})
}

optUser := usersResp.Users[optUserId]
err = g.PushMessage(ctx, pb.PushCode_PC_ADD_GROUP_MEMBERS, &pb.AddGroupMembersPush{
OptId: optUser.UserId,
OptName: optUser.Nickname,
Members: members,
}, true)
if err != nil {
logger.Sugar.Error(err)
}
return nil
}

func (g *Group) GetMember(ctx context.Context, userId int64) *GroupUser {
for i := range g.Members {
if g.Members[i].UserId == userId {
return &g.Members[i]
}
}
return nil
}

// UpdateMember 更新群组成员信息
func (g *Group) UpdateMember(ctx context.Context, in *pb.UpdateGroupMemberReq) error {
member := g.GetMember(ctx, in.UserId)
if member == nil {
return nil
}

member.MemberType = int(in.MemberType)
member.Remarks = in.Remarks
member.Extra = in.Extra
member.UpdateTime = time.Now()
member.UpdateType = UpdateTypeUpdate
return nil
}

// DeleteMember 删除用户群组
func (g *Group) DeleteMember(ctx context.Context, userId int64) error {
member := g.GetMember(ctx, userId)
if member == nil {
return nil
}

member.UpdateType = UpdateTypeDelete
return nil
}

func (g *Group) PushDeleteMember(ctx context.Context, optId, userId int64) error {
userResp, err := rpc.GetBusinessIntClient().GetUser(ctx, &pb.GetUserReq{UserId: optId})
if err != nil {
return err
}
err = g.PushMessage(ctx, pb.PushCode_PC_REMOVE_GROUP_MEMBER, &pb.RemoveGroupMemberPush{
OptId: optId,
OptName: userResp.User.Nickname,
DeletedUserId: userId,
}, true)
if err != nil {
return err
}
return nil
}

+ 48
- 0
internal/logic/domain/group/repo/group_cache.go Näytä tiedosto

@@ -0,0 +1,48 @@
package repo

import (
"gim/internal/logic/domain/group/model"
"gim/pkg/db"
"gim/pkg/gerrors"
"strconv"
"time"

"github.com/go-redis/redis"
)

const GroupKey = "group:"

type groupCache struct{}

var GroupCache = new(groupCache)

// Get 获取群组缓存
func (c *groupCache) Get(groupId int64) (*model.Group, error) {
var user model.Group
err := db.RedisUtil.Get(GroupKey+strconv.FormatInt(groupId, 10), &user)
if err != nil && err != redis.Nil {
return nil, gerrors.WrapError(err)
}
if err == redis.Nil {
return nil, nil
}
return &user, nil
}

// Set 设置群组缓存
func (c *groupCache) Set(group *model.Group) error {
err := db.RedisUtil.Set(GroupKey+strconv.FormatInt(group.Id, 10), group, 24*time.Hour)
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// Del 删除群组缓存
func (c *groupCache) Del(groupId int64) error {
_, err := db.RedisCli.Del(GroupKey + strconv.FormatInt(groupId, 10)).Result()
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

+ 35
- 0
internal/logic/domain/group/repo/group_dao.go Näytä tiedosto

@@ -0,0 +1,35 @@
package repo

import (
"gim/internal/logic/domain/group/model"
"gim/pkg/db"
"gim/pkg/gerrors"

"github.com/jinzhu/gorm"
)

type groupDao struct{}

var GroupDao = new(groupDao)

// Get 获取群组信息
func (*groupDao) Get(groupId int64) (*model.Group, error) {
var group = model.Group{Id: groupId}
err := db.DB.First(&group).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &group, nil
}

// Save 插入一条群组
func (*groupDao) Save(group *model.Group) error {
err := db.DB.Save(&group).Error
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

+ 11
- 0
internal/logic/domain/group/repo/group_dao_test.go Näytä tiedosto

@@ -0,0 +1,11 @@
package repo

import (
"fmt"
"testing"
)

func TestGroupDao_Get(t *testing.T) {
group, err := GroupDao.Get(1)
fmt.Printf("%+v\n %+v\n", group, err)
}

+ 70
- 0
internal/logic/domain/group/repo/group_repo.go Näytä tiedosto

@@ -0,0 +1,70 @@
package repo

import (
"gim/internal/logic/domain/group/model"
)

type groupRepo struct{}

var GroupRepo = new(groupRepo)

// Get 获取群组信息
func (*groupRepo) Get(groupId int64) (*model.Group, error) {
group, err := GroupCache.Get(groupId)
if err != nil {
return nil, err
}
if group != nil {
return group, nil
}

group, err = GroupDao.Get(groupId)
if err != nil {
return nil, err
}
members, err := GroupUserRepo.ListUser(groupId)
if err != nil {
return nil, err
}
group.Members = members

err = GroupCache.Set(group)
if err != nil {
return nil, err
}
return group, nil
}

// Save 获取群组信息
func (*groupRepo) Save(group *model.Group) error {
groupId := group.Id
err := GroupDao.Save(group)
if err != nil {
return err
}

members := group.Members
for i := range members {
members[i].GroupId = group.Id
if members[i].UpdateType == model.UpdateTypeUpdate {
err = GroupUserRepo.Save(&(members[i]))
if err != nil {
return err
}
}
if members[i].UpdateType == model.UpdateTypeDelete {
err = GroupUserRepo.Delete(group.Id, members[i].UserId)
if err != nil {
return err
}
}
}

if groupId != 0 {
err = GroupCache.Del(groupId)
if err != nil {
return err
}
}
return nil
}

+ 69
- 0
internal/logic/domain/group/repo/group_user_repo.go Näytä tiedosto

@@ -0,0 +1,69 @@
package repo

import (
"gim/internal/logic/domain/group/model"
"gim/pkg/db"
"gim/pkg/gerrors"

"github.com/jinzhu/gorm"
)

type groupUserRepo struct{}

var GroupUserRepo = new(groupUserRepo)

// ListByUserId 获取用户加入的群组信息
func (*groupUserRepo) ListByUserId(userId int64) ([]model.Group, error) {
var groups []model.Group
err := db.DB.Select("g.id,g.name,g.avatar_url,g.introduction,g.user_num,g.extra,g.create_time,g.update_time").
Table("group_user u").
Joins("join `group` g on u.group_id = g.id").
Where("u.user_id = ?", userId).
Find(&groups).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return groups, nil
}

// ListUser 获取群组用户信息
func (*groupUserRepo) ListUser(groupId int64) ([]model.GroupUser, error) {
var groupUsers []model.GroupUser
err := db.DB.Find(&groupUsers, "group_id = ?", groupId).Error
if err != nil {
return nil, gerrors.WrapError(err)
}
return groupUsers, nil
}

// Get 获取群组用户信息,用户不存在返回nil
func (*groupUserRepo) Get(groupId, userId int64) (*model.GroupUser, error) {
var groupUser model.GroupUser
err := db.DB.First(&groupUser, "group_id = ? and user_id = ?", groupId, userId).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, gerrors.WrapError(err)
}
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &groupUser, nil
}

// Save 将用户添加到群组
func (*groupUserRepo) Save(groupUser *model.GroupUser) error {
err := db.DB.Save(&groupUser).Error
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// Delete 将用户从群组删除
func (d *groupUserRepo) Delete(groupId int64, userId int64) error {
err := db.DB.Exec("delete from group_user where group_id = ? and user_id = ?",
groupId, userId).Error
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

+ 24
- 0
internal/logic/domain/group/repo/group_user_repo_test.go Näytä tiedosto

@@ -0,0 +1,24 @@
package repo

import (
"fmt"
"testing"
)

func TestGroupUserDao_ListByUserId(t *testing.T) {
groups, err := GroupUserRepo.ListByUserId(1)
fmt.Printf("%+v\n %+v\n", groups, err)
}

func TestGroupUserDao_ListGroupUser(t *testing.T) {
users, err := GroupUserRepo.ListUser(1)
fmt.Printf("%+v\n %+v\n", users, err)
}

func TestGroupUserDao_Get(t *testing.T) {
fmt.Println(GroupUserRepo.Get(1, 1))
}

func TestGroupUserDao_Delete(t *testing.T) {
fmt.Println(GroupUserRepo.Delete(1, 1))
}

+ 83
- 0
internal/logic/domain/message/model/message.go Näytä tiedosto

@@ -0,0 +1,83 @@
package model

import (
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/util"
"strconv"
"strings"
"time"
)

// Message 消息
type Message struct {
Id int64 // 自增主键
UserId int64 // 所属类型id
RequestId int64 // 请求id
SenderType int32 // 发送者类型
SenderId int64 // 发送者账户id
ReceiverType int32 // 接收者账户id
ReceiverId int64 // 接收者id,如果是单聊信息,则为user_id,如果是群组消息,则为group_id
ToUserIds string // 需要@的用户id列表,多个用户用,隔开
Type int // 消息类型
Content []byte // 消息内容
Seq int64 // 消息同步序列
SendTime time.Time // 消息发送时间
Status int32 // 创建时间
}

func (m *Message) MessageToPB() *pb.Message {
return &pb.Message{
Sender: &pb.Sender{
SenderType: pb.SenderType(m.SenderType),
SenderId: m.SenderId,
},
ReceiverType: pb.ReceiverType(m.ReceiverType),
ReceiverId: m.ReceiverId,
ToUserIds: UnformatUserIds(m.ToUserIds),
MessageType: pb.MessageType(m.Type),
MessageContent: m.Content,
Seq: m.Seq,
SendTime: util.UnixMilliTime(m.SendTime),
Status: pb.MessageStatus(m.Status),
}
}

func FormatUserIds(userId []int64) string {
build := strings.Builder{}
for i, v := range userId {
build.WriteString(strconv.FormatInt(v, 10))
if i != len(userId)-1 {
build.WriteString(",")
}
}
return build.String()
}

func UnformatUserIds(userIdStr string) []int64 {
if userIdStr == "" {
return []int64{}
}
toUserIdStrs := strings.Split(userIdStr, ",")
toUserIds := make([]int64, 0, len(toUserIdStrs))
for i := range toUserIdStrs {
userId, err := strconv.ParseInt(toUserIdStrs[i], 10, 64)
if err != nil {
logger.Sugar.Error(err)
continue
}
toUserIds = append(toUserIds, userId)
}
return toUserIds
}

func MessagesToPB(messages []Message) []*pb.Message {
pbMessages := make([]*pb.Message, 0, len(messages))
for i := range messages {
pbMessage := messages[i].MessageToPB()
if pbMessages != nil {
pbMessages = append(pbMessages, pbMessage)
}
}
return pbMessages
}

+ 12
- 0
internal/logic/domain/message/model/sender.go Näytä tiedosto

@@ -0,0 +1,12 @@
package model

import "gim/pkg/pb"

type Sender struct {
SenderType pb.SenderType // 发送者类型,1:系统,2:用户,3:业务方
SenderId int64 // 发送者id
DeviceId int64 // 发送者设备id
Nickname string // 昵称
AvatarUrl string // 头像
Extra string // 扩展字段
}

+ 41
- 0
internal/logic/domain/message/repo/device_ack_repo.go Näytä tiedosto

@@ -0,0 +1,41 @@
package repo

import (
"gim/pkg/db"
"gim/pkg/gerrors"
"strconv"
)

const (
DeviceACKKey = "device_ack:"
)

type deviceACKRepo struct{}

var DeviceACKRepo = new(deviceACKRepo)

// Set 设置设备同步序列号
func (c *deviceACKRepo) Set(userId int64, deviceId int64, ack int64) error {
_, err := db.RedisCli.HSet(DeviceACKKey+strconv.FormatInt(userId, 10), strconv.FormatInt(deviceId, 10),
strconv.FormatInt(ack, 10)).Result()

if err != nil {
return gerrors.WrapError(err)
}
return nil
}

func (c *deviceACKRepo) Get(userId int64) (map[int64]int64, error) {
result, err := db.RedisCli.HGetAll(DeviceACKKey + strconv.FormatInt(userId, 10)).Result()
if err != nil {
return nil, gerrors.WrapError(err)
}

acks := make(map[int64]int64, len(result))
for k, v := range result {
deviceId, _ := strconv.ParseInt(k, 10, 64)
ack, _ := strconv.ParseInt(v, 10, 64)
acks[deviceId] = ack
}
return acks, nil
}

+ 49
- 0
internal/logic/domain/message/repo/message_repo.go Näytä tiedosto

@@ -0,0 +1,49 @@
package repo

import (
"fmt"
"gim/internal/logic/domain/message/model"
"gim/pkg/db"
"gim/pkg/gerrors"
)

const messageTableNum = 1

type messageRepo struct{}

var MessageRepo = new(messageRepo)

func (*messageRepo) tableName(userId int64) string {
return fmt.Sprintf("message_%03d", userId%messageTableNum)
}

// Save 插入一条消息
func (d *messageRepo) Save(message model.Message) error {
err := db.DB.Table(d.tableName(message.UserId)).Create(&message).Error
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// ListBySeq 根据类型和id查询大于序号大于seq的消息
func (d *messageRepo) ListBySeq(userId, seq, limit int64) ([]model.Message, bool, error) {
DB := db.DB.Table(d.tableName(userId)).
Where("user_id = ? and seq > ?", userId, seq)

var count int64
err := DB.Count(&count).Error
if err != nil {
return nil, false, gerrors.WrapError(err)
}
if count == 0 {
return nil, false, nil
}

var messages []model.Message
err = DB.Limit(limit).Find(&messages).Error
if err != nil {
return nil, false, gerrors.WrapError(err)
}
return messages, count > limit, nil
}

+ 39
- 0
internal/logic/domain/message/repo/message_repo_test.go Näytä tiedosto

@@ -0,0 +1,39 @@
package repo

import (
"fmt"
"gim/internal/logic/domain/message/model"
"testing"
"time"
)

func TestMessageDao_Add(t *testing.T) {
message := model.Message{
UserId: 1,
RequestId: 1,
SenderType: 1,
SenderId: 1,
ReceiverType: 1,
ReceiverId: 1,
ToUserIds: "1",
Type: 1,
Content: []byte("123456"),
Seq: 2,
SendTime: time.Now(),
Status: 0,
}
fmt.Println(MessageRepo.Save(message))
}

func TestMessageDao_ListByUserIdAndUserSeq(t *testing.T) {
messages, hasMore, err := MessageRepo.ListBySeq(1, 0, 100)
fmt.Println(err)
fmt.Println(hasMore)
for i := range messages {
fmt.Printf("%+v\n", messages[i])
}
}

func Test_messageDao_tableName(t *testing.T) {
fmt.Println(MessageRepo.tableName(1001))
}

+ 43
- 0
internal/logic/domain/message/repo/seq_repo.go Näytä tiedosto

@@ -0,0 +1,43 @@
package repo

import (
"database/sql"
"gim/pkg/db"
"gim/pkg/gerrors"
)

const (
SeqObjectTypeUser = 1 // 用户
SeqObjectTypeRoom = 2 // 房间
)

type seqRepo struct{}

var SeqRepo = new(seqRepo)

// Incr 自增seq,并且获取自增后的值
func (*seqRepo) Incr(objectType int, objectId int64) (int64, error) {
tx := db.DB.Begin()
defer tx.Rollback()

var seq int64
err := db.DB.Raw("select seq from seq where object_type = ? and object_id = ? for update", objectType, objectId).
Row().Scan(&seq)
if err != nil && err != sql.ErrNoRows {
return 0, gerrors.WrapError(err)
}
if err == sql.ErrNoRows {
err = db.DB.Exec("insert into seq (object_type,object_id,seq) values (?,?,?)", objectType, objectId, seq+1).Error
if err != nil {
return 0, gerrors.WrapError(err)
}
} else {
err = db.DB.Exec("update seq set seq = seq + 1 where object_type = ? and object_id = ?", objectType, objectId).Error
if err != nil {
return 0, gerrors.WrapError(err)
}
}

tx.Commit()
return seq + 1, nil
}

+ 10
- 0
internal/logic/domain/message/repo/seq_repo_test.go Näytä tiedosto

@@ -0,0 +1,10 @@
package repo

import (
"fmt"
"testing"
)

func Test_seqDao_Incr(t *testing.T) {
fmt.Println(SeqRepo.Incr(1, 5))
}

+ 34
- 0
internal/logic/domain/message/service/device_ack.go Näytä tiedosto

@@ -0,0 +1,34 @@
package service

import (
"context"
"gim/internal/logic/domain/message/repo"
)

type deviceAckService struct{}

var DeviceAckService = new(deviceAckService)

// Update 更新ack
func (*deviceAckService) Update(ctx context.Context, userId, deviceId, ack int64) error {
if ack <= 0 {
return nil
}
return repo.DeviceACKRepo.Set(userId, deviceId, ack)
}

// GetMaxByUserId 根据用户id获取最大ack
func (*deviceAckService) GetMaxByUserId(ctx context.Context, userId int64) (int64, error) {
acks, err := repo.DeviceACKRepo.Get(userId)
if err != nil {
return 0, err
}

var max int64 = 0
for i := range acks {
if acks[i] > max {
max = acks[i]
}
}
return max, nil
}

+ 20
- 0
internal/logic/domain/message/service/device_ack_test.go Näytä tiedosto

@@ -0,0 +1,20 @@
package service

import (
"context"
"fmt"
"gim/pkg/db"
"testing"
)

func init() {
db.InitByTest()
}

func Test_deviceAckService_GetMaxByUserId(t *testing.T) {
fmt.Println(DeviceAckService.Update(context.TODO(), 1, 2, 2))
}

func Test_deviceAckService_Update(t *testing.T) {
fmt.Println(DeviceAckService.GetMaxByUserId(context.TODO(), 1))
}

+ 194
- 0
internal/logic/domain/message/service/message_service.go Näytä tiedosto

@@ -0,0 +1,194 @@
package service

import (
"context"
"gim/internal/logic/domain/message/model"
"gim/internal/logic/domain/message/repo"
"gim/internal/logic/proxy"
"gim/pkg/grpclib"
"gim/pkg/grpclib/picker"
"gim/pkg/logger"
"gim/pkg/pb"
"gim/pkg/rpc"
"gim/pkg/util"

"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)

const MessageLimit = 50 // 最大消息同步数量

const MaxSyncBufLen = 65536 // 最大字节数组长度

type messageService struct{}

var MessageService = new(messageService)

// Sync 消息同步
func (*messageService) Sync(ctx context.Context, userId, seq int64) (*pb.SyncResp, error) {
messages, hasMore, err := MessageService.ListByUserIdAndSeq(ctx, userId, seq)
if err != nil {
return nil, err
}
pbMessages := model.MessagesToPB(messages)
length := len(pbMessages)

resp := &pb.SyncResp{Messages: pbMessages, HasMore: hasMore}
bytes, err := proto.Marshal(resp)
if err != nil {
return nil, err
}

// 如果字节数组大于一个包的长度,需要减少字节数组
for len(bytes) > MaxSyncBufLen {
length = length * 2 / 3
resp = &pb.SyncResp{Messages: pbMessages[0:length], HasMore: true}
bytes, err = proto.Marshal(resp)
if err != nil {
return nil, err
}
}

var userIds = make(map[int64]int32, len(resp.Messages))
for i := range resp.Messages {
if resp.Messages[i].Sender.SenderType == pb.SenderType_ST_USER {
userIds[resp.Messages[i].Sender.SenderId] = 0
}
}
usersResp, err := rpc.GetBusinessIntClient().GetUsers(ctx, &pb.GetUsersReq{UserIds: userIds})
if err != nil {
return nil, err
}
for i := range resp.Messages {
if resp.Messages[i].Sender.SenderType == pb.SenderType_ST_USER {
user, ok := usersResp.Users[resp.Messages[i].Sender.SenderId]
if ok {
resp.Messages[i].Sender.Nickname = user.Nickname
resp.Messages[i].Sender.AvatarUrl = user.AvatarUrl
resp.Messages[i].Sender.Extra = user.Extra
} else {
logger.Logger.Warn("get user failed", zap.Int64("user_id", resp.Messages[i].Sender.SenderId))
}
}
}

return resp, nil
}

// ListByUserIdAndSeq 查询消息
func (*messageService) ListByUserIdAndSeq(ctx context.Context, userId, seq int64) ([]model.Message, bool, error) {
var err error
if seq == 0 {
seq, err = DeviceAckService.GetMaxByUserId(ctx, userId)
if err != nil {
return nil, false, err
}
}
return repo.MessageRepo.ListBySeq(userId, seq, MessageLimit)
}

// SendToUser 将消息发送给用户
func (*messageService) SendToUser(ctx context.Context, sender *pb.Sender, toUserId int64, req *pb.SendMessageReq) (int64, error) {
logger.Logger.Debug("SendToUser",
zap.Int64("request_id", grpclib.GetCtxRequestId(ctx)),
zap.Int64("to_user_id", toUserId))
var (
seq int64 = 0
err error
)

if req.IsPersist {
seq, err = SeqService.GetUserNext(ctx, toUserId)
if err != nil {
return 0, err
}

selfMessage := model.Message{
UserId: toUserId,
RequestId: grpclib.GetCtxRequestId(ctx),
SenderType: int32(sender.SenderType),
SenderId: sender.SenderId,
ReceiverType: int32(req.ReceiverType),
ReceiverId: req.ReceiverId,
ToUserIds: model.FormatUserIds(req.ToUserIds),
Type: int(req.MessageType),
Content: req.MessageContent,
Seq: seq,
SendTime: util.UnunixMilliTime(req.SendTime),
Status: int32(pb.MessageStatus_MS_NORMAL),
}
err = repo.MessageRepo.Save(selfMessage)
if err != nil {
logger.Sugar.Error(err)
return 0, err
}

if sender.SenderType == pb.SenderType_ST_USER && sender.SenderId == toUserId {
// 用户需要增加自己的已经同步的序列号
err = repo.DeviceACKRepo.Set(sender.SenderId, sender.DeviceId, seq)
if err != nil {
return 0, err
}
}
}

message := pb.Message{
Sender: sender,
ReceiverType: req.ReceiverType,
ReceiverId: req.ReceiverId,
ToUserIds: req.ToUserIds,
MessageType: req.MessageType,
MessageContent: req.MessageContent,
Seq: seq,
SendTime: req.SendTime,
Status: pb.MessageStatus_MS_NORMAL,
}

// 查询用户在线设备
devices, err := proxy.DeviceProxy.ListOnlineByUserId(ctx, toUserId)
if err != nil {
logger.Sugar.Error(err)
return 0, err
}

for i := range devices {
// 消息不需要投递给发送消息的设备
if sender.DeviceId == devices[i].DeviceId {
continue
}

err = MessageService.SendToDevice(ctx, devices[i], &message)
if err != nil {
logger.Sugar.Error(err, zap.Any("SendToUser error", devices[i]), zap.Error(err))
}
}

return seq, nil
}

// SendToDevice 将消息发送给设备
func (*messageService) SendToDevice(ctx context.Context, device *pb.Device, message *pb.Message) error {
messageSend := pb.MessageSend{Message: message}
_, err := rpc.GetConnectIntClient().DeliverMessage(picker.ContextWithAddr(ctx, device.ConnAddr), &pb.DeliverMessageReq{
DeviceId: device.DeviceId,
MessageSend: &messageSend,
})
if err != nil {
logger.Logger.Error("SendToDevice error", zap.Error(err))
return err
}

// todo 其他推送厂商
return nil
}

func (*messageService) AddSenderInfo(sender *pb.Sender) {
if sender.SenderType == pb.SenderType_ST_USER {
user, err := rpc.GetBusinessIntClient().GetUser(context.TODO(), &pb.GetUserReq{UserId: sender.SenderId})
if err == nil && user != nil {
sender.AvatarUrl = user.User.AvatarUrl
sender.Nickname = user.User.Nickname
sender.Extra = user.User.Extra
}
}
}

+ 40
- 0
internal/logic/domain/message/service/message_service_test.go Näytä tiedosto

@@ -0,0 +1,40 @@
package service

import (
"context"
"fmt"
"testing"

jsoniter "github.com/json-iterator/go"
)

func TestMessageService_Add(t *testing.T) {

}

func TestMessageService_ListByUserIdAndSequence(t *testing.T) {

}

func TestJson(t *testing.T) {
var st = struct {
Nickname string `json:"nickname"`
}{}

json := `{
"user_id":3,
"nickname":"h",
"sex":2,
"avatar_url":"no",
"extra":{"nickname":"hjkladsjfkl"}
}`
jsoniter.Get([]byte(json), "extra").ToVal(&st)
fmt.Println(st)
}

func Test_messageService_Sync(t *testing.T) {
resp, err := MessageService.Sync(context.TODO(), 6, 0)
fmt.Println(err)
fmt.Println(resp.HasMore)
fmt.Println(len(resp.Messages))
}

+ 86
- 0
internal/logic/domain/message/service/push.go Näytä tiedosto

@@ -0,0 +1,86 @@
package service

import (
"context"
"gim/pkg/gerrors"
"gim/pkg/grpclib"
"gim/pkg/logger"
"gim/pkg/mq"
"gim/pkg/pb"
"gim/pkg/util"
"time"

"go.uber.org/zap"
"google.golang.org/protobuf/proto"
)

type pushService struct{}

var PushService = new(pushService)

// PushToUser 向用户推送消息
func (s *pushService) PushToUser(ctx context.Context, userId int64, code pb.PushCode, message proto.Message, isPersist bool) error {
logger.Logger.Debug("push_to_user",
zap.Int64("request_id", grpclib.GetCtxRequestId(ctx)),
zap.Int64("user_id", userId),
zap.Int32("code", int32(code)),
zap.Any("message", message))

messageBuf, err := proto.Marshal(message)
if err != nil {
return gerrors.WrapError(err)
}

commandBuf, err := proto.Marshal(&pb.Command{Code: int32(code), Data: messageBuf})
if err != nil {
return gerrors.WrapError(err)
}

_, err = MessageService.SendToUser(ctx,
&pb.Sender{
SenderType: pb.SenderType_ST_SYSTEM,
SenderId: 0,
},
userId,
&pb.SendMessageReq{
ReceiverType: pb.ReceiverType_RT_USER,
ReceiverId: userId,
ToUserIds: nil,
MessageType: pb.MessageType_MT_COMMAND,
MessageContent: commandBuf,
SendTime: util.UnixMilliTime(time.Now()),
IsPersist: isPersist,
},
)
if err != nil {
return err
}
return nil
}

// PushAll 全服推送
func (s *pushService) PushAll(ctx context.Context, req *pb.PushAllReq) error {
msg := pb.PushAllMsg{
MessageSend: &pb.MessageSend{
Message: &pb.Message{
Sender: &pb.Sender{SenderType: pb.SenderType_ST_BUSINESS},
ReceiverType: pb.ReceiverType_RT_ROOM,
ToUserIds: nil,
MessageType: req.MessageType,
MessageContent: req.MessageContent,
Seq: 0,
SendTime: util.UnixMilliTime(time.Now()),
Status: 0,
},
},
}
bytes, err := proto.Marshal(&msg)
if err != nil {
return gerrors.WrapError(err)
}
err = mq.Publish(mq.PushAllTopic, bytes)
if err != nil {
return err
}
return nil
}

+ 15
- 0
internal/logic/domain/message/service/seq.go Näytä tiedosto

@@ -0,0 +1,15 @@
package service

import (
"context"
"gim/internal/logic/domain/message/repo"
)

type seqService struct{}

var SeqService = new(seqService)

// GetUserNext 获取下一个序列号
func (*seqService) GetUserNext(ctx context.Context, userId int64) (int64, error) {
return repo.SeqRepo.Incr(repo.SeqObjectTypeUser, userId)
}

+ 12
- 0
internal/logic/domain/message/service/seq_test.go Näytä tiedosto

@@ -0,0 +1,12 @@
package service

import (
"context"
"fmt"
"testing"
)

func Test_seqService_GetUserNext(t *testing.T) {
seq, err := SeqService.GetUserNext(context.TODO(), 1)
fmt.Println(seq, err)
}

+ 94
- 0
internal/logic/domain/room/room_message_repo.go Näytä tiedosto

@@ -0,0 +1,94 @@
package room

import (
"fmt"
"gim/pkg/db"
"gim/pkg/gerrors"
"gim/pkg/pb"
"gim/pkg/util"
"strconv"
"time"

"github.com/go-redis/redis"
"google.golang.org/protobuf/proto"
)

const RoomMessageKey = "room_message:%d"

const RoomMessageExpireTime = 2 * time.Minute

type roomMessageRepo struct{}

var RoomMessageRepo = new(roomMessageRepo)

// Add 将消息添加到队列
func (*roomMessageRepo) Add(roomId int64, msg *pb.Message) error {
key := fmt.Sprintf(RoomMessageKey, roomId)
buf, err := proto.Marshal(msg)
if err != nil {
return gerrors.WrapError(err)
}
_, err = db.RedisCli.ZAdd(key, redis.Z{
Score: float64(msg.Seq),
Member: buf,
}).Result()

db.RedisCli.Expire(key, RoomMessageExpireTime)
if err != nil {
return gerrors.WrapError(err)
}
return nil
}

// List 获取指定房间序列号大于seq的消息
func (*roomMessageRepo) List(roomId int64, seq int64) ([]*pb.Message, error) {
key := fmt.Sprintf(RoomMessageKey, roomId)
result, err := db.RedisCli.ZRangeByScore(key, redis.ZRangeBy{
Min: strconv.FormatInt(seq, 10),
Max: "+inf",
}).Result()
if err != nil {
return nil, gerrors.WrapError(err)
}

var msgs []*pb.Message
for i := range result {
buf := util.Str2bytes(result[i])
var msg pb.Message
err = proto.Unmarshal(buf, &msg)
if err != nil {
return nil, gerrors.WrapError(err)
}
msgs = append(msgs, &msg)
}
return msgs, nil
}

func (*roomMessageRepo) ListByIndex(roomId int64, start, stop int64) ([]*pb.Message, error) {
key := fmt.Sprintf(RoomMessageKey, roomId)
result, err := db.RedisCli.ZRange(key, start, stop).Result()
if err != nil {
return nil, gerrors.WrapError(err)
}

var msgs []*pb.Message
for i := range result {
buf := util.Str2bytes(result[i])
var msg pb.Message
err = proto.Unmarshal(buf, &msg)
if err != nil {
return nil, gerrors.WrapError(err)
}
msgs = append(msgs, &msg)
}
return msgs, nil
}

func (*roomMessageRepo) DelBySeq(roomId int64, min, max int64) error {
if min == 0 && max == 0 {
return nil
}
key := fmt.Sprintf(RoomMessageKey, roomId)
_, err := db.RedisCli.ZRemRangeByScore(key, strconv.FormatInt(min, 10), strconv.FormatInt(max, 10)).Result()
return gerrors.WrapError(err)
}

+ 21
- 0
internal/logic/domain/room/room_seq_repo.go Näytä tiedosto

@@ -0,0 +1,21 @@
package room

import (
"fmt"
"gim/pkg/db"
"gim/pkg/gerrors"
)

const RoomSeqKey = "room_seq:%d"

type roomSeqRepo struct{}

var RoomSeqRepo = new(roomSeqRepo)

func (*roomSeqRepo) GetNextSeq(roomId int64) (int64, error) {
num, err := db.RedisCli.Incr(fmt.Sprintf(RoomSeqKey, roomId)).Result()
if err != nil {
return 0, gerrors.WrapError(err)
}
return num, err
}

+ 148
- 0
internal/logic/domain/room/room_service.go Näytä tiedosto

@@ -0,0 +1,148 @@
package room

import (
"context"
"gim/pkg/gerrors"
"gim/pkg/grpclib/picker"
"gim/pkg/logger"
"gim/pkg/mq"
"gim/pkg/pb"
"gim/pkg/rpc"
"gim/pkg/util"
"time"

"google.golang.org/protobuf/proto"
)

type roomService struct{}

var RoomService = new(roomService)

func (s *roomService) Push(ctx context.Context, sender *pb.Sender, req *pb.PushRoomReq) error {
s.AddSenderInfo(sender)

seq, err := RoomSeqRepo.GetNextSeq(req.RoomId)
if err != nil {
return err
}

msg := &pb.Message{
Sender: sender,
ReceiverType: pb.ReceiverType_RT_ROOM,
ReceiverId: req.RoomId,
ToUserIds: nil,
MessageType: req.MessageType,
MessageContent: req.MessageContent,
Seq: seq,
SendTime: util.UnixMilliTime(time.Now()),
Status: 0,
}

if req.IsPersist {
err = s.AddMessage(req.RoomId, msg)
if err != nil {
return err
}
}

pushRoomMsg := pb.PushRoomMsg{
RoomId: req.RoomId,
MessageSend: &pb.MessageSend{
Message: msg,
},
}
bytes, err := proto.Marshal(&pushRoomMsg)
if err != nil {
return gerrors.WrapError(err)
}
var topicName = mq.PushRoomTopic
if req.IsPriority {
topicName = mq.PushRoomPriorityTopic
}
err = mq.Publish(topicName, bytes)
if err != nil {
return err
}
return nil
}

func (s *roomService) AddMessage(roomId int64, msg *pb.Message) error {
err := RoomMessageRepo.Add(roomId, msg)
if err != nil {
return err
}
return s.DelExpireMessage(roomId)
}

// DelExpireMessage 删除过期消息
func (s *roomService) DelExpireMessage(roomId int64) error {
var (
index int64 = 0
stop bool
min int64
max int64
)

for {
msgs, err := RoomMessageRepo.ListByIndex(roomId, index, index+20)
if err != nil {
return err
}
if len(msgs) == 0 {
break
}

for _, v := range msgs {
if v.SendTime > util.UnixMilliTime(time.Now().Add(-RoomMessageExpireTime)) {
stop = true
break
}

if min == 0 {
min = v.Seq
}
max = v.Seq
}
if stop {
break
}
}

return RoomMessageRepo.DelBySeq(roomId, min, max)
}

// SubscribeRoom 订阅房间
func (s *roomService) SubscribeRoom(ctx context.Context, req *pb.SubscribeRoomReq) error {
if req.Seq == 0 {
return nil
}

messages, err := RoomMessageRepo.List(req.RoomId, req.Seq)
if err != nil {
return err
}

for i := range messages {
_, err := rpc.GetConnectIntClient().DeliverMessage(picker.ContextWithAddr(ctx, req.ConnAddr), &pb.DeliverMessageReq{
DeviceId: req.DeviceId,
MessageSend: &pb.MessageSend{
Message: messages[i],
},
})
if err != nil {
logger.Sugar.Error(err)
}
}
return nil
}

func (*roomService) AddSenderInfo(sender *pb.Sender) {
if sender.SenderType == pb.SenderType_ST_USER {
user, err := rpc.GetBusinessIntClient().GetUser(context.TODO(), &pb.GetUserReq{UserId: sender.SenderId})
if err == nil && user != nil {
sender.AvatarUrl = user.User.AvatarUrl
sender.Nickname = user.User.Nickname
sender.Extra = user.User.Extra
}
}
}

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

Ladataan…
Peruuta
Tallenna