add: experimental real time websocket-based pastes
This commit is contained in:
@@ -31,6 +31,14 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
@@ -60,6 +68,14 @@
|
||||
<version>1.1.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.miarma.backend.mpaste.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
RedisSerializer<Object> jsonSerializer = RedisSerializer.json();
|
||||
|
||||
template.setKeySerializer(RedisSerializer.string());
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashKeySerializer(RedisSerializer.string());
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package net.miarma.backend.mpaste.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@EnableScheduling
|
||||
@Configuration
|
||||
public class SchedulingConfig {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.miarma.backend.mpaste.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.enableSimpleBroker("/topic");
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.withSockJS();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.miarma.backend.mpaste.controller;
|
||||
|
||||
import net.miarma.backend.mpaste.dto.PasteDto;
|
||||
import net.miarma.backend.mpaste.dto.PastePassword;
|
||||
import net.miarma.backend.mpaste.mapper.PasteMapper;
|
||||
import net.miarma.backend.mpaste.service.PasteService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -35,7 +34,7 @@ public class PasteController {
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/{paste_key}")
|
||||
@GetMapping("/s/{paste_key}")
|
||||
public ResponseEntity<PasteDto.Response> getByKey(
|
||||
@PathVariable("paste_key") String pasteKey,
|
||||
@RequestHeader(value = "X-Paste-Password", required = false) String password
|
||||
@@ -63,10 +62,6 @@ public class PasteController {
|
||||
|
||||
@DeleteMapping("/{paste_id}")
|
||||
public ResponseEntity<PasteDto.Response> delete(@PathVariable("paste_id") UUID pasteId) {
|
||||
return ResponseEntity.ok(
|
||||
PasteMapper.toResponse(
|
||||
pasteService.delete(pasteId)
|
||||
)
|
||||
);
|
||||
throw new UnsupportedOperationException("Pastes cannot be deleted manually");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.miarma.backend.mpaste.controller;
|
||||
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.messaging.handler.annotation.DestinationVariable;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.SendTo;
|
||||
import org.springframework.messaging.simp.annotation.SendToUser;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Controller
|
||||
public class RTPasteController {
|
||||
private final RedisTemplate<String, Object> template;
|
||||
private static final String REDIS_PREFIX = "rt_paste:";
|
||||
|
||||
public RTPasteController(RedisTemplate<String, Object> template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
@MessageMapping("/edit/{key}")
|
||||
@SendTo("/topic/session/{key}")
|
||||
public Map<String, Object> handleUpdate(@DestinationVariable("key") String key, Map<String, Object> payload) {
|
||||
template.opsForValue().set(REDIS_PREFIX + key, payload, 24, TimeUnit.HOURS);
|
||||
return payload;
|
||||
}
|
||||
|
||||
@MessageMapping("/join/{key}")
|
||||
@SendTo("/topic/session/{key}")
|
||||
public Map<String, Object> handleJoin(@DestinationVariable("key") String key) {
|
||||
Object data = template.opsForValue().get(REDIS_PREFIX + key);
|
||||
if (data instanceof Map) {
|
||||
return (Map<String, Object>) data;
|
||||
}
|
||||
return Map.of("content", "", "syntax", "plaintext", "title", "");
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,24 @@ public class PasteDto {
|
||||
|
||||
public static class Request {
|
||||
|
||||
private String pasteKey;
|
||||
private String title;
|
||||
private String content;
|
||||
private String syntax;
|
||||
|
||||
private Boolean burnAfter;
|
||||
private Boolean isPrivate;
|
||||
private Boolean isRt;
|
||||
private String password;
|
||||
|
||||
public String getPasteKey() {
|
||||
return pasteKey;
|
||||
}
|
||||
|
||||
public void setPasteKey(String pasteKey) {
|
||||
this.pasteKey = pasteKey;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
@@ -41,7 +51,7 @@ public class PasteDto {
|
||||
this.syntax = syntax;
|
||||
}
|
||||
|
||||
public Boolean getBurnAfter() {
|
||||
public Boolean isBurnAfter() {
|
||||
return burnAfter;
|
||||
}
|
||||
|
||||
@@ -49,7 +59,7 @@ public class PasteDto {
|
||||
this.burnAfter = burnAfter;
|
||||
}
|
||||
|
||||
public Boolean getIsPrivate() {
|
||||
public Boolean isPrivate() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
@@ -57,6 +67,14 @@ public class PasteDto {
|
||||
this.isPrivate = isPrivate;
|
||||
}
|
||||
|
||||
public Boolean isRt() {
|
||||
return isRt;
|
||||
}
|
||||
|
||||
public void setRt(Boolean rt) {
|
||||
isRt = rt;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
@@ -79,6 +97,7 @@ public class PasteDto {
|
||||
|
||||
private Boolean burnAfter;
|
||||
private Boolean isPrivate;
|
||||
private Boolean isRt;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
@@ -130,7 +149,7 @@ public class PasteDto {
|
||||
this.views = views;
|
||||
}
|
||||
|
||||
public Boolean getBurnAfter() {
|
||||
public Boolean isBurnAfter() {
|
||||
return burnAfter;
|
||||
}
|
||||
|
||||
@@ -138,14 +157,22 @@ public class PasteDto {
|
||||
this.burnAfter = burnAfter;
|
||||
}
|
||||
|
||||
public Boolean getIsPrivate() {
|
||||
public Boolean isPrivate() {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setIsPrivate(Boolean isPrivate) {
|
||||
public void setPrivate(Boolean isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
}
|
||||
|
||||
public Boolean isRt() {
|
||||
return isRt;
|
||||
}
|
||||
|
||||
public void setRt(Boolean rt) {
|
||||
isRt = rt;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package net.miarma.backend.mpaste.dto;
|
||||
|
||||
public record PastePassword(String password) {}
|
||||
@@ -16,9 +16,11 @@ public final class PasteMapper {
|
||||
paste.setTitle(request.getTitle());
|
||||
paste.setContent(request.getContent());
|
||||
paste.setSyntax(request.getSyntax());
|
||||
paste.setPasteKey(request.getPasteKey());
|
||||
|
||||
paste.setBurnAfter(Boolean.TRUE.equals(request.getBurnAfter()));
|
||||
paste.setPrivate(Boolean.TRUE.equals(request.getIsPrivate()));
|
||||
paste.setBurnAfter(Boolean.TRUE.equals(request.isBurnAfter()));
|
||||
paste.setPrivate(Boolean.TRUE.equals(request.isPrivate()));
|
||||
paste.setRt(Boolean.TRUE.equals(request.isRt()));
|
||||
|
||||
paste.setPassword(request.getPassword());
|
||||
|
||||
@@ -42,7 +44,8 @@ public final class PasteMapper {
|
||||
response.setViews(paste.getViews());
|
||||
|
||||
response.setBurnAfter(paste.isBurnAfter());
|
||||
response.setIsPrivate(paste.isPrivate());
|
||||
response.setPrivate(paste.isPrivate());
|
||||
response.setRt(paste.isRt());
|
||||
|
||||
response.setCreatedAt(paste.getCreatedAt());
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ public class Paste {
|
||||
@Column(name = "is_private")
|
||||
private Boolean isPrivate;
|
||||
|
||||
@Column(name = "is_rt")
|
||||
private Boolean isRt;
|
||||
|
||||
private String password;
|
||||
|
||||
@Column(name = "created_at")
|
||||
@@ -136,10 +139,14 @@ public class Paste {
|
||||
return isPrivate;
|
||||
}
|
||||
|
||||
public void setPrivate(Boolean aPrivate) {
|
||||
isPrivate = aPrivate;
|
||||
public void setPrivate(Boolean isPrivate) {
|
||||
this.isPrivate = isPrivate;
|
||||
}
|
||||
|
||||
public Boolean isRt() { return isRt; }
|
||||
|
||||
public void setRt(Boolean rt) { isRt = rt; }
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public class PasteService {
|
||||
public List<Paste> getAll() {
|
||||
return pasteRepository.findAll().stream()
|
||||
.filter(p -> !Boolean.TRUE.equals(p.isPrivate()))
|
||||
.filter(p -> !Boolean.TRUE.equals(p.isRt()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -64,14 +65,26 @@ public class PasteService {
|
||||
public Paste create(Paste paste) {
|
||||
PasteValidator.validate(paste);
|
||||
|
||||
return pasteRepository.findByPasteKey(paste.getPasteKey())
|
||||
.map(existing -> {
|
||||
existing.setContent(paste.getContent());
|
||||
existing.setSyntax(paste.getSyntax());
|
||||
existing.setTitle(paste.getTitle());
|
||||
return pasteRepository.save(existing);
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
if (Boolean.TRUE.equals(paste.isPrivate()) && paste.getPassword() != null) {
|
||||
String encodedPassword = passwordEncoder.encode(paste.getPassword());
|
||||
paste.setPassword(encodedPassword);
|
||||
paste.setPassword(passwordEncoder.encode(paste.getPassword()));
|
||||
}
|
||||
|
||||
paste.setPasteId(UUID.randomUUID());
|
||||
|
||||
if (paste.getPasteKey() == null || paste.getPasteKey().isEmpty()) {
|
||||
paste.setPasteKey(PasteKeyGenerator.generate(6));
|
||||
}
|
||||
|
||||
return pasteRepository.save(paste);
|
||||
});
|
||||
}
|
||||
|
||||
public Paste update(UUID pasteId, Paste changes) {
|
||||
|
||||
@@ -5,6 +5,14 @@ import net.miarma.backlib.exception.ValidationException;
|
||||
|
||||
public class PasteValidator {
|
||||
public static void validate(Paste paste) {
|
||||
if (Boolean.TRUE.equals(paste.isRt())) {
|
||||
if (paste.getTitle() == null || paste.getTitle().trim().isEmpty()) {
|
||||
String uuidStr = paste.getPasteId().toString();
|
||||
String shortId = uuidStr.substring(0, 8);
|
||||
paste.setTitle("Sesión: " + shortId);
|
||||
}
|
||||
}
|
||||
|
||||
if (paste.getTitle() == null || paste.getTitle().trim().isEmpty()) {
|
||||
throw new ValidationException("title", "The title cannot be empty");
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ spring:
|
||||
default-property-inclusion: non_null
|
||||
time-zone: Europe/Madrid
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
|
||||
jwt:
|
||||
expiration-ms: 3600000
|
||||
|
||||
|
||||
Reference in New Issue
Block a user