├── controllers ├── controller.go ├── auth_api.go ├── user_api.go ├── room_api.go └── api_test.go ├── docker-compose.yaml ├── .gitignore ├── Dockerfile ├── log ├── logger_test.go └── logger.go ├── models ├── room.go ├── broker.go ├── chat_event.go └── client.go ├── database ├── utils.go ├── connect_database.go ├── mongo.go └── mongo_test.go ├── README.MD ├── main.go ├── websocket ├── wsServer.go └── manager.go ├── go.mod ├── templates ├── chat.tmpl └── index.tmpl └── go.sum /controllers/controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | 5 | "chicko_chat/database" 6 | 7 | ) 8 | 9 | type Controller struct { 10 | DB *database.ChatDatabase 11 | } 12 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "8080:8080" 7 | mongo: 8 | container_name: mongo 9 | image: mongo:4.4 10 | ports: 11 | - "27017:27017" 12 | command: mongod -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-buster 2 | 3 | 4 | 5 | WORKDIR /go/src/app 6 | COPY . . 7 | 8 | 9 | # Build the Go app 10 | RUN GOPROXY=https://goproxy.cn go get github.com/klauspost/compress 11 | RUN go build -o chat . 12 | 13 | # Expose port 8080 to the outside world 14 | EXPOSE 8009 15 | 16 | # Command to run the executable 17 | CMD ["./chat"] -------------------------------------------------------------------------------- /log/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNew(t *testing.T) { 9 | var buf bytes.Buffer 10 | logger := New(&buf) 11 | if logger == nil { 12 | t.Error("should not be nil") 13 | } else { 14 | logger.Log("log package.") 15 | if buf.String() != "log package.\n" { 16 | t.Errorf("Trace should not write '%s'.", buf.String()) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /models/room.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // struct representing a chat room 10 | type ChatRoom struct { 11 | ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` 12 | Title string `json:"title" bson:"title"` 13 | CreatedAt time.Time `json:"createdAt" bson:"created"` 14 | Clients []primitive.ObjectID `json:"users" bson:"users"` 15 | } 16 | -------------------------------------------------------------------------------- /log/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // logging events throughout code. 9 | type Logger interface { 10 | Log(...interface{}) 11 | } 12 | 13 | // New creates a new Logger that will write the output to 14 | // the specified io.Writer. 15 | func New(w io.Writer) Logger { 16 | return &logger{out: w} 17 | } 18 | 19 | type logger struct { 20 | out io.Writer 21 | } 22 | 23 | // writes the arguments io.Writer. 24 | func (t *logger) Log(a ...interface{}) { 25 | fmt.Fprint(t.out, a...) 26 | fmt.Fprintln(t.out) 27 | } 28 | -------------------------------------------------------------------------------- /database/utils.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "errors" 5 | "go.mongodb.org/mongo-driver/bson/primitive" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | ) 8 | 9 | 10 | // For converting result of insert document to type primitive.ObjectID 11 | func convertId(result *mongo.InsertOneResult) (primitive.ObjectID, error) { 12 | if id, ok := result.InsertedID.(primitive.ObjectID); ok { 13 | return id, nil 14 | } else { 15 | 16 | return primitive.NilObjectID, errors.New("failed converting") 17 | } 18 | 19 | } 20 | 21 | func ObjectIDFromHex(hexString string) (primitive.ObjectID, error){ 22 | objID, err := primitive.ObjectIDFromHex(hexString) 23 | return objID, err 24 | } -------------------------------------------------------------------------------- /models/broker.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "sync" 4 | 5 | // struct representing client connections 6 | type Broker struct { 7 | // Registered Clients. 8 | Clients map[*Client]bool 9 | 10 | // messages from the Clients. 11 | Notification chan *ChatEvent 12 | 13 | // Register requests from the Clients. 14 | Join chan *Client 15 | 16 | // Unregister requests from Clients. 17 | Leave chan *Client 18 | 19 | Room *ChatRoom 20 | 21 | Mutex sync.Mutex 22 | } 23 | 24 | func NewBroker(room *ChatRoom) *Broker { 25 | return &Broker{ 26 | Notification: make(chan *ChatEvent, 100), 27 | Join: make(chan *Client), 28 | Leave: make(chan *Client), 29 | Clients: make(map[*Client]bool), 30 | Room: room, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /models/chat_event.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | const ( 10 | // Subscribe is used to broadcast a message indicating user has joined ChatRoom 11 | Subscribe = "join" 12 | // Broadcast is used to broadcast messages to all subscribed users 13 | Broadcast = "send" 14 | // Unsubscribe is used to broadcast a message indicating user has left ChatRoom 15 | Unsubscribe = "leave" 16 | ) 17 | 18 | // struct representing a message event in an ChatRoom 19 | type ChatEvent struct { 20 | ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` 21 | EventType string `json:"type" bson:"type ,omitempty"` 22 | UserID primitive.ObjectID `json:"user_id,omitempty"` 23 | RoomID primitive.ObjectID `json:"room_id,omitempty"` 24 | Message string `json:"message,omitempty"` 25 | Timestamp time.Time `json:"time"` 26 | } 27 | -------------------------------------------------------------------------------- /controllers/auth_api.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "chicko_chat/models" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | 11 | 12 | func (c *Controller) StartConversationApi(ctx *gin.Context) { 13 | // Validate input 14 | var user *data.UserData 15 | if err := ctx.ShouldBindJSON(&user); err != nil { 16 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 17 | return 18 | } 19 | 20 | // Search For existing user 21 | usr, err := c.DB.FindByEmail(user.Email) 22 | if err == nil{ 23 | if usr.Name == "" { 24 | usr.Name = user.Name 25 | usr , err = c.DB.UpdateUserName(usr) 26 | if err != nil { 27 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 28 | return 29 | } 30 | 31 | } 32 | 33 | ctx.JSON(http.StatusOK, gin.H{"data": usr}) 34 | return 35 | 36 | } 37 | 38 | id, err := c.DB.AddUser(user) 39 | if err != nil { 40 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 41 | return 42 | 43 | } 44 | user.ID = id 45 | ctx.JSON(http.StatusOK, gin.H{"data": user}) 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /controllers/user_api.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "chicko_chat/models" 8 | ) 9 | 10 | func (c *Controller) GetUserRoomsApi(ctx *gin.Context) { 11 | // Validate input 12 | var user *data.UserData 13 | if err := ctx.ShouldBindJSON(&user); err != nil { 14 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 15 | return 16 | } 17 | 18 | var rooms []data.ChatRoom 19 | // Search For rooms 20 | rooms, err := c.DB.GetHistoryOfUser(user) 21 | if err != nil { 22 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 23 | return 24 | 25 | } 26 | ctx.JSON(http.StatusOK, gin.H{"data": rooms}) 27 | } 28 | 29 | func (c *Controller) GetUserDetailsRoomApi(ctx *gin.Context) { 30 | // Validate input 31 | var room *data.ChatRoom 32 | if err := ctx.ShouldBindJSON(&room); err != nil { 33 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 34 | return 35 | } 36 | 37 | var users []data.UserData 38 | // Search For rooms 39 | users, err := c.DB.GetUserData(room.Clients) 40 | if err != nil { 41 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 42 | return 43 | 44 | } 45 | ctx.JSON(http.StatusOK, gin.H{"users": users}) 46 | } 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /models/client.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gorilla/websocket" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | ) 9 | 10 | const ( 11 | // Max wait time when writing message to peer 12 | writeWait = 10 * time.Second 13 | 14 | // Max time till next pong from peer 15 | pongWait = 60 * time.Second 16 | 17 | // Send ping interval, must be less then pong wait time 18 | pingPeriod = (pongWait * 9) / 10 19 | 20 | // Maximum message size allowed from peer. 21 | maxMessageSize = 10000 22 | ) 23 | 24 | // struct representing a user in a ChatRoom 25 | type Client struct { 26 | User UserData 27 | LastActivity time.Time `json:"last_activity"` 28 | // The websocket Connection. 29 | Conn *websocket.Conn `json:"-"` 30 | // Buffered channel of outbound messages. 31 | Send chan *ChatEvent `json:"-"` 32 | // Broker for connection 33 | Broker *Broker 34 | } 35 | 36 | type UserData struct { 37 | ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"` 38 | Email string `json:"email" bson:"email" binding:"required"` 39 | Name string `json:"name" bson:"name"` 40 | Active bool `json:"active" bson:"active"` 41 | } 42 | 43 | func NewClient(conn *websocket.Conn, user *UserData, broker *Broker) *Client { 44 | 45 | client := &Client{ 46 | User: *user, 47 | Conn: conn, 48 | Send: make(chan *ChatEvent, 100), 49 | Broker: broker, 50 | } 51 | return client 52 | } 53 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Chicko Chat 2 | 3 | 4 | Chicko chat is a real time multi room multi user chat using golang, websocket , mongodb with restful api 5 | 6 | [ Sample Front ](https://github.com/younes-nb/chicko-frontend) 7 | 8 | 9 | 10 | ## Install ## 11 | All that you need is [Golang](https://golang.org/). 12 | ```sh 13 | $ git clone https://github.com/oldcorvus/chicko_chat.git 14 | 15 | ``` 16 | ```sh 17 | $ go mod tidy 18 | 19 | ``` 20 | ```sh 21 | $ go build -o chat 22 | 23 | ``` 24 | ```sh 25 | $ ./chat --mongoURI="mongodb://localhost:27017" 26 | 27 | ``` 28 | And navigate to `http://127.0.0.1:8080/`. 29 | 30 | 31 | ## Running Locally with Docker 32 | 33 | 1.build the image: 34 | 35 | ```sh 36 | $ docker-compose build . 37 | ``` 38 | 2.Spin up the containers 39 | 40 | ```sh 41 | $ docker-compose up 42 | ``` 43 | And navigate to `http://127.0.0.1:8080/`. 44 | 45 | ## ChatRoom API ## 46 | 47 | * `GET /chats/`: to join a room and start chat 48 | * `GET /ws/:roomId/:userId/`: to start a websocket connection 49 | 50 | * `POST /start/`: register and obtain user id 51 | * `POST /user-rooms/`: ro get user rooms based on user id 52 | * `POST /room-history/`: to retrieve messages of room based on room id 53 | * `POST /add-user-room/`: to register user in room 54 | * `POST /room-user-details/`: to retrieve user data of a room 55 | 56 | ## Test ## 57 | 58 | to run tests 59 | 60 | Controllers : 61 | ```sh 62 | $ cd controllers 63 | 64 | ``` 65 | ```sh 66 | $ go test 67 | 68 | ``` 69 | Database 70 | 71 | ```sh 72 | $ cd database 73 | 74 | ``` 75 | ```sh 76 | $ go test 77 | 78 | ``` 79 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "chicko_chat/controllers" 5 | "chicko_chat/database" 6 | "chicko_chat/models" 7 | "chicko_chat/websocket" 8 | "github.com/gin-contrib/cors" 9 | 10 | "flag" 11 | "net/http" 12 | 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | func main() { 17 | mongoURI := flag.String("mongoURI", "mongodb://mongo:27017", "Database hostname url") 18 | enableCredentials := flag.Bool("enableCredentials", false, "Enable the use of credentials for mongo connection") 19 | flag.Parse() 20 | 21 | db := database.ConnectDatabse(*mongoURI, *enableCredentials) 22 | router := gin.Default() 23 | controller := controllers.Controller{ 24 | DB: db, 25 | } 26 | manager := &websocket.BrokerManager{ 27 | Brokers: make(map[*data.Broker]bool), 28 | DB: db, 29 | } 30 | websocketServer := &websocket.WsServer{ 31 | Manager: manager, 32 | } 33 | router.LoadHTMLGlob("templates/*") 34 | router.GET("/", func(c *gin.Context) { 35 | 36 | c.HTML(http.StatusOK, "index.tmpl", gin.H{ 37 | "title": "Sample Front", 38 | "name": "Moel", 39 | }) 40 | }) 41 | router.Use(cors.Default()) 42 | router.GET("/chat/", controller.JoinRoom) 43 | router.POST("/start/", controller.StartConversationApi) 44 | router.POST("/user-rooms/", controller.GetUserRoomsApi) 45 | router.POST("/create-room/", controller.CreateRoomApi) 46 | router.POST("/room-history/", controller.RoomHistoryApi) 47 | router.POST("/add-user-room/", controller.AddUserToRoomApi) 48 | router.POST("/room-user-details/", controller.GetUserDetailsRoomApi) 49 | router.GET("/ws/:roomId/:userId/", func(c *gin.Context) { 50 | roomId := c.Param("roomId") 51 | userId := c.Param("userId") 52 | websocketServer.ServeWs(c.Writer, c.Request, roomId, userId) 53 | }) 54 | router.Run() 55 | 56 | } 57 | -------------------------------------------------------------------------------- /websocket/wsServer.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "chicko_chat/database" 5 | 6 | "log" 7 | "net/http" 8 | "chicko_chat/models" 9 | "github.com/gorilla/websocket" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | const ( 14 | socketBufferSize = 1024 15 | messageBufferSize = 256 16 | ) 17 | 18 | var upgrader = &websocket.Upgrader{ReadBufferSize: socketBufferSize, 19 | WriteBufferSize: socketBufferSize, 20 | CheckOrigin: func(r *http.Request) bool { 21 | //origin := r.Header.Get("Origin") 22 | return true 23 | },} 24 | 25 | type WsServer struct { 26 | Manager *BrokerManager 27 | } 28 | 29 | func (server *WsServer) findBrokerbyRoomID(ID primitive.ObjectID) *data.Broker { 30 | for broker := range server.Manager.Brokers { 31 | if broker.Room.ID == ID { 32 | return broker 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | func (server *WsServer) createBroker(room *data.ChatRoom) *data.Broker { 39 | broker := data.NewBroker(room) 40 | go server.Manager.RunBroker(broker) 41 | server.Manager.Brokers[broker] = true 42 | 43 | return broker 44 | } 45 | 46 | func (server *WsServer) ServeWs(w http.ResponseWriter, req *http.Request, roomId string, userId string) { 47 | socket, err := upgrader.Upgrade(w, req, nil) 48 | if err != nil { 49 | log.Fatal("ServeHTTP:", err) 50 | return 51 | } 52 | roomID, err := database.ObjectIDFromHex(roomId) 53 | userID, err := database.ObjectIDFromHex(userId) 54 | 55 | if err != nil { 56 | return 57 | } 58 | user := &data.UserData{ 59 | ID: userID, 60 | } 61 | 62 | room := &data.ChatRoom{ 63 | ID: roomID, 64 | } 65 | broker := server.findBrokerbyRoomID(room.ID) 66 | if broker == nil { 67 | broker = server.createBroker(room) 68 | } 69 | client := data.NewClient(socket, user, broker) 70 | clientManager := clientManager{ 71 | client: client, 72 | } 73 | broker.Clients[client] = true 74 | broker.Join <- client 75 | defer func() { broker.Leave <- client }() 76 | go clientManager.clientWrite() 77 | clientManager.clientRead() 78 | } 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module chicko_chat 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gin-contrib/cors v1.4.0 7 | github.com/gin-gonic/gin v1.9.0 8 | github.com/gorilla/websocket v1.5.0 9 | github.com/stretchr/testify v1.8.2 10 | go.mongodb.org/mongo-driver v1.11.2 11 | ) 12 | 13 | require ( 14 | github.com/bytedance/sonic v1.8.0 // indirect 15 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/gin-contrib/sse v0.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.11.2 // indirect 21 | github.com/goccy/go-json v0.10.0 // indirect 22 | github.com/golang/snappy v0.0.1 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/compress v1.13.6 // indirect 25 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 26 | github.com/leodido/go-urn v1.2.1 // indirect 27 | github.com/mattn/go-isatty v0.0.17 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect 31 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 32 | github.com/pkg/errors v0.9.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 35 | github.com/ugorji/go/codec v1.2.9 // indirect 36 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 37 | github.com/xdg-go/scram v1.1.1 // indirect 38 | github.com/xdg-go/stringprep v1.0.3 // indirect 39 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 40 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 41 | golang.org/x/crypto v0.5.0 // indirect 42 | golang.org/x/net v0.7.0 // indirect 43 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 44 | golang.org/x/sys v0.5.0 // indirect 45 | golang.org/x/text v0.7.0 // indirect 46 | google.golang.org/protobuf v1.28.1 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /database/connect_database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "go.mongodb.org/mongo-driver/mongo/readpref" 9 | 10 | "chicko_chat/models" 11 | "os" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | "go.mongodb.org/mongo-driver/mongo/options" 14 | ) 15 | 16 | func ConnectDatabseTest() *ChatDatabase { 17 | 18 | // Create mongo client configuration 19 | co := options.Client().ApplyURI("mongodb://localhost:27017") 20 | 21 | // Establish database connection 22 | client, err := mongo.NewClient(co) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) 27 | defer cancel() 28 | 29 | err = client.Connect(ctx) 30 | 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | db := &ChatDatabase{ 36 | Users: client.Database("chicko_chat").Collection("users_test"), 37 | Rooms: client.Database("chicko_chat").Collection("rooms_test"), 38 | Messages: client.Database("chicko_chat").Collection("message_test"), 39 | } 40 | defer func() { 41 | if err = db.Users.Drop(context.TODO()); err != nil { 42 | log.Fatal(err) 43 | } 44 | if err = db.Rooms.Drop(context.TODO()); err != nil { 45 | log.Fatal(err) 46 | } 47 | if err = db.Messages.Drop(context.TODO()); err != nil { 48 | log.Fatal(err) 49 | } 50 | }() 51 | room := &data.ChatRoom{ 52 | Title: "Data For Test", 53 | } 54 | 55 | _, err = db.Rooms.InsertOne(context.TODO(), room) 56 | 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | return db 61 | 62 | } 63 | func ConnectDatabse(mongoURI string, enableCredentials bool ) *ChatDatabase { 64 | // Create mongo client configuration 65 | co := options.Client().ApplyURI(mongoURI) 66 | if enableCredentials { 67 | co.Auth = &options.Credential{ 68 | Username: os.Getenv("MONGODB_USERNAME"), 69 | Password: os.Getenv("MONGODB_PASSWORD"), 70 | } 71 | } 72 | // Establish database connection 73 | client, err := mongo.NewClient(co) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) 78 | defer cancel() 79 | 80 | err = client.Connect(ctx) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | 85 | 86 | if err := client.Ping(context.TODO(), readpref.Primary()); err != nil { 87 | panic(err) 88 | } 89 | db := &ChatDatabase{ 90 | Users: client.Database("chicko_chat").Collection("users"), 91 | Messages: client.Database("chicko_chat").Collection("messages"), 92 | Rooms: client.Database("chicko_chat").Collection("rooms"), 93 | } 94 | 95 | log.Printf("Database connection established") 96 | 97 | return db 98 | 99 | } 100 | -------------------------------------------------------------------------------- /controllers/room_api.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "chicko_chat/models" 8 | "chicko_chat/database" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | "time" 11 | ) 12 | 13 | func (c *Controller) CreateRoomApi(ctx *gin.Context) { 14 | // Validate input 15 | var room *data.ChatRoom 16 | if err := ctx.ShouldBindJSON(&room); err != nil { 17 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 18 | return 19 | } 20 | // Create room 21 | room.CreatedAt = time.Now() 22 | room, err := c.DB.CreateRoom(room) 23 | if err != nil { 24 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 25 | return 26 | } 27 | ctx.JSON(http.StatusOK, gin.H{"data": room}) 28 | 29 | } 30 | 31 | 32 | func (c *Controller) RoomHistoryApi(ctx *gin.Context) { 33 | // Validate input 34 | var room *data.ChatRoom 35 | if err := ctx.ShouldBindJSON(&room); err != nil { 36 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 37 | return 38 | } 39 | var messages []data.ChatEvent 40 | messages, err := c.DB.GetHistoryOfRoom(room) 41 | if err != nil { 42 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 43 | return 44 | } 45 | ctx.JSON(http.StatusOK, gin.H{"data": messages}) 46 | } 47 | 48 | func (c *Controller) AddUserToRoomApi(ctx *gin.Context) { 49 | // Validate input 50 | var room *data.ChatRoom 51 | if err := ctx.ShouldBindJSON(&room); err != nil { 52 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 53 | return 54 | } 55 | room, err := c.DB.AddClientToRoom(room) 56 | if err != nil { 57 | ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 58 | return 59 | } 60 | ctx.JSON(http.StatusOK, gin.H{"data": room}) 61 | } 62 | 63 | 64 | 65 | func (c *Controller) JoinRoom(ctx *gin.Context) { 66 | // Validate input 67 | var room *data.ChatRoom 68 | roomId := ctx.Query("roomId") 69 | userId := ctx.Query("userId") 70 | id , err := database.ObjectIDFromHex(roomId) 71 | userID, err := database.ObjectIDFromHex(userId) 72 | room , err = c.DB.FindRoomByID(id ) 73 | if err != nil { 74 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "room not found"}) 75 | return 76 | } 77 | var found bool = false 78 | for i := range room.Clients { 79 | if room.Clients[i] == userID { 80 | found = true 81 | } 82 | } 83 | if found != true { 84 | room , err = c.DB.AddClientToRoom(&data.ChatRoom{ 85 | ID : id, 86 | Clients : []primitive.ObjectID{userID}, 87 | }) 88 | } 89 | 90 | if err != nil { 91 | ctx.JSON(http.StatusBadRequest, gin.H{"error": "room not found"}) 92 | return 93 | } 94 | 95 | ctx.HTML(http.StatusOK, "chat.tmpl", gin.H{ 96 | "title": "Sample Front", 97 | "name": "Moel", 98 | "roomId" : roomId, 99 | "room" : room, 100 | "userId": userId, 101 | }) 102 | 103 | } 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /templates/chat.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{.title}} 4 | 10 | 11 | 12 | {{.name}} 13 |

