Skip to content

Commit

Permalink
feat: add initial version of authorized WS chat
Browse files Browse the repository at this point in the history
  • Loading branch information
litsynp committed Jul 28, 2024
1 parent 35e34d4 commit 0cbb584
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 13 deletions.
36 changes: 23 additions & 13 deletions cmd/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import (
"github.com/labstack/echo/v4/middleware"

"github.com/pet-sitter/pets-next-door-api/cmd/server/handler"
"github.com/pet-sitter/pets-next-door-api/internal/chat"
"github.com/pet-sitter/pets-next-door-api/internal/configs"
"github.com/pet-sitter/pets-next-door-api/internal/domain/auth"
s3infra "github.com/pet-sitter/pets-next-door-api/internal/infra/bucket"
"github.com/pet-sitter/pets-next-door-api/internal/infra/database"
kakaoinfra "github.com/pet-sitter/pets-next-door-api/internal/infra/kakao"
"github.com/pet-sitter/pets-next-door-api/internal/service"
"github.com/pet-sitter/pets-next-door-api/internal/wschat"
pndmiddleware "github.com/pet-sitter/pets-next-door-api/lib/middleware"
"github.com/rs/zerolog"
echoswagger "github.com/swaggo/echo-swagger"
Expand Down Expand Up @@ -56,7 +56,7 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) {
breedService := service.NewBreedService(db)
sosPostService := service.NewSOSPostService(db)
conditionService := service.NewSOSConditionService(db)
chatService := service.NewChatService(db)
// chatService := service.NewChatService(db)

// Initialize handlers
authHandler := handler.NewAuthHandler(authService, kakaoinfra.NewKakaoDefaultClient())
Expand All @@ -66,14 +66,14 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) {
sosPostHandler := handler.NewSOSPostHandler(*sosPostService, authService)
conditionHandler := handler.NewConditionHandler(*conditionService)

// InMemoryStateManager는 클라이언트와 채팅방의 상태를 메모리에 저장하고 관리합니다.
// 이 메서드는 단순하고 빠르며 테스트 목적으로 적합합니다.
// 전략 패턴을 사용하여 이 부분을 다른 상태 관리 구현체로 쉽게 교체할 수 있습니다.
stateManager := chat.NewInMemoryStateManager()
wsServer := chat.NewWebSocketServer(stateManager)
go wsServer.Run()
chat.InitializeWebSocketServer(ctx, wsServer, chatService)
chatHandler := handler.NewChatController(wsServer, stateManager, authService, *chatService)
// // InMemoryStateManager는 클라이언트와 채팅방의 상태를 메모리에 저장하고 관리합니다.
// // 이 메서드는 단순하고 빠르며 테스트 목적으로 적합합니다.
// // 전략 패턴을 사용하여 이 부분을 다른 상태 관리 구현체로 쉽게 교체할 수 있습니다.
// stateManager := chat.NewInMemoryStateManager()
// wsServer := chat.NewWebSocketServer(stateManager)
// go wsServer.Run()
// chat.InitializeWebSocketServer(ctx, wsServer, chatService)
// chatHandler := handler.NewChatController(wsServer, stateManager, authService, *chatService)

// RegisterChan middlewares
logger := zerolog.New(os.Stdout)
Expand Down Expand Up @@ -143,11 +143,21 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) {
postAPIGroup.GET("/sos/conditions", conditionHandler.FindConditions)
}

// chatAPIGroup := apiRouteGroup.Group("/chat")
// {
// chatAPIGroup.GET("/ws", func(c echo.Context) error {
// return chatHandler.ServerWebsocket(c, c.Response().Writer, c.Request())
// })
// }

upgrader := wschat.NewDefaultUpgrader()
wsServerV2 := wschat.NewWSServer(upgrader, authService)

go wsServerV2.LoopOverClientMessages()

chatAPIGroup := apiRouteGroup.Group("/chat")
{
chatAPIGroup.GET("/ws", func(c echo.Context) error {
return chatHandler.ServerWebsocket(c, c.Response().Writer, c.Request())
})
chatAPIGroup.GET("/ws", wsServerV2.HandleConnections)
}

return e, nil
Expand Down
172 changes: 172 additions & 0 deletions internal/wschat/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package wschat

