Skip to content
Open
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
10 changes: 9 additions & 1 deletion src/main/java/com/open/spring/mvc/S3uploads/FileHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ public interface FileHandler {

/**
* Deletes all files associated with a user.
*
*
* @param uid User ID
* @return true if successful
*/
boolean deleteFiles(String uid);

/**
* Lists all file keys under a given prefix.
*
* @param prefix The S3 key prefix to list
* @return List of S3 object keys matching the prefix
*/
java.util.List<String> listFiles(String prefix);
}
25 changes: 25 additions & 0 deletions src/main/java/com/open/spring/mvc/S3uploads/S3FileHandler.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.open.spring.mvc.S3uploads;

import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -153,6 +154,30 @@ public boolean deleteFiles(String uid) {
}
}

@Override
public List<String> listFiles(String prefix) {
if (s3Client == null) {
log.warn("S3 list attempted but S3 client is not configured.");
return new ArrayList<>();
}

try {
ListObjectsV2Request listReq = ListObjectsV2Request.builder()
.bucket(bucketName)
.prefix(prefix)
.build();

ListObjectsV2Response listRes = s3Client.listObjectsV2(listReq);

return listRes.contents().stream()
.map(s3Object -> s3Object.key())
.collect(Collectors.toList());
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<>();
}
}

private String generateKey(String uid, String filename) {
return uid + "/" + filename;
}
Expand Down
116 changes: 116 additions & 0 deletions src/main/java/com/open/spring/mvc/chat/ChatApiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.open.spring.mvc.chat;

import java.time.Instant;
import java.util.List;
import java.util.Optional;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.open.spring.mvc.groups.Groups;
import com.open.spring.mvc.groups.GroupsJpaRepository;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@RestController
@RequestMapping("/api/chat")
@CrossOrigin
public class ChatApiController {

private final ChatService chatService;
private final GroupsJpaRepository groupsRepository;

public ChatApiController(ChatService chatService, GroupsJpaRepository groupsRepository) {
this.chatService = chatService;
this.groupsRepository = groupsRepository;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ChatMessageRequest {
private String content;
}

@GetMapping("/{groupId}")
@Transactional(readOnly = true)
public ResponseEntity<List<ChatMessage>> getChat(@PathVariable Long groupId) {
Optional<Groups> groupOpt = groupsRepository.findById(groupId);
if (groupOpt.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}

// String currentUsername = getCurrentUsername();
// if (currentUsername == null) {
// return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
// }

// if (!isMember(groupOpt.get(), currentUsername)) {
// return new ResponseEntity<>(HttpStatus.FORBIDDEN);
// }

return new ResponseEntity<>(chatService.getChatHistory(groupId), HttpStatus.OK);
}

@PostMapping("/{groupId}")
@Transactional
public ResponseEntity<List<ChatMessage>> postChat(
@PathVariable Long groupId,
@RequestBody ChatMessageRequest request) {
Optional<Groups> groupOpt = groupsRepository.findById(groupId);
if (groupOpt.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}

String currentUsername = getCurrentUsername();
if (currentUsername == null) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}

if (!isMember(groupOpt.get(), currentUsername)) {
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
}

if (request == null || request.getContent() == null || request.getContent().isBlank()) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}

ChatMessage message = new ChatMessage(
currentUsername,
request.getContent().trim(),
Instant.now().toEpochMilli()
);

List<ChatMessage> updated = chatService.addMessage(groupId, message);
return new ResponseEntity<>(updated, HttpStatus.OK);
}

private String getCurrentUsername() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
return null;
}
String name = auth.getName();
if (name == null || name.isBlank() || "anonymousUser".equals(name)) {
return null;
}
return name;
}

private boolean isMember(Groups group, String uid) {
return group.getGroupMembers().stream()
.anyMatch(member -> uid.equals(member.getUid()));
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/open/spring/mvc/chat/ChatMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.open.spring.mvc.chat;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
private String sender;
private String content;
private Long timestamp;
}
98 changes: 98 additions & 0 deletions src/main/java/com/open/spring/mvc/chat/ChatService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.open.spring.mvc.chat;

import java.io.BufferedReader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.open.spring.mvc.S3uploads.S3FileHandler;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class ChatService {
private static final String CHAT_FILENAME = "chat_history.jsonl";

private final S3FileHandler s3FileHandler;
private final ObjectMapper objectMapper;

public List<ChatMessage> getChatHistory(Long groupId) {
return readHistory(groupId.toString());
}

public List<ChatMessage> addMessage(Long groupId, ChatMessage message) {
List<ChatMessage> history = readHistory(groupId.toString());
history.add(message);
writeHistory(groupId.toString(), history);
return history;
}

private List<ChatMessage> readHistory(String groupId) {
String base64Data = s3FileHandler.decodeFile(groupId, CHAT_FILENAME);
if (base64Data == null || base64Data.isBlank()) {
return new ArrayList<>();
}

byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Data);
} catch (IllegalArgumentException e) {
log.warn("Invalid base64 data for group {} chat history.", groupId, e);
return new ArrayList<>();
}

String jsonl = new String(decoded, StandardCharsets.UTF_8);
if (jsonl.isBlank()) {
return new ArrayList<>();
}

List<ChatMessage> messages = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new StringReader(jsonl))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isBlank()) {
continue;
}
try {
ChatMessage message = objectMapper.readValue(line, ChatMessage.class);
messages.add(message);
} catch (Exception e) {
log.warn("Skipping invalid chat line for group {}: {}", groupId, line, e);
}
}
} catch (Exception e) {
log.warn("Failed reading chat history for group {}", groupId, e);
}

return messages;
}

private void writeHistory(String groupId, List<ChatMessage> history) {
String jsonl = history.stream()
.map(this::toJson)
.collect(Collectors.joining("\n"));

String base64Data = Base64.getEncoder()
.encodeToString(jsonl.getBytes(StandardCharsets.UTF_8));

s3FileHandler.uploadFile(base64Data, CHAT_FILENAME, groupId);
}

private String toJson(ChatMessage message) {
try {
return objectMapper.writeValueAsString(message);
} catch (Exception e) {
log.warn("Failed to serialize chat message: {}", message, e);
return "{}";
}
}
}
Loading