Room ID {{ .roomId}}

14 |

room title : {{ .room.Title}}

15 | 16 | 17 | 18 |
19 | 20 | 21 |
22 | 23 | 24 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /database/mongo.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "chicko_chat/models" 5 | "context" 6 | "errors" 7 | "go.mongodb.org/mongo-driver/bson" 8 | "go.mongodb.org/mongo-driver/bson/primitive" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | "go.mongodb.org/mongo-driver/mongo/options" 11 | "time" 12 | ) 13 | 14 | type ChatDatabase struct { 15 | Users *mongo.Collection 16 | 17 | Messages *mongo.Collection 18 | 19 | Rooms *mongo.Collection 20 | } 21 | 22 | 23 | func (c *ChatDatabase) AddUser(user *data.UserData) (primitive.ObjectID, error) { 24 | 25 | res, err := c.Users.InsertOne(context.TODO(), user) 26 | 27 | if err != nil { 28 | return primitive.NilObjectID, err 29 | } 30 | 31 | return convertId(res) 32 | 33 | } 34 | 35 | // FindByEmail will be used to find a new user registry by email 36 | func (c *ChatDatabase) FindByEmail(email string) (*data.UserData, error) { 37 | 38 | // Find user by email 39 | var user = data.UserData{} 40 | err := c.Users.FindOne(context.TODO(), bson.M{"email": email}).Decode(&user) 41 | 42 | if err != nil { 43 | // Checks if the user was not found 44 | if err == mongo.ErrNoDocuments { 45 | return nil, errors.New("user not found") 46 | } 47 | return nil, err 48 | } 49 | 50 | return &user, nil 51 | 52 | } 53 | 54 | 55 | func (c *ChatDatabase) CreateRoom(room *data.ChatRoom) (*data.ChatRoom, error) { 56 | 57 | res, err := c.Rooms.InsertOne(context.TODO(), room) 58 | 59 | if err != nil { 60 | return room, err 61 | } 62 | 63 | room.ID, err = convertId(res) 64 | 65 | if err != nil { 66 | return room, err 67 | } 68 | return room, nil 69 | 70 | } 71 | 72 | 73 | func (c *ChatDatabase) UpdateUserName(user *data.UserData) (*data.UserData, error) { 74 | filter := bson.D{{"_id", user.ID}} 75 | update := bson.D{{"$set", bson.D{{"name",user.Name}}}} 76 | _, err := c.Users.UpdateOne(context.TODO(), filter, update) 77 | if err != nil { 78 | return nil, err 79 | } 80 | usr := &data.UserData{} 81 | err = c.Users.FindOne(context.TODO(),filter).Decode(&usr) 82 | 83 | if err != nil { 84 | return nil, err 85 | } 86 | return usr, nil 87 | } 88 | 89 | func (c *ChatDatabase) AddClientToRoom(room *data.ChatRoom) (*data.ChatRoom, error) { 90 | change := bson.M{ 91 | "$push": bson.M{ 92 | "users": bson.M{"$each":room.Clients}, 93 | }, 94 | } 95 | filter := bson.M{ 96 | "_id": room.ID, 97 | } 98 | 99 | _, err := c.Rooms.UpdateOne(context.Background(), filter, change) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | rm := &data.ChatRoom{} 105 | err = c.Rooms.FindOne(context.TODO(),filter).Decode(&rm) 106 | 107 | if err != nil { 108 | return nil, err 109 | } 110 | return rm, nil 111 | 112 | } 113 | 114 | func (c *ChatDatabase) SaveMessage(message *data.ChatEvent) (primitive.ObjectID, error) { 115 | message.Timestamp = time.Now() 116 | 117 | res, err := c.Messages.InsertOne(context.TODO(), message) 118 | 119 | if err != nil { 120 | return primitive.NilObjectID, err 121 | } 122 | 123 | return convertId(res) 124 | } 125 | 126 | 127 | // Get history of chat from the databse 128 | func (c *ChatDatabase) GetHistoryOfRoom(room *data.ChatRoom) ([]data.ChatEvent, error) { 129 | 130 | findOptions := options.Find() 131 | cur, err := c.Messages.Find(context.TODO(), bson.D{{"roomid", room.ID}}, findOptions) 132 | if err != nil { 133 | return nil, err 134 | } 135 | defer cur.Close(context.TODO()) 136 | 137 | var messages []data.ChatEvent 138 | err = cur.All(context.TODO(), &messages) 139 | 140 | return messages, nil 141 | } 142 | 143 | // Get chat rooms of user 144 | func (c *ChatDatabase) GetHistoryOfUser(user *data.UserData) ([]data.ChatRoom, error) { 145 | 146 | findOptions := options.Find() 147 | cur, err := c.Rooms.Find(context.TODO(), bson.M{"users": bson.M{"$in":[]primitive.ObjectID{user.ID}}}, findOptions) 148 | if err != nil { 149 | return nil, err 150 | } 151 | defer cur.Close(context.TODO()) 152 | 153 | var rooms []data.ChatRoom 154 | err = cur.All(context.TODO(), &rooms) 155 | 156 | return rooms, nil 157 | } 158 | 159 | 160 | // Get user detail 161 | func (c *ChatDatabase) GetUserData(ids []primitive.ObjectID) ([]data.UserData, error) { 162 | 163 | findOptions := options.Find() 164 | cur, err := c.Users.Find(context.TODO(), bson.M{"_id": bson.M{"$in":ids}}, findOptions) 165 | if err != nil { 166 | return nil, err 167 | } 168 | defer cur.Close(context.TODO()) 169 | var users []data.UserData 170 | err = cur.All(context.TODO(), &users) 171 | return users, nil 172 | } 173 | 174 | 175 | 176 | func (c *ChatDatabase) FindRoomByID(id primitive.ObjectID) (*data.ChatRoom, error) { 177 | 178 | // Find room by id 179 | var room = data.ChatRoom{} 180 | err := c.Rooms.FindOne(context.TODO(), bson.M{"_id": id}).Decode(&room) 181 | 182 | if err != nil { 183 | // Checks if the room was not found 184 | if err == mongo.ErrNoDocuments { 185 | return nil, errors.New("room not found") 186 | } 187 | return nil, err 188 | } 189 | 190 | return &room, nil 191 | 192 | } 193 | -------------------------------------------------------------------------------- /websocket/manager.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "chicko_chat/database" 5 | data "chicko_chat/models" 6 | "log" 7 | 8 | "fmt" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | const ( 15 | // Max wait time when writing message to peer 16 | writeWait = 10 * time.Second 17 | 18 | // Max time till next pong from peer 19 | pongWait = 60 * time.Second 20 | 21 | // Send ping interval, must be less then pong wait time 22 | pingPeriod = (pongWait * 9) / 10 23 | 24 | // Maximum message size allowed from peer. 25 | maxMessageSize = 10000 26 | ) 27 | 28 | // the amount of time to wait when pushing a message to 29 | // a slow client or a client that closed after `range Clients` started. 30 | const patience time.Duration = time.Second * 1 31 | 32 | type BrokerManager struct { 33 | Brokers map[*data.Broker]bool 34 | DB *database.ChatDatabase 35 | } 36 | 37 | type clientManager struct { 38 | client *data.Client 39 | } 40 | 41 | // runs broker accepting various requests 42 | func (manager *BrokerManager) RunBroker(broker *data.Broker) { 43 | for { 44 | select { 45 | case client := <-broker.Join: 46 | manager.registerClient(client, broker) 47 | 48 | case client := <-broker.Leave: 49 | manager.unregisterClient(client, broker) 50 | 51 | case message := <-broker.Notification: 52 | manager.broadcastToClients(message, broker) 53 | } 54 | 55 | } 56 | } 57 | 58 | func (manager *BrokerManager) registerClient(client *data.Client, broker *data.Broker) { 59 | broker.Mutex.Lock() 60 | broker.Clients[client] = true 61 | broker.Mutex.Unlock() 62 | message := data.ChatEvent{ 63 | EventType: data.Subscribe, 64 | RoomID: broker.Room.ID, 65 | UserID: client.User.ID, 66 | } 67 | broker.Notification <- &message 68 | 69 | log.Printf("Client added. %d registered Clients", len(broker.Clients)) 70 | 71 | } 72 | 73 | func (manager *BrokerManager) unregisterClient(client *data.Client, broker *data.Broker) { 74 | broker.Mutex.Lock() 75 | if _, ok := broker.Clients[client]; ok { 76 | delete(broker.Clients, client) 77 | close(client.Send) 78 | } 79 | broker.Mutex.Unlock() 80 | 81 | message := data.ChatEvent{ 82 | EventType: data.Unsubscribe, 83 | RoomID: broker.Room.ID, 84 | UserID: client.User.ID, 85 | } 86 | broker.Notification <- &message 87 | 88 | log.Printf("Removed client. %d registered Clients", len(broker.Clients)) 89 | 90 | } 91 | 92 | func (manager *BrokerManager) broadcastToClients(message *data.ChatEvent, broker *data.Broker) { 93 | message.Timestamp = time.Now() 94 | msg, err := manager.DB.SaveMessage(message) 95 | if err != nil { 96 | log.Print("message not sent: " + msg.Hex()) 97 | 98 | } 99 | broker.Mutex.Lock() 100 | 101 | for client := range broker.Clients { 102 | select { 103 | case client.Send <- message: 104 | log.Print("message sent to: " + client.User.ID.Hex()) 105 | case <-time.After(patience): 106 | log.Print("Skipping client: " + client.User.ID.Hex()) 107 | default: 108 | log.Print("Deleting client: " + client.User.ID.Hex()) 109 | close(client.Send) 110 | delete(broker.Clients, client) 111 | } 112 | } 113 | broker.Mutex.Unlock() 114 | 115 | } 116 | 117 | func (manager *clientManager) clientRead() { 118 | defer func() { 119 | manager.ClientDisconnect() 120 | }() 121 | 122 | manager.client.Conn.SetReadLimit(maxMessageSize) 123 | manager.client.Conn.SetReadDeadline(time.Now().Add(pongWait)) 124 | manager.client.Conn.SetPongHandler(func(string) error { manager.client.Conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 125 | 126 | // Start endless read loop, waiting for messages from client 127 | for { 128 | var msg data.ChatEvent 129 | // Read in a new message as JSON and map it to a Message object 130 | err := manager.client.Conn.ReadJSON(&msg) 131 | 132 | if err != nil { 133 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 134 | log.Printf("unexpected close error: %v", err) 135 | } 136 | break 137 | } 138 | // handel message 139 | manager.handleNewMessage(&msg) 140 | } 141 | 142 | } 143 | 144 | func (manager *clientManager) clientWrite() { 145 | ticker := time.NewTicker(pingPeriod) 146 | defer func() { 147 | ticker.Stop() 148 | manager.client.Conn.Close() 149 | }() 150 | for { 151 | select { 152 | case message, ok := <-manager.client.Send: 153 | manager.client.Conn.SetWriteDeadline(time.Now().Add(writeWait)) 154 | if !ok { 155 | // The WsServer closed the channel. 156 | manager.client.Conn.WriteMessage(websocket.CloseMessage, []byte{}) 157 | return 158 | } 159 | 160 | manager.client.Conn.WriteJSON(message) 161 | 162 | case <-ticker.C: 163 | manager.client.Conn.SetWriteDeadline(time.Now().Add(writeWait)) 164 | if err := manager.client.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { 165 | return 166 | } 167 | } 168 | } 169 | } 170 | 171 | func (manager *clientManager) ClientDisconnect() { 172 | manager.client.Conn.Close() 173 | } 174 | 175 | func (manager *clientManager) handleNewMessage(message *data.ChatEvent) { 176 | fmt.Println(message) 177 | switch message.EventType { 178 | case data.Broadcast: 179 | manager.client.Broker.Notification <- message 180 | 181 | case data.Subscribe, data.Unsubscribe: 182 | manager.notifyJoinedLeft(message) 183 | 184 | } 185 | 186 | } 187 | 188 | func (manager *clientManager) notifyJoinedLeft(message *data.ChatEvent) { 189 | 190 | manager.client.Send <- message 191 | } 192 | -------------------------------------------------------------------------------- /templates/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ .title }} 4 | 10 | 11 | 12 | {{.name}} 13 | 14 | 15 |
16 | 17 | 18 |
19 |
20 |