import (
"net/http"
"time"

"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/pet-sitter/pets-next-door-api/internal/service"
"github.com/rs/zerolog/log"
)

type WSServer struct {
// key: UserID, value: WSClient
clients map[int64]WSClient
broadcast chan Message
upgrader websocket.Upgrader

authService service.AuthService
}

type WSClient struct {
conn *websocket.Conn
userID int64
}

func NewWSClient(
conn *websocket.Conn,
userID int64,
) WSClient {
return WSClient{conn, userID}
}

func (c *WSClient) WriteJSON(v interface{}) error {
return c.conn.WriteJSON(v)
}

func (c *WSClient) Close() error {
return c.conn.Close()
}

type Message struct {
Sender Sender `json:"user"`
Room Room `json:"room"`
MessageType string `json:"messageType"`
Media *Media `json:"media,omitempty"`
Message string `json:"message"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

type Sender struct {
ID int64 `json:"id"`
}

type Room struct {
ID int64 `json:"id"`
}

type Media struct {
ID int64 `json:"id"`
MediaType string `json:"type"`
URL string `json:"url"`
}

func NewPlainMessage(sender Sender, Room Room, message string, now time.Time) Message {

Check failure on line 66 in internal/wschat/server.go

View workflow job for this annotation

GitHub Actions / build

captLocal: `Room' should not be capitalized (gocritic)
return Message{
Sender: sender,
Room: Room,
MessageType: "plain",
Message: message,
CreatedAt: now.Format(time.RFC3339),
UpdatedAt: now.Format(time.RFC3339),
}
}

func NewMediaMessage(sender Sender, Room Room, media *Media, now time.Time) Message {

Check failure on line 77 in internal/wschat/server.go

View workflow job for this annotation

GitHub Actions / build

captLocal: `Room' should not be capitalized (gocritic)
return Message{
Sender: sender,
Room: Room,
MessageType: "media",
Media: media,
CreatedAt: now.Format(time.RFC3339),
UpdatedAt: now.Format(time.RFC3339),
}
}

func NewWSServer(
upgrader websocket.Upgrader,
authService service.AuthService,
) *WSServer {
return &WSServer{
clients: make(map[int64]WSClient),
broadcast: make(chan Message),
upgrader: upgrader,
authService: authService,
}
}

func NewDefaultUpgrader() websocket.Upgrader {
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {

Check warning on line 102 in internal/wschat/server.go

View workflow job for this annotation

GitHub Actions / build

unused-parameter: parameter 'r' seems to be unused, consider removing or renaming it to match ^_ (revive)
return true
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}

return upgrader
}

// Server-side WebSocket handler
func (s *WSServer) HandleConnections(
c echo.Context,
) error {
log.Info().Msg("Handling connections")

foundUser, err2 := s.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization"))
if err2 != nil {
return c.JSON(err2.StatusCode, err2)
}
userID := foundUser.ID

conn, err := s.upgrader.Upgrade(c.Response().Writer, c.Request(), nil)
defer conn.Close()

Check failure on line 125 in internal/wschat/server.go

View workflow job for this annotation

GitHub Actions / build

SA5001: should check returned error before deferring conn.Close() (staticcheck)
if err != nil {
log.Error().Err(err).Msg("Failed to upgrade connection")
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
}
client := NewWSClient(conn, userID)

s.clients[userID] = client

for {
var msg Message
err := conn.ReadJSON(&msg)
if err != nil {
log.Error().Err(err).Msg("Failed to read message")
delete(s.clients, userID)
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
}

s.broadcast <- msg
}
}

// Broadcast messages to all clients
func (s *WSServer) LoopOverClientMessages() {
log.Info().Msg("Looping over client messages")

for {
msg := <-s.broadcast

for _, client := range s.clients {
// Filter messages from the same user
if client.userID == msg.Sender.ID {
return
}

// TODO: Check if the message is for the room
msg.Sender.ID = client.userID

if err := client.WriteJSON(msg); err != nil {
// No way but to close the connection
log.Error().Err(err).Msg("Failed to write message")
client.Close()

Check warning on line 166 in internal/wschat/server.go

View workflow job for this annotation

GitHub Actions / build

unhandled-error: Unhandled error in call to function wschat.WSClient.Close (revive)
delete(s.clients, client.userID)
return
}
}
}
}

0 comments on commit 0cbb584

Please sign in to comment.