Skip to content

Commit

Permalink
rework one-time paste workflow
Browse files Browse the repository at this point in the history
* limit tracking to public pastes
* address TS errors
  • Loading branch information
querwurzel committed Apr 29, 2024
1 parent c2df20c commit 9320f61
Show file tree
Hide file tree
Showing 25 changed files with 549 additions and 373 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
import com.github.binpastes.paste.api.model.DetailView;
import com.github.binpastes.paste.api.model.ListView;
import com.github.binpastes.paste.api.model.SearchView;
import com.github.binpastes.paste.api.model.SearchView.SearchItemView;
import com.github.binpastes.paste.application.PasteService;
import com.github.binpastes.paste.domain.Paste;
import com.github.binpastes.paste.application.PasteViewService;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
Expand All @@ -28,9 +26,6 @@
import java.nio.charset.Charset;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

import static com.github.binpastes.paste.api.model.ListView.ListItemView;

@Validated
@RestController
Expand All @@ -39,11 +34,11 @@ class PasteController {

private static final Logger log = LoggerFactory.getLogger(PasteController.class);

private final PasteService pasteService;
private final PasteViewService pasteViewService;

@Autowired
public PasteController(final PasteService pasteService) {
this.pasteService = pasteService;
public PasteController(final PasteViewService pasteViewService) {
this.pasteViewService = pasteViewService;
}

@GetMapping("/{pasteId:[a-zA-Z0-9]{40}}")
Expand All @@ -53,34 +48,41 @@ public Mono<DetailView> findPaste(
ServerHttpRequest request,
ServerHttpResponse response
) {
return pasteService
.find(pasteId)
return pasteViewService
.viewPaste(pasteId, remoteAddress(request))
.doOnNext(paste -> {
if (paste.isOneTime()) {
response.getHeaders().setCacheControl(CacheControl.noStore());
return;
}

var now = LocalDateTime.now();
if (paste.isPermanent() || paste.getDateOfExpiry().plusMinutes(5).isAfter(now)) {
if (paste.isPermanent() || paste.dateOfExpiry().plusMinutes(1).isAfter(now)) {
response.getHeaders().setCacheControl(
CacheControl.maxAge(5, TimeUnit.MINUTES));
CacheControl.maxAge(Duration.ofMinutes(1)));
} else {
response.getHeaders().setCacheControl(
CacheControl.maxAge(Duration.between(now, paste.getDateOfExpiry())));
CacheControl.maxAge(Duration.between(now, paste.dateOfExpiry())).mustRevalidate());
}
})
.map(reference -> DetailView.of(reference, remoteAddress(request)))
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)));
}

@PostMapping("/{pasteId:[a-zA-Z0-9]{40}}")
public Mono<DetailView> findAndBurnOneTimePaste(
@PathVariable("pasteId")
String pasteId,
ServerHttpResponse response
) {
response.getHeaders().setCacheControl(CacheControl.noStore());
return pasteViewService
.viewOneTimePaste(pasteId)
.onErrorMap(ignored -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}

@GetMapping
public Mono<ListView> findPastes() {
return pasteService
.findAll()
.map(ListItemView::of)
.collectList()
.map(ListView::of);
return pasteViewService.viewAllPastes();
}

@GetMapping("/search")
Expand All @@ -92,33 +94,20 @@ public Mono<SearchView> searchPastes(
final ServerHttpResponse response
) {
var decodedTerm = URLDecoder.decode(term, Charset.defaultCharset());
response.getHeaders().setCacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES));
return pasteService
.findByFullText(decodedTerm)
.map(paste -> SearchItemView.of(paste, decodedTerm))
.collectList()
.map(SearchView::of);
response.getHeaders().setCacheControl(CacheControl.maxAge(Duration.ofMinutes(1)));
return pasteViewService.searchByFullText(decodedTerm);
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<DetailView> createPaste(@Valid @RequestBody Mono<CreateCmd> createCmd, ServerHttpRequest request) {
return createCmd
.flatMap(cmd -> pasteService.create(
cmd.title(),
cmd.content(),
cmd.dateOfExpiry(),
cmd.isEncrypted(),
cmd.pasteExposure(),
remoteAddress(request)
))
.map((Paste reference) -> DetailView.of(reference, remoteAddress(request)));
public Mono<DetailView> createPaste(@Valid @RequestBody CreateCmd createCmd, ServerHttpRequest request) {
return pasteViewService.createPaste(createCmd, remoteAddress(request));
}

@DeleteMapping("/{pasteId:[a-zA-Z0-9]{40}}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ResponseStatus(HttpStatus.ACCEPTED)
public void deletePaste(@PathVariable("pasteId") String pasteId, ServerHttpRequest request) {
pasteService.delete(pasteId, remoteAddress(request));
pasteViewService.requestDeletion(pasteId, remoteAddress(request));
}

@ExceptionHandler({ConstraintViolationException.class, WebExchangeBindException.class})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.github.binpastes.paste.application;

import com.github.binpastes.paste.api.model.CreateCmd;
import com.github.binpastes.paste.api.model.DetailView;
import com.github.binpastes.paste.api.model.ListView;
import com.github.binpastes.paste.api.model.ListView.ListItemView;
import com.github.binpastes.paste.api.model.SearchView;
import com.github.binpastes.paste.api.model.SearchView.SearchItemView;
import com.github.binpastes.paste.application.tracking.TrackingService;
import com.github.binpastes.paste.domain.PasteService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;

@Service
public class PasteViewService {

private static final Logger log = LoggerFactory.getLogger(PasteViewService.class);

private final PasteService pasteService;
private final TrackingService trackingService;

@Autowired
public PasteViewService(PasteService pasteService, TrackingService trackingService) {
this.pasteService = pasteService;
this.trackingService = trackingService;
}

public Mono<DetailView> viewPaste(String id, String remoteAddress) {
return pasteService.find(id)
.doOnNext(paste -> {
if (paste.isPublic()) {
trackingService.trackView(paste.getId());
}
})
.map(paste -> {
if (paste.isOneTime()) {
return new DetailView(
paste.getId(),
null,
null,
0,
paste.isPublic(),
paste.isErasable(remoteAddress),
paste.isEncrypted(),
paste.isOneTime(),
paste.isPermanent(),
paste.getDateCreated(),
paste.getDateOfExpiry(),
paste.getLastViewed(),
paste.getViews()
);
} else {
return DetailView.of(paste, remoteAddress);
}
});
}

public Mono<DetailView> viewOneTimePaste(String id) {
var now = LocalDateTime.now();
return pasteService.findAndBurn(id)
.map(paste -> new DetailView(
paste.getId(),
paste.getTitle(),
paste.getContent(),
paste.getContent().getBytes().length,
paste.isPublic(),
false, // paste just burnt
paste.isEncrypted(),
paste.isOneTime(),
paste.isPermanent(),
paste.getDateCreated(),
paste.getDateOfExpiry(),
paste.getLastViewed(),
paste.getViews()
));
}

public Mono<ListView> viewAllPastes() {
return pasteService.findAll()
.map(ListItemView::of)
.collectList()
.map(ListView::of);
}

public Mono<SearchView> searchByFullText(String term) {
return pasteService.findByFullText(term)
.map(paste -> SearchItemView.of(paste, term))
.collectList()
.map(SearchView::of)
.doOnSuccess(searchView -> log.info("Found {} pastes searching for: {}", searchView.pastes().size(), term));
}

public Mono<DetailView> createPaste(CreateCmd cmd, String remoteAddress) {
return pasteService.create(
cmd.title(),
cmd.content(),
cmd.dateOfExpiry(),
cmd.isEncrypted(),
cmd.pasteExposure(),
remoteAddress
).map(paste -> {
if (paste.isOneTime()) {
return new DetailView(
paste.getId(),
null,
null,
0,
paste.isPublic(),
paste.isErasable(remoteAddress),
paste.isEncrypted(),
paste.isOneTime(),
paste.isPermanent(),
paste.getDateCreated(),
paste.getDateOfExpiry(),
paste.getLastViewed(),
paste.getViews()
);
} else {
return DetailView.of(paste, remoteAddress);
}
});
}

public void requestDeletion(String id, String remoteAddress) {
pasteService.requestDeletion(id, remoteAddress);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.github.binpastes.paste.application.tracking;

import com.github.binpastes.paste.domain.PasteRepository;
import com.github.binpastes.paste.application.tracking.MessagingClient.Message;
import com.github.binpastes.paste.domain.PasteService;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import reactor.util.retry.Retry;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

Expand All @@ -18,23 +16,23 @@ public class TrackingService {

private static final Logger log = LoggerFactory.getLogger(TrackingService.class);

private final PasteRepository pasteRepository;
private final PasteService pasteService;
private final MessagingClient messagingClient;

@Autowired
public TrackingService(
final PasteRepository pasteRepository,
final PasteService pasteService,
final MessagingClient messagingClient
) {
this.pasteRepository = pasteRepository;
this.pasteService = pasteService;
this.messagingClient = messagingClient;
}

@PostConstruct
private void run() {
this.messagingClient
.receiveMessage()
.doOnNext(message -> receiveView(message.pasteId(), message.timeViewed()))
.doOnNext(this::receiveView)
.repeat()
.subscribe();
}
Expand All @@ -44,14 +42,13 @@ public void trackView(String pasteId) {
messagingClient.sendMessage(pasteId, LocalDateTime.now().toInstant(ZoneOffset.UTC));
}

public void receiveView(String pasteId, Instant timeViewed) {
var timestamp = LocalDateTime.ofInstant(timeViewed, ZoneOffset.UTC);
pasteRepository
.findById(pasteId)
.flatMap(paste -> pasteRepository.save(paste.trackView(timestamp)))
.retryWhen(Retry.indefinitely()
.filter(ex -> ex instanceof OptimisticLockingFailureException))
.doOnNext(paste -> log.debug("Tracked view on paste {}", paste.getId()))
.subscribe();
public void receiveView(String pasteId, LocalDateTime timeViewed) {
pasteService.trackView(pasteId, timeViewed);
log.debug("Tracked view on paste {} at {}", pasteId, timeViewed);
}

private void receiveView(Message message) {
var timestamp = LocalDateTime.ofInstant(message.timeViewed(), ZoneOffset.UTC);
this.receiveView(message.pasteId(), timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class Paste {
private String id;
@Version
@Column(PasteSchema.VERSION)
@SuppressWarnings("FieldCanBeLocal")
private Long version;
@Column(PasteSchema.TITLE)
private String title;
Expand Down Expand Up @@ -86,11 +87,11 @@ public LocalDateTime getDateOfExpiry() {
return dateOfExpiry;
}

protected PasteExposure getExposure() {
private PasteExposure getExposure() {
return exposure;
}

protected String getRemoteAddress() {
private String getRemoteAddress() {
return remoteAddress;
}

Expand Down Expand Up @@ -144,13 +145,7 @@ public Paste trackView(LocalDateTime lastViewed) {
setLastViewed(lastViewed);
}

setViews(getViews() + 1);

if (isOneTime()) {
markAsExpired();
}

return this;
return setViews(getViews() + 1);
}

public Paste markAsExpired() {
Expand Down
Loading

0 comments on commit 9320f61

Please sign in to comment.