create new room

21 | 22 | 23 |
24 |
25 |

enter room id to join

26 | 27 | 28 |
29 |

Your chat history

30 | 31 | 32 | 33 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /database/mongo_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "chicko_chat/models" 5 | "context" 6 | "go.mongodb.org/mongo-driver/bson" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | "testing" 9 | ) 10 | 11 | func TestFindByEmail(t *testing.T) { 12 | 13 | db := ConnectDatabseTest() 14 | //test for not existing email 15 | _, err := db.FindByEmail("moelcrow@gmail.com") 16 | if err == nil { 17 | t.Fatalf("user found!") 18 | } 19 | 20 | //adding user to databse 21 | db.Users.InsertOne(context.TODO(), bson.M{"email": "moelcrow@gmail.com"}) 22 | 23 | _, err = db.FindByEmail("moelcrow@gmail.com") 24 | if err != nil { 25 | t.Fatalf("error user not found") 26 | } 27 | 28 | } 29 | func TestFindRoomByID(t *testing.T) { 30 | 31 | db := ConnectDatabseTest() 32 | id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55d") 33 | 34 | _, err = db.FindRoomByID(id) 35 | if err == nil { 36 | t.Fatalf("room found!") 37 | } 38 | 39 | //adding room to databse 40 | db.Rooms.InsertOne(context.TODO(), bson.M{"_id": id}) 41 | 42 | _, err = db.FindRoomByID(id) 43 | if err != nil { 44 | t.Fatalf("error room not found") 45 | } 46 | 47 | } 48 | 49 | func TestAddUser(t *testing.T) { 50 | 51 | db := ConnectDatabseTest() 52 | user := &data.UserData{ 53 | Email: "testregister@gmail.com", 54 | Name: "test user", 55 | Active: true, 56 | } 57 | 58 | _, err := db.AddUser(user) 59 | 60 | if err != nil { 61 | t.Fatalf("failure in adding user data to databse") 62 | } 63 | var usr = data.UserData{} 64 | res := db.Users.FindOne(context.TODO(), bson.M{"email": user.Email}).Decode(&usr) 65 | 66 | if res != nil { 67 | t.Fatalf("failure finding added user ") 68 | } 69 | 70 | } 71 | 72 | func TestCreateRoom(t *testing.T) { 73 | 74 | db := ConnectDatabseTest() 75 | room := &data.ChatRoom{ 76 | Title: "Test", 77 | } 78 | 79 | _, err := db.CreateRoom(room) 80 | 81 | if err != nil { 82 | t.Fatalf("failure in adding room data to database") 83 | } 84 | var rm = data.ChatRoom{} 85 | res := db.Rooms.FindOne(context.TODO(), bson.M{"title": room.Title}).Decode(&rm) 86 | 87 | if res != nil { 88 | t.Fatalf("failure finding added room ") 89 | } 90 | 91 | } 92 | 93 | func TestAddClientToRoom(t *testing.T) { 94 | 95 | db := ConnectDatabseTest() 96 | room := &data.ChatRoom{ 97 | Title: "Data For Test", 98 | Clients: []primitive.ObjectID{}, 99 | } 100 | 101 | _, err := db.Rooms.InsertOne(context.TODO(), room) 102 | 103 | err = db.Rooms.FindOne(context.TODO(), bson.M{"title": "Data For Test"}).Decode(&room) 104 | if err != nil { 105 | t.Fatalf("failure finding added room ") 106 | } 107 | 108 | user := &data.UserData{ 109 | Email: "testregister@gmail.com", 110 | Name: "test user", 111 | Active: true, 112 | } 113 | db.Users.InsertOne(context.TODO(), user) 114 | err = db.Users.FindOne(context.TODO(), user).Decode(&user) 115 | if err != nil { 116 | t.Fatalf("failure finding added user ") 117 | 118 | } 119 | room.Clients = append(room.Clients, user.ID) 120 | 121 | res, err := db.AddClientToRoom(room) 122 | if err != nil { 123 | t.Fatalf("failure adding user into room ") 124 | 125 | } 126 | if res.Clients[0] != user.ID { 127 | t.Fatalf("incorect user data ") 128 | 129 | } 130 | } 131 | 132 | func TestAddMessage(t *testing.T) { 133 | 134 | db := ConnectDatabseTest() 135 | message := &data.ChatEvent{ 136 | EventType: data.Broadcast, 137 | UserID: primitive.NewObjectID(), 138 | RoomID: primitive.NewObjectID(), 139 | Message: "test message", 140 | } 141 | 142 | res, err := db.SaveMessage(message) 143 | 144 | if err != nil { 145 | t.Fatalf("failure in adding message data to databse") 146 | } 147 | msg := &data.ChatEvent{} 148 | err = db.Messages.FindOne(context.TODO(), bson.M{"_id": res}).Decode(&msg) 149 | 150 | if err != nil || msg.ID != res { 151 | t.Fatalf("failure finding added message ") 152 | } 153 | 154 | } 155 | 156 | func TestGetHitoryOfRoom(t *testing.T) { 157 | 158 | db := ConnectDatabseTest() 159 | var messages []interface{} 160 | id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55b") 161 | 162 | room := &data.ChatRoom{ 163 | ID: id, 164 | Title: "test", 165 | } 166 | 167 | for i := 1; i < 5; i++ { 168 | message := data.ChatEvent{ 169 | EventType: data.Broadcast, 170 | UserID: primitive.NewObjectID(), 171 | RoomID: id, 172 | Message: "test message", 173 | } 174 | message2 := data.ChatEvent{ 175 | EventType: data.Broadcast, 176 | UserID: primitive.NewObjectID(), 177 | RoomID: primitive.NewObjectID(), 178 | Message: "another room", 179 | } 180 | messages = append(messages, message) 181 | messages = append(messages, message2) 182 | 183 | } 184 | _, err = db.Messages.InsertMany(context.TODO(), messages) 185 | if err != nil { 186 | t.Fatalf("failure in adding messages data to databse") 187 | } 188 | var result []data.ChatEvent 189 | result, err = db.GetHistoryOfRoom(room) 190 | if err != nil || len(result) != 4 { 191 | t.Fatalf("failure in retriveing messages data from databse") 192 | } 193 | 194 | } 195 | 196 | func TestHistoryOfUser(t *testing.T) { 197 | 198 | db := ConnectDatabseTest() 199 | var rooms []interface{} 200 | id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55b") 201 | 202 | user := &data.UserData{ 203 | ID: id, 204 | Email: "testuser@mail.com", 205 | } 206 | 207 | for i := 1; i < 5; i++ { 208 | room := data.ChatRoom{ 209 | Title: "test", 210 | Clients: []primitive.ObjectID{id, primitive.NewObjectID()}, 211 | } 212 | room2 := data.ChatRoom{ 213 | Title: "test", 214 | Clients: []primitive.ObjectID{primitive.NewObjectID(), primitive.NewObjectID()}, 215 | } 216 | rooms = append(rooms, room) 217 | rooms = append(rooms, room2) 218 | 219 | } 220 | _, err = db.Rooms.InsertMany(context.TODO(), rooms) 221 | if err != nil { 222 | t.Fatalf("failure in adding rooms data to databse") 223 | } 224 | var result []data.ChatRoom 225 | result, err = db.GetHistoryOfUser(user) 226 | if err != nil || len(result) != 4 { 227 | t.Fatalf("failure in retriveing rooms data from databse") 228 | } 229 | 230 | } 231 | 232 | func TestGetUserData(t *testing.T) { 233 | 234 | db := ConnectDatabseTest() 235 | var users []interface{} 236 | ids := []primitive.ObjectID{} 237 | for i := 1; i < 10; i++ { 238 | user := data.UserData{ 239 | Email: "test", 240 | ID: primitive.NewObjectID(), 241 | } 242 | 243 | users = append(users, user) 244 | ids = append(ids, user.ID) 245 | 246 | } 247 | _, err := db.Users.InsertMany(context.TODO(), users) 248 | if err != nil { 249 | t.Fatalf("failure in adding user data to databse") 250 | } 251 | var result []data.UserData 252 | 253 | result, err = db.GetUserData(ids) 254 | if err != nil || len(result) != 9 { 255 | t.Fatalf("failure in retriveing user data from databse") 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /controllers/api_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "bytes" 5 | "chicko_chat/database" 6 | "chicko_chat/models" 7 | "context" 8 | "encoding/json" 9 | "github.com/gin-gonic/gin" 10 | "github.com/stretchr/testify/assert" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | ) 16 | 17 | func SetUpRouter() *gin.Engine { 18 | router := gin.Default() 19 | return router 20 | } 21 | 22 | func TestGetUserRoomsApi(t *testing.T) { 23 | db := database.ConnectDatabseTest() 24 | controller := Controller{ 25 | DB: db, 26 | } 27 | r := SetUpRouter() 28 | r.POST("/user-rooms/", controller.GetUserRoomsApi) 29 | 30 | var rooms []interface{} 31 | id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55b") 32 | 33 | user := &data.UserData{ 34 | ID: id, 35 | Email: "testuser@mail.com", 36 | } 37 | 38 | for i := 1; i < 5; i++ { 39 | room := data.ChatRoom{ 40 | Title: "test", 41 | Clients: []primitive.ObjectID{id, primitive.NewObjectID()}, 42 | } 43 | rooms = append(rooms, room) 44 | 45 | } 46 | _, err = db.Rooms.InsertMany(context.TODO(), rooms) 47 | if err != nil { 48 | t.Fatalf("failure in adding rooms data to databse") 49 | } 50 | 51 | var result []data.ChatRoom 52 | result, err = db.GetHistoryOfUser(user) 53 | if err != nil || len(result) != 4 { 54 | t.Fatalf("failure in retriveing rooms data from databse") 55 | } 56 | 57 | jsonValue, _ := json.Marshal(user) 58 | req, _ := http.NewRequest("POST", "/user-rooms/", bytes.NewBuffer(jsonValue)) 59 | 60 | w := httptest.NewRecorder() 61 | r.ServeHTTP(w, req) 62 | 63 | assert.Equal(t, http.StatusOK, w.Code) 64 | // Convert the JSON response to a map 65 | var response map[string][]data.ChatRoom 66 | err = json.Unmarshal([]byte(w.Body.String()), &response) 67 | // Grab the value & whether or not it exists 68 | value, exists := response["data"] 69 | // Make some assertions on the correctness of the response. 70 | assert.Nil(t, err) 71 | assert.True(t, exists) 72 | assert.Equal(t, value, result) 73 | } 74 | 75 | func TestCreateRoomApi(t *testing.T) { 76 | db := database.ConnectDatabseTest() 77 | controller := Controller{ 78 | DB: db, 79 | } 80 | r := SetUpRouter() 81 | r.POST("/create-room/", controller.CreateRoomApi) 82 | 83 | id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55b") 84 | 85 | room := &data.ChatRoom{ 86 | Title: "test", 87 | Clients: []primitive.ObjectID{id}, 88 | } 89 | 90 | jsonValue, _ := json.Marshal(room) 91 | req, _ := http.NewRequest("POST", "/create-room/", bytes.NewBuffer(jsonValue)) 92 | 93 | w := httptest.NewRecorder() 94 | r.ServeHTTP(w, req) 95 | 96 | assert.Equal(t, http.StatusOK, w.Code) 97 | // Convert the JSON response to a map 98 | var response map[string]data.ChatRoom 99 | err = json.Unmarshal([]byte(w.Body.String()), &response) 100 | // Grab the value & whether or not it exists 101 | value, exists := response["data"] 102 | // Make some assertions on the correctness of the response. 103 | assert.Nil(t, err) 104 | assert.True(t, exists) 105 | assert.Equal(t, value.Clients, room.Clients) 106 | } 107 | 108 | func TestRoomHistoryApi(t *testing.T) { 109 | db := database.ConnectDatabseTest() 110 | controller := Controller{ 111 | DB: db, 112 | } 113 | r := SetUpRouter() 114 | r.POST("/room-history/", controller.RoomHistoryApi) 115 | 116 | id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55b") 117 | 118 | room := &data.ChatRoom{ 119 | ID: id, 120 | Title: "test", 121 | Clients: []primitive.ObjectID{id}, 122 | } 123 | 124 | var messages []interface{} 125 | for i := 1; i < 5; i++ { 126 | message := data.ChatEvent{ 127 | EventType: data.Broadcast, 128 | ID: primitive.NewObjectID(), 129 | RoomID: id, 130 | Message: "test message", 131 | } 132 | messages = append(messages, message) 133 | 134 | } 135 | _, err = db.Messages.InsertMany(context.TODO(), messages) 136 | if err != nil { 137 | t.Fatalf("failure in adding messages data to databse") 138 | } 139 | 140 | jsonValue, _ := json.Marshal(room) 141 | req, _ := http.NewRequest("POST", "/room-history/", bytes.NewBuffer(jsonValue)) 142 | 143 | w := httptest.NewRecorder() 144 | r.ServeHTTP(w, req) 145 | 146 | assert.Equal(t, http.StatusOK, w.Code) 147 | // Convert the JSON response to a map 148 | var response map[string][]data.ChatEvent 149 | err = json.Unmarshal([]byte(w.Body.String()), &response) 150 | // Grab the value & whether or not it exists 151 | value, exists := response["data"] 152 | // Make some assertions on the correctness of the response. 153 | assert.Nil(t, err) 154 | assert.True(t, exists) 155 | assert.Equal(t, len(value), len(messages)) 156 | assert.Equal(t, value[0], messages[0]) 157 | 158 | } 159 | 160 | func TestGetUserDetailsRoomApi(t *testing.T) { 161 | db := database.ConnectDatabseTest() 162 | controller := Controller{ 163 | DB: db, 164 | } 165 | r := SetUpRouter() 166 | r.POST("/room-user-details/", controller.GetUserDetailsRoomApi) 167 | 168 | id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55b") 169 | user_id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55a") 170 | user_id2, err := primitive.ObjectIDFromHex("640778694829658eebc2d55c") 171 | 172 | user1 := &data.UserData{ 173 | ID: user_id, 174 | Email: "test@gamil.com", 175 | } 176 | 177 | user2 := &data.UserData{ 178 | ID: user_id2, 179 | Email: "test2@gamil.com", 180 | } 181 | 182 | room := &data.ChatRoom{ 183 | ID: id, 184 | Title: "test", 185 | Clients: []primitive.ObjectID{user_id, user_id2}, 186 | } 187 | 188 | var users []interface{} 189 | 190 | users = append(users, user1) 191 | users = append(users, user2) 192 | 193 | _, err = db.Users.InsertMany(context.TODO(), users) 194 | if err != nil { 195 | t.Fatalf("failure in adding user data to databse") 196 | } 197 | 198 | jsonValue, _ := json.Marshal(room) 199 | req, _ := http.NewRequest("POST", "/room-user-details/", bytes.NewBuffer(jsonValue)) 200 | 201 | w := httptest.NewRecorder() 202 | r.ServeHTTP(w, req) 203 | 204 | assert.Equal(t, http.StatusOK, w.Code) 205 | // Convert the JSON response to a map 206 | var response map[string][]data.UserData 207 | err = json.Unmarshal([]byte(w.Body.String()), &response) 208 | // Grab the value & whether or not it exists 209 | value, exists := response["users"] 210 | // Make some assertions on the correctness of the response. 211 | assert.Nil(t, err) 212 | assert.True(t, exists) 213 | 214 | assert.Equal(t, value[0].ID, user1.ID) 215 | assert.Equal(t, value[1].ID, user2.ID) 216 | assert.Equal(t, value[0].Email, user1.Email) 217 | assert.Equal(t, value[1].Email, user2.Email) 218 | 219 | } 220 | 221 | func TestAddUserToRoomApi(t *testing.T) { 222 | db := database.ConnectDatabseTest() 223 | controller := Controller{ 224 | DB: db, 225 | } 226 | r := SetUpRouter() 227 | r.POST("/add-user-room/", controller.AddUserToRoomApi) 228 | 229 | id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55b") 230 | user_id, err := primitive.ObjectIDFromHex("640778694829658eebc2d55a") 231 | user_id2, err := primitive.ObjectIDFromHex("640778694829658eebc2d55c") 232 | 233 | room := &data.ChatRoom{ 234 | ID: id, 235 | Title: "test", 236 | Clients: []primitive.ObjectID{user_id}, 237 | } 238 | 239 | _, err = db.Rooms.InsertOne(context.TODO(), room) 240 | 241 | if err != nil { 242 | t.Fatalf("failure in adding room data to databse") 243 | } 244 | room.Clients[0] = user_id2 245 | jsonValue, _ := json.Marshal(room) 246 | req, _ := http.NewRequest("POST", "/add-user-room/", bytes.NewBuffer(jsonValue)) 247 | 248 | w := httptest.NewRecorder() 249 | r.ServeHTTP(w, req) 250 | 251 | assert.Equal(t, http.StatusOK, w.Code) 252 | // Convert the JSON response to a map 253 | var response map[string]data.ChatRoom 254 | err = json.Unmarshal([]byte(w.Body.String()), &response) 255 | // Grab the value & whether or not it exists 256 | value, exists := response["data"] 257 | // Make some assertions on the correctness of the response. 258 | assert.Nil(t, err) 259 | assert.True(t, exists) 260 | assert.Equal(t, value.ID, room.ID) 261 | assert.Equal(t, len(value.Clients), 2) 262 | assert.Equal(t, value.Clients[0], user_id) 263 | assert.Equal(t, value.Clients[1], user_id2) 264 | 265 | } 266 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= 3 | github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= 12 | github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= 13 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 14 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 15 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 16 | github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= 17 | github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= 18 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 19 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 20 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 21 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 22 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 23 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 24 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 25 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 26 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 27 | github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= 28 | github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= 29 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 30 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 31 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 32 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 33 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 34 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 35 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 37 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 39 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 40 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 41 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 42 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 43 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 44 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 45 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 46 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 47 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 48 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 49 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 50 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 51 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 52 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 53 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 54 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 55 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 56 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 57 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 58 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 59 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 60 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 61 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 62 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 63 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 64 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= 65 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 66 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 67 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 68 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 69 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 70 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 71 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 75 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 76 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 79 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 80 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 81 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 84 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 85 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 86 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 87 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 88 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 89 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 90 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 91 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 92 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 93 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 94 | github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= 95 | github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 96 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 97 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 98 | github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= 99 | github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= 100 | github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= 101 | github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= 102 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 103 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 104 | go.mongodb.org/mongo-driver v1.11.2 h1:+1v2rDQUWNcGW7/7E0Jvdz51V38XXxJfhzbV17aNHCw= 105 | go.mongodb.org/mongo-driver v1.11.2/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= 106 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 107 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 108 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 109 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 110 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 111 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 112 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 113 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 114 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 115 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 116 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 117 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 125 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 127 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 128 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 129 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 130 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 131 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 132 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 133 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 134 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 136 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 137 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 138 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 139 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 140 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 141 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 142 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 143 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 144 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 145 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 146 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 147 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 148 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 149 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 150 | --------------------------------------------------------------------------------