Skip to content
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

Feature/#616 test #702

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-amqp' // rabbitmq
implementation 'org.springframework.boot:spring-boot-starter-reactor-netty' // rabbitmq
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation "org.springframework.boot:spring-boot-starter-actuator"
Expand Down Expand Up @@ -91,12 +93,12 @@ tasks.withType(GenerateSwaggerUI) {
doFirst {
def swaggerUIFile = file("${openapi3.outputDirectory}/openapi3.yaml")

def securitySchemesContent = " securitySchemes:\n" + \
" APIKey:\n" + \
" type: apiKey\n" + \
" name: Authorization\n" + \
" in: header\n" + \
"security:\n" +
def securitySchemesContent = " securitySchemes:\n" + \
" APIKey:\n" + \
" type: apiKey\n" + \
" name: Authorization\n" + \
" in: header\n" + \
"security:\n" +
" - APIKey: [] # Apply the security scheme here"

swaggerUIFile.append securitySchemesContent
Expand Down Expand Up @@ -130,5 +132,5 @@ resolveMainClassName {

test {
systemProperty 'user.timezone', 'Asia/Seoul'

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.happy.friendogly.chat.config;

public interface ChatTemplate {

void convertAndSend(Long chatRoomId, Object payload);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.happy.friendogly.chat.config;

import org.springframework.context.annotation.Profile;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

@Component
@Profile("local")
public class InMemoryChatTemplate implements ChatTemplate {

private static final String TOPIC_CHAT_PREFIX = "/exchange/chat.exchange/room.";

private final SimpMessagingTemplate template;

public InMemoryChatTemplate(SimpMessagingTemplate template) {
this.template = template;
}

@Override
public void convertAndSend(Long chatRoomId, Object payload) {
template.convertAndSend(TOPIC_CHAT_PREFIX + chatRoomId, payload);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.happy.friendogly.chat.config;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("!local")
public class RabbitChatTemplate implements ChatTemplate {

private final RabbitTemplate rabbitTemplate;

public RabbitChatTemplate(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}

@Override
public void convertAndSend(Long chatRoomId, Object payload) {
rabbitTemplate.convertAndSend("chat.exchange", "room." + chatRoomId, payload);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.happy.friendogly.auth.WebSocketAuth;
import com.happy.friendogly.chat.dto.request.ChatMessageSocketRequest;
import com.happy.friendogly.chat.dto.request.InviteToChatRoomRequest;
import com.happy.friendogly.chat.dto.response.InviteToChatRoomResponse;
import com.happy.friendogly.chat.service.ChatCommandService;
import com.happy.friendogly.chat.service.ChatRoomQueryService;
import com.happy.friendogly.common.ApiResponse;
Expand All @@ -16,48 +15,35 @@
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatSocketController {

private final ChatCommandService chatCommandService;
private final ChatRoomQueryService chatRoomQueryService;
private final SimpMessagingTemplate template;

public ChatSocketController(
ChatCommandService chatCommandService,
ChatRoomQueryService chatRoomQueryService,
SimpMessagingTemplate template
ChatRoomQueryService chatRoomQueryService
) {
this.chatCommandService = chatCommandService;
this.chatRoomQueryService = chatRoomQueryService;
this.template = template;
}

@MessageMapping("/invite")
@MessageMapping("/invite") // TODO: 1대1 채팅방 구현 시 수정 필요
public void invite(
@WebSocketAuth Long senderMemberId,
@Payload InviteToChatRoomRequest request
) {
chatRoomQueryService.validateInvitation(senderMemberId, request);
template.convertAndSend(
"/topic/invite/" + request.receiverMemberId(),
new InviteToChatRoomResponse(request.chatRoomId())
);
}

@Deprecated
@MessageMapping("/enter/{chatRoomId}")
public void enter(
@WebSocketAuth Long memberId,
@DestinationVariable(value = "chatRoomId") Long chatRoomId
) {
chatCommandService.sendEnter(memberId, chatRoomId);
// template.convertAndSend(
// "/topic/invite/" + request.receiverMemberId(),
// new InviteToChatRoomResponse(request.chatRoomId())
// );
}

@MessageMapping("/chat/{chatRoomId}")
@MessageMapping("chat.{chatRoomId}")
public void sendMessage(
@WebSocketAuth Long memberId,
@DestinationVariable(value = "chatRoomId") Long chatRoomId,
Expand All @@ -66,15 +52,6 @@ public void sendMessage(
chatCommandService.sendChat(memberId, chatRoomId, request);
}

@Deprecated
@MessageMapping("/leave/{chatRoomId}")
public void leave(
@WebSocketAuth Long memberId,
@DestinationVariable(value = "chatRoomId") Long chatRoomId
) {
chatCommandService.sendLeave(memberId, chatRoomId);
}

@MessageExceptionHandler
public ResponseEntity<ApiResponse<ErrorResponse>> handleException(FriendoglyException exception) {
ErrorResponse errorResponse = new ErrorResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static com.happy.friendogly.chat.domain.MessageType.ENTER;
import static com.happy.friendogly.chat.domain.MessageType.LEAVE;

import com.happy.friendogly.chat.config.ChatTemplate;
import com.happy.friendogly.chat.domain.ChatMessage;
import com.happy.friendogly.chat.domain.ChatRoom;
import com.happy.friendogly.chat.domain.MessageType;
Expand All @@ -16,35 +17,33 @@
import com.happy.friendogly.member.repository.MemberRepository;
import com.happy.friendogly.notification.service.NotificationService;
import java.time.LocalDateTime;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ChatCommandService {

private static final String TOPIC_CHAT_PREFIX = "/topic/chat/";
private static final String EMPTY_CONTENT = "";

private final MemberRepository memberRepository;
private final ChatRoomRepository chatRoomRepository;
private final ChatMessageRepository chatMessageRepository;
private final NotificationService notificationService;
private final SimpMessagingTemplate template;
private final ChatTemplate chatTemplate;

public ChatCommandService(
MemberRepository memberRepository,
ChatRoomRepository chatRoomRepository,
ChatMessageRepository chatMessageRepository,
NotificationService notificationService,
SimpMessagingTemplate template
ChatTemplate chatTemplate
) {
this.memberRepository = memberRepository;
this.chatRoomRepository = chatRoomRepository;
this.chatMessageRepository = chatMessageRepository;
this.notificationService = notificationService;
this.template = template;
this.chatTemplate = chatTemplate;
}

public void sendEnter(Long senderMemberId, Long chatRoomId) {
Expand All @@ -71,9 +70,10 @@ public void sendLeave(Long senderMemberId, Long chatRoomId) {
}

private void sendAndSave(MessageType messageType, String content, ChatRoom chatRoom, Member senderMember) {
ChatMessageSocketResponse chat = new ChatMessageSocketResponse(messageType, content, senderMember, LocalDateTime.now());
ChatMessageSocketResponse chat = new ChatMessageSocketResponse(messageType, content, senderMember,
LocalDateTime.now());
notificationService.sendChatNotification(chatRoom.getId(), chat);
template.convertAndSend(TOPIC_CHAT_PREFIX + chatRoom.getId(), chat);
chatTemplate.convertAndSend(chatRoom.getId(), chat);
chatMessageRepository.save(new ChatMessage(chatRoom, messageType, senderMember, content));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.happy.friendogly.config;

import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.rabbitmq.client.ConnectionFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@EnableRabbit
@Profile("!local")
public class RabbitMqConfig {

private static final String CHAT_QUEUE_NAME = "chat.queue";
private static final String CHAT_EXCHANGE_NAME = "chat.exchange";
private static final String ROUTING_KEY = "*.room.*";

private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

@Value("${spring.rabbitmq.host}")
private String host;

@Value("${spring.rabbitmq.username}")
private String username;

@Value("${spring.rabbitmq.password}")
private String password;

@Value("${spring.rabbitmq.stomp-port}")
private int port;

@Bean
public Queue queue() {
return new Queue(CHAT_QUEUE_NAME, true);
}

@Bean
public TopicExchange topicExchange() {
return new TopicExchange(CHAT_EXCHANGE_NAME, true, false);
}

@Bean
public Binding binding(Queue queue, TopicExchange topicExchange) {
return BindingBuilder.bind(queue)
.to(topicExchange)
.with(ROUTING_KEY);
}

@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost("/");
return connectionFactory.getRabbitConnectionFactory();
}

@Bean
public AmqpAdmin amqpAdmin(RabbitTemplate rabbitTemplate) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate.getConnectionFactory());
rabbitAdmin.declareExchange(topicExchange());
return rabbitAdmin;
}

@Bean
public RabbitTemplate rabbitTemplate(
org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory) {

RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter());
return rabbitTemplate;
}

@Bean
public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(javaTimeModule());
return new Jackson2JsonMessageConverter(objectMapper);
}

@Bean
public Module javaTimeModule() {
return new JavaTimeModule()
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(TIME_FORMAT))
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(TIME_FORMAT));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
@Component
public class WebSocketInterceptor implements ChannelInterceptor {

private static final String TOPIC_CHAT_ENDPOINT = "/topic/chat/";
private static final String TOPIC_INVITE_ENDPOINT = "/topic/invite/";
private static final String TOPIC_CHAT_ENDPOINT = "/exchange/chat.exchange/room.";
// private static final String TOPIC_INVITE_ENDPOINT = "/topic/invite/";

private final ClubRepository clubRepository;
private final ChatRoomRepository chatRoomRepository;
Expand Down Expand Up @@ -50,9 +50,10 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
String accessToken = accessor.getFirstNativeHeader(AUTHORIZATION);
long memberId = validateAndExtractMemberIdFrom(accessToken);

if (destination.startsWith(TOPIC_INVITE_ENDPOINT)) {
validateInviteSubscription(memberId, destination);
}
// TODO: 1대1 채팅방 구현할 때 고려하기
// if (destination.startsWith(TOPIC_INVITE_ENDPOINT)) {
// validateInviteSubscription(memberId, destination);
// }

if (destination.startsWith(TOPIC_CHAT_ENDPOINT)) {
validateChatSubscription(memberId, destination);
Expand All @@ -68,9 +69,9 @@ private void validateDestination(String destination) {
if (destination.startsWith(TOPIC_CHAT_ENDPOINT)) {
return;
}
if (destination.startsWith(TOPIC_INVITE_ENDPOINT)) {
return;
}
// if (destination.startsWith(TOPIC_INVITE_ENDPOINT)) {
// return;
// }
throw new FriendoglyWebSocketException(String.format("%s는 올바른 sub 경로가 아닙니다.", destination));
}

Expand All @@ -88,14 +89,14 @@ private long validateAndExtractMemberIdFrom(String accessToken) {
}
}

private void validateInviteSubscription(long memberId, String destination) {
String rawMemberIdToSubscribe = destination.substring(TOPIC_INVITE_ENDPOINT.length());
long memberIdToSubscribe = convertToLong(rawMemberIdToSubscribe);

if (memberId != memberIdToSubscribe) {
throw new FriendoglyWebSocketException("자신의 초대 endpoint만 구독할 수 있습니다.");
}
}
// private void validateInviteSubscription(long memberId, String destination) {
// String rawMemberIdToSubscribe = destination.substring(TOPIC_INVITE_ENDPOINT.length());
// long memberIdToSubscribe = convertToLong(rawMemberIdToSubscribe);
//
// if (memberId != memberIdToSubscribe) {
// throw new FriendoglyWebSocketException("자신의 초대 endpoint만 구독할 수 있습니다.");
// }
// }

private void validateChatSubscription(long memberId, String destination) {
String rawChatRoomId = destination.substring(TOPIC_CHAT_ENDPOINT.length());
Expand Down
Loading
Loading