Skip to content

Commit 73d20b3

Browse files
authored
Merge pull request #43 from Chat-Your-Way/CHAT-63-refactored
Chat 63 refactored notification feature
2 parents a85d631 + 6e3512f commit 73d20b3

32 files changed

+877
-128
lines changed

src/main/java/com/chat/yourway/config/websocket/WebsocketProperties.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ public class WebsocketProperties {
1414
private String[] destPrefixes;
1515
private String appPrefix;
1616
private String endpoint;
17+
private String topicPrefix;
18+
private String notifyPrefix;
19+
private String errorPrefix;
1720

1821
}

src/main/java/com/chat/yourway/controller/ChatController.java

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,31 @@
55
import com.chat.yourway.dto.response.MessageResponseDto;
66
import com.chat.yourway.service.interfaces.ChatMessageService;
77
import jakarta.validation.Valid;
8-
import java.util.List;
8+
import java.security.Principal;
99
import lombok.RequiredArgsConstructor;
1010
import org.springframework.messaging.handler.annotation.DestinationVariable;
1111
import org.springframework.messaging.handler.annotation.MessageMapping;
1212
import org.springframework.messaging.handler.annotation.Payload;
1313
import org.springframework.stereotype.Controller;
1414

15-
import java.security.Principal;
16-
1715
@Controller
1816
@RequiredArgsConstructor
1917
public class ChatController {
2018

2119
private final ChatMessageService chatMessageService;
2220

23-
@MessageMapping("/topic/{topicId}")
24-
public MessageResponseDto sendToTopic(@DestinationVariable Integer topicId,
21+
@MessageMapping("/topic/public/{topicId}")
22+
public MessageResponseDto sendToPublicTopic(@DestinationVariable Integer topicId,
2523
@Valid @Payload MessagePublicRequestDto message, Principal principal) {
2624
String email = principal.getName();
27-
return chatMessageService.sendToTopic(topicId, message, email);
25+
return chatMessageService.sendToPublicTopic(topicId, message, email);
2826
}
2927

30-
@MessageMapping("/private")
31-
public MessageResponseDto sendToContact(@Valid @Payload MessagePrivateRequestDto message,
32-
Principal principal) {
28+
@MessageMapping("/topic/private/{topicId}")
29+
public MessageResponseDto sendToPrivateTopic(@DestinationVariable Integer topicId,
30+
@Valid @Payload MessagePrivateRequestDto message, Principal principal) {
3331
String email = principal.getName();
34-
return chatMessageService.sendToContact(message, email);
32+
return chatMessageService.sendToPrivateTopic(topicId, message, email);
3533
}
3634

37-
@MessageMapping("/get/messages/{topicId}")
38-
public List<MessageResponseDto> getMessages(@DestinationVariable Integer topicId) {
39-
return chatMessageService.getMessages(topicId);
40-
}
4135
}

src/main/java/com/chat/yourway/controller/TopicController.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
package com.chat.yourway.controller;
22

3-
import static com.chat.yourway.config.openapi.OpenApiMessages.*;
43
import static com.chat.yourway.config.openapi.OpenApiMessages.ALREADY_SUBSCRIBED;
54
import static com.chat.yourway.config.openapi.OpenApiMessages.CONTACT_UNAUTHORIZED;
65
import static com.chat.yourway.config.openapi.OpenApiMessages.CONTACT_WASNT_SUBSCRIBED;
76
import static com.chat.yourway.config.openapi.OpenApiMessages.INVALID_VALUE;
8-
import static com.chat.yourway.config.openapi.OpenApiMessages.SEARCH_TOPIC_VALIDATION;
97
import static com.chat.yourway.config.openapi.OpenApiMessages.OWNER_CANT_UNSUBSCRIBED;
108
import static com.chat.yourway.config.openapi.OpenApiMessages.RECIPIENT_EMAIL_NOT_EXIST;
9+
import static com.chat.yourway.config.openapi.OpenApiMessages.SEARCH_TOPIC_VALIDATION;
10+
import static com.chat.yourway.config.openapi.OpenApiMessages.SUCCESSFULLY_ADD_TOPIC_TO_FAVOURITE;
1111
import static com.chat.yourway.config.openapi.OpenApiMessages.SUCCESSFULLY_CREATED_TOPIC;
1212
import static com.chat.yourway.config.openapi.OpenApiMessages.SUCCESSFULLY_DELETE_TOPIC;
1313
import static com.chat.yourway.config.openapi.OpenApiMessages.SUCCESSFULLY_FOUND_TOPIC;
14+
import static com.chat.yourway.config.openapi.OpenApiMessages.SUCCESSFULLY_REMOVE_TOPIC_FROM_FAVOURITE;
1415
import static com.chat.yourway.config.openapi.OpenApiMessages.SUCCESSFULLY_SUBSCRIBED;
1516
import static com.chat.yourway.config.openapi.OpenApiMessages.SUCCESSFULLY_UNSUBSCRIBED;
1617
import static com.chat.yourway.config.openapi.OpenApiMessages.SUCCESSFULLY_UPDATED_TOPIC;
1718
import static com.chat.yourway.config.openapi.OpenApiMessages.TOPIC_NOT_ACCESS;
1819
import static com.chat.yourway.config.openapi.OpenApiMessages.TOPIC_NOT_FOUND;
20+
import static com.chat.yourway.config.openapi.OpenApiMessages.USER_DID_NOT_SUBSCRIBED_TO_TOPIC;
1921
import static com.chat.yourway.config.openapi.OpenApiMessages.VALUE_NOT_UNIQUE;
2022
import static java.nio.charset.StandardCharsets.UTF_8;
2123
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@@ -42,7 +44,17 @@
4244
import org.springframework.security.core.annotation.AuthenticationPrincipal;
4345
import org.springframework.security.core.userdetails.UserDetails;
4446
import org.springframework.validation.annotation.Validated;
45-
import org.springframework.web.bind.annotation.*;
47+
import org.springframework.web.bind.annotation.DeleteMapping;
48+
import org.springframework.web.bind.annotation.GetMapping;
49+
import org.springframework.web.bind.annotation.PatchMapping;
50+
import org.springframework.web.bind.annotation.PathVariable;
51+
import org.springframework.web.bind.annotation.PostMapping;
52+
import org.springframework.web.bind.annotation.PutMapping;
53+
import org.springframework.web.bind.annotation.RequestBody;
54+
import org.springframework.web.bind.annotation.RequestMapping;
55+
import org.springframework.web.bind.annotation.RequestParam;
56+
import org.springframework.web.bind.annotation.ResponseStatus;
57+
import org.springframework.web.bind.annotation.RestController;
4658

4759
@RestController
4860
@RequestMapping("/topics")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.chat.yourway.dto.response;
2+
3+
import com.chat.yourway.model.event.EventType;
4+
import java.time.LocalDateTime;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
import lombok.Setter;
9+
import lombok.ToString;
10+
11+
@AllArgsConstructor
12+
@NoArgsConstructor
13+
@Setter
14+
@Getter
15+
@ToString
16+
public class MessageNotificationResponseDto {
17+
18+
private String email;
19+
private Integer topicId;
20+
private EventType status;
21+
private Integer unreadMessages;
22+
private LocalDateTime lastRead;
23+
private String lastMessage;
24+
25+
}

src/main/java/com/chat/yourway/exception/handler/WebsocketExceptionHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ public class WebsocketExceptionHandler {
2020
TopicSubscriberNotFoundException.class,
2121
MessagePermissionDeniedException.class
2222
})
23-
@SendToUser("/specific")
23+
@SendToUser("/specific/error")
2424
public MessageErrorResponseDto<String> handleException(RuntimeException e) {
2525
return new MessageErrorResponseDto<>(e.getMessage());
2626
}
2727

2828
@MessageExceptionHandler(MethodArgumentNotValidException.class)
29-
@SendToUser("/specific")
29+
@SendToUser("/specific/error")
3030
public MessageErrorResponseDto<List<String>> handleValidationException(
3131
MethodArgumentNotValidException e) {
3232
List<FieldError> fieldErrors = Objects.requireNonNull(e.getBindingResult()).getFieldErrors();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.chat.yourway.listener;
2+
3+
import static com.chat.yourway.model.event.EventType.OFFLINE;
4+
import static com.chat.yourway.model.event.EventType.ONLINE;
5+
6+
import com.chat.yourway.service.interfaces.ChatNotificationService;
7+
import com.chat.yourway.service.interfaces.ContactEventService;
8+
import java.util.Objects;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.context.event.EventListener;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.web.socket.messaging.AbstractSubProtocolEvent;
14+
import org.springframework.web.socket.messaging.SessionConnectEvent;
15+
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
16+
17+
@Component
18+
@Slf4j
19+
@RequiredArgsConstructor
20+
public class StompConnectionListener {
21+
22+
private final ContactEventService contactEventService;
23+
private final ChatNotificationService chatNotificationService;
24+
25+
@EventListener
26+
public void handleWebSocketConnectListener(SessionConnectEvent event) {
27+
String email = getEmail(event);
28+
contactEventService.updateEventTypeByEmail(ONLINE, email);
29+
log.info("Contact [{}] is connected", getEmail(event));
30+
}
31+
32+
@EventListener
33+
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
34+
String email = getEmail(event);
35+
contactEventService.updateEventTypeByEmail(OFFLINE, email);
36+
chatNotificationService.notifyAllWhoSubscribedToSameUserTopic(email);
37+
log.info("Contact [{}] is disconnected", email);
38+
}
39+
40+
private String getEmail(AbstractSubProtocolEvent event) {
41+
return Objects.requireNonNull(event.getUser()).getName();
42+
}
43+
44+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package com.chat.yourway.listener;
2+
3+
import static com.chat.yourway.model.event.EventType.SUBSCRIBED;
4+
import static com.chat.yourway.model.event.EventType.UNSUBSCRIBED;
5+
6+
import com.chat.yourway.config.websocket.WebsocketProperties;
7+
import com.chat.yourway.model.event.ContactEvent;
8+
import com.chat.yourway.service.interfaces.ChatMessageService;
9+
import com.chat.yourway.service.interfaces.ChatNotificationService;
10+
import com.chat.yourway.service.interfaces.ContactEventService;
11+
import java.time.Instant;
12+
import java.time.LocalDateTime;
13+
import java.util.Objects;
14+
import java.util.TimeZone;
15+
import lombok.RequiredArgsConstructor;
16+
import lombok.extern.slf4j.Slf4j;
17+
import org.springframework.context.event.EventListener;
18+
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
19+
import org.springframework.stereotype.Component;
20+
import org.springframework.web.socket.messaging.AbstractSubProtocolEvent;
21+
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
22+
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
23+
24+
@Component
25+
@Slf4j
26+
@RequiredArgsConstructor
27+
public class StompSubscriptionListener {
28+
29+
private final WebsocketProperties properties;
30+
private final ContactEventService contactEventService;
31+
private final ChatMessageService chatMessageService;
32+
private final ChatNotificationService chatNotificationService;
33+
34+
private static String lastMessage;
35+
private static final String USER_DESTINATION = "/user";
36+
private static final String SLASH = "/";
37+
38+
@EventListener
39+
public void handleWebSocketSubscribeListener(SessionSubscribeEvent event) {
40+
String destination = getDestination(event);
41+
String email = getEmail(event);
42+
43+
try {
44+
if (isPrivateTopicDestination(destination)) {
45+
chatMessageService.sendMessageHistoryByTopicId(getTopicId(event), email);
46+
}
47+
48+
if (isTopicDestination(destination)) {
49+
lastMessage = contactEventService.getByTopicIdAndEmail(getTopicId(event), email)
50+
.getLastMessage();
51+
var contactEvent = new ContactEvent(email, getTopicId(event), SUBSCRIBED,
52+
getTimestamp(event), lastMessage);
53+
contactEventService.save(contactEvent);
54+
}
55+
56+
chatNotificationService.notifyTopicSubscribers(getTopicId(event));
57+
58+
} catch (NumberFormatException e) {
59+
log.warn("Contact [{}] subscribe to destination [{}] without topic id", email, destination);
60+
}
61+
62+
log.info("Contact [{}] subscribe to [{}]", email, destination);
63+
}
64+
65+
@EventListener
66+
public void handleWebSocketUnsubscribeListener(SessionUnsubscribeEvent event) {
67+
String destination = getDestination(event);
68+
String email = getEmail(event);
69+
70+
try {
71+
if (isTopicDestination(destination)) {
72+
var contactEvent = new ContactEvent(email, getTopicId(event), UNSUBSCRIBED,
73+
getTimestamp(event), lastMessage);
74+
contactEventService.save(contactEvent);
75+
}
76+
77+
chatNotificationService.notifyTopicSubscribers(getTopicId(event));
78+
79+
} catch (NumberFormatException e) {
80+
log.warn("Contact [{}] unsubscribe from destination [{}] without topic id", email,
81+
destination);
82+
}
83+
84+
log.info("Contact [{}] unsubscribe from [{}]", email, destination);
85+
}
86+
87+
private String getEmail(AbstractSubProtocolEvent event) {
88+
return Objects.requireNonNull(event.getUser()).getName();
89+
}
90+
91+
private String getDestination(AbstractSubProtocolEvent event) {
92+
return SimpMessageHeaderAccessor.wrap(event.getMessage())
93+
.getDestination();
94+
}
95+
96+
private LocalDateTime getTimestamp(AbstractSubProtocolEvent event) {
97+
return LocalDateTime.ofInstant(Instant.ofEpochMilli(event.getTimestamp()),
98+
TimeZone.getDefault().toZoneId());
99+
}
100+
101+
private Integer getTopicId(AbstractSubProtocolEvent event) throws NumberFormatException {
102+
String destination = getDestination(event);
103+
104+
if (isNotificationDestination(destination)) {
105+
return Integer.valueOf(destination.substring(getNotifyDestination().length()));
106+
} else if (isPrivateTopicDestination(destination)) {
107+
return Integer.valueOf(destination.substring(getPrivateTopicDestination().length()));
108+
}
109+
return Integer.valueOf(destination.substring(getTopicDestination().length()));
110+
}
111+
112+
private String getTopicDestination() {
113+
return properties.getTopicPrefix() + SLASH;
114+
}
115+
116+
private String getNotifyDestination() {
117+
return USER_DESTINATION + properties.getNotifyPrefix() + SLASH;
118+
}
119+
120+
private String getPrivateTopicDestination() {
121+
return USER_DESTINATION + getTopicDestination();
122+
}
123+
124+
private boolean isTopicDestination(String destination) {
125+
return destination.startsWith(getTopicDestination());
126+
}
127+
128+
private boolean isPrivateTopicDestination(String destination) {
129+
return destination.startsWith(getPrivateTopicDestination());
130+
}
131+
132+
private boolean isNotificationDestination(String destination) {
133+
return destination.startsWith(getNotifyDestination());
134+
}
135+
136+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.chat.yourway.mapper;
2+
3+
import com.chat.yourway.dto.response.MessageNotificationResponseDto;
4+
import com.chat.yourway.model.event.ContactEvent;
5+
import org.mapstruct.Mapper;
6+
import org.mapstruct.Mapping;
7+
8+
@Mapper(componentModel = "spring")
9+
public interface MessageNotificationMapper {
10+
11+
@Mapping(target = "unreadMessages", ignore = true, defaultValue = "0")
12+
@Mapping(target = "status", source = "eventType")
13+
@Mapping(target = "lastRead", source = "timestamp")
14+
@Mapping(target = "email", source = "email")
15+
MessageNotificationResponseDto toNotificationResponseDto(ContactEvent event);
16+
17+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.chat.yourway.model.event;
2+
3+
import java.time.LocalDateTime;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import lombok.Setter;
7+
import lombok.ToString;
8+
import org.springframework.data.annotation.Id;
9+
import org.springframework.data.redis.core.RedisHash;
10+
import org.springframework.data.redis.core.index.Indexed;
11+
12+
@NoArgsConstructor
13+
@Getter
14+
@Setter
15+
@ToString
16+
@RedisHash("ContactEvent")
17+
public class ContactEvent {
18+
19+
@Id
20+
@Indexed
21+
private String id;
22+
@Indexed
23+
private String email;
24+
@Indexed
25+
private Integer topicId;
26+
private EventType eventType;
27+
private LocalDateTime timestamp;
28+
private String lastMessage;
29+
30+
private static final int MAX_LENGTH = 20;
31+
32+
public ContactEvent(String email, Integer topicId, EventType eventType, LocalDateTime timestamp, String lastMessage) {
33+
this.id = email + "_" + topicId;
34+
this.email = email;
35+
this.topicId = topicId;
36+
this.eventType = eventType;
37+
this.timestamp = timestamp;
38+
this.lastMessage = lastMessage;
39+
}
40+
41+
public void setLastMessage(String lastMessage) {
42+
if (lastMessage.length() <= MAX_LENGTH) {
43+
this.lastMessage = lastMessage;
44+
} else {
45+
this.lastMessage = lastMessage.substring(0, MAX_LENGTH) + "...";
46+
}
47+
48+
}
49+
}

0 commit comments

Comments
 (0)