From 180952a30fe1553479e74ff7ecc00b600a48b733 Mon Sep 17 00:00:00 2001 From: Raydberg Gabriel Date: Fri, 19 Sep 2025 13:43:48 -0500 Subject: [PATCH 1/4] feat: add backend for chat with websockets --- pom.xml | 71 ++++++++------- src/main/java/com/msvcchat/DTOs/.gitkeep | 0 .../msvcchat/config/ReactiveMapperConfig.java | 8 -- .../com/msvcchat/config/SwaggerConfig.java | 15 +--- .../websockets/ChatWebSocketHandler.java | 88 +++++++++++-------- .../config/websockets/WebSocketConfig.java | 2 +- .../com/msvcchat/dtos/ChatMessageDto.java | 17 ++++ .../msvcchat/dtos/CreateChatMessageDto.java | 13 +++ .../java/com/msvcchat/entity/ChatMessage.java | 12 ++- src/main/java/com/msvcchat/entity/Room.java | 6 +- src/main/java/com/msvcchat/mappers/.gitkeep | 0 .../msvcchat/mappers/ChatMessageMapper.java | 27 ++++++ src/main/java/com/msvcchat/service/.gitkeep | 0 .../com/msvcchat/service/ChatRoomManager.java | 54 ++++++++++++ .../com/msvcchat/service/ChatService.java | 13 +++ .../service/Impl/ChatServiceImpl.java | 39 ++++++++ src/main/resources/banner.txt | 18 ++++ 17 files changed, 282 insertions(+), 101 deletions(-) delete mode 100644 src/main/java/com/msvcchat/DTOs/.gitkeep create mode 100644 src/main/java/com/msvcchat/dtos/ChatMessageDto.java create mode 100644 src/main/java/com/msvcchat/dtos/CreateChatMessageDto.java delete mode 100644 src/main/java/com/msvcchat/mappers/.gitkeep create mode 100644 src/main/java/com/msvcchat/mappers/ChatMessageMapper.java delete mode 100644 src/main/java/com/msvcchat/service/.gitkeep create mode 100644 src/main/java/com/msvcchat/service/ChatRoomManager.java create mode 100644 src/main/java/com/msvcchat/service/ChatService.java create mode 100644 src/main/java/com/msvcchat/service/Impl/ChatServiceImpl.java create mode 100644 src/main/resources/banner.txt diff --git a/pom.xml b/pom.xml index 9a85c78..d07abec 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,9 @@ 21 2025.0.0 + 1.5.5.Final + 21 + UTF-8 @@ -45,18 +48,18 @@ org.springframework.boot spring-boot-starter-webflux - - org.springframework.cloud - spring-cloud-starter-config - - - org.springframework.cloud - spring-cloud-starter-netflix-eureka-client - - - org.springframework.cloud - spring-cloud-starter-bootstrap - + + org.springframework.cloud + spring-cloud-starter-config + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-bootstrap + org.springframework.boot spring-boot-devtools @@ -89,32 +92,36 @@ org.springframework.boot spring-boot-starter-aop - - org.mapstruct - mapstruct - 1.5.5.Final - - - org.mapstruct - mapstruct-processor - 1.5.5.Final - provided - + org.springdoc springdoc-openapi-starter-webflux-api 2.8.11 - org.springdoc - springdoc-openapi-starter-webflux-ui - 2.8.11 - + org.springdoc + springdoc-openapi-starter-webflux-ui + 2.8.11 + org.springframework.cloud spring-cloud-starter-circuitbreaker-reactor-resilience4j + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + @@ -135,15 +142,15 @@ maven-compiler-plugin - - org.mapstruct - mapstruct-processor - 1.5.5.Final - org.projectlombok lombok + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + diff --git a/src/main/java/com/msvcchat/DTOs/.gitkeep b/src/main/java/com/msvcchat/DTOs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/msvcchat/config/ReactiveMapperConfig.java b/src/main/java/com/msvcchat/config/ReactiveMapperConfig.java index 60072fe..fee16c1 100644 --- a/src/main/java/com/msvcchat/config/ReactiveMapperConfig.java +++ b/src/main/java/com/msvcchat/config/ReactiveMapperConfig.java @@ -15,12 +15,4 @@ nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE ) public interface ReactiveMapperConfig { - - default Mono mapMono(Mono mono, Function mapper) { - return mono.map(mapper); - } - - default Flux mapFlux(Flux flux, Function mapper) { - return flux.map(mapper); - } } \ No newline at end of file diff --git a/src/main/java/com/msvcchat/config/SwaggerConfig.java b/src/main/java/com/msvcchat/config/SwaggerConfig.java index d8dfd3c..b0de96c 100644 --- a/src/main/java/com/msvcchat/config/SwaggerConfig.java +++ b/src/main/java/com/msvcchat/config/SwaggerConfig.java @@ -26,26 +26,13 @@ servers = { @Server( description = "Local Server", - url = "http://localhost:9005" + url = "http://localhost:9096" ), @Server( description = "Production Server", url = "https://" ) } -// , -// security = @SecurityRequirement( -// name = "securityToken" -// ) -//) -//@SecurityScheme( -// name = "securityToken", -// description = "Access Token For My API", -// type = SecuritySchemeType.HTTP, -// paramName = HttpHeaders.AUTHORIZATION, -// in = SecuritySchemeIn.HEADER, -// scheme = "bearer", -// bearerFormat = "JWT" ) public class SwaggerConfig { diff --git a/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java b/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java index 06ac8e5..2b49625 100644 --- a/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java +++ b/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java @@ -1,8 +1,12 @@ package com.msvcchat.config.websockets; import com.fasterxml.jackson.databind.ObjectMapper; +import com.msvcchat.dtos.CreateChatMessageDto; import com.msvcchat.entity.ChatMessage; +import com.msvcchat.mappers.ChatMessageMapper; import com.msvcchat.repositories.ChatMessageRepository; +import com.msvcchat.service.ChatRoomManager; +import com.msvcchat.service.ChatService; import lombok.RequiredArgsConstructor; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.stereotype.Component; @@ -19,11 +23,13 @@ @Component @RequiredArgsConstructor public class ChatWebSocketHandler implements WebSocketHandler { - + private final ChatService chatService; private final ChatMessageRepository repo; - private final ObjectMapper mapper = new ObjectMapper(); + private final ChatRoomManager roomManager; + private final ChatMessageMapper mapper; private final Map> sinks = new ConcurrentHashMap<>(); private final ReactiveMongoTemplate mongoTemplate; + private final ObjectMapper objectMapper = new ObjectMapper(); // public ChatWebSocketHandler(ChatMessageRepository repo, ReactiveMongoTemplate mongoTemplate) { // this.repo = repo; @@ -39,17 +45,17 @@ private Sinks.Many sinkFor(String roomId) { public Mono handle(WebSocketSession session) { String path = session.getHandshakeInfo().getUri().getPath(); String roomId = path.substring(path.lastIndexOf('/') + 1); - Sinks.Many sink = sinkFor(roomId); + + Sinks.Many sink = roomManager.sinkFor(roomId); Mono inbound = session.receive() .map(WebSocketMessage::getPayloadAsText) .flatMap(text -> { try { - ChatMessage msg = mapper.readValue(text, ChatMessage.class); - msg.setRoomId(roomId); - return repo.save(msg) - .doOnNext(saved -> sink.tryEmitNext(saved)); - } catch (Exception e) { + CreateChatMessageDto createDto = objectMapper.readValue(text, CreateChatMessageDto.class); + return chatService.saveMessage(roomId, createDto).then(); + } catch ( + Exception e) { return Mono.empty(); } }) @@ -57,40 +63,50 @@ public Mono handle(WebSocketSession session) { Flux outbound = sink.asFlux() .map(m -> { - try { return mapper.writeValueAsString(m); } catch (Exception e) { return "{}";} + try { + return objectMapper.writeValueAsString(mapper.toDto(m)); + } catch ( + Exception e) { + return "{}"; + } }) .map(session::textMessage); - // cuando cliente se conecta, opcional: enviar historial reciente - Flux history = repo.findByRoomIdOrderByCreatedAtAsc(roomId) - .map(m -> { - try { return mapper.writeValueAsString(m); } catch (Exception e) { return "{}"; } + Flux history = chatService.getHistory(roomId) + .map(dto -> { + try { + return objectMapper.writeValueAsString(dto); + } catch ( + Exception e) { + return "{}"; + } }) .map(session::textMessage); return session.send(Flux.concat(history, outbound)).and(inbound); } - - /** - * Change Stream listener: cuando hay inserts en collection "messages", - * emitimos a los sinks locales (para propagar mensajes entre instancias). - * Requiere replica set. - */ - private void startChangeStreamListener() { - // escucha sin filtro (puedes filtrar por ns/collection o por roomId) - mongoTemplate.changeStream(ChatMessage.class) - .listen() // devuelve Flux> - .map(event -> event.getBody()) // ChatMessage - .subscribe(msg -> { - if (msg != null && msg.getRoomId() != null) { - Sinks.Many s = sinks.get(msg.getRoomId()); - if (s != null) { - s.tryEmitNext(msg); - } - } - }, err -> { - // en prod haz reintentos/monitorización - err.printStackTrace(); - }); - } } + +/** + * Change Stream listener: cuando hay inserts en collection "messages", + * emitimos a los sinks locales (para propagar mensajes entre instancias). + * Requiere replica set. + */ +//private void startChangeStreamListener() { +// // escucha sin filtro (puedes filtrar por ns/collection o por roomId) +// mongoTemplate.changeStream(ChatMessage.class) +// .listen() // devuelve Flux> +// .map(event -> event.getBody()) // ChatMessage +// .subscribe(msg -> { +// if (msg != null && msg.getRoomId() != null) { +// Sinks.Many s = sinks.get(msg.getRoomId()); +// if (s != null) { +// s.tryEmitNext(msg); +// } +// } +// }, err -> { +// // en prod haz reintentos/monitorización +// err.printStackTrace(); +// }); +//} +//} diff --git a/src/main/java/com/msvcchat/config/websockets/WebSocketConfig.java b/src/main/java/com/msvcchat/config/websockets/WebSocketConfig.java index 27fe767..4a78cd7 100644 --- a/src/main/java/com/msvcchat/config/websockets/WebSocketConfig.java +++ b/src/main/java/com/msvcchat/config/websockets/WebSocketConfig.java @@ -13,7 +13,7 @@ public class WebSocketConfig { @Bean public SimpleUrlHandlerMapping webSocketMapping(ChatWebSocketHandler handler) { - Map map = Map.of("/ws/chat/{roomId}", handler); + Map map = Map.of("/ws/chat/*", handler); SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setUrlMap(map); mapping.setOrder(10); diff --git a/src/main/java/com/msvcchat/dtos/ChatMessageDto.java b/src/main/java/com/msvcchat/dtos/ChatMessageDto.java new file mode 100644 index 0000000..b373f5b --- /dev/null +++ b/src/main/java/com/msvcchat/dtos/ChatMessageDto.java @@ -0,0 +1,17 @@ +package com.msvcchat.dtos; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@NoArgsConstructor +public class ChatMessageDto { + private String id; + private String roomId; + private String fromId; + private String fromRole; + private String text; + private Instant createdAt; +} diff --git a/src/main/java/com/msvcchat/dtos/CreateChatMessageDto.java b/src/main/java/com/msvcchat/dtos/CreateChatMessageDto.java new file mode 100644 index 0000000..960a30c --- /dev/null +++ b/src/main/java/com/msvcchat/dtos/CreateChatMessageDto.java @@ -0,0 +1,13 @@ +package com.msvcchat.dtos; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class CreateChatMessageDto { + private String fromId; + private String fromRole; + private String text; + +} diff --git a/src/main/java/com/msvcchat/entity/ChatMessage.java b/src/main/java/com/msvcchat/entity/ChatMessage.java index 8b1a5a1..4b50699 100644 --- a/src/main/java/com/msvcchat/entity/ChatMessage.java +++ b/src/main/java/com/msvcchat/entity/ChatMessage.java @@ -1,8 +1,6 @@ package com.msvcchat.entity; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @@ -11,13 +9,13 @@ @Document(collection = "messages") @Data @NoArgsConstructor -//@Builder +@AllArgsConstructor public class ChatMessage { @Id private String id; - private String roomId; // room identificador (por ejemplo "user:{userId}:trainer:{trainerId}") - private String fromId; // id del emisor (user o trainer) - private String fromRole; // "USER" o "TRAINER" + private String roomId; + private String fromId; + private String fromRole; private String text; private Instant createdAt = Instant.now(); } diff --git a/src/main/java/com/msvcchat/entity/Room.java b/src/main/java/com/msvcchat/entity/Room.java index b9a2c79..9db18dc 100644 --- a/src/main/java/com/msvcchat/entity/Room.java +++ b/src/main/java/com/msvcchat/entity/Room.java @@ -14,7 +14,7 @@ @AllArgsConstructor public class Room { @Id - private String id; // roomId - private Set users; // ids de usuarios - private Set trainers;// ids de entrenadores + private String id; + private Set users; + private Set trainers; } \ No newline at end of file diff --git a/src/main/java/com/msvcchat/mappers/.gitkeep b/src/main/java/com/msvcchat/mappers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/msvcchat/mappers/ChatMessageMapper.java b/src/main/java/com/msvcchat/mappers/ChatMessageMapper.java new file mode 100644 index 0000000..c20cf06 --- /dev/null +++ b/src/main/java/com/msvcchat/mappers/ChatMessageMapper.java @@ -0,0 +1,27 @@ +package com.msvcchat.mappers; + +import com.msvcchat.config.ReactiveMapperConfig; +import com.msvcchat.dtos.ChatMessageDto; +import com.msvcchat.dtos.CreateChatMessageDto; +import com.msvcchat.entity.ChatMessage; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper(config = ReactiveMapperConfig.class) +public interface ChatMessageMapper { + ChatMessageDto toDto(ChatMessage entity); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "roomId", ignore = true) + @Mapping(target = "createdAt", ignore = true) + ChatMessage toEntity(CreateChatMessageDto dto); + + + @Mapping(target = "id", ignore = true) + @Mapping(target = "roomId", ignore = true) + @Mapping(target = "createdAt", ignore = true) + void updateEntityFromDto(CreateChatMessageDto dto, @MappingTarget ChatMessage entity); + + +} diff --git a/src/main/java/com/msvcchat/service/.gitkeep b/src/main/java/com/msvcchat/service/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/msvcchat/service/ChatRoomManager.java b/src/main/java/com/msvcchat/service/ChatRoomManager.java new file mode 100644 index 0000000..56c247d --- /dev/null +++ b/src/main/java/com/msvcchat/service/ChatRoomManager.java @@ -0,0 +1,54 @@ +package com.msvcchat.service; + +import com.msvcchat.entity.ChatMessage; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.ChangeStreamEvent; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Sinks; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +@RequiredArgsConstructor +public class ChatRoomManager { + + private final ReactiveMongoTemplate mongoTemplate; + private final Map> sinks = new ConcurrentHashMap<>(); + + public Sinks.Many sinkFor(String roomId) { + return sinks.computeIfAbsent(roomId, rid -> Sinks.many().multicast().onBackpressureBuffer()); + } + + + public void broadcast(ChatMessage msg) { + if (msg == null || msg.getRoomId() == null) + return; + Sinks.Many s = sinks.get(msg.getRoomId()); + if (s != null) { + s.tryEmitNext(msg); + } + } + + + @PostConstruct + void startChangeStreamListener() { + //Va a estar esuchando inserts o updates en la colencion de ChatMessage + mongoTemplate.changeStream(ChatMessage.class) + .listen() + .mapNotNull(ChangeStreamEvent::getBody) + .subscribe(msg -> { + if (msg != null && msg.getRoomId() != null) { + Sinks.Many s = sinks.get(msg.getRoomId()); + if (s != null) + s.tryEmitNext(msg); + } + }, err -> { + err.printStackTrace(); + }); + } + + +} diff --git a/src/main/java/com/msvcchat/service/ChatService.java b/src/main/java/com/msvcchat/service/ChatService.java new file mode 100644 index 0000000..5efb998 --- /dev/null +++ b/src/main/java/com/msvcchat/service/ChatService.java @@ -0,0 +1,13 @@ +package com.msvcchat.service; + +import com.msvcchat.dtos.ChatMessageDto; +import com.msvcchat.dtos.CreateChatMessageDto; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface ChatService { + Mono saveMessage(String roomId, CreateChatMessageDto dto); + + Flux getHistory(String roomId); + +} diff --git a/src/main/java/com/msvcchat/service/Impl/ChatServiceImpl.java b/src/main/java/com/msvcchat/service/Impl/ChatServiceImpl.java new file mode 100644 index 0000000..ced8c40 --- /dev/null +++ b/src/main/java/com/msvcchat/service/Impl/ChatServiceImpl.java @@ -0,0 +1,39 @@ +package com.msvcchat.service.Impl; + +import com.msvcchat.dtos.ChatMessageDto; +import com.msvcchat.dtos.CreateChatMessageDto; +import com.msvcchat.entity.ChatMessage; +import com.msvcchat.mappers.ChatMessageMapper; +import com.msvcchat.repositories.ChatMessageRepository; +import com.msvcchat.service.ChatRoomManager; +import com.msvcchat.service.ChatService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ChatServiceImpl implements ChatService { + + private final ChatMessageRepository chatMessageRepository; + private final ChatMessageMapper mapper; + private final ChatRoomManager chatRoomManager; + + @Override + public Mono saveMessage(String roomId, CreateChatMessageDto dto) { + ChatMessage entity = mapper.toEntity(dto); + entity.setRoomId(roomId); + return chatMessageRepository + .save(entity) + .doOnNext(chatRoomManager::broadcast) + .map(mapper::toDto); + } + + @Override + public Flux getHistory(String roomId) { + return chatMessageRepository.findByRoomIdOrderByCreatedAtAsc(roomId).map(mapper::toDto); + } +} diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..92469a3 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,18 @@ + ,----, + ____ ,--, ,/ .`| + ,' , `. .--.--. ,----.. ,----.. ,--.'| ,---, ,` .' : + ,-+-,.' _ | / / '. ,---. / / \ / / \ ,--, | : ' .' \ ; ; / + ,-+-. ; , ||| : /`. / /__./|| : : ,---,.| : :,---.'| : ' / ; '. .'___,/ ,' + ,--.'|' | ;|; | |--` ,---.; ; |. | ;. / ,' .' |. | ;. /| | : _' |: : \ | : | +| | ,', | ':| : ;_ /___/ \ | |. ; /--` ,---.' ,. ; /--` : : |.' |: | /\ \ ; |.'; ; +| | / | | || \ \ `.\ ; \ ' |; | ; | | |; | ; | ' ' ; :| : ' ;. :`----' | | +' | : | : |, `----. \\ \ \: || : | : : .' | : | ' | .'. || | ;/ \ \ ' : ; +; . | ; |--' __ \ \ | ; \ ' .. | '___ : |.' . | '___ | | : | '' : | \ \ ,' | | ' +| : | | , / /`--' / \ \ '' ; : .'|`---' ' ; : .'|' : | : ;| | ' '--' ' : | +| : ' |/ '--'. / \ ` ;' | '/ : ' | '/ :| | ' ,/ | : : ; |.' +; | |`-' `--'---' : \ || : / | : / ; : ;--' | | ,' '---' +| ;/ '---" \ \ .' \ \ .' | ,/ `--'' +'---' `---` `---` '---' + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} \ No newline at end of file From ddae0eef5c4617a6f7b1860bb5230db8cc4ff9c6 Mon Sep 17 00:00:00 2001 From: Raydberg Gabriel Date: Wed, 24 Sep 2025 18:31:10 -0500 Subject: [PATCH 2/4] feat: add CORS configuration and user retrieval endpoint with UserDto --- pom.xml | 5 +- .../java/com/msvcchat/config/CorsConfig.java | 25 ++++++++++ .../websockets/ChatWebSocketHandler.java | 47 +++++++++++++++++-- .../msvcchat/controller/ChatController.java | 22 +++++++++ src/main/java/com/msvcchat/dtos/UserDto.java | 9 ++++ 5 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/msvcchat/config/CorsConfig.java create mode 100644 src/main/java/com/msvcchat/controller/ChatController.java create mode 100644 src/main/java/com/msvcchat/dtos/UserDto.java diff --git a/pom.xml b/pom.xml index d07abec..2aacd74 100644 --- a/pom.xml +++ b/pom.xml @@ -108,7 +108,10 @@ spring-cloud-starter-circuitbreaker-reactor-resilience4j - + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + org.mapstruct mapstruct diff --git a/src/main/java/com/msvcchat/config/CorsConfig.java b/src/main/java/com/msvcchat/config/CorsConfig.java new file mode 100644 index 0000000..c0e3edb --- /dev/null +++ b/src/main/java/com/msvcchat/config/CorsConfig.java @@ -0,0 +1,25 @@ +package com.msvcchat.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + @Bean + public CorsWebFilter corsWebFilter() { + CorsConfiguration configuration = new CorsConfiguration(); + + // Especifica los orígenes permitidos + configuration.addAllowedOrigin("http://localhost:5173"); // Cambia esto según tu frontend + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return new CorsWebFilter(source); + } +} diff --git a/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java b/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java index 2b49625..7932bda 100644 --- a/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java +++ b/src/main/java/com/msvcchat/config/websockets/ChatWebSocketHandler.java @@ -1,13 +1,18 @@ package com.msvcchat.config.websockets; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.msvcchat.dtos.ChatMessageDto; import com.msvcchat.dtos.CreateChatMessageDto; import com.msvcchat.entity.ChatMessage; import com.msvcchat.mappers.ChatMessageMapper; import com.msvcchat.repositories.ChatMessageRepository; import com.msvcchat.service.ChatRoomManager; import com.msvcchat.service.ChatService; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.mongodb.core.ChangeStreamEvent; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.stereotype.Component; import org.springframework.web.reactive.socket.WebSocketHandler; @@ -22,6 +27,7 @@ @Component @RequiredArgsConstructor +@Slf4j public class ChatWebSocketHandler implements WebSocketHandler { private final ChatService chatService; private final ChatMessageRepository repo; @@ -29,7 +35,7 @@ public class ChatWebSocketHandler implements WebSocketHandler { private final ChatMessageMapper mapper; private final Map> sinks = new ConcurrentHashMap<>(); private final ReactiveMongoTemplate mongoTemplate; - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); // public ChatWebSocketHandler(ChatMessageRepository repo, ReactiveMongoTemplate mongoTemplate) { // this.repo = repo; @@ -53,7 +59,13 @@ public Mono handle(WebSocketSession session) { .flatMap(text -> { try { CreateChatMessageDto createDto = objectMapper.readValue(text, CreateChatMessageDto.class); - return chatService.saveMessage(roomId, createDto).then(); + log.info("**** ENVIANDO MENSAJE {} ****", text); + log.info("***** CHAT DTO ****"); + log.info(createDto.toString()); + Mono response = chatService.saveMessage(roomId, createDto).then(); + log.info("***** RESPONSE ****"); + log.info(response.toString()); + return response; } catch ( Exception e) { return Mono.empty(); @@ -62,11 +74,18 @@ public Mono handle(WebSocketSession session) { .then(); Flux outbound = sink.asFlux() + .distinct(ChatMessage::getId) .map(m -> { try { - return objectMapper.writeValueAsString(mapper.toDto(m)); + ChatMessageDto dto = mapper.toDto(m); + dto.setId(m.getId()); + log.info("****** CHAT MESSAGE DTO {} ", dto); + return objectMapper.writeValueAsString(dto); } catch ( Exception e) { + e.printStackTrace(); + log.error("*************ERRROR *********"); + log.error(e.getMessage()); return "{}"; } }) @@ -83,8 +102,28 @@ public Mono handle(WebSocketSession session) { }) .map(session::textMessage); - return session.send(Flux.concat(history, outbound)).and(inbound); + return session.send(history.concatWith(outbound)).and(inbound); + } + + + @PostConstruct + void startChangeStreamListener() { + mongoTemplate.changeStream(ChatMessage.class) + .listen() + .mapNotNull(ChangeStreamEvent::getBody) + .distinct(ChatMessage::getId) // Evita procesar mensajes duplicados + .subscribe(msg -> { + if (msg != null && msg.getRoomId() != null) { + Sinks.Many s = sinks.get(msg.getRoomId()); + if (s != null) { + s.tryEmitNext(msg); + } + } + }, err -> { + log.error("Error en ChangeStream: {}", err.getMessage(), err); + }); } + } /** diff --git a/src/main/java/com/msvcchat/controller/ChatController.java b/src/main/java/com/msvcchat/controller/ChatController.java new file mode 100644 index 0000000..7790a64 --- /dev/null +++ b/src/main/java/com/msvcchat/controller/ChatController.java @@ -0,0 +1,22 @@ +package com.msvcchat.controller; + +import com.msvcchat.dtos.UserDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/chat/users") +public class ChatController { + private final ReactiveMongoTemplate mongoTemplate; + + @GetMapping + public Flux getAllUsers() { + return mongoTemplate.findAll(UserDto.class, "users"); + } + +} diff --git a/src/main/java/com/msvcchat/dtos/UserDto.java b/src/main/java/com/msvcchat/dtos/UserDto.java new file mode 100644 index 0000000..4630d53 --- /dev/null +++ b/src/main/java/com/msvcchat/dtos/UserDto.java @@ -0,0 +1,9 @@ +package com.msvcchat.dtos; + +public record UserDto( + String id, + String name, + String role, + String avatar +) { +} From ae5e619911944aa0300ee1623078eaa57e3927c7 Mon Sep 17 00:00:00 2001 From: Raydberg Gabriel Date: Thu, 25 Sep 2025 09:28:56 -0500 Subject: [PATCH 3/4] feat: reorganizar copias de archivos en Dockerfile.dev para mejorar la estructura --- Dockerfile.dev | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 7b84d42..e9ed051 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,10 +1,9 @@ FROM openjdk:21-jdk-slim WORKDIR /app -COPY pom.xml . -COPY mvnw . COPY .mvn .mvn +COPY mvnw pom.xml ./ RUN chmod +x mvnw -RUN ./mvnw dependency:go-offline +RUN ./mvnw -T 4 dependency:go-offline COPY src ./src EXPOSE 9096 5005 CMD ["./mvnw", "spring-boot:run", "-Dspring-boot.run.jvmArguments=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"] \ No newline at end of file From 8ba35fb9312eaf14f6775a2607adfb91d4b575d3 Mon Sep 17 00:00:00 2001 From: Raydberg Gabriel Date: Sun, 28 Sep 2025 22:34:33 -0500 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20actualizar=20banner=20de=20la=20apl?= =?UTF-8?q?icaci=C3=B3n=20para=20mejorar=20la=20presentaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 9 --------- src/main/resources/banner.txt | 22 ++++++---------------- 2 files changed, 6 insertions(+), 25 deletions(-) delete mode 100644 src/main/resources/application.yml diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 6877265..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,9 +0,0 @@ -spring: - application: - name: msvc-chat -springdoc: - api-docs: - path: /v3/api-docs - swagger-ui: - path: / - url: /v3/api-docs \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index 92469a3..8680f88 100644 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -1,18 +1,8 @@ - ,----, - ____ ,--, ,/ .`| - ,' , `. .--.--. ,----.. ,----.. ,--.'| ,---, ,` .' : - ,-+-,.' _ | / / '. ,---. / / \ / / \ ,--, | : ' .' \ ; ; / - ,-+-. ; , ||| : /`. / /__./|| : : ,---,.| : :,---.'| : ' / ; '. .'___,/ ,' - ,--.'|' | ;|; | |--` ,---.; ; |. | ;. / ,' .' |. | ;. /| | : _' |: : \ | : | -| | ,', | ':| : ;_ /___/ \ | |. ; /--` ,---.' ,. ; /--` : : |.' |: | /\ \ ; |.'; ; -| | / | | || \ \ `.\ ; \ ' |; | ; | | |; | ; | ' ' ; :| : ' ;. :`----' | | -' | : | : |, `----. \\ \ \: || : | : : .' | : | ' | .'. || | ;/ \ \ ' : ; -; . | ; |--' __ \ \ | ; \ ' .. | '___ : |.' . | '___ | | : | '' : | \ \ ,' | | ' -| : | | , / /`--' / \ \ '' ; : .'|`---' ' ; : .'|' : | : ;| | ' '--' ' : | -| : ' |/ '--'. / \ ` ;' | '/ : ' | '/ :| | ' ,/ | : : ; |.' -; | |`-' `--'---' : \ || : / | : / ; : ;--' | | ,' '---' -| ;/ '---" \ \ .' \ \ .' | ,/ `--'' -'---' `---` `---` '---' - +______ ____________ __________ ______________ ________________ +___ |/ /_ ___/_ | / /_ ____/ __ ____/__ / / /__ |__ __/ +__ /|_/ /_____ \__ | / /_ / ________ / __ /_/ /__ /| |_ / +_ / / / ____/ /__ |/ / / /___/_____/ /___ _ __ / _ ___ | / +/_/ /_/ /____/ _____/ \____/ \____/ /_/ /_/ /_/ |_/_/ + ${application.title} ${application.version} Powered by Spring Boot ${spring-boot.version} \ No newline at end of file