Skip to content

Commit 9239c2e

Browse files
committed
Room v1
1 parent 4512010 commit 9239c2e

15 files changed

Lines changed: 1036 additions & 39 deletions

File tree

heroes/src/main/java/ru/mifi/practice/voln/Application.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5-
import org.springframework.context.annotation.Import;
6-
import ru.mifi.practice.voln.configuration.ApplicationConfiguration;
7-
import ru.mifi.practice.voln.configuration.SessionConfiguration;
8-
import ru.mifi.practice.voln.configuration.TelegramConfiguration;
95

106
@SuppressWarnings("PMD.UseUtilityClass")
11-
@SpringBootApplication
12-
@Import({ApplicationConfiguration.class, SessionConfiguration.class, TelegramConfiguration.class})
7+
@SpringBootApplication(scanBasePackages = "ru.mifi.practice.voln.configuration")
138
public class Application {
149
public static void main(String[] args) {
1510
SpringApplication.run(Application.class, args);

heroes/src/main/java/ru/mifi/practice/voln/configuration/AuthenticationConfiguration2.java renamed to heroes/src/main/java/ru/mifi/practice/voln/configuration/GlobalConfiguration.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,26 @@
55
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
66
import org.springframework.security.core.userdetails.User;
77
import org.springframework.security.crypto.password.PasswordEncoder;
8+
import org.springframework.security.provisioning.JdbcUserDetailsManager;
89

910
import javax.sql.DataSource;
1011

1112
@Configuration
12-
public class AuthenticationConfiguration2 {
13+
public class GlobalConfiguration {
1314
@Autowired
1415
public void configureGlobal(AuthenticationManagerBuilder auth, DataSource dataSource, PasswordEncoder encoder) {
15-
auth.jdbcAuthentication().dataSource(dataSource)
16+
var managerConfigurer = auth.jdbcAuthentication();
17+
managerConfigurer.dataSource(dataSource)
1618
.usersByUsernameQuery("SELECT username, password, enabled FROM users WHERE username=?")
1719
.authoritiesByUsernameQuery("SELECT username, authority FROM authorities WHERE username=?")
1820
.withUser(User.withUsername("admin").password(encoder.encode("admin")).roles("ADMIN"));
21+
JdbcUserDetailsManager detailsService = managerConfigurer.getUserDetailsService();
22+
detailsService.setCreateAuthoritySql("INSERT INTO authorities (id, user_id, authority) " +
23+
"VALUES (nextval('authority_id_seq'), (SELECT id FROM users WHERE username = ?),?) " +
24+
"ON CONFLICT (user_id, authority) DO NOTHING");
25+
detailsService.setDeleteUserAuthoritiesSql("DELETE FROM authorities WHERE user_id = " +
26+
"(SELECT id FROM users WHERE username = ?)");
27+
detailsService.setCreateUserSql("INSERT INTO users (username, password, enabled) VALUES (?,?,?) " +
28+
"ON CONFLICT (username) DO NOTHING");
1929
}
2030
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package ru.mifi.practice.voln.configuration;
2+
3+
import io.micrometer.core.instrument.MeterRegistry;
4+
import io.micrometer.core.instrument.internal.TimedScheduledExecutorService;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.scheduling.annotation.EnableScheduling;
8+
9+
import java.util.concurrent.Executors;
10+
import java.util.concurrent.ScheduledExecutorService;
11+
12+
@Configuration
13+
@EnableScheduling
14+
public class SchedulerConfiguration {
15+
16+
@Bean
17+
public ScheduledExecutorService schedulingService(MeterRegistry registry) {
18+
return new TimedScheduledExecutorService(
19+
registry,
20+
Executors.newScheduledThreadPool(10),
21+
"scheduling",
22+
null,
23+
null
24+
);
25+
}
26+
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package ru.mifi.practice.voln.controller;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.CrossOrigin;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.PathVariable;
11+
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
import org.springframework.web.bind.annotation.RequestHeader;
14+
import org.springframework.web.bind.annotation.RequestParam;
15+
import org.springframework.web.bind.annotation.RestController;
16+
import org.springframework.web.server.ResponseStatusException;
17+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
18+
import ru.mifi.practice.voln.domain.RoomUser;
19+
import ru.mifi.practice.voln.service.UserPresenceService;
20+
21+
import java.io.IOException;
22+
import java.time.LocalDateTime;
23+
import java.util.ArrayList;
24+
import java.util.Date;
25+
import java.util.HashMap;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.UUID;
29+
import java.util.stream.Collectors;
30+
31+
@RestController
32+
@CrossOrigin
33+
public class ChatController {
34+
35+
private static final int MAX_HISTORY = 100;
36+
private final UserPresenceService userPresenceService;
37+
private final List<RoomMessage> messageHistory = new ArrayList<>();
38+
39+
public ChatController(UserPresenceService userPresenceService) {
40+
this.userPresenceService = userPresenceService;
41+
}
42+
43+
@GetMapping("/api/chat/connect")
44+
public SseEmitter connect(@RequestParam String username,
45+
HttpServletRequest request) {
46+
47+
// Генерируем уникальный ID сессии
48+
UUID sessionId = UUID.randomUUID();
49+
50+
// Проверяем, не занято ли имя пользователя (опционально)
51+
if (userPresenceService.isUsernameTaken(username)) {
52+
throw new ResponseStatusException(
53+
HttpStatus.CONFLICT,
54+
"Имя пользователя уже занято"
55+
);
56+
}
57+
58+
SseEmitter emitter = new SseEmitter(60L * 1000 * 60); // 60 минут timeout
59+
60+
// Создаем пользователя
61+
RoomUser user = userPresenceService.addUser(username, sessionId, emitter);
62+
63+
// Отправляем информацию о пользователе
64+
Map<String, Object> userInfo = new HashMap<>();
65+
userInfo.put("sessionId", sessionId);
66+
userInfo.put("username", username);
67+
userInfo.put("connectedAt", user.connected());
68+
69+
try {
70+
// Отправляем данные о соединении
71+
emitter.send(SseEmitter.event()
72+
.name("connected")
73+
.data(userInfo));
74+
75+
// Отправляем историю сообщений
76+
for (RoomMessage message : messageHistory) {
77+
emitter.send(SseEmitter.event()
78+
.name("message")
79+
.data(message));
80+
}
81+
82+
// Отправляем текущий список пользователей
83+
sendUserListUpdate();
84+
85+
} catch (IOException e) {
86+
userPresenceService.removeUser(sessionId);
87+
emitter.completeWithError(e);
88+
}
89+
90+
// Обработчики событий
91+
emitter.onCompletion(() -> {
92+
handleUserDisconnect(sessionId, username, "disconnected");
93+
});
94+
95+
emitter.onTimeout(() -> {
96+
handleUserDisconnect(sessionId, username, "timeout");
97+
});
98+
99+
emitter.onError((e) -> {
100+
handleUserDisconnect(sessionId, username, "error");
101+
});
102+
103+
return emitter;
104+
}
105+
106+
@PostMapping("/api/chat/send")
107+
public ResponseEntity<?> sendMessage(@RequestBody RoomMessage message,
108+
@RequestHeader(value = "X-Session-Id", required = false) UUID sessionId) {
109+
110+
message = message.toBuilder().timestamp(LocalDateTime.now()).build();
111+
112+
// Обновляем активность пользователя
113+
if (sessionId != null) {
114+
userPresenceService.updateUserActivity(sessionId);
115+
}
116+
117+
// Сохраняем в историю
118+
messageHistory.add(message);
119+
if (messageHistory.size() > MAX_HISTORY) {
120+
messageHistory.remove(0);
121+
}
122+
123+
// Рассылаем всем подключенным клиентам
124+
broadcastMessage(message);
125+
126+
return ResponseEntity.ok().build();
127+
}
128+
129+
@GetMapping("/api/chat/users")
130+
public Map<String, Object> getActiveUsers() {
131+
Map<String, Object> response = new HashMap<>();
132+
133+
response.put("uniqueUsers", userPresenceService.getUniqueUsernames());
134+
response.put("totalConnections", userPresenceService.getTotalConnections());
135+
response.put("uniqueUsersCount", userPresenceService.getUniqueUsersCount());
136+
response.put("usersWithInfo", userPresenceService.getUsersWithInfo());
137+
138+
return response;
139+
}
140+
141+
@GetMapping("/api/chat/users/{username}")
142+
public ResponseEntity<?> getUserInfo(@PathVariable String username) {
143+
List<RoomUser> users = userPresenceService.getUsersByUsername(username);
144+
145+
if (users.isEmpty()) {
146+
return ResponseEntity.notFound().build();
147+
}
148+
149+
Map<String, Object> userInfo = new HashMap<>();
150+
userInfo.put("username", username);
151+
userInfo.put("activeConnections", users.size());
152+
userInfo.put("sessions", users.stream()
153+
.map(RoomUser::userId)
154+
.collect(Collectors.toList()));
155+
userInfo.put("connectedAt", users.stream()
156+
.map(RoomUser::connected)
157+
.min(LocalDateTime::compareTo)
158+
.orElse(null));
159+
userInfo.put("lastActivity", users.stream()
160+
.map(RoomUser::lastActivity)
161+
.max(LocalDateTime::compareTo)
162+
.orElse(null));
163+
164+
return ResponseEntity.ok(userInfo);
165+
}
166+
167+
@PostMapping("/api/chat/ping")
168+
public ResponseEntity<?> ping(@RequestHeader("X-Session-Id") UUID sessionId) {
169+
userPresenceService.updateUserActivity(sessionId);
170+
return ResponseEntity.ok().build();
171+
}
172+
173+
@PostMapping("/api/chat/disconnect")
174+
public ResponseEntity<?> disconnect(@RequestHeader("X-Session-Id") UUID sessionId) {
175+
RoomUser user = userPresenceService.getUserBySessionId(sessionId);
176+
if (user != null) {
177+
handleUserDisconnect(sessionId, user.username(), "manual");
178+
}
179+
return ResponseEntity.ok().build();
180+
}
181+
182+
@GetMapping("/api/chat/stats")
183+
public Map<String, Object> getChatStats() {
184+
Map<String, Object> stats = new HashMap<>();
185+
186+
stats.put("totalMessages", messageHistory.size());
187+
stats.put("uniqueUsers", userPresenceService.getUniqueUsersCount());
188+
stats.put("totalConnections", userPresenceService.getTotalConnections());
189+
stats.put("activeSince", messageHistory.isEmpty() ? null : messageHistory.get(0).getTimestamp());
190+
191+
return stats;
192+
}
193+
194+
private void handleUserDisconnect(UUID sessionId, String username, String reason) {
195+
userPresenceService.removeUser(sessionId);
196+
197+
// Отправляем уведомление об отключении
198+
RoomEvent disconnectEvent = new RoomEvent(
199+
"user_disconnected",
200+
String.format("%s отключился (%s)", username, reason),
201+
LocalDateTime.now(),
202+
Map.of("username", username, "reason", reason)
203+
);
204+
205+
broadcastEvent(disconnectEvent);
206+
207+
// Обновляем список пользователей
208+
sendUserListUpdate();
209+
}
210+
211+
private void broadcastMessage(RoomMessage message) {
212+
List<RoomUser> allUsers = userPresenceService.getAllUsers();
213+
List<RoomUser> deadEmitters = new ArrayList<>();
214+
215+
for (RoomUser user : allUsers) {
216+
try {
217+
user.emitter().send(SseEmitter.event()
218+
.name("message")
219+
.data(message));
220+
} catch (IOException e) {
221+
deadEmitters.add(user);
222+
}
223+
}
224+
225+
// Удаляем мертвые соединения
226+
for (RoomUser deadUser : deadEmitters) {
227+
userPresenceService.removeUser(deadUser.userId());
228+
}
229+
}
230+
231+
private void broadcastEvent(RoomEvent event) {
232+
List<RoomUser> allUsers = userPresenceService.getAllUsers();
233+
List<RoomUser> deadEmitters = new ArrayList<>();
234+
235+
for (RoomUser user : allUsers) {
236+
try {
237+
user.emitter().send(SseEmitter.event()
238+
.name(event.getUsername())
239+
.data(event));
240+
} catch (IOException e) {
241+
deadEmitters.add(user);
242+
}
243+
}
244+
245+
for (RoomUser deadUser : deadEmitters) {
246+
userPresenceService.removeUser(deadUser.userId());
247+
}
248+
}
249+
250+
private void sendUserListUpdate() {
251+
List<String> onlineUsers = userPresenceService.getUniqueUsernames();
252+
253+
Map<String, Object> userListEvent = new HashMap<>();
254+
userListEvent.put("type", "user_list_update");
255+
userListEvent.put("users", onlineUsers);
256+
userListEvent.put("timestamp", new Date());
257+
userListEvent.put("totalConnections", userPresenceService.getTotalConnections());
258+
userListEvent.put("uniqueUsers", userPresenceService.getUniqueUsersCount());
259+
260+
broadcastEvent(new RoomEvent("user_list", "Обновление списка пользователей",
261+
LocalDateTime.now(), userListEvent));
262+
}
263+
264+
@Builder(toBuilder = true)
265+
@Getter
266+
public static final class RoomMessage {
267+
private final String username;
268+
private final String text;
269+
private final LocalDateTime timestamp;
270+
}
271+
272+
@Builder(toBuilder = true)
273+
@Getter
274+
public static final class RoomEvent {
275+
private final String username;
276+
private final String message;
277+
private final LocalDateTime timestamp;
278+
private final Map<String, Object> data;
279+
}
280+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package ru.mifi.practice.voln.domain;
2+
3+
import lombok.Builder;
4+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
5+
6+
import java.time.LocalDateTime;
7+
import java.util.UUID;
8+
9+
@Builder(toBuilder = true)
10+
public record RoomUser(UUID userId, String username, SseEmitter emitter,
11+
LocalDateTime connected, LocalDateTime lastActivity) {
12+
}

0 commit comments

Comments
 (0)