-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
인증된 채팅 기능 초안을 추가합니다. #85
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,15 +8,14 @@ import ( | |
|
||
"github.com/labstack/echo/v4" | ||
"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" | ||
|
@@ -56,7 +55,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()) | ||
|
@@ -66,14 +65,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) | ||
|
@@ -91,7 +90,7 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) { | |
})) | ||
e.Use(pndmiddleware.BuildAuthMiddleware(authService, auth.FirebaseAuthClientKey)) | ||
|
||
// RegisterChan routes | ||
// Register routes | ||
e.GET("/health", func(c echo.Context) error { | ||
return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) | ||
}) | ||
|
@@ -143,11 +142,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()) | ||
// }) | ||
// } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 해당 부분은 왜 주석으로 되어있나여 ? 만약 사용안하시는거면 지우셔도 되지 않을까 싶슴당 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 어느 쪽이 임시가 될지 몰라서요. 일단 남겨둘게요! 에픽 브랜치에서 최종 점검할 때 지우면 될 것 같아요 |
||
|
||
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package wschat | ||
|
||
import ( | ||
"net/http" | ||
"strconv" | ||
"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 MessageRequest | ||
upgrader websocket.Upgrader | ||
|
||
authService service.AuthService | ||
} | ||
|
||
func NewWSServer( | ||
upgrader websocket.Upgrader, | ||
authService service.AuthService, | ||
) *WSServer { | ||
return &WSServer{ | ||
clients: make(map[int64]WSClient), | ||
broadcast: make(chan MessageRequest), | ||
upgrader: upgrader, | ||
authService: authService, | ||
} | ||
} | ||
|
||
func NewDefaultUpgrader() websocket.Upgrader { | ||
upgrader := websocket.Upgrader{ | ||
CheckOrigin: func(_ *http.Request) bool { | ||
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 func() { | ||
err2 := conn.Close() | ||
if err2 != nil { | ||
log.Error().Err(err2).Msg("Failed to close connection") | ||
} | ||
delete(s.clients, userID) | ||
}() | ||
|
||
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 msgReq MessageRequest | ||
err := conn.ReadJSON(&msgReq) | ||
msgReq.Sender = Sender{ID: userID} | ||
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 <- msgReq | ||
} | ||
} | ||
|
||
// Broadcast messages to all clients | ||
func (s *WSServer) LoopOverClientMessages() { | ||
log.Info().Msg("Looping over client messages") | ||
|
||
for { | ||
msgReq := <-s.broadcast | ||
|
||
for _, client := range s.clients { | ||
log.Info().Msg( | ||
"Message from user: " + | ||
strconv.Itoa(int(client.userID)) + | ||
" to user: " + strconv.Itoa(int(msgReq.Sender.ID))) | ||
|
||
// TODO: Check if the message is for the room | ||
msg := NewPlainMessageResponse(msgReq.Sender, msgReq.Room, msgReq.Message, time.Now()) | ||
|
||
if err := client.WriteJSON(msg); err != nil { | ||
// No way but to close the connection | ||
log.Error().Err(err).Msg("Failed to write message") | ||
err := client.Close() | ||
if err != nil { | ||
log.Error().Err(err).Msg("Failed to close connection") | ||
delete(s.clients, client.userID) | ||
return | ||
} | ||
delete(s.clients, client.userID) | ||
return | ||
} | ||
} | ||
} | ||
} | ||
|
||
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 MessageRequest struct { | ||
Sender Sender `json:"sender"` | ||
Room Room `json:"room"` | ||
MessageType string `json:"messageType"` | ||
Media *Media `json:"media,omitempty"` | ||
Message string `json:"message"` | ||
} | ||
|
||
type MessageResponse struct { | ||
Sender Sender `json:"sender"` | ||
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 NewPlainMessageResponse(sender Sender, room Room, message string, now time.Time) MessageResponse { | ||
return MessageResponse{ | ||
Sender: sender, | ||
Room: room, | ||
MessageType: "plain", | ||
Message: message, | ||
CreatedAt: now.Format(time.RFC3339), | ||
UpdatedAt: now.Format(time.RFC3339), | ||
} | ||
} | ||
|
||
func NewMediaMessageResponse(sender Sender, room Room, media *Media, now time.Time) MessageResponse { | ||
return MessageResponse{ | ||
Sender: sender, | ||
Room: room, | ||
MessageType: "media", | ||
Media: media, | ||
CreatedAt: now.Format(time.RFC3339), | ||
UpdatedAt: now.Format(time.RFC3339), | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거 주석 커맨드로 넣으시면서 중복 발생한거같아여
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넹 어차피 지워질 예정이라 당장은 큰 상관 없을 것 같습니다~