großes refactoring
This commit is contained in:
@@ -136,7 +136,7 @@ public class BdsmGameService {
|
||||
newLock.setTasks(template.getTasks());
|
||||
newLock.setRequiresVerification(template.isRequiresVerification());
|
||||
newLock.setTestLock(false);
|
||||
newLock.setTaskCardMode(template.getTaskCardMode());
|
||||
newLock.setTaskMode(template.getTaskMode());
|
||||
|
||||
int codeLines = template.getUnlockCodeLength() != null ? template.getUnlockCodeLength() : 5;
|
||||
newLock.setUnlockCodeLength(codeLines);
|
||||
|
||||
@@ -37,25 +37,24 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationEntity;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteEntity;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationEntity;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
||||
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationEntity;
|
||||
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
|
||||
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryRepository;
|
||||
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationEntity;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteEntity;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.vote.CommunityTaskVoteEntity;
|
||||
import de.oaa.xxx.games.chastity.vote.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@@ -66,8 +65,8 @@ public class CardLockController {
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final KeyholderInvitationRepository invitationRepository;
|
||||
private final VerificationRepository verificationRepository;
|
||||
private final VerificationVoteRepository verificationVoteRepository;
|
||||
private final CommunityVerificationRepository verificationRepository;
|
||||
private final CommunityVerificationVoteRepository verificationVoteRepository;
|
||||
private final KeyholderNotificationRepository keyholderNotificationRepository;
|
||||
private final LockeeInvitationRepository lockeeInvitationRepository;
|
||||
private final AssignedTaskRepository assignedTaskRepository;
|
||||
@@ -81,16 +80,20 @@ public class CardLockController {
|
||||
@Value("${app.base-url:http://localhost:8080}")
|
||||
private String baseUrl;
|
||||
|
||||
public CardLockController(CardlockRepository cardlockRepository, UserRepository userRepository,
|
||||
KeyholderInvitationRepository invitationRepository, VerificationRepository verificationRepository,
|
||||
VerificationVoteRepository verificationVoteRepository,
|
||||
public CardLockController(CardlockRepository cardlockRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderInvitationRepository invitationRepository,
|
||||
CommunityVerificationRepository verificationRepository,
|
||||
CommunityVerificationVoteRepository verificationVoteRepository,
|
||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||
LockeeInvitationRepository lockeeInvitationRepository, AssignedTaskRepository assignedTaskRepository,
|
||||
LockeeInvitationRepository lockeeInvitationRepository,
|
||||
AssignedTaskRepository assignedTaskRepository,
|
||||
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||
CommunityTaskVoteRepository communityTaskVoteRepository,
|
||||
UnlockCodeHistoryRepository unlockCodeHistoryRepository, UnlockCodeHistoryService unlockCodeHistoryService,
|
||||
|
||||
SystemMessageService systemMessageService, CardLockServiceFactory cardLockServiceFactory) {
|
||||
UnlockCodeHistoryRepository unlockCodeHistoryRepository,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService,
|
||||
SystemMessageService systemMessageService,
|
||||
CardLockServiceFactory cardLockServiceFactory) {
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.invitationRepository = invitationRepository;
|
||||
@@ -111,7 +114,7 @@ public class CardLockController {
|
||||
List<CardEnum> initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards,
|
||||
LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites,
|
||||
List<Task> tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines,
|
||||
String taskCardMode) {
|
||||
TaskMode taskMode) {
|
||||
}
|
||||
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
@@ -161,7 +164,7 @@ public class CardLockController {
|
||||
lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
|
||||
lock.setRequiresVerification(req.requiresVerification());
|
||||
lock.setTestLock(false);
|
||||
lock.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM");
|
||||
lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
|
||||
// startTime, unlockCode, unlockCodeLines left null until lockee accepts
|
||||
cardlockRepository.save(lock);
|
||||
|
||||
@@ -204,7 +207,7 @@ public class CardLockController {
|
||||
lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
|
||||
lock.setRequiresVerification(req.requiresVerification());
|
||||
lock.setTestLock(req.testLock());
|
||||
lock.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM");
|
||||
lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
|
||||
lock.setUnlockCodeLength(codeLines);
|
||||
lock.setUnlockCode(unlockCode);
|
||||
|
||||
@@ -267,34 +270,7 @@ public class CardLockController {
|
||||
if (dto == null)
|
||||
return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar"));
|
||||
|
||||
// Task-Karte in nicht-zufälligem Modus → Entscheidung delegieren
|
||||
String taskPending = null;
|
||||
if (dto.card() == CardEnum.TASK && !"RANDOM".equals(l.getTaskCardMode()) && l.getTasks() != null
|
||||
&& !l.getTasks().isEmpty()) {
|
||||
|
||||
if ("KEYHOLDER".equals(l.getTaskCardMode()) && l.getKeyholder() != null) {
|
||||
KeyholderTaskChoiceEntity choice = new KeyholderTaskChoiceEntity();
|
||||
choice.setLockId(l.getLockId());
|
||||
choice.setCreatedAt(LocalDateTime.now());
|
||||
choice.setStatus("PENDING");
|
||||
keyholderTaskChoiceRepository.save(choice);
|
||||
userRepository.findById(l.getKeyholder())
|
||||
.ifPresent(kh -> sendMessage(l.getLockee(), kh.getUserId(),
|
||||
"Deine Lockee hat eine Aufgaben-Karte gezogen – wähle eine Aufgabe aus.",
|
||||
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE));
|
||||
taskPending = "KEYHOLDER";
|
||||
|
||||
} else if ("COMMUNITY".equals(l.getTaskCardMode())) {
|
||||
CommunityTaskVoteEntity vote = new CommunityTaskVoteEntity();
|
||||
vote.setLockId(l.getLockId());
|
||||
vote.setCreatedAt(LocalDateTime.now());
|
||||
vote.setExpiresAt(LocalDateTime.now().plusHours(1));
|
||||
vote.setStatus("ACTIVE");
|
||||
vote.setTestLock(l.isTestLock());
|
||||
communityTaskVoteRepository.save(vote);
|
||||
taskPending = l.isTestLock() ? "RANDOM" : "COMMUNITY";
|
||||
}
|
||||
}
|
||||
String taskPending = (dto.card() == CardEnum.TASK) ? service.getPendingTaskMode() : null;
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("card", dto.card().name());
|
||||
@@ -455,10 +431,10 @@ public class CardLockController {
|
||||
result.put("totalCards", totalCards);
|
||||
result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0);
|
||||
result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : "");
|
||||
result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null);
|
||||
result.put("frozenUntill", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
|
||||
result.put("currentTask", l.getCurrentTask() != null ? l.getCurrentTask() : null);
|
||||
result.put("currentTaskDescription", l.getCurrentTaskDescription());
|
||||
result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null);
|
||||
result.put("taskFrozenUntil", l.getTaskUntil() != null ? l.getTaskUntil().toString() : null);
|
||||
result.put("hygieneEnabled", hygieneEnabled);
|
||||
result.put("hygieneOpeningDue", hygieneOpeningDue);
|
||||
result.put("hygieneSecondsRemaining", hygieneSecondsRemaining);
|
||||
@@ -489,19 +465,19 @@ public class CardLockController {
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
var completed = verificationRepository
|
||||
.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(l.getLockId(), todayStart, todayEnd);
|
||||
.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(l.getLockId(), todayStart, todayEnd);
|
||||
if (!completed.isEmpty()) {
|
||||
var todayV = completed.get(0);
|
||||
verificationTodayId = todayV.getVerficationId().toString();
|
||||
var votes = verificationVoteRepository.findAllByVerificationId(todayV.getVerficationId());
|
||||
verificationUpvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count();
|
||||
verificationTodayId = todayV.getDisplayId().toString();
|
||||
var votes = verificationVoteRepository.findAllByVerificationId(todayV.getDisplayId());
|
||||
verificationUpvotes = votes.stream().filter(CommunityVerificationVoteEntity::isUpvote).count();
|
||||
verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count();
|
||||
} else {
|
||||
verificationDue = true;
|
||||
var pending = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNull(l.getLockId(),
|
||||
var pending = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNull(l.getLockId(),
|
||||
todayStart, todayEnd);
|
||||
if (!pending.isEmpty()) {
|
||||
verificationPendingId = pending.get(0).getVerficationId().toString();
|
||||
verificationPendingId = pending.get(0).getDisplayId().toString();
|
||||
verificationPendingCode = pending.get(0).getCode();
|
||||
}
|
||||
}
|
||||
@@ -515,20 +491,19 @@ public class CardLockController {
|
||||
result.put("verificationPendingCode", verificationPendingCode);
|
||||
|
||||
// Abgelaufene Aufgaben prüfen und Strafe anwenden
|
||||
boolean lockDirty = false;
|
||||
var expiredTasks = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING").stream()
|
||||
.filter(t -> t.getAcceptDeadline().isBefore(LocalDateTime.now())).toList();
|
||||
for (var t : expiredTasks) {
|
||||
t.setStatus("EXPIRED");
|
||||
applyAssignedTaskPenalty(l, t);
|
||||
assignedTaskRepository.save(t);
|
||||
lockDirty = true;
|
||||
sendMessage(l.getKeyholder(), l.getLockee(),
|
||||
"Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.",
|
||||
"/activelock.html?lockId=" + l.getLockId(), de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||
if (!expiredTasks.isEmpty()) {
|
||||
CardLockService penaltyService = cardLockServiceFactory.create(l);
|
||||
for (var t : expiredTasks) {
|
||||
t.setStatus("EXPIRED");
|
||||
penaltyService.applyAssignedTaskPenalty(t);
|
||||
assignedTaskRepository.save(t);
|
||||
sendMessage(l.getKeyholder(), l.getLockee(),
|
||||
"Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.",
|
||||
"/activelock.html?lockId=" + l.getLockId(), de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||
}
|
||||
}
|
||||
if (lockDirty)
|
||||
cardlockRepository.save(l);
|
||||
|
||||
// Ausstehende Keyholder-Aufgaben (ohne Aufgabentext)
|
||||
var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING").stream()
|
||||
@@ -546,20 +521,20 @@ public class CardLockController {
|
||||
return m;
|
||||
}).toList();
|
||||
result.put("assignedTasks", pendingAssigned);
|
||||
result.put("taskCardMode", l.getTaskCardMode());
|
||||
result.put("taskMode", l.getTaskMode());
|
||||
|
||||
// Ausstehende Keyholder-Choices
|
||||
boolean pendingKeyholderChoice = !keyholderTaskChoiceRepository.findByLockIdAndStatus(l.getLockId(), "PENDING")
|
||||
boolean pendingKeyholderChoice = !keyholderTaskChoiceRepository.findByLockIdAndActiveTrue(l.getLockId())
|
||||
.isEmpty();
|
||||
result.put("pendingKeyholderChoice", pendingKeyholderChoice);
|
||||
|
||||
// Aktive Community-Vote
|
||||
var activeVotes = communityTaskVoteRepository.findByStatus("ACTIVE").stream()
|
||||
var activeVotes = communityTaskVoteRepository.findByActiveTrue().stream()
|
||||
.filter(v -> v.getLockId().equals(l.getLockId())).findFirst();
|
||||
if (activeVotes.isPresent()) {
|
||||
var v = activeVotes.get();
|
||||
result.put("activeCommunityVote",
|
||||
Map.of("voteSessionId", v.getVoteSessionId().toString(), "expiresAt", v.getExpiresAt().toString()));
|
||||
Map.of("voteSessionId", v.getDisplayId().toString(), "expiresAt", v.getExpiresAt().toString()));
|
||||
}
|
||||
|
||||
// Notfall-Entsperrung: nach 1 Stunde automatisch öffnen
|
||||
@@ -608,30 +583,30 @@ public class CardLockController {
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
|
||||
// Existierende Verifikation für heute zurückgeben statt neue anlegen
|
||||
var existing = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNull(lockId, todayStart,
|
||||
var existing = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNull(lockId, todayStart,
|
||||
todayEnd);
|
||||
if (!existing.isEmpty()) {
|
||||
var ev = existing.get(0);
|
||||
return ResponseEntity.ok(Map.of("verificationId", ev.getVerficationId().toString(), "code", ev.getCode()));
|
||||
return ResponseEntity.ok(Map.of("verificationId", ev.getDisplayId().toString(), "code", ev.getCode()));
|
||||
}
|
||||
var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId,
|
||||
var completed = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId,
|
||||
todayStart, todayEnd);
|
||||
if (!completed.isEmpty()) {
|
||||
var cv = completed.get(0);
|
||||
return ResponseEntity.ok(Map.of("verificationId", cv.getVerficationId().toString(), "code", cv.getCode()));
|
||||
return ResponseEntity.ok(Map.of("verificationId", cv.getDisplayId().toString(), "code", cv.getCode()));
|
||||
}
|
||||
|
||||
VerificationEntity v = new VerificationEntity();
|
||||
v.setVerficationId(UUID.randomUUID());
|
||||
CommunityVerificationEntity v = new CommunityVerificationEntity();
|
||||
v.setDisplayId(UUID.randomUUID());
|
||||
v.setLockId(lockId);
|
||||
v.setLockeeId(myId);
|
||||
v.setCode(CodeCreator.createAlphanumericCode(6));
|
||||
v.setVerificationTime(LocalDateTime.now());
|
||||
v.setCode(CodeCreator.createAlphanumeric(6));
|
||||
v.setCreatedAt(LocalDateTime.now());
|
||||
if (l.getKeyholder() != null)
|
||||
v.setKeyholderId(l.getKeyholder());
|
||||
verificationRepository.save(v);
|
||||
|
||||
return ResponseEntity.ok(Map.of("verificationId", v.getVerficationId().toString(), "code", v.getCode()));
|
||||
return ResponseEntity.ok(Map.of("verificationId", v.getDisplayId().toString(), "code", v.getCode()));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/cardlock/{lockId}/verification/{verificationId}/complete", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@@ -707,10 +682,10 @@ public class CardLockController {
|
||||
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId,
|
||||
var completed = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId,
|
||||
todayStart, todayEnd);
|
||||
for (var v : completed) {
|
||||
verificationVoteRepository.deleteAllByVerificationId(v.getVerficationId());
|
||||
verificationVoteRepository.deleteAllByVerificationId(v.getDisplayId());
|
||||
verificationRepository.delete(v);
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
@@ -866,7 +841,7 @@ public class CardLockController {
|
||||
item.put("lockeeProfilePic", lockee.getProfilePicture());
|
||||
item.put("totalCards", lock.getAvailableCards() != null ? lock.getAvailableCards().size() : 0);
|
||||
item.put("startTime", lock.getStartTime() != null ? lock.getStartTime().toString() : null);
|
||||
boolean frozenByKh = lock.getFrozenUntill() != null && lock.getFrozenUntill().isAfter(LocalDateTime.now())
|
||||
boolean frozenByKh = lock.getFrozenUntil() != null && lock.getFrozenUntil().isAfter(LocalDateTime.now())
|
||||
&& (lock.getCurrentTask() == null || lock.getCurrentTask().isBlank());
|
||||
item.put("isFrozenByKeyholder", frozenByKh);
|
||||
result.add(item);
|
||||
@@ -919,16 +894,16 @@ public class CardLockController {
|
||||
if (l.isRequiresVerification()) {
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId,
|
||||
var completed = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId,
|
||||
todayStart, todayEnd);
|
||||
if (!completed.isEmpty()) {
|
||||
verificationDoneToday = true;
|
||||
var v = completed.get(0);
|
||||
var votes = verificationVoteRepository.findAllByVerificationId(v.getVerficationId());
|
||||
verificationUpvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count();
|
||||
var votes = verificationVoteRepository.findAllByVerificationId(v.getDisplayId());
|
||||
verificationUpvotes = votes.stream().filter(CommunityVerificationVoteEntity::isUpvote).count();
|
||||
verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count();
|
||||
verificationTodayId = v.getVerficationId().toString();
|
||||
var myVoteOpt = verificationVoteRepository.findByVerificationIdAndUserId(v.getVerficationId(), myId);
|
||||
verificationTodayId = v.getDisplayId().toString();
|
||||
var myVoteOpt = verificationVoteRepository.findByVerificationIdAndUserId(v.getDisplayId(), myId);
|
||||
if (myVoteOpt.isPresent()) {
|
||||
verificationMyVote = myVoteOpt.get().isUpvote() ? "upvote" : "downvote";
|
||||
} else if (v.getImage() != null) {
|
||||
@@ -954,9 +929,9 @@ public class CardLockController {
|
||||
result.put("cardCounts", cardCounts);
|
||||
result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0);
|
||||
result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : null);
|
||||
result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null);
|
||||
result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null);
|
||||
boolean isFrozenByKeyholder = l.getFrozenUntill() != null && l.getFrozenUntill().isAfter(LocalDateTime.now());
|
||||
result.put("frozenUntill", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
|
||||
result.put("taskFrozenUntil", l.getTaskUntil() != null ? l.getTaskUntil().toString() : null);
|
||||
boolean isFrozenByKeyholder = l.getFrozenUntil() != null && l.getFrozenUntil().isAfter(LocalDateTime.now());
|
||||
result.put("isFrozenByKeyholder", isFrozenByKeyholder);
|
||||
result.put("currentTask", l.getCurrentTask());
|
||||
result.put("currentTaskDescription", l.getCurrentTaskDescription());
|
||||
@@ -978,7 +953,7 @@ public class CardLockController {
|
||||
if (l.getTasks() != null) {
|
||||
var taskList = l.getTasks().stream().map(t -> {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("title", t.resolveTitle());
|
||||
m.put("title", t.getTitle());
|
||||
m.put("description", t.getDescription() != null ? t.getDescription() : "");
|
||||
m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
|
||||
return m;
|
||||
@@ -1002,7 +977,7 @@ public class CardLockController {
|
||||
return m;
|
||||
}).toList();
|
||||
result.put("pendingAssignedTasks", pendingAssigned);
|
||||
result.put("taskCardMode", l.getTaskCardMode());
|
||||
result.put("taskMode", l.getTaskMode());
|
||||
|
||||
// Ausstehende Task-Karten-Choices (KEYHOLDER-Modus)
|
||||
List<Task> lockTasks = l.getTasks() != null ? l.getTasks() : List.of();
|
||||
@@ -1011,12 +986,12 @@ public class CardLockController {
|
||||
Task t = lockTasks.get(i);
|
||||
Map<String, Object> tm = new LinkedHashMap<>();
|
||||
tm.put("index", i);
|
||||
tm.put("title", t.resolveTitle());
|
||||
tm.put("title", t.getTitle());
|
||||
tm.put("description", t.getDescription() != null ? t.getDescription() : "");
|
||||
tm.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
|
||||
taskListForChoice.add(tm);
|
||||
}
|
||||
var pendingChoices = keyholderTaskChoiceRepository.findByLockIdAndStatus(lockId, "PENDING").stream().map(c -> {
|
||||
var pendingChoices = keyholderTaskChoiceRepository.findByLockIdAndActiveTrue(lockId).stream().map(c -> {
|
||||
Map<String, Object> cm = new LinkedHashMap<>();
|
||||
cm.put("choiceId", c.getChoiceId().toString());
|
||||
cm.put("createdAt", c.getCreatedAt().toString());
|
||||
@@ -1053,7 +1028,7 @@ public class CardLockController {
|
||||
service.unlock(l.getUnlockCode());
|
||||
|
||||
var verifications = verificationRepository.findByLockId(lockId);
|
||||
verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getVerficationId()));
|
||||
verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getDisplayId()));
|
||||
verificationRepository.deleteAll(verifications);
|
||||
invitationRepository.deleteByLockId(lockId);
|
||||
cardlockRepository.deleteById(lockId);
|
||||
@@ -1180,24 +1155,6 @@ public class CardLockController {
|
||||
|
||||
// ── Hilfsmethoden ──────────────────────────────────────────────────────────
|
||||
|
||||
private void applyAssignedTaskPenalty(CardLockEntity l, AssignedTaskEntity task) {
|
||||
if (task.getPenaltyFreezeMinutes() != null && task.getPenaltyFreezeMinutes() > 0) {
|
||||
LocalDateTime until = LocalDateTime.now().plusMinutes(task.getPenaltyFreezeMinutes());
|
||||
// Bestehenden Freeze nur verlängern, nie verkürzen
|
||||
if (l.getFrozenUntill() == null || until.isAfter(l.getFrozenUntill())) {
|
||||
l.setFrozenUntill(until);
|
||||
l.setNextCardIn(until);
|
||||
}
|
||||
}
|
||||
if (task.getPenaltyRedCards() != null && task.getPenaltyRedCards() > 0) {
|
||||
List<CardEnum> cards = new ArrayList<>(l.getAvailableCards() != null ? l.getAvailableCards() : List.of());
|
||||
for (int i = 0; i < task.getPenaltyRedCards(); i++) {
|
||||
cards.add(CardEnum.RED);
|
||||
}
|
||||
l.setAvailableCards(cards);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl,
|
||||
de.oaa.xxx.social.entity.MessageCause cause) {
|
||||
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
|
||||
@@ -1263,9 +1220,9 @@ public class CardLockController {
|
||||
Task task = tasks.get(req.taskIndex());
|
||||
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
||||
assigned.setLockId(lockId);
|
||||
assigned.setTaskTitle(task.resolveTitle());
|
||||
assigned.setTaskTitle(task.getTitle());
|
||||
assigned.setTaskDescription(task.getDescription());
|
||||
assigned.setTaskText(task.resolveTitle()); // Compat
|
||||
assigned.setTaskText(task.getTitle()); // Compat
|
||||
assigned.setTaskMinutes(task.getMinutes());
|
||||
assigned.setAssignedAt(LocalDateTime.now());
|
||||
assigned.setAcceptDeadline(LocalDateTime.now().plusMinutes(req.acceptDeadlineMinutes()));
|
||||
@@ -1309,14 +1266,13 @@ public class CardLockController {
|
||||
if (task.getAcceptDeadline().isBefore(LocalDateTime.now())) {
|
||||
// Bereits abgelaufen – Strafe anwenden
|
||||
task.setStatus("EXPIRED");
|
||||
applyAssignedTaskPenalty(l, task);
|
||||
cardLockServiceFactory.create(l).applyAssignedTaskPenalty(task);
|
||||
assignedTaskRepository.save(task);
|
||||
cardlockRepository.save(l);
|
||||
return ResponseEntity.status(409)
|
||||
.body(Map.of("error", "Die Annahme-Frist ist abgelaufen. Die Strafe wurde angewendet."));
|
||||
}
|
||||
boolean hasActiveTask = (l.getCurrentTask() != null && !l.getCurrentTask().isBlank())
|
||||
|| (l.getTaskFrozenUntil() != null && l.getTaskFrozenUntil().isAfter(LocalDateTime.now()));
|
||||
|| (l.getTaskUntil() != null && l.getTaskUntil().isAfter(LocalDateTime.now()));
|
||||
if (hasActiveTask)
|
||||
return ResponseEntity.status(409).body(Map.of("error", "Du hast bereits eine laufende Aufgabe."));
|
||||
|
||||
@@ -1325,7 +1281,7 @@ public class CardLockController {
|
||||
l.setCurrentTask(title);
|
||||
l.setCurrentTaskDescription(task.getTaskDescription());
|
||||
if (task.getTaskMinutes() != null && task.getTaskMinutes() > 0) {
|
||||
l.setTaskFrozenUntil(LocalDateTime.now().plusMinutes(task.getTaskMinutes()));
|
||||
l.setTaskUntil(LocalDateTime.now().plusMinutes(task.getTaskMinutes()));
|
||||
|
||||
// Fälligkeit aller anderen offenen Aufgaben um die Task-Dauer verschieben
|
||||
final int extraMinutes = task.getTaskMinutes();
|
||||
@@ -1370,9 +1326,8 @@ public class CardLockController {
|
||||
return ResponseEntity.status(409).body(Map.of("error", "Diese Aufgabe ist nicht mehr ausstehend."));
|
||||
|
||||
task.setStatus("DECLINED");
|
||||
applyAssignedTaskPenalty(l, task);
|
||||
cardLockServiceFactory.create(l).applyAssignedTaskPenalty(task);
|
||||
assignedTaskRepository.save(task);
|
||||
cardlockRepository.save(l);
|
||||
|
||||
sendMessage(myId, l.getKeyholder(),
|
||||
meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.",
|
||||
@@ -1443,7 +1398,7 @@ public class CardLockController {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Zeitpunkt muss in der Zukunft liegen."));
|
||||
}
|
||||
|
||||
l.setFrozenUntill(until);
|
||||
l.setFrozenUntil(until);
|
||||
cardlockRepository.save(l);
|
||||
|
||||
sendMessage(myId, l.getLockee(),
|
||||
@@ -1476,7 +1431,7 @@ public class CardLockController {
|
||||
"Das Lock ist durch eine Aufgabe eingefroren und kann nicht manuell entfroren werden."));
|
||||
}
|
||||
|
||||
l.setFrozenUntill(null);
|
||||
l.setFrozenUntil(null);
|
||||
cardlockRepository.save(l);
|
||||
|
||||
sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.",
|
||||
|
||||
@@ -2,40 +2,24 @@ package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
|
||||
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.DiscriminatorValue;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "card_lock")
|
||||
public class CardLockEntity {
|
||||
@DiscriminatorValue("CARDLOCK")
|
||||
public class CardLockEntity extends BaseLockEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID lockId;
|
||||
|
||||
@Column
|
||||
private String name;
|
||||
|
||||
// Settings
|
||||
@Column(nullable = false)
|
||||
private UUID lockee;
|
||||
@Column
|
||||
private UUID keyholder;
|
||||
@Convert(converter = CardEnumListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<CardEnum> initialCards;
|
||||
@@ -47,69 +31,17 @@ public class CardLockEntity {
|
||||
private boolean showRemainingCards;
|
||||
@Column
|
||||
private LocalDateTime latestOpeningtime;
|
||||
@Column
|
||||
private LocalDateTime frozenUntill;
|
||||
@Column
|
||||
private Integer hygineOpeningDurationMinutes;
|
||||
@Column
|
||||
private Integer hygineOpeningEveryMinites;
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasks;
|
||||
@Column
|
||||
private boolean requiresVerification;
|
||||
@Column
|
||||
private boolean testLock;
|
||||
@Column
|
||||
private Integer unlockCodeLength;
|
||||
|
||||
|
||||
// State
|
||||
@Column
|
||||
private LocalDateTime startTime;
|
||||
@Column
|
||||
private LocalDateTime nextCardIn;
|
||||
@Column
|
||||
private Integer openPicks;
|
||||
@Convert(converter = CardEnumListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<CardEnum> availableCards;
|
||||
@Column
|
||||
private LocalDateTime lastHygineOpening;
|
||||
@Column
|
||||
private LocalDateTime tempOpeningTime; // If null, not while hygine opening
|
||||
@Column
|
||||
private Integer tempOpeningDuration;
|
||||
@Column
|
||||
private TempOpeningReason tempOpeningReason;
|
||||
@Column
|
||||
private LocalDateTime unlockTime;
|
||||
@Column
|
||||
private String currentTask;
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String currentTaskDescription;
|
||||
@Column
|
||||
private LocalDateTime taskFrozenUntil;
|
||||
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasksInQueue;
|
||||
@Column
|
||||
private String unlockCode;
|
||||
|
||||
/** Keyholder hat Unlock angefordert – nächste Aktion der Lockee zeigt grüne Karte */
|
||||
@Column(nullable = false)
|
||||
private boolean keyholderRequestedUnlock = false;
|
||||
|
||||
/** Lockee hat Notfall-Entsperrung angefordert */
|
||||
@Column
|
||||
private java.time.LocalDateTime emergencyUnlockRequestedAt;
|
||||
|
||||
/** true = System hat automatisch entsperrt (Keyholderin nicht reagiert) */
|
||||
@Column(nullable = false)
|
||||
private boolean emergencyAutoUnlocked = false;
|
||||
|
||||
/** RANDOM | KEYHOLDER | COMMUNITY */
|
||||
@Column(nullable = false)
|
||||
private String taskCardMode = "RANDOM";
|
||||
|
||||
public String getTaskCardMode() { return taskCardMode != null ? taskCardMode : "RANDOM"; }
|
||||
}
|
||||
|
||||
@@ -1,332 +1,261 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.AbstractLockService;
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockEntity;
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockService;
|
||||
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
|
||||
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
|
||||
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
|
||||
import de.oaa.xxx.games.history.GameHistoryEntity;
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.games.history.GameType;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
public class CardLockService extends AbstractLockService {
|
||||
public class CardLockService extends BaseLockService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CardLockService.class);
|
||||
private final CardLockEntity lock;
|
||||
private CardLockRepository cardLockRepository;
|
||||
private VerificationRepository verificationRepository;
|
||||
private GameHistoryRepository gameHistoryRepository;
|
||||
private UserRepository userRepository;
|
||||
private UnlockCodeHistoryService unlockCodeHistoryService;
|
||||
private KeyholderNotificationRepository keyholderNotificationRepository;
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CardLockService.class);
|
||||
private final CardLockEntity lock;
|
||||
private final CardLockRepository cardLockRepository;
|
||||
private String pendingTaskMode;
|
||||
|
||||
public CardLockService(CardLockEntity lock, VerificationRepository verificationRepository,
|
||||
VerificationVoteRepository verificationVoteRepository, CardLockRepository cardLockRepository,
|
||||
GameHistoryRepository gameHistoryRepository, UserRepository userRepository, KeyholderNotificationRepository keyholderNotificationRepository,UnlockCodeHistoryService unlockCodeHistoryService) {
|
||||
super(verificationVoteRepository);
|
||||
this.lock = lock;
|
||||
this.cardLockRepository = cardLockRepository;
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.keyholderNotificationRepository = keyholderNotificationRepository;
|
||||
this.unlockCodeHistoryService = unlockCodeHistoryService;
|
||||
}
|
||||
public CardLockService(
|
||||
CardLockEntity lock,
|
||||
CommunityVerificationVoteRepository communityVerificationVoteRepository,
|
||||
CommunityVerificationRepository communityVerificationRepository,
|
||||
KeyholderVerificationRepository keyholderVerificationRepository,
|
||||
GameHistoryRepository gameHistoryRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||
SystemMessageService systemMessageService,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService,
|
||||
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||
CommunityTaskVoteRepository communityTaskVoteRepository,
|
||||
CardLockRepository cardLockRepository) {
|
||||
super(communityVerificationVoteRepository, communityVerificationRepository, keyholderVerificationRepository,
|
||||
gameHistoryRepository, userRepository, keyholderNotificationRepository, systemMessageService,
|
||||
unlockCodeHistoryService, keyholderTaskChoiceRepository, communityTaskVoteRepository);
|
||||
this.lock = lock;
|
||||
this.cardLockRepository = cardLockRepository;
|
||||
}
|
||||
|
||||
public CardDTO getNextCard() {
|
||||
LOGGER.debug("New Card requested by user {}", lock.getLockee());
|
||||
CardDTO card = null;
|
||||
if (lock.isKeyholderRequestedUnlock() || (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
|
||||
card = getGreenCard();
|
||||
} else if (lock.isAccumulatePicks()) {
|
||||
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
|
||||
lock.setOpenPicks(lock.getOpenPicks() == null ? 1 : lock.getOpenPicks() + 1);
|
||||
}
|
||||
if (lock.getOpenPicks() != null && lock.getOpenPicks() > 0) {
|
||||
lock.setOpenPicks(lock.getOpenPicks() - 1);
|
||||
card = getRandomCard();
|
||||
}
|
||||
} else {
|
||||
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
|
||||
lock.setNextCardIn(LocalDateTime.now().plusMinutes(lock.getPickEveryMinute()));
|
||||
card = getRandomCard();
|
||||
}
|
||||
}
|
||||
cardLockRepository.save(lock);
|
||||
return card;
|
||||
}
|
||||
// ── Abstract method implementations ──────────────────────────────────────
|
||||
|
||||
private CardDTO getRandomCard() {
|
||||
var cards = lock.getAvailableCards();
|
||||
if (!cards.isEmpty()) {
|
||||
var card = cards.get(new Random().nextInt(cards.size()));
|
||||
LOGGER.debug("Card drafted: {}", card);
|
||||
lock.getAvailableCards().remove(card);
|
||||
return card.get().processCard(this);
|
||||
}
|
||||
LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte");
|
||||
return getGreenCard();
|
||||
}
|
||||
@Override
|
||||
protected BaseLockEntity getLock() {
|
||||
return lock;
|
||||
}
|
||||
|
||||
private CardDTO getGreenCard() {
|
||||
return new CardDTO(CardEnum.GREEN, lock.getUnlockCode());
|
||||
}
|
||||
@Override
|
||||
protected void saveLock() {
|
||||
cardLockRepository.save(lock);
|
||||
}
|
||||
|
||||
public String doubleUp() {
|
||||
var cards = lock.getAvailableCards();
|
||||
LOGGER.debug("Double up {} cards", cards.size());
|
||||
lock.getAvailableCards().addAll(cards);
|
||||
LOGGER.debug("Now {} cards", lock.getAvailableCards().size());
|
||||
return "";
|
||||
}
|
||||
@Override
|
||||
protected GameType getGameType() {
|
||||
return GameType.CARDLOCK;
|
||||
}
|
||||
|
||||
public String reset() {
|
||||
LOGGER.debug("Reset to initial cards");
|
||||
lock.setAvailableCards(lock.getInitialCards());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String green() {
|
||||
LOGGER.debug("Green Card drafted");
|
||||
return lock.getUnlockCode();
|
||||
}
|
||||
@Override
|
||||
protected void applyHygieneOvertime(Long overtime) {
|
||||
if (lock.getFrozenUntil() != null) {
|
||||
lock.setFrozenUntil(lock.getFrozenUntil().plusMinutes(overtime * 4));
|
||||
} else {
|
||||
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(overtime * 4));
|
||||
}
|
||||
}
|
||||
|
||||
public void unlock(String unlockCode) {
|
||||
this.lock.setUnlockTime(LocalDateTime.now());
|
||||
boolean valid = true;
|
||||
if (lock.isEmergencyAutoUnlocked()) {
|
||||
valid = false;
|
||||
LOGGER.debug("Lock invalid - Emergency Auto-Unlock (1h timer)");
|
||||
}
|
||||
if (lock.isTestLock()) {
|
||||
valid = false;
|
||||
} else if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
|
||||
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
|
||||
.filter(verification -> isValid(verification))
|
||||
.map(verification -> verification.getVerificationTime().toLocalDate())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
LocalDate current = this.lock.getStartTime().toLocalDate();
|
||||
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1);
|
||||
|
||||
while (!current.isAfter(last)) {
|
||||
if (!verifications.contains(current)) {
|
||||
valid = false;
|
||||
LOGGER.debug("Lock invalid - no daily verification on %s", current.toString());
|
||||
break;
|
||||
}
|
||||
current = current.plusDays(1);
|
||||
}
|
||||
}
|
||||
// ── Card drawing ──────────────────────────────────────────────────────────
|
||||
|
||||
lock.setUnlockTime(LocalDateTime.now());
|
||||
LOGGER.debug("Unlocked at {}", lock.getUnlockTime());
|
||||
cardLockRepository.save(lock);
|
||||
public CardDTO getNextCard() {
|
||||
LOGGER.debug("New Card requested by user {}", lock.getLockee());
|
||||
CardDTO card = null;
|
||||
if (lock.isKeyholderRequestedUnlock()
|
||||
|| (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
|
||||
card = getGreenCard();
|
||||
} else if (lock.isAccumulatePicks()) {
|
||||
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
|
||||
lock.setOpenPicks(lock.getOpenPicks() == null ? 1 : lock.getOpenPicks() + 1);
|
||||
}
|
||||
if (lock.getOpenPicks() != null && lock.getOpenPicks() > 0) {
|
||||
lock.setOpenPicks(lock.getOpenPicks() - 1);
|
||||
card = getRandomCard();
|
||||
}
|
||||
} else {
|
||||
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
|
||||
lock.setNextCardIn(LocalDateTime.now().plusMinutes(lock.getPickEveryMinute()));
|
||||
card = getRandomCard();
|
||||
}
|
||||
}
|
||||
cardLockRepository.save(lock);
|
||||
return card;
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes();
|
||||
private CardDTO getRandomCard() {
|
||||
var cards = lock.getAvailableCards();
|
||||
if (!cards.isEmpty()) {
|
||||
var card = cards.get(new Random().nextInt(cards.size()));
|
||||
LOGGER.debug("Card drafted: {}", card);
|
||||
lock.getAvailableCards().remove(card);
|
||||
return card.get().processCard(this);
|
||||
}
|
||||
LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte");
|
||||
return getGreenCard();
|
||||
}
|
||||
|
||||
// Gemeinsamer History-Eintrag mit Teilnehmerliste
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameType(de.oaa.xxx.games.history.GameType.CARDLOCK);
|
||||
entry.setGameName(lock.getName());
|
||||
entry.setStartTime(lock.getStartTime());
|
||||
entry.setEndTime(lock.getUnlockTime());
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(lock.getLockee(), de.oaa.xxx.games.history.GameRole.LOCKEE);
|
||||
if (lock.getKeyholder() != null) {
|
||||
entry.addParticipant(lock.getKeyholder(), de.oaa.xxx.games.history.GameRole.KEYHOLDER);
|
||||
}
|
||||
gameHistoryRepository.save(entry);
|
||||
private CardDTO getGreenCard() {
|
||||
return new CardDTO(CardEnum.GREEN, lock.getUnlockCode());
|
||||
}
|
||||
|
||||
int minutes = (int) durationMinutes;
|
||||
userRepository.findById(lock.getLockee()).ifPresent(u -> {
|
||||
u.setLockeeXp(u.getLockeeXp() + minutes);
|
||||
userRepository.save(u);
|
||||
});
|
||||
if (lock.getKeyholder() != null) {
|
||||
userRepository.findById(lock.getKeyholder()).ifPresent(u -> {
|
||||
u.setKeyholderXp(u.getKeyholderXp() + minutes);
|
||||
userRepository.save(u);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void putBackGreen() {
|
||||
LOGGER.debug("Green Card was put Back");
|
||||
lock.getAvailableCards().add(CardEnum.GREEN);
|
||||
cardLockRepository.save(lock);
|
||||
}
|
||||
|
||||
public String freeze() {
|
||||
var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0);
|
||||
freeze(multiplier);
|
||||
return "";
|
||||
}
|
||||
// ── Card effects ──────────────────────────────────────────────────────────
|
||||
|
||||
private String freeze(double multiplier) {
|
||||
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
|
||||
lock.setFrozenUntill(frozenTill);
|
||||
lock.setNextCardIn(frozenTill);
|
||||
LOGGER.debug("Frozen until {}", lock.getFrozenUntill());
|
||||
return "";
|
||||
}
|
||||
public String doubleUp() {
|
||||
var cards = lock.getAvailableCards();
|
||||
LOGGER.debug("Double up {} cards", cards.size());
|
||||
lock.getAvailableCards().addAll(cards);
|
||||
LOGGER.debug("Now {} cards", lock.getAvailableCards().size());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String task() {
|
||||
// Non-RANDOM modes are handled by the controller after the card is drawn
|
||||
if (!"RANDOM".equals(lock.getTaskCardMode())) {
|
||||
LOGGER.debug("Task card drawn in {} mode – skipping random assignment", lock.getTaskCardMode());
|
||||
return "";
|
||||
}
|
||||
LOGGER.debug("Apply random task");
|
||||
var tasks = lock.getTasks();
|
||||
if (!tasks.isEmpty()) {
|
||||
task(tasks.get(new Random().nextInt(tasks.size())));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
public String reset() {
|
||||
LOGGER.debug("Reset to initial cards");
|
||||
lock.setAvailableCards(lock.getInitialCards());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String task(Task task) {
|
||||
LOGGER.debug("Apply task {}", task);
|
||||
lock.setCurrentTask(task.resolveTitle());
|
||||
lock.setCurrentTaskDescription(task.getDescription());
|
||||
if (task.getMinutes() != null && task.getMinutes() > 0) {
|
||||
lock.setTaskFrozenUntil(LocalDateTime.now().plusMinutes(task.getMinutes()));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
public String green() {
|
||||
LOGGER.debug("Green Card drafted");
|
||||
return lock.getUnlockCode();
|
||||
}
|
||||
|
||||
public String clearTask() {
|
||||
LOGGER.debug("Clear task");
|
||||
lock.setCurrentTask(null);
|
||||
lock.setCurrentTaskDescription(null);
|
||||
lock.setTaskFrozenUntil(null);
|
||||
return "";
|
||||
}
|
||||
public String freeze() {
|
||||
var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0);
|
||||
freeze(multiplier);
|
||||
return "";
|
||||
}
|
||||
|
||||
public String redCard() {
|
||||
return "";
|
||||
}
|
||||
private String freeze(double multiplier) {
|
||||
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
|
||||
lock.setFrozenUntil(frozenTill);
|
||||
lock.setNextCardIn(frozenTill);
|
||||
LOGGER.debug("Frozen until {}", lock.getFrozenUntil());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String yellowCard() {
|
||||
Random random = new Random();
|
||||
if (random.nextBoolean()) {
|
||||
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
||||
LOGGER.debug("Adding Red card");
|
||||
lock.getAvailableCards().add(CardEnum.RED);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
||||
LOGGER.debug("Removing Red card if possible");
|
||||
lock.getAvailableCards().remove(CardEnum.RED);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public void startHygieneOpening() {
|
||||
tempOperning(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
|
||||
}
|
||||
|
||||
private Long calcOvertime() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Long overtime = null;
|
||||
if (lock.getTempOpeningTime() != null && lock.getTempOpeningDuration() != null) {
|
||||
LocalDateTime dueTime = lock.getTempOpeningTime().plusMinutes(lock.getTempOpeningDuration());
|
||||
if (LocalDateTime.now().isAfter(dueTime)) {
|
||||
overtime = ChronoUnit.MINUTES.between(dueTime, now);
|
||||
}
|
||||
}
|
||||
return overtime;
|
||||
}
|
||||
|
||||
public String endHygieneOpening() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
/** Called by TaskCard. Dispatches based on TaskMode and stores result for controller. */
|
||||
public String task() {
|
||||
switch (lock.getTaskMode()) {
|
||||
case RANDOM -> applyRandomTask();
|
||||
case KEYHOLDER -> {
|
||||
if (lock.isTestLock()) applyRandomTask();
|
||||
else startKeyholderVote();
|
||||
}
|
||||
case COMMUNITY -> {
|
||||
if (lock.isTestLock()) applyRandomTask();
|
||||
else startCommunityVote();
|
||||
}
|
||||
}
|
||||
pendingTaskMode = lock.getTaskMode().name();
|
||||
return "";
|
||||
}
|
||||
|
||||
Long overtime = calcOvertime();
|
||||
if (overtime != null) {
|
||||
if (lock.getKeyholder() != null) {
|
||||
reportKeyholder(overtime);
|
||||
}
|
||||
freezeForOvertime(overtime);
|
||||
/** Returns the TaskMode that was triggered by the last task() call, or null if no task card was drawn. */
|
||||
public String getPendingTaskMode() {
|
||||
return pendingTaskMode;
|
||||
}
|
||||
|
||||
}
|
||||
lock.setLastHygineOpening(now);
|
||||
lock.setTempOpeningDuration(null);
|
||||
lock.setTempOpeningTime(null);
|
||||
|
||||
var code = CodeCreator.createAlphanumericCode(lock.getUnlockCodeLength());
|
||||
lock.setUnlockCode(code);
|
||||
cardLockRepository.save(lock);
|
||||
return code;
|
||||
}
|
||||
public String redCard() {
|
||||
return "";
|
||||
}
|
||||
|
||||
private void reportKeyholder(Long overtime) {
|
||||
KeyholderNotificationEntity notification = new KeyholderNotificationEntity();
|
||||
notification.setLockId(lock.getLockId());
|
||||
notification.setLockeeId(lock.getLockee());
|
||||
notification.setKeyholderUserId(lock.getKeyholder());
|
||||
notification.setViolationTime(LocalDateTime.now());
|
||||
notification.setOvertimeMinutes(overtime);
|
||||
keyholderNotificationRepository.save(notification);
|
||||
}
|
||||
public String yellowCard() {
|
||||
Random random = new Random();
|
||||
if (random.nextBoolean()) {
|
||||
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
||||
LOGGER.debug("Adding Red card");
|
||||
lock.getAvailableCards().add(CardEnum.RED);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
||||
LOGGER.debug("Removing Red card if possible");
|
||||
lock.getAvailableCards().remove(CardEnum.RED);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private void freezeForOvertime(Long overtime) {
|
||||
if (lock.getFrozenUntill() != null) {
|
||||
lock.setFrozenUntill(lock.getFrozenUntill().plusMinutes(overtime * 4));
|
||||
} else {
|
||||
lock.setFrozenUntill(LocalDateTime.now().plusMinutes(overtime * 4));
|
||||
}
|
||||
}
|
||||
|
||||
private void tempOperning(TempOpeningReason reason, Integer duration) {
|
||||
assert duration != null;
|
||||
lock.setTempOpeningReason(reason);
|
||||
lock.setTempOpeningTime(LocalDateTime.now());;
|
||||
lock.setTempOpeningDuration(duration);
|
||||
cardLockRepository.save(lock);
|
||||
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), reason.toString());
|
||||
}
|
||||
public void putBackGreen() {
|
||||
LOGGER.debug("Green Card was put Back");
|
||||
lock.getAvailableCards().add(CardEnum.GREEN);
|
||||
cardLockRepository.save(lock);
|
||||
}
|
||||
|
||||
public String cum(boolean tempUnlock) {
|
||||
if (tempUnlock) {
|
||||
tempOperning(TempOpeningReason.CARD, 0); // Je länger man braucht, desto länger wird gefreezed
|
||||
}
|
||||
return lock.getUnlockCode();
|
||||
}
|
||||
|
||||
public String endCumming() {
|
||||
Long overtime = calcOvertime();
|
||||
if (overtime != null) {
|
||||
if (lock.getKeyholder() == null) {
|
||||
freezeForOvertime(overtime);
|
||||
} else {
|
||||
reportKeyholder(overtime);
|
||||
}
|
||||
}
|
||||
lock.setTempOpeningDuration(null);
|
||||
lock.setTempOpeningTime(null);
|
||||
lock.setTempOpeningReason(null);
|
||||
|
||||
var code = CodeCreator.createAlphanumericCode(lock.getUnlockCodeLength());
|
||||
lock.setUnlockCode(code);
|
||||
cardLockRepository.save(lock);
|
||||
return code;
|
||||
}
|
||||
// ── Hygiene opening ───────────────────────────────────────────────────────
|
||||
|
||||
public void startHygieneOpening() {
|
||||
startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
|
||||
}
|
||||
|
||||
// ── Cum cards ─────────────────────────────────────────────────────────────
|
||||
|
||||
public String cum(boolean tempUnlock) {
|
||||
if (tempUnlock) {
|
||||
startTempOpening(TempOpeningReason.CARD, 0);
|
||||
}
|
||||
return lock.getUnlockCode();
|
||||
}
|
||||
|
||||
public String endCumming() {
|
||||
Long overtime = calcOvertime();
|
||||
if (overtime != null) {
|
||||
if (lock.getKeyholder() == null) {
|
||||
applyHygieneOvertime(overtime);
|
||||
} else {
|
||||
reportKeyholder(overtime);
|
||||
}
|
||||
}
|
||||
lock.setTempOpeningDuration(null);
|
||||
lock.setTempOpeningTime(null);
|
||||
lock.setTempOpeningReason(null);
|
||||
|
||||
var code = CodeCreator.createNumeric(lock.getUnlockCodeLength());
|
||||
lock.setUnlockCode(code);
|
||||
cardLockRepository.save(lock);
|
||||
return code;
|
||||
}
|
||||
|
||||
// ── Assigned task penalty ─────────────────────────────────────────────────
|
||||
|
||||
public void applyAssignedTaskPenalty(AssignedTaskEntity task) {
|
||||
if (task.getPenaltyFreezeMinutes() != null && task.getPenaltyFreezeMinutes() > 0) {
|
||||
LocalDateTime until = LocalDateTime.now().plusMinutes(task.getPenaltyFreezeMinutes());
|
||||
if (lock.getFrozenUntil() == null || until.isAfter(lock.getFrozenUntil())) {
|
||||
lock.setFrozenUntil(until);
|
||||
lock.setNextCardIn(until);
|
||||
}
|
||||
}
|
||||
if (task.getPenaltyRedCards() != null && task.getPenaltyRedCards() > 0) {
|
||||
List<CardEnum> cards = new ArrayList<>(
|
||||
lock.getAvailableCards() != null ? lock.getAvailableCards() : List.of());
|
||||
for (int i = 0; i < task.getPenaltyRedCards(); i++) {
|
||||
cards.add(CardEnum.RED);
|
||||
}
|
||||
lock.setAvailableCards(cards);
|
||||
}
|
||||
cardLockRepository.save(lock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -18,43 +22,50 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class CardLockServiceFactory {
|
||||
|
||||
private final VerificationRepository verificationRepository;
|
||||
private final VerificationVoteRepository verificationVoteRepository;
|
||||
private final CardLockRepository cardLockRepository;
|
||||
private final CommunityVerificationRepository communityVerificationRepository;
|
||||
private final CommunityVerificationVoteRepository communityVerificationVoteRepository;
|
||||
private final GameHistoryRepository gameHistoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
private KeyholderNotificationRepository keyholderNotificationRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final UnlockCodeHistoryService unlockCodeHistoryService;
|
||||
private final KeyholderNotificationRepository keyholderNotificationRepository;
|
||||
private final KeyholderVerificationRepository keyholderVerificationRepository;
|
||||
private final SystemMessageService systemMessageService;
|
||||
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
||||
private final CommunityTaskVoteRepository communityTaskVoteRepository;
|
||||
|
||||
public CardLockServiceFactory(VerificationRepository verificationRepository,
|
||||
VerificationVoteRepository verificationVoteRepository,
|
||||
CardLockRepository cardLockRepository,
|
||||
GameHistoryRepository gameHistoryRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService) {
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
public CardLockServiceFactory(
|
||||
CommunityVerificationRepository communityVerificationRepository,
|
||||
CommunityVerificationVoteRepository communityVerificationVoteRepository,
|
||||
CardLockRepository cardLockRepository,
|
||||
GameHistoryRepository gameHistoryRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||
KeyholderVerificationRepository keyholderVerificationRepository,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService,
|
||||
SystemMessageService systemMessageService,
|
||||
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||
CommunityTaskVoteRepository communityTaskVoteRepository) {
|
||||
this.cardLockRepository = cardLockRepository;
|
||||
this.communityVerificationRepository = communityVerificationRepository;
|
||||
this.communityVerificationVoteRepository = communityVerificationVoteRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.keyholderNotificationRepository = keyholderNotificationRepository;
|
||||
this.keyholderNotificationRepository = keyholderNotificationRepository;
|
||||
this.unlockCodeHistoryService = unlockCodeHistoryService;
|
||||
this.keyholderVerificationRepository = keyholderVerificationRepository;
|
||||
this.systemMessageService = systemMessageService;
|
||||
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
|
||||
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue CardLockService-Instanz für das gegebene Lock.
|
||||
*/
|
||||
public CardLockService create(CardLockEntity lock) {
|
||||
return new CardLockService(
|
||||
lock,
|
||||
verificationRepository,
|
||||
verificationVoteRepository,
|
||||
cardLockRepository,
|
||||
gameHistoryRepository,
|
||||
userRepository,
|
||||
keyholderNotificationRepository,
|
||||
unlockCodeHistoryService
|
||||
);
|
||||
return new CardLockService(lock, communityVerificationVoteRepository, communityVerificationRepository,
|
||||
keyholderVerificationRepository, gameHistoryRepository, userRepository,
|
||||
keyholderNotificationRepository, systemMessageService, unlockCodeHistoryService,
|
||||
keyholderTaskChoiceRepository, communityTaskVoteRepository, cardLockRepository);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -33,7 +34,7 @@ public class CardlockTemplateController {
|
||||
Integer hygineOpeningEveryMinites,
|
||||
List<Task> tasks,
|
||||
boolean requiresVerification,
|
||||
String taskCardMode
|
||||
TaskMode taskMode
|
||||
) {}
|
||||
|
||||
private Map<String, Object> toDto(CardlockTemplateEntity t) {
|
||||
@@ -129,6 +130,6 @@ public class CardlockTemplateController {
|
||||
t.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes());
|
||||
t.setTasks(req.tasks() != null ? req.tasks() : List.of());
|
||||
t.setRequiresVerification(req.requiresVerification());
|
||||
t.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM");
|
||||
t.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,34 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
|
||||
import jakarta.persistence.*;
|
||||
import java.util.Map;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockTemplateEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.DiscriminatorValue;
|
||||
import jakarta.persistence.Entity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "cardlock_template")
|
||||
public class CardlockTemplateEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID templateId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID owner;
|
||||
|
||||
@Column
|
||||
private String name;
|
||||
@DiscriminatorValue("CARDLOCK")
|
||||
public class CardlockTemplateEntity extends BaseLockTemplateEntity {
|
||||
|
||||
@Convert(converter = CardCountMapConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private Map<String, Integer> cardCountsMin;
|
||||
|
||||
@Convert(converter = CardCountMapConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private Map<String, Integer> cardCountsMax;
|
||||
|
||||
@Column
|
||||
private Integer pickEveryMinute;
|
||||
|
||||
@Column
|
||||
private boolean accumulatePicks;
|
||||
|
||||
@Column
|
||||
private boolean showRemainingCards;
|
||||
|
||||
@Column
|
||||
private Integer hygineOpeningDurationMinutes;
|
||||
|
||||
@Column
|
||||
private Integer hygineOpeningEveryMinites;
|
||||
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasks;
|
||||
|
||||
@Column
|
||||
private boolean requiresVerification;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String taskCardMode = "RANDOM";
|
||||
|
||||
public String getTaskCardMode() { return taskCardMode != null ? taskCardMode : "RANDOM"; }
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationEntity;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteEntity;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
|
||||
|
||||
public abstract class AbstractLockService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLockService.class);
|
||||
private final VerificationVoteRepository verificationVoteRepository;
|
||||
|
||||
public AbstractLockService(VerificationVoteRepository verificationVoteRepository) {
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
}
|
||||
|
||||
protected boolean isValid(VerificationEntity entity) {
|
||||
LOGGER.trace("isValid");
|
||||
int count = 0;
|
||||
for (VerificationVoteEntity vote : verificationVoteRepository.findAllByVerificationId(entity.getVerficationId())) {
|
||||
if (vote.isUpvote()) {
|
||||
count++;
|
||||
} else {
|
||||
count--;
|
||||
}
|
||||
}
|
||||
return count >= 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("games/chastity/lock/")
|
||||
public class BaseLockController {
|
||||
|
||||
@GetMapping("/{lockId}/tasks")
|
||||
public ResponseEntity<List<Task>> getTasks() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.DiscriminatorColumn;
|
||||
import jakarta.persistence.DiscriminatorType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Inheritance;
|
||||
import jakarta.persistence.InheritanceType;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "current_lock")
|
||||
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
||||
@DiscriminatorColumn(name = "lock_type", discriminatorType = DiscriminatorType.STRING)
|
||||
public class BaseLockEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID lockId;
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
// --- Gemeinsame Settings ---
|
||||
@Column(nullable = false)
|
||||
private UUID lockee;
|
||||
@Column
|
||||
private UUID keyholder;
|
||||
@Column(nullable = false)
|
||||
private boolean testLock;
|
||||
@Column
|
||||
private boolean requiresVerification;
|
||||
@Column
|
||||
private Integer unlockCodeLength;
|
||||
@Column
|
||||
private String unlockCode;
|
||||
|
||||
// --- Timing & Hygiene ---
|
||||
@Column
|
||||
private LocalDateTime startTime;
|
||||
@Column
|
||||
private LocalDateTime unlockTime;
|
||||
@Column
|
||||
private LocalDateTime lastHygineOpening;
|
||||
@Column
|
||||
private Integer hygineOpeningDurationMinutes;
|
||||
@Column
|
||||
private Integer hygineOpeningEveryMinites;
|
||||
@Column
|
||||
private LocalDateTime tempOpeningTime; // If null, not while hygine opening
|
||||
@Column
|
||||
private Integer tempOpeningDuration;
|
||||
@Column
|
||||
private TempOpeningReason tempOpeningReason;
|
||||
@Column
|
||||
private LocalDateTime frozenUntil;
|
||||
|
||||
// --- Aufgaben-System (Basis) ---
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasks;
|
||||
@Column
|
||||
private String currentTask;
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String currentTaskDescription;
|
||||
@Column
|
||||
private LocalDateTime taskUntil;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private TaskMode taskMode = TaskMode.RANDOM;
|
||||
|
||||
// --- Notfall- & Keyholder-Status ---
|
||||
@Column(nullable = false)
|
||||
private boolean keyholderRequestedUnlock = false;
|
||||
@Column
|
||||
private LocalDateTime emergencyUnlockRequestedAt;
|
||||
@Column(nullable = false)
|
||||
private boolean emergencyAutoUnlocked = false;
|
||||
|
||||
// Getter & Setter
|
||||
public TaskMode getTaskMode() {
|
||||
return taskMode != null ? taskMode : TaskMode.RANDOM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface BaseLockRepository extends JpaRepository<BaseLockEntity, UUID>{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteEntity;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
|
||||
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
|
||||
import de.oaa.xxx.games.history.GameHistoryEntity;
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.games.history.GameRole;
|
||||
import de.oaa.xxx.games.history.GameType;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
public abstract class BaseLockService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BaseLockService.class);
|
||||
|
||||
protected final CommunityVerificationVoteRepository communityVerificationVoteRepository;
|
||||
protected final CommunityVerificationRepository communityVerificationRepository;
|
||||
protected final KeyholderVerificationRepository keyholderVerificationRepository;
|
||||
protected final GameHistoryRepository gameHistoryRepository;
|
||||
protected final UserRepository userRepository;
|
||||
protected final KeyholderNotificationRepository keyholderNotificationRepository;
|
||||
protected final SystemMessageService systemMessageService;
|
||||
protected final UnlockCodeHistoryService unlockCodeHistoryService;
|
||||
protected final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
||||
protected final CommunityTaskVoteRepository communityTaskVoteRepository;
|
||||
|
||||
// ── Abstrakte Methoden ────────────────────────────────────────────────────
|
||||
|
||||
protected abstract BaseLockEntity getLock();
|
||||
protected abstract void saveLock();
|
||||
protected abstract GameType getGameType();
|
||||
/** Wie wird Überschreitung der Hygiene-Öffnung bestraft? CardLock friert ein, TimeLock verlängert die Zeit. */
|
||||
protected abstract void applyHygieneOvertime(Long overtime);
|
||||
|
||||
// ── Hook-Methoden (Standard: No-Op) ───────────────────────────────────────
|
||||
|
||||
/** TimeLock: lockControl.unlock() vor dem finalen Entsperren aufrufen. */
|
||||
protected void beforePhysicalUnlock() {}
|
||||
/** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */
|
||||
protected void afterHygieneClosing() {}
|
||||
|
||||
public BaseLockService(
|
||||
CommunityVerificationVoteRepository communityVerificationVoteRepository,
|
||||
CommunityVerificationRepository communityVerificationRepository,
|
||||
KeyholderVerificationRepository keyholderVerificationRepository,
|
||||
GameHistoryRepository gameHistoryRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||
SystemMessageService systemMessageService,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService,
|
||||
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||
CommunityTaskVoteRepository communityTaskVoteRepository) {
|
||||
this.communityVerificationVoteRepository = communityVerificationVoteRepository;
|
||||
this.communityVerificationRepository = communityVerificationRepository;
|
||||
this.keyholderVerificationRepository = keyholderVerificationRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.keyholderNotificationRepository = keyholderNotificationRepository;
|
||||
this.systemMessageService = systemMessageService;
|
||||
this.unlockCodeHistoryService = unlockCodeHistoryService;
|
||||
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
|
||||
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
||||
}
|
||||
|
||||
// ── Gemeinsame Hilfsmethoden ──────────────────────────────────────────────
|
||||
|
||||
protected Long calcOvertime() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
BaseLockEntity lock = getLock();
|
||||
if (lock.getTempOpeningTime() != null && lock.getTempOpeningDuration() != null) {
|
||||
LocalDateTime dueTime = lock.getTempOpeningTime().plusMinutes(lock.getTempOpeningDuration());
|
||||
if (now.isAfter(dueTime)) {
|
||||
return ChronoUnit.MINUTES.between(dueTime, now);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void reportKeyholder(Long overtime) {
|
||||
BaseLockEntity lock = getLock();
|
||||
KeyholderNotificationEntity notification = new KeyholderNotificationEntity();
|
||||
notification.setLockId(lock.getLockId());
|
||||
notification.setLockeeId(lock.getLockee());
|
||||
notification.setKeyholderUserId(lock.getKeyholder());
|
||||
notification.setViolationTime(LocalDateTime.now());
|
||||
notification.setOvertimeMinutes(overtime);
|
||||
keyholderNotificationRepository.save(notification);
|
||||
}
|
||||
|
||||
protected void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl,
|
||||
de.oaa.xxx.social.entity.MessageCause cause) {
|
||||
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
|
||||
}
|
||||
|
||||
// ── Aufgaben ──────────────────────────────────────────────────────────────
|
||||
|
||||
public void task(Task task) {
|
||||
BaseLockEntity lock = getLock();
|
||||
LOGGER.debug("Apply task {}", task);
|
||||
lock.setCurrentTask(task.getTitle());
|
||||
lock.setCurrentTaskDescription(task.getDescription());
|
||||
if (task.getMinutes() != null && task.getMinutes() > 0) {
|
||||
lock.setTaskUntil(LocalDateTime.now().plusMinutes(task.getMinutes()));
|
||||
}
|
||||
}
|
||||
|
||||
public String clearTask() {
|
||||
BaseLockEntity lock = getLock();
|
||||
LOGGER.debug("Clear task");
|
||||
lock.setCurrentTask(null);
|
||||
lock.setCurrentTaskDescription(null);
|
||||
lock.setTaskUntil(null);
|
||||
return "";
|
||||
}
|
||||
|
||||
protected void applyRandomTask() {
|
||||
LOGGER.debug("Apply random task");
|
||||
var tasks = getLock().getTasks();
|
||||
if (tasks != null && !tasks.isEmpty()) {
|
||||
task(tasks.get(new Random().nextInt(tasks.size())));
|
||||
}
|
||||
}
|
||||
|
||||
protected void startKeyholderVote() {
|
||||
BaseLockEntity lock = getLock();
|
||||
KeyholderTaskChoiceEntity choice = new KeyholderTaskChoiceEntity();
|
||||
choice.setLockId(lock.getLockId());
|
||||
choice.setCreatedAt(LocalDateTime.now());
|
||||
choice.setActive(true);
|
||||
choice.setExpiresAt(LocalDateTime.now().plusHours(1));
|
||||
keyholderTaskChoiceRepository.save(choice);
|
||||
userRepository.findById(lock.getKeyholder())
|
||||
.ifPresent(kh -> sendMessage(lock.getLockee(), kh.getUserId(),
|
||||
"Deine Lockee hat eine Aufgaben-Karte gezogen – wähle eine Aufgabe aus.",
|
||||
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE));
|
||||
}
|
||||
|
||||
protected void startCommunityVote() {
|
||||
BaseLockEntity lock = getLock();
|
||||
CommunityTaskVoteEntity vote = new CommunityTaskVoteEntity();
|
||||
vote.setLockId(lock.getLockId());
|
||||
vote.setCreatedAt(LocalDateTime.now());
|
||||
vote.setExpiresAt(LocalDateTime.now().plusHours(1));
|
||||
vote.setActive(true);
|
||||
communityTaskVoteRepository.save(vote);
|
||||
}
|
||||
|
||||
// ── Temporäre Öffnung ─────────────────────────────────────────────────────
|
||||
|
||||
protected void startTempOpening(TempOpeningReason reason, Integer duration) {
|
||||
BaseLockEntity lock = getLock();
|
||||
assert duration != null;
|
||||
lock.setTempOpeningReason(reason);
|
||||
lock.setTempOpeningTime(LocalDateTime.now());
|
||||
lock.setTempOpeningDuration(duration);
|
||||
saveLock();
|
||||
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), reason.toString());
|
||||
}
|
||||
|
||||
public String endHygieneOpening() {
|
||||
BaseLockEntity lock = getLock();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Long overtime = calcOvertime();
|
||||
if (overtime != null) {
|
||||
if (lock.getKeyholder() != null) {
|
||||
reportKeyholder(overtime);
|
||||
}
|
||||
applyHygieneOvertime(overtime);
|
||||
}
|
||||
afterHygieneClosing();
|
||||
lock.setLastHygineOpening(now);
|
||||
lock.setTempOpeningDuration(null);
|
||||
lock.setTempOpeningTime(null);
|
||||
String code = CodeCreator.createNumeric(lock.getUnlockCodeLength());
|
||||
lock.setUnlockCode(code);
|
||||
saveLock();
|
||||
return code;
|
||||
}
|
||||
|
||||
// ── Entsperren ────────────────────────────────────────────────────────────
|
||||
|
||||
public void unlock(String unlockCode) {
|
||||
BaseLockEntity lock = getLock();
|
||||
beforePhysicalUnlock();
|
||||
lock.setUnlockTime(LocalDateTime.now());
|
||||
|
||||
boolean valid = true;
|
||||
if (lock.isEmergencyAutoUnlocked()) {
|
||||
valid = false;
|
||||
LOGGER.debug("Lock invalid - Emergency Auto-Unlock (1h timer)");
|
||||
} else if (lock.isTestLock()) {
|
||||
valid = false;
|
||||
} else if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
|
||||
Set<LocalDate> verifications;
|
||||
if (lock.getKeyholder() != null) {
|
||||
verifications = keyholderVerificationRepository.findByLockId(lock.getLockId()).stream()
|
||||
.filter(v -> v.isValid())
|
||||
.map(v -> v.getVerificationDate())
|
||||
.collect(Collectors.toSet());
|
||||
} else {
|
||||
verifications = communityVerificationRepository.findByLockId(lock.getLockId()).stream()
|
||||
.filter(v -> v.isValid())
|
||||
.map(v -> v.getVerificationDate())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
LocalDate current = lock.getStartTime().toLocalDate();
|
||||
LocalDate last = lock.getUnlockTime().toLocalDate().minusDays(1);
|
||||
while (!current.isAfter(last)) {
|
||||
if (!verifications.contains(current)) {
|
||||
valid = false;
|
||||
LOGGER.debug("Lock invalid - no daily verification on {}", current);
|
||||
break;
|
||||
}
|
||||
current = current.plusDays(1);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("Unlocked at {}", lock.getUnlockTime());
|
||||
saveLock();
|
||||
|
||||
if (valid) {
|
||||
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes();
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameType(getGameType());
|
||||
entry.setGameName(lock.getName());
|
||||
entry.setStartTime(lock.getStartTime());
|
||||
entry.setEndTime(lock.getUnlockTime());
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(lock.getLockee(), GameRole.LOCKEE);
|
||||
if (lock.getKeyholder() != null) {
|
||||
entry.addParticipant(lock.getKeyholder(), GameRole.KEYHOLDER);
|
||||
}
|
||||
gameHistoryRepository.save(entry);
|
||||
|
||||
int minutes = (int) durationMinutes;
|
||||
userRepository.findById(lock.getLockee()).ifPresent(u -> {
|
||||
u.setLockeeXp(u.getLockeeXp() + minutes);
|
||||
userRepository.save(u);
|
||||
});
|
||||
if (lock.getKeyholder() != null) {
|
||||
userRepository.findById(lock.getKeyholder()).ifPresent(u -> {
|
||||
u.setKeyholderXp(u.getKeyholderXp() + minutes);
|
||||
userRepository.save(u);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockTemplateEntity;
|
||||
import de.oaa.xxx.games.chastity.timelock.TimeLockTemplateEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/templates")
|
||||
public class BaseLockTemplateController {
|
||||
|
||||
private final BaseLockTemplateRepository templateRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public BaseLockTemplateController(BaseLockTemplateRepository templateRepository,
|
||||
UserRepository userRepository) {
|
||||
this.templateRepository = templateRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> list(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var pageable = PageRequest.of(page, Math.min(size, 50), Sort.by("name"));
|
||||
var pageResult = templateRepository.findByOwner(myId, pageable);
|
||||
|
||||
var content = pageResult.getContent().stream().map(t -> {
|
||||
Map<String, Object> dto = new LinkedHashMap<>();
|
||||
dto.put("templateId", t.getTemplateId());
|
||||
dto.put("name", t.getName());
|
||||
dto.put("lockType", t instanceof CardlockTemplateEntity ? "CARDLOCK" : "TIMELOCK");
|
||||
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
|
||||
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
|
||||
dto.put("taskCount", t.getTasks() != null ? t.getTasks().size() : 0);
|
||||
dto.put("requiresVerification", t.isRequiresVerification());
|
||||
return dto;
|
||||
}).toList();
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("content", content);
|
||||
response.put("page", pageResult.getNumber());
|
||||
response.put("totalPages", pageResult.getTotalPages());
|
||||
response.put("last", pageResult.isLast());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getById(@PathVariable UUID id, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var opt = templateRepository.findById(id);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var t = opt.get();
|
||||
if (!t.getOwner().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
if (t instanceof CardlockTemplateEntity c) {
|
||||
return ResponseEntity.ok(toCardlockDto(c));
|
||||
} else if (t instanceof TimeLockTemplateEntity tl) {
|
||||
return ResponseEntity.ok(toTimelockDto(tl));
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
private Map<String, Object> toCardlockDto(CardlockTemplateEntity t) {
|
||||
Map<String, Object> dto = new LinkedHashMap<>();
|
||||
dto.put("_type", "CARDLOCK");
|
||||
dto.put("templateId", t.getTemplateId());
|
||||
dto.put("name", t.getName());
|
||||
dto.put("cardCountsMin", t.getCardCountsMin() != null ? t.getCardCountsMin() : Map.of());
|
||||
dto.put("cardCountsMax", t.getCardCountsMax() != null ? t.getCardCountsMax() : Map.of());
|
||||
dto.put("pickEveryMinute", t.getPickEveryMinute());
|
||||
dto.put("accumulatePicks", t.isAccumulatePicks());
|
||||
dto.put("showRemainingCards", t.isShowRemainingCards());
|
||||
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
|
||||
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
|
||||
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
|
||||
dto.put("requiresVerification", t.isRequiresVerification());
|
||||
dto.put("taskCardMode", t.getTaskCardMode());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private Map<String, Object> toTimelockDto(TimeLockTemplateEntity t) {
|
||||
Map<String, Object> dto = new LinkedHashMap<>();
|
||||
dto.put("_type", "TIMELOCK");
|
||||
dto.put("templateId", t.getTemplateId());
|
||||
dto.put("name", t.getName());
|
||||
dto.put("minTimeInMinutes", t.getMinTimeInMinutes());
|
||||
dto.put("maxTimeInMinutes", t.getMaxTimeInMinutes());
|
||||
dto.put("endTimeVisible", t.isEndTimeVisible());
|
||||
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
|
||||
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
|
||||
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
|
||||
dto.put("taskEveryMinutes", t.getTaskEveryMinutes());
|
||||
dto.put("minTasksPerDay", t.getMinTasksPerDay());
|
||||
dto.put("spinningWheelEntries", t.getSpinningWheelEntries() != null ? t.getSpinningWheelEntries() : List.of());
|
||||
dto.put("spinsEveryMinutes", t.getSpinsEveryMinutes());
|
||||
dto.put("minSpinsPerDay", t.getMinSpinsPerDay());
|
||||
dto.put("requiresVerification", t.isRequiresVerification());
|
||||
dto.put("taskMode", t.getTaskCardMode());
|
||||
dto.put("penaltyType", t.getPenaltyType());
|
||||
dto.put("penaltyValue", t.getPenaltyValue());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.DiscriminatorColumn;
|
||||
import jakarta.persistence.DiscriminatorType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Inheritance;
|
||||
import jakarta.persistence.InheritanceType;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "lock_template")
|
||||
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
||||
@DiscriminatorColumn(name = "lock_type", discriminatorType = DiscriminatorType.STRING)
|
||||
public class BaseLockTemplateEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID templateId;
|
||||
@Column(nullable = false)
|
||||
private UUID owner;
|
||||
@Column
|
||||
private String name;
|
||||
@Column
|
||||
private Integer hygineOpeningDurationMinutes;
|
||||
@Column
|
||||
private Integer hygineOpeningEveryMinites;
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasks;
|
||||
@Column
|
||||
private boolean requiresVerification;
|
||||
@Column(nullable = false)
|
||||
private TaskMode taskMode = TaskMode.RANDOM;
|
||||
|
||||
public TaskMode getTaskCardMode() {
|
||||
return taskMode != null ? taskMode : TaskMode.RANDOM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface BaseLockTemplateRepository extends JpaRepository<BaseLockTemplateEntity, UUID>{
|
||||
List<BaseLockTemplateEntity> findByOwner(UUID owner);
|
||||
Page<BaseLockTemplateEntity> findByOwner(UUID owner, Pageable pageable);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public class CodeCreator {
|
||||
return create(digits, CHARS_N);
|
||||
}
|
||||
|
||||
public static String createAlphanumericCode(int digits) {
|
||||
public static String createAlphanumeric(int digits) {
|
||||
return create(digits, CHARS_AN);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface Verification {
|
||||
|
||||
LocalDate getVerificationDate();
|
||||
|
||||
boolean isValid();
|
||||
|
||||
VerificationCommonDTO toCommonVerification();
|
||||
|
||||
String getCode();
|
||||
|
||||
void setCode(String code);
|
||||
|
||||
LocalDateTime getCreatedAt();
|
||||
|
||||
void setCreatedAt(LocalDateTime createdAt);
|
||||
|
||||
UUID getKeyholderId();
|
||||
|
||||
void setKeyholderId(UUID id);
|
||||
|
||||
UUID getLockeeId();
|
||||
|
||||
void setLockeeId(UUID id);
|
||||
|
||||
UUID getLockId();
|
||||
|
||||
void setLockId(UUID id);
|
||||
|
||||
UUID getId();
|
||||
|
||||
void setId(UUID id);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record VerificationCommonDTO(UUID verficationId, UUID lockId, String code,
|
||||
LocalDateTime createdAt, byte[] image, UUID lockeeId, UUID keyholderID) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/games/chastity/community")
|
||||
public class BaseCommunityDisplayController {
|
||||
|
||||
private final BaseCommunityDisplayRepository baseCommunityDisplayRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public BaseCommunityDisplayController(BaseCommunityDisplayRepository baseCommunityDisplayRepository,
|
||||
UserRepository userRepository) {
|
||||
this.baseCommunityDisplayRepository = baseCommunityDisplayRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<BaseCommunityDisplayDTO>> getAllDisplays(
|
||||
@PageableDefault(size = 10, sort = "createdAt", direction = Direction.DESC) Pageable pageable) {
|
||||
Page<BaseCommunityDisplayEntity> page = baseCommunityDisplayRepository.findAll(pageable);
|
||||
|
||||
Set<UUID> lockeeIds = page.getContent().stream()
|
||||
.map(BaseCommunityDisplayEntity::getLockeeId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<UUID, String> nameMap = userRepository.findAllById(lockeeIds).stream()
|
||||
.collect(Collectors.toMap(UserEntity::getUserId, UserEntity::getName));
|
||||
|
||||
Page<BaseCommunityDisplayDTO> result = page.map(e ->
|
||||
e.toBaseCommunityDisplay(nameMap.getOrDefault(e.getLockeeId(), "")));
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record BaseCommunityDisplayDTO(UUID displayId, UUID lockId, LocalDateTime createdAt, UUID lockeeId,
|
||||
UUID keyholderId, String type, String lockeeName) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.DiscriminatorColumn;
|
||||
import jakarta.persistence.DiscriminatorType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Inheritance;
|
||||
import jakarta.persistence.InheritanceType;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "community_display")
|
||||
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
||||
@DiscriminatorColumn(name = "display_type", discriminatorType = DiscriminatorType.STRING)
|
||||
public abstract class BaseCommunityDisplayEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID displayId;
|
||||
@Column(nullable = false)
|
||||
private UUID lockId;
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
@Column(nullable = false)
|
||||
private UUID lockeeId;
|
||||
@Column
|
||||
private UUID keyholderId;
|
||||
|
||||
public abstract String getType();
|
||||
|
||||
public BaseCommunityDisplayDTO toBaseCommunityDisplay(String lockeeName) {
|
||||
return new BaseCommunityDisplayDTO(displayId, lockId, createdAt, lockeeId, keyholderId, getType(), lockeeName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface BaseCommunityDisplayRepository extends JpaRepository<BaseCommunityDisplayEntity, UUID> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/games/chastity/community/pillory")
|
||||
public class CommunityPilloryController {
|
||||
|
||||
private CommunityPilloryRepository repo;
|
||||
|
||||
public CommunityPilloryController(CommunityPilloryRepository repo) {
|
||||
this.repo = repo;
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<CommunityPilloryDTO> getPillory(@PathVariable UUID id) {
|
||||
var pillory = repo.findById(id);
|
||||
if (pillory.isPresent()) {
|
||||
return ResponseEntity.ok(pillory.get().toPillory());
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record CommunityPilloryDTO(UUID displayId, UUID lockId, LocalDateTime createdAt, UUID lockeeId,
|
||||
UUID keyholderId, CommunityPilloryReason reason, String message) {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.DiscriminatorValue;
|
||||
import jakarta.persistence.Entity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@DiscriminatorValue("PILLORY")
|
||||
public class CommunityPilloryEntity extends BaseCommunityDisplayEntity {
|
||||
|
||||
@Column
|
||||
private CommunityPilloryReason reason;
|
||||
@Column
|
||||
private String message;
|
||||
|
||||
@Override
|
||||
public String getType() { return "PILLORY"; }
|
||||
|
||||
public CommunityPilloryDTO toPillory() {
|
||||
return new CommunityPilloryDTO(getDisplayId(), getLockId(), getCreatedAt(), getLockeeId(), getKeyholderId(),
|
||||
reason, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
public enum CommunityPilloryReason {
|
||||
|
||||
HYGIENE_OPENING_EXEEDED,
|
||||
KEYHOLDER_DESCESSION;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface CommunityPilloryRepository extends JpaRepository<CommunityPilloryEntity, UUID> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/games/chastity/community/taskvote")
|
||||
public class CommunityTaskVoteController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final CommunityTaskVoteRepository taskVoteRepository;
|
||||
private final CommunityTaskVoteEntryRepository taskVoteEntryRepository;
|
||||
private final BaseLockRepository baseLockRepository;
|
||||
|
||||
public CommunityTaskVoteController(UserRepository userRepository,
|
||||
CommunityTaskVoteRepository taskVoteRepository,
|
||||
CommunityTaskVoteEntryRepository taskVoteEntryRepository,
|
||||
BaseLockRepository baseLockRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.taskVoteRepository = taskVoteRepository;
|
||||
this.taskVoteEntryRepository = taskVoteEntryRepository;
|
||||
this.baseLockRepository = baseLockRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{displayId}")
|
||||
public ResponseEntity<CommunityTaskVoteDisplayDTO> getVote(@PathVariable UUID displayId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty())
|
||||
return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var voteOpt = taskVoteRepository.findById(displayId);
|
||||
if (voteOpt.isEmpty() || !voteOpt.get().isActive()) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
var vote = voteOpt.get();
|
||||
var lockOpt = baseLockRepository.findById(vote.getLockId());
|
||||
if (lockOpt.isEmpty()) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
var lock = lockOpt.get();
|
||||
boolean isOwnLock = lock.getLockee().equals(myId);
|
||||
var entryList = new ArrayList<CommunityTaskVoteDisplayEntryDTO>();
|
||||
var tasks = lock.getTasks();
|
||||
var userVote = taskVoteEntryRepository.findByDisplayIdAndUserId(displayId, myId);
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
var task = tasks.get(i);
|
||||
boolean myVote = false;
|
||||
int votes = taskVoteEntryRepository.countByDisplayIdAndTaskIndex(displayId, i);
|
||||
if (userVote != null && userVote.getTaskIndex() == i) {
|
||||
myVote = true;
|
||||
}
|
||||
entryList.add(new CommunityTaskVoteDisplayEntryDTO(task.getTitle(),
|
||||
task.getDescription(), task.getMinutes(), votes, myVote));
|
||||
}
|
||||
return ResponseEntity.ok(new CommunityTaskVoteDisplayDTO(displayId, voteOpt.get().getCreatedAt(), displayId, myId, isOwnLock, entryList));
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/{displayId}/vote/{taskIndex}")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> castVote(@PathVariable UUID displayId, @PathVariable int taskIndex,
|
||||
Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty())
|
||||
return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var voteOpt = taskVoteRepository.findById(displayId);
|
||||
if (voteOpt.isEmpty())
|
||||
return ResponseEntity.notFound().build();
|
||||
var vote = voteOpt.get();
|
||||
if (!vote.isActive() || vote.getExpiresAt().isBefore(LocalDateTime.now()))
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
var lockOpt = baseLockRepository.findById(vote.getLockId());
|
||||
if (lockOpt.isEmpty())
|
||||
return ResponseEntity.notFound().build();
|
||||
var lock = lockOpt.get();
|
||||
|
||||
if (lock.getLockee().equals(myId))
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
if (lock.getTasks() == null || taskIndex < 0 || taskIndex >= lock.getTasks().size())
|
||||
return ResponseEntity.badRequest().build();
|
||||
|
||||
if (taskVoteEntryRepository.existsByDisplayIdAndUserId(displayId, myId))
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
CommunityTaskVoteEntryEntity entry = new CommunityTaskVoteEntryEntity();
|
||||
entry.setDisplayId(displayId);
|
||||
entry.setUserId(myId);
|
||||
entry.setTaskIndex(taskIndex);
|
||||
taskVoteEntryRepository.save(entry);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record CommunityTaskVoteDTO(UUID displayId, UUID lockId, LocalDateTime createdAt, UUID lockeeId,
|
||||
UUID keyholderId, boolean active, LocalDateTime expiresAt, int winningTaskIndex, List<CommunityTaskVoteEntryDTO> entries) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record CommunityTaskVoteDisplayDTO(UUID displayId, LocalDateTime createdAt, UUID lockeeId, UUID keyholderId,
|
||||
boolean isOwnLock, List<CommunityTaskVoteDisplayEntryDTO> entries) {
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
public record CommunityTaskVoteDisplayEntryDTO(String title, String description, Integer minutes, Integer votes,
|
||||
boolean ownVote) {
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.DiscriminatorValue;
|
||||
import jakarta.persistence.Entity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@DiscriminatorValue("TASK_VOTE")
|
||||
public class CommunityTaskVoteEntity extends BaseCommunityDisplayEntity {
|
||||
|
||||
@Column
|
||||
private boolean active;
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime expiresAt;
|
||||
/** null until completed */
|
||||
@Column
|
||||
private Integer winningTaskIndex;
|
||||
|
||||
@Override
|
||||
public String getType() { return "TASK_VOTE"; }
|
||||
|
||||
public CommunityTaskVoteDTO toCommunityTaskVote(List<CommunityTaskVoteEntryDTO> entries) {
|
||||
return new CommunityTaskVoteDTO(getDisplayId(), getLockId(), expiresAt, getLockeeId(), getKeyholderId(), active,
|
||||
expiresAt, 0, entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record CommunityTaskVoteEntryDTO(UUID entryId, UUID displayId, UUID userId, int taskIndex) {
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.oaa.xxx.games.chastity.vote;
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
@@ -18,11 +18,15 @@ public class CommunityTaskVoteEntryEntity {
|
||||
private UUID entryId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID voteSessionId;
|
||||
private UUID displayId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID voterUserId;
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int taskIndex;
|
||||
|
||||
public CommunityTaskVoteEntryDTO toCommunityTaskVoteEntry() {
|
||||
return new CommunityTaskVoteEntryDTO(entryId, displayId, userId, taskIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface CommunityTaskVoteEntryRepository extends JpaRepository<CommunityTaskVoteEntryEntity, UUID> {
|
||||
List<CommunityTaskVoteEntryEntity> findByDisplayId(UUID voteSessionId);
|
||||
boolean existsByDisplayIdAndUserId(UUID displayId, UUID userId);
|
||||
CommunityTaskVoteEntryEntity findByDisplayIdAndUserId(UUID displayId, UUID userId);
|
||||
Integer countByDisplayIdAndTaskIndex(UUID displayId, int taskIndex);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface CommunityTaskVoteRepository extends JpaRepository<CommunityTaskVoteEntity, UUID> {
|
||||
List<CommunityTaskVoteEntity> findByActiveTrue();
|
||||
List<CommunityTaskVoteEntity> findByActiveTrueAndExpiresAtBefore(LocalDateTime time);
|
||||
boolean existsByLockIdAndActiveTrue(UUID lockId);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@@ -13,25 +13,24 @@ import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.vote.CommunityTaskVoteEntryRepository;
|
||||
import de.oaa.xxx.games.chastity.vote.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
|
||||
@Component
|
||||
public class TaskVoteScheduler {
|
||||
public class CommunityTaskVoteScheduler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TaskVoteScheduler.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CommunityTaskVoteScheduler.class);
|
||||
|
||||
private final CommunityTaskVoteRepository communityTaskVoteRepository;
|
||||
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final AssignedTaskRepository assignedTaskRepository;
|
||||
private final SystemMessageService systemMessageService;
|
||||
private SystemMessageService systemMessageService;
|
||||
|
||||
public TaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository,
|
||||
public CommunityTaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository,
|
||||
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
|
||||
CardlockRepository cardlockRepository,
|
||||
AssignedTaskRepository assignedTaskRepository,
|
||||
@@ -47,27 +46,26 @@ public class TaskVoteScheduler {
|
||||
@Transactional
|
||||
public void processExpiredVotes() {
|
||||
var expired = communityTaskVoteRepository
|
||||
.findByStatusAndExpiresAtBefore("ACTIVE", LocalDateTime.now());
|
||||
.findByActiveTrueAndExpiresAtBefore(LocalDateTime.now());
|
||||
|
||||
for (var vote : expired) {
|
||||
LOG.debug("Processing expired community task vote {}", vote.getVoteSessionId());
|
||||
LOG.debug("Processing expired community task vote {}", vote.getDisplayId());
|
||||
|
||||
var lockOpt = cardlockRepository.findById(vote.getLockId());
|
||||
if (lockOpt.isEmpty()) {
|
||||
vote.setStatus("COMPLETED");
|
||||
vote.setActive(false);
|
||||
communityTaskVoteRepository.save(vote);
|
||||
continue;
|
||||
}
|
||||
var lock = lockOpt.get();
|
||||
List<Task> tasks = lock.getTasks();
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
vote.setStatus("COMPLETED");
|
||||
vote.setActive(false);
|
||||
communityTaskVoteRepository.save(vote);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stimmen auszählen
|
||||
var entries = communityTaskVoteEntryRepository.findByVoteSessionId(vote.getVoteSessionId());
|
||||
var entries = communityTaskVoteEntryRepository.findByDisplayId(vote.getDisplayId());
|
||||
int winnerIndex;
|
||||
if (entries.isEmpty()) {
|
||||
winnerIndex = new Random().nextInt(tasks.size());
|
||||
@@ -80,7 +78,6 @@ public class TaskVoteScheduler {
|
||||
}
|
||||
}
|
||||
int max = Arrays.stream(counts).max().getAsInt();
|
||||
// Alle Tasks mit Maximalstimmen sammeln → zufällige Auswahl bei Gleichstand
|
||||
List<Integer> winners = new ArrayList<>();
|
||||
for (int i = 0; i < counts.length; i++) {
|
||||
if (counts[i] == max) winners.add(i);
|
||||
@@ -92,22 +89,21 @@ public class TaskVoteScheduler {
|
||||
Task task = tasks.get(winnerIndex);
|
||||
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
||||
assigned.setLockId(lock.getLockId());
|
||||
assigned.setTaskTitle(task.resolveTitle());
|
||||
assigned.setTaskTitle(task.getTitle());
|
||||
assigned.setTaskDescription(task.getDescription());
|
||||
assigned.setTaskText(task.resolveTitle());
|
||||
assigned.setTaskText(task.getTitle());
|
||||
assigned.setTaskMinutes(task.getMinutes());
|
||||
assigned.setAssignedAt(LocalDateTime.now());
|
||||
assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1));
|
||||
assigned.setStatus("PENDING");
|
||||
assignedTaskRepository.save(assigned);
|
||||
|
||||
vote.setStatus("COMPLETED");
|
||||
vote.setActive(false);
|
||||
vote.setWinningTaskIndex(winnerIndex);
|
||||
communityTaskVoteRepository.save(vote);
|
||||
|
||||
// Lockee benachrichtigen
|
||||
sendMessage(lock.getLockee(),
|
||||
"Die Community hat für deine Aufgabe abgestimmt: \"" + task.resolveTitle() + "\"",
|
||||
"Die Community hat für deine Aufgabe abgestimmt: \"" + task.getTitle() + "\"",
|
||||
"/activelock.html?lockId=" + lock.getLockId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/games/chastity/community/verification")
|
||||
@Transactional
|
||||
public class CommunityVerificationController {
|
||||
|
||||
private final CommunityVerificationRepository verificationRepository;
|
||||
private final CommunityVerificationVoteRepository verificationVoteRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public CommunityVerificationController(CommunityVerificationRepository verificationRepository,
|
||||
CommunityVerificationVoteRepository verificationVoteRepository, UserRepository userRepository) {
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{displayId}")
|
||||
public ResponseEntity<CommunityVerificationDTO> get(@PathVariable UUID displayId, Principal principal) {
|
||||
var optional = verificationRepository.findById(displayId);
|
||||
if (optional.isEmpty()) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
var entity = optional.get();
|
||||
|
||||
boolean isOwnLock = false;
|
||||
Boolean myVote = null;
|
||||
if (principal != null) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
if (userOpt.isPresent()) {
|
||||
var myId = userOpt.get().getUserId();
|
||||
isOwnLock = myId.equals(entity.getLockeeId());
|
||||
if (!isOwnLock) {
|
||||
myVote = verificationVoteRepository
|
||||
.findByVerificationIdAndUserId(displayId, myId)
|
||||
.map(CommunityVerificationVoteEntity::isUpvote)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dto = entity.toVerification(isOwnLock, myVote);
|
||||
verificationVoteRepository.findAllByVerificationId(displayId).stream()
|
||||
.map(CommunityVerificationVoteEntity::toVerificationVote)
|
||||
.forEach(dto.votes()::add);
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public ResponseEntity<CommunityVerificationDTO> createVerification() {
|
||||
var verification = new CommunityVerificationEntity();
|
||||
verification.setDisplayId(UUID.randomUUID());
|
||||
verification.setCode(CodeCreator.createAlphanumeric(6));
|
||||
verification.setCreatedAt(LocalDateTime.now());
|
||||
verificationRepository.save(verification);
|
||||
return ResponseEntity.ok(verification.toVerification(true, null));
|
||||
}
|
||||
|
||||
@PutMapping("/{displayId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID displayId, @RequestBody CommunityVerificationDTO dto,
|
||||
Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
var entity = verificationRepository.findById(displayId).orElse(null);
|
||||
if (entity == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (entity.getCreatedAt().isBefore(LocalDateTime.now().minusHours(1))) {
|
||||
return ResponseEntity.status(HttpStatus.GONE).build();
|
||||
}
|
||||
if (dto.image() != null) {
|
||||
entity.setImage(dto.image());
|
||||
}
|
||||
verificationRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{verificationId}/vote/")
|
||||
public ResponseEntity<Void> addVote(@PathVariable UUID verificationId, @RequestBody CommunityVerificationVoteDTO dto,
|
||||
Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
if (!verificationRepository.existsById(verificationId)) return ResponseEntity.notFound().build();
|
||||
|
||||
var vEntity = verificationRepository.findById(verificationId).orElse(null);
|
||||
if (vEntity == null) return ResponseEntity.notFound().build();
|
||||
if (user.getUserId().equals(vEntity.getLockeeId())) return ResponseEntity.status(403).build();
|
||||
if (verificationVoteRepository.findByVerificationIdAndUserId(verificationId, user.getUserId()).isPresent()) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
var vote = new CommunityVerificationVoteEntity();
|
||||
vote.setVoteId(UUID.randomUUID());
|
||||
vote.setVerificationId(verificationId);
|
||||
vote.setUserId(user.getUserId());
|
||||
vote.setUpvote(dto.upvote());
|
||||
if (dto.upvote()) {
|
||||
vEntity.setCountUpvotes(vEntity.getCountUpvotes() + 1);
|
||||
} else {
|
||||
vEntity.setCountDownvotes(vEntity.getCountDownvotes() + 1);
|
||||
}
|
||||
verificationVoteRepository.save(vote);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record CommunityVerificationDTO(
|
||||
UUID displayId, String code, LocalDateTime verificationTime,
|
||||
byte[] image, List<CommunityVerificationVoteDTO> votes, int upvotes, int downvotes,
|
||||
boolean isOwnLock, Boolean myVote) {}
|
||||
@@ -0,0 +1,62 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.Verification;
|
||||
import de.oaa.xxx.games.chastity.common.VerificationCommonDTO;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.DiscriminatorValue;
|
||||
import jakarta.persistence.Entity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@DiscriminatorValue("VERIFICATION")
|
||||
public class CommunityVerificationEntity extends BaseCommunityDisplayEntity implements Verification {
|
||||
|
||||
@Column(nullable = false)
|
||||
private String code;
|
||||
@Column(columnDefinition = "MEDIUMBLOB")
|
||||
private byte[] image;
|
||||
@Column
|
||||
private int countUpvotes;
|
||||
@Column
|
||||
private int countDownvotes;
|
||||
|
||||
@Override
|
||||
public String getType() { return "VERIFICATION"; }
|
||||
|
||||
public CommunityVerificationDTO toVerification(boolean isOwnLock, Boolean myVote) {
|
||||
return new CommunityVerificationDTO(getDisplayId(), code, getCreatedAt(), image, new ArrayList<>(), countUpvotes, countDownvotes, isOwnLock, myVote);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate getVerificationDate() {
|
||||
return getCreatedAt().toLocalDate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return countUpvotes > countDownvotes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VerificationCommonDTO toCommonVerification() {
|
||||
return new VerificationCommonDTO(getDisplayId(), getLockId(), code, getCreatedAt(), image, getLockeeId(),
|
||||
getKeyholderId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getId() {
|
||||
return getDisplayId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(UUID id) {
|
||||
setDisplayId(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface CommunityVerificationRepository extends JpaRepository<CommunityVerificationEntity, UUID> {
|
||||
|
||||
org.springframework.data.domain.Page<CommunityVerificationEntity> findAllByImageIsNotNull(Pageable pageable);
|
||||
|
||||
java.util.List<CommunityVerificationEntity> findByLockId(UUID lockId);
|
||||
|
||||
java.util.List<CommunityVerificationEntity> findByLockIdAndCreatedAtBetweenAndImageIsNotNull(UUID lockId, java.time.LocalDateTime from, java.time.LocalDateTime to);
|
||||
|
||||
java.util.List<CommunityVerificationEntity> findByLockIdAndCreatedAtBetweenAndImageIsNull(UUID lockId, java.time.LocalDateTime from, java.time.LocalDateTime to);
|
||||
|
||||
org.springframework.data.domain.Page<CommunityVerificationEntity> findByKeyholderIdIsNullAndCreatedAtBetweenAndImageIsNotNull(
|
||||
java.time.LocalDateTime from, java.time.LocalDateTime to, org.springframework.data.domain.Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record CommunityVerificationVoteDTO (UUID voteId, UUID userId, boolean upvote) {}
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.oaa.xxx.games.chastity.verification;
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -13,7 +13,7 @@ import lombok.Setter;
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name="verification_vote")
|
||||
public class VerificationVoteEntity {
|
||||
public class CommunityVerificationVoteEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
@@ -25,7 +25,7 @@ public class VerificationVoteEntity {
|
||||
@Column(nullable = false)
|
||||
private boolean upvote;
|
||||
|
||||
public VerificationVoteDTO toVerificationVote() {
|
||||
return new VerificationVoteDTO(voteId, userId, upvote);
|
||||
public CommunityVerificationVoteDTO toVerificationVote() {
|
||||
return new CommunityVerificationVoteDTO(voteId, userId, upvote);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.oaa.xxx.games.chastity.community;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface CommunityVerificationVoteRepository extends JpaRepository<CommunityVerificationVoteEntity, UUID> {
|
||||
|
||||
List<CommunityVerificationVoteEntity> findAllByVerificationId(UUID verificationId);
|
||||
|
||||
java.util.Optional<CommunityVerificationVoteEntity> findByVerificationIdAndUserId(UUID verificationId, UUID userId);
|
||||
|
||||
void deleteAllByVerificationId(UUID verificationId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +1,52 @@
|
||||
package de.oaa.xxx.games.chastity.cardlock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.vote.CommunityTaskVoteEntryEntity;
|
||||
import de.oaa.xxx.games.chastity.vote.CommunityTaskVoteEntryRepository;
|
||||
import de.oaa.xxx.games.chastity.vote.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
package de.oaa.xxx.games.chastity.keyholder;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/task-card")
|
||||
public class TaskCardController {
|
||||
@RequestMapping("/games/chastity/keyholder/choices")
|
||||
public class KeyholderTaskChoiceController {
|
||||
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
||||
private final CommunityTaskVoteRepository communityTaskVoteRepository;
|
||||
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
|
||||
private final AssignedTaskRepository assignedTaskRepository;
|
||||
private final SystemMessageService systemMessageService;
|
||||
|
||||
public TaskCardController(CardlockRepository cardlockRepository,
|
||||
public KeyholderTaskChoiceController(CardlockRepository cardlockRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||
CommunityTaskVoteRepository communityTaskVoteRepository,
|
||||
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
|
||||
AssignedTaskRepository assignedTaskRepository,
|
||||
SystemMessageService systemMessageService) {
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
|
||||
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
||||
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
|
||||
this.assignedTaskRepository = assignedTaskRepository;
|
||||
this.systemMessageService = systemMessageService;
|
||||
}
|
||||
|
||||
|
||||
// ── Keyholder: ausstehende Aufgaben-Karten-Entscheidungen ─────────────────
|
||||
|
||||
@GetMapping("/keyholder/choices")
|
||||
@GetMapping
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<List<Map<String, Object>>> getPendingChoices(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
@@ -59,7 +58,7 @@ public class TaskCardController {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
|
||||
for (var lock : locks) {
|
||||
var pending = keyholderTaskChoiceRepository.findByLockIdAndStatus(lock.getLockId(), "PENDING");
|
||||
var pending = keyholderTaskChoiceRepository.findByLockIdAndActiveTrue(lock.getLockId());
|
||||
if (pending.isEmpty()) continue;
|
||||
|
||||
var lockee = userRepository.findById(lock.getLockee()).orElse(null);
|
||||
@@ -81,7 +80,7 @@ public class TaskCardController {
|
||||
|
||||
record PenaltyRequest(Integer penaltyFreezeMinutes, Integer penaltyRedCards) {}
|
||||
|
||||
@PostMapping("/keyholder/choices/{choiceId}/choose/{taskIndex}")
|
||||
@PostMapping("/{choiceId}/choose/{taskIndex}")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> chooseTask(@PathVariable UUID choiceId,
|
||||
@PathVariable int taskIndex,
|
||||
@@ -100,7 +99,7 @@ public class TaskCardController {
|
||||
var lock = lockOpt.get();
|
||||
|
||||
if (!myId.equals(lock.getKeyholder())) return ResponseEntity.status(403).build();
|
||||
if (!"PENDING".equals(choice.getStatus())) return ResponseEntity.status(409).build();
|
||||
if (!choice.isActive()) return ResponseEntity.status(409).build();
|
||||
|
||||
List<Task> tasks = lock.getTasks();
|
||||
if (tasks == null || taskIndex < 0 || taskIndex >= tasks.size())
|
||||
@@ -109,9 +108,9 @@ public class TaskCardController {
|
||||
Task task = tasks.get(taskIndex);
|
||||
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
||||
assigned.setLockId(lock.getLockId());
|
||||
assigned.setTaskTitle(task.resolveTitle());
|
||||
assigned.setTaskTitle(task.getTitle());
|
||||
assigned.setTaskDescription(task.getDescription());
|
||||
assigned.setTaskText(task.resolveTitle());
|
||||
assigned.setTaskText(task.getTitle());
|
||||
assigned.setTaskMinutes(task.getMinutes());
|
||||
assigned.setAssignedAt(LocalDateTime.now());
|
||||
assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1));
|
||||
@@ -122,7 +121,7 @@ public class TaskCardController {
|
||||
}
|
||||
assignedTaskRepository.save(assigned);
|
||||
|
||||
choice.setStatus("CHOSEN");
|
||||
choice.setActive(false);
|
||||
keyholderTaskChoiceRepository.save(choice);
|
||||
|
||||
sendMessage(myId, lock.getLockee(),
|
||||
@@ -132,94 +131,6 @@ public class TaskCardController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Community: aktive Abstimmungen ────────────────────────────────────────
|
||||
|
||||
@GetMapping("/community/votes")
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<List<Map<String, Object>>> getActiveVotes(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var activeVotes = communityTaskVoteRepository.findByStatus("ACTIVE");
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
|
||||
for (var vote : activeVotes) {
|
||||
if (vote.isTestLock()) continue;
|
||||
var lockOpt = cardlockRepository.findById(vote.getLockId());
|
||||
if (lockOpt.isEmpty()) continue;
|
||||
var lock = lockOpt.get();
|
||||
|
||||
var lockee = userRepository.findById(lock.getLockee()).orElse(null);
|
||||
List<Task> tasks = lock.getTasks();
|
||||
if (tasks == null || tasks.isEmpty()) continue;
|
||||
|
||||
List<Map<String, Object>> taskList = buildTaskList(tasks);
|
||||
|
||||
// Stimmenanzahl pro Task
|
||||
List<Integer> voteCounts = new ArrayList<>();
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
voteCounts.add(communityTaskVoteEntryRepository
|
||||
.countByVoteSessionIdAndTaskIndex(vote.getVoteSessionId(), i));
|
||||
}
|
||||
|
||||
Integer myVote = communityTaskVoteEntryRepository.findByVoteSessionId(vote.getVoteSessionId())
|
||||
.stream().filter(e -> myId.equals(e.getVoterUserId()))
|
||||
.map(CommunityTaskVoteEntryEntity::getTaskIndex).findFirst().orElse(null);
|
||||
|
||||
boolean isOwnLock = lock.getLockee().equals(myId);
|
||||
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("voteSessionId", vote.getVoteSessionId().toString());
|
||||
m.put("lockId", lock.getLockId().toString());
|
||||
m.put("lockeeName", lockee != null ? lockee.getName() : "");
|
||||
m.put("createdAt", vote.getCreatedAt().toString());
|
||||
m.put("expiresAt", vote.getExpiresAt().toString());
|
||||
m.put("tasks", taskList);
|
||||
m.put("voteCounts", voteCounts);
|
||||
m.put("myVote", isOwnLock ? "own" : myVote);
|
||||
m.put("isOwnLock", isOwnLock);
|
||||
result.add(m);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/community/votes/{voteSessionId}/vote/{taskIndex}")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> castVote(@PathVariable UUID voteSessionId,
|
||||
@PathVariable int taskIndex,
|
||||
Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var voteOpt = communityTaskVoteRepository.findById(voteSessionId);
|
||||
if (voteOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var vote = voteOpt.get();
|
||||
|
||||
if (!"ACTIVE".equals(vote.getStatus()) || vote.getExpiresAt().isBefore(LocalDateTime.now()))
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
var lockOpt = cardlockRepository.findById(vote.getLockId());
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var lock = lockOpt.get();
|
||||
|
||||
if (lock.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
if (lock.getTasks() == null || taskIndex < 0 || taskIndex >= lock.getTasks().size())
|
||||
return ResponseEntity.badRequest().build();
|
||||
|
||||
if (communityTaskVoteEntryRepository.existsByVoteSessionIdAndVoterUserId(voteSessionId, myId))
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
CommunityTaskVoteEntryEntity entry = new CommunityTaskVoteEntryEntity();
|
||||
entry.setVoteSessionId(voteSessionId);
|
||||
entry.setVoterUserId(myId);
|
||||
entry.setTaskIndex(taskIndex);
|
||||
communityTaskVoteEntryRepository.save(entry);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -230,7 +141,7 @@ public class TaskCardController {
|
||||
Task t = tasks.get(i);
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("index", i);
|
||||
m.put("title", t.resolveTitle());
|
||||
m.put("title", t.getTitle());
|
||||
m.put("description", t.getDescription() != null ? t.getDescription() : "");
|
||||
m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
|
||||
list.add(m);
|
||||
@@ -20,10 +20,12 @@ public class KeyholderTaskChoiceEntity {
|
||||
@Column(nullable = false)
|
||||
private UUID lockId;
|
||||
|
||||
/** PENDING | CHOSEN */
|
||||
@Column(nullable = false)
|
||||
private String status = "PENDING";
|
||||
private boolean active = true;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column
|
||||
private LocalDateTime expiresAt;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.keyholder;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface KeyholderTaskChoiceRepository extends JpaRepository<KeyholderTaskChoiceEntity, UUID> {
|
||||
List<KeyholderTaskChoiceEntity> findByLockIdAndStatus(UUID lockId, String status);
|
||||
List<KeyholderTaskChoiceEntity> findByLockIdAndActiveTrue(UUID lockId);
|
||||
List<KeyholderTaskChoiceEntity> findByActiveTrueAndExpiresAtBefore(LocalDateTime time);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package de.oaa.xxx.games.chastity.keyholder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
|
||||
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
|
||||
@Component
|
||||
public class KeyholderTaskChoiceScheduler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(KeyholderTaskChoiceScheduler.class);
|
||||
|
||||
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final AssignedTaskRepository assignedTaskRepository;
|
||||
private final SystemMessageService systemMessageService;
|
||||
|
||||
public KeyholderTaskChoiceScheduler(KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||
CardlockRepository cardlockRepository,
|
||||
AssignedTaskRepository assignedTaskRepository,
|
||||
SystemMessageService systemMessageService) {
|
||||
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
this.assignedTaskRepository = assignedTaskRepository;
|
||||
this.systemMessageService = systemMessageService;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 60_000)
|
||||
@Transactional
|
||||
public void processExpiredChoices() {
|
||||
var expired = keyholderTaskChoiceRepository
|
||||
.findByActiveTrueAndExpiresAtBefore(LocalDateTime.now());
|
||||
|
||||
for (var choice : expired) {
|
||||
LOG.debug("Processing expired keyholder task choice {}", choice.getChoiceId());
|
||||
|
||||
var lockOpt = cardlockRepository.findById(choice.getLockId());
|
||||
if (lockOpt.isEmpty()) {
|
||||
choice.setActive(false);
|
||||
keyholderTaskChoiceRepository.save(choice);
|
||||
continue;
|
||||
}
|
||||
var lock = lockOpt.get();
|
||||
List<Task> tasks = lock.getTasks();
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
choice.setActive(false);
|
||||
keyholderTaskChoiceRepository.save(choice);
|
||||
continue;
|
||||
}
|
||||
|
||||
int taskIndex = new Random().nextInt(tasks.size());
|
||||
Task task = tasks.get(taskIndex);
|
||||
LOG.debug("Keyholder did not choose in time → random task index {}", taskIndex);
|
||||
|
||||
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
||||
assigned.setLockId(lock.getLockId());
|
||||
assigned.setTaskTitle(task.getTitle());
|
||||
assigned.setTaskDescription(task.getDescription());
|
||||
assigned.setTaskText(task.getTitle());
|
||||
assigned.setTaskMinutes(task.getMinutes());
|
||||
assigned.setAssignedAt(LocalDateTime.now());
|
||||
assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1));
|
||||
assigned.setStatus("PENDING");
|
||||
assignedTaskRepository.save(assigned);
|
||||
|
||||
choice.setActive(false);
|
||||
keyholderTaskChoiceRepository.save(choice);
|
||||
|
||||
sendMessage(lock.getLockee(),
|
||||
"Dein Keyholder hat nicht rechtzeitig gewählt – eine zufällige Aufgabe wurde vergeben: \"" + task.getTitle() + "\"",
|
||||
"/activelock.html?lockId=" + lock.getLockId());
|
||||
|
||||
if (lock.getKeyholder() != null) {
|
||||
sendMessage(lock.getKeyholder(),
|
||||
"Du hast die Aufgabenwahl für \"" + (lock.getName() != null ? lock.getName() : "ein Schloss") + "\" nicht rechtzeitig getroffen. Eine zufällige Aufgabe wurde vergeben.",
|
||||
"/keyholder.html");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage(UUID toId, String text, String targetUrl) {
|
||||
systemMessageService.send(toId, toId, text, targetUrl, MessageCause.GAME_STATE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.oaa.xxx.games.chastity.keyholder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.Verification;
|
||||
import de.oaa.xxx.games.chastity.common.VerificationCommonDTO;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "keyholder_verification")
|
||||
public class KeyholderVerificationEntity implements Verification {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID verificationId;
|
||||
@Column(nullable = false)
|
||||
private UUID lockId;
|
||||
@Column(nullable = false)
|
||||
private String code;
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
@Column(columnDefinition = "MEDIUMBLOB")
|
||||
private byte[] image;
|
||||
@Column(nullable = false)
|
||||
private UUID lockeeId;
|
||||
@Column(nullable = false)
|
||||
private UUID keyholderId;
|
||||
@Column
|
||||
private boolean accepted;
|
||||
|
||||
@Override
|
||||
public LocalDate getVerificationDate() {
|
||||
return createdAt.toLocalDate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return accepted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VerificationCommonDTO toCommonVerification() {
|
||||
return new VerificationCommonDTO(verificationId, lockId, code, createdAt, image, lockeeId, keyholderId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getId() {
|
||||
return verificationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(UUID id) {
|
||||
setVerificationId(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.oaa.xxx.games.chastity.keyholder;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface KeyholderVerificationRepository extends JpaRepository<KeyholderVerificationEntity, UUID>{
|
||||
|
||||
java.util.List<KeyholderVerificationEntity> findByLockId(UUID lockId);
|
||||
}
|
||||
@@ -2,12 +2,12 @@ package de.oaa.xxx.games.chastity.lockcontroll;
|
||||
|
||||
public class TTLockControl extends LockControl {
|
||||
|
||||
// private static final String BASE_URL = "https://euapi.ttlock.com/";
|
||||
|
||||
public TTLockControl() {
|
||||
super(new NoInteractionCallback());
|
||||
}
|
||||
|
||||
private static final String BASE_URL = "https://euapi.ttlock.com/";
|
||||
|
||||
@Override
|
||||
public boolean init() {
|
||||
// TODO Auto-generated method stub
|
||||
@@ -25,8 +25,5 @@ public class TTLockControl extends LockControl {
|
||||
// TODO Auto-generated method stub
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class UnlockcodeLockControl extends LockControl {
|
||||
|
||||
@Override
|
||||
public boolean lock() {
|
||||
var code = CodeCreator.createAlphanumericCode(callback.getUnlockcodeLenght());
|
||||
var code = CodeCreator.createNumeric(callback.getUnlockcodeLenght());
|
||||
callback.setUnlockCode(code);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -21,7 +22,12 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockRepository;
|
||||
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
||||
import de.oaa.xxx.games.chastity.timelock.TimeLockEntity;
|
||||
import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@@ -31,6 +37,8 @@ public class LockeeInvitationController {
|
||||
|
||||
private final LockeeInvitationRepository lockeeInvitationRepository;
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final BaseLockRepository baseLockRepository;
|
||||
private final TimeLockRepository timeLockRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final SystemMessageService systemMessageService;
|
||||
|
||||
@@ -41,10 +49,14 @@ public class LockeeInvitationController {
|
||||
|
||||
public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository,
|
||||
CardlockRepository cardlockRepository,
|
||||
BaseLockRepository baseLockRepository,
|
||||
TimeLockRepository timeLockRepository,
|
||||
UserRepository userRepository,
|
||||
SystemMessageService systemMessageService) {
|
||||
this.lockeeInvitationRepository = lockeeInvitationRepository;
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
this.baseLockRepository = baseLockRepository;
|
||||
this.timeLockRepository = timeLockRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.systemMessageService = systemMessageService;
|
||||
}
|
||||
@@ -59,6 +71,12 @@ public class LockeeInvitationController {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private int randomBetween(Integer min, Integer max) {
|
||||
if (max == null) max = 60;
|
||||
if (min == null || min >= max) return max;
|
||||
return min + new Random().nextInt(max - min);
|
||||
}
|
||||
|
||||
@GetMapping("/invitations/mine")
|
||||
public ResponseEntity<List<Map<String, Object>>> getMyInvitations(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
@@ -68,7 +86,7 @@ public class LockeeInvitationController {
|
||||
var invitations = lockeeInvitationRepository.findByLockeeUserId(myId);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (var inv : invitations) {
|
||||
var lockOpt = cardlockRepository.findById(inv.getLockId());
|
||||
var lockOpt = baseLockRepository.findById(inv.getLockId());
|
||||
if (lockOpt.isEmpty()) continue;
|
||||
var lock = lockOpt.get();
|
||||
if (lock.getStartTime() != null) continue; // already accepted
|
||||
@@ -97,7 +115,7 @@ public class LockeeInvitationController {
|
||||
var invitations = lockeeInvitationRepository.findByKeyholderUserId(myId);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (var inv : invitations) {
|
||||
var lockOpt = cardlockRepository.findById(inv.getLockId());
|
||||
var lockOpt = baseLockRepository.findById(inv.getLockId());
|
||||
if (lockOpt.isEmpty()) continue;
|
||||
var lock = lockOpt.get();
|
||||
if (lock.getStartTime() != null) continue; // already accepted
|
||||
@@ -130,12 +148,12 @@ public class LockeeInvitationController {
|
||||
var inv = invOpt.get();
|
||||
if (!inv.getKeyholderUserId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var lockOpt = cardlockRepository.findById(inv.getLockId());
|
||||
var lockOpt = baseLockRepository.findById(inv.getLockId());
|
||||
lockeeInvitationRepository.delete(inv);
|
||||
|
||||
if (lockOpt.isPresent()) {
|
||||
var lock = lockOpt.get();
|
||||
cardlockRepository.delete(lock);
|
||||
baseLockRepository.delete(lock);
|
||||
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
|
||||
sendMessage(myId, inv.getLockeeUserId(),
|
||||
me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
|
||||
@@ -156,7 +174,7 @@ public class LockeeInvitationController {
|
||||
var inv = invOpt.get();
|
||||
if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var lockOpt = cardlockRepository.findById(inv.getLockId());
|
||||
var lockOpt = baseLockRepository.findById(inv.getLockId());
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var lock = lockOpt.get();
|
||||
|
||||
@@ -173,18 +191,18 @@ public class LockeeInvitationController {
|
||||
result.put("createdAt", inv.getCreatedAt().toString());
|
||||
result.put("detailsVisible", inv.isDetailsVisible());
|
||||
|
||||
if (inv.isDetailsVisible() && lock.getInitialCards() != null) {
|
||||
Map<String, Long> cardCounts = lock.getInitialCards().stream()
|
||||
if (inv.isDetailsVisible() && lock instanceof CardLockEntity cardLock && cardLock.getInitialCards() != null) {
|
||||
Map<String, Long> cardCounts = cardLock.getInitialCards().stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(
|
||||
c -> c.name(), java.util.stream.Collectors.counting()));
|
||||
result.put("cardCounts", cardCounts);
|
||||
result.put("pickEveryMinute", lock.getPickEveryMinute());
|
||||
result.put("accumulatePicks", lock.isAccumulatePicks());
|
||||
result.put("showRemainingCards", lock.isShowRemainingCards());
|
||||
result.put("hygineOpeningEveryMinites", lock.getHygineOpeningEveryMinites());
|
||||
result.put("hygineOpeningDurationMinutes", lock.getHygineOpeningDurationMinutes());
|
||||
result.put("requiresVerification", lock.isRequiresVerification());
|
||||
result.put("taskCount", lock.getTasks() != null ? lock.getTasks().size() : 0);
|
||||
result.put("pickEveryMinute", cardLock.getPickEveryMinute());
|
||||
result.put("accumulatePicks", cardLock.isAccumulatePicks());
|
||||
result.put("showRemainingCards", cardLock.isShowRemainingCards());
|
||||
result.put("hygineOpeningEveryMinites", cardLock.getHygineOpeningEveryMinites());
|
||||
result.put("hygineOpeningDurationMinutes", cardLock.getHygineOpeningDurationMinutes());
|
||||
result.put("requiresVerification", cardLock.isRequiresVerification());
|
||||
result.put("taskCount", cardLock.getTasks() != null ? cardLock.getTasks().size() : 0);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
@@ -207,30 +225,52 @@ public class LockeeInvitationController {
|
||||
var inv = invOpt.get();
|
||||
if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var lockOpt = cardlockRepository.findById(inv.getLockId());
|
||||
var lockOpt = baseLockRepository.findById(inv.getLockId());
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var lock = lockOpt.get();
|
||||
if (lock.getStartTime() != null) return ResponseEntity.status(409).body(Map.of("error", "already_accepted"));
|
||||
if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId))
|
||||
|
||||
if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId)
|
||||
|| timeLockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId))
|
||||
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
||||
|
||||
int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) ? req.unlockCodeLines() : 5;
|
||||
String unlockCode = generateUnlockCode(codeLines);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
lock.setStartTime(now);
|
||||
lock.setUnlockCode(unlockCode);
|
||||
lock.setUnlockCodeLength(codeLines);
|
||||
lock.setAvailableCards(new ArrayList<>(lock.getInitialCards()));
|
||||
lock.setOpenPicks(0);
|
||||
lock.setNextCardIn(now.plusMinutes(lock.getPickEveryMinute()));
|
||||
if (lock.getHygineOpeningEveryMinites() != null) {
|
||||
lock.setLastHygineOpening(now);
|
||||
|
||||
String unlockCode;
|
||||
String lockName;
|
||||
|
||||
if (lock instanceof CardLockEntity cardLock) {
|
||||
unlockCode = generateUnlockCode(codeLines);
|
||||
cardLock.setStartTime(now);
|
||||
cardLock.setUnlockCode(unlockCode);
|
||||
cardLock.setUnlockCodeLength(codeLines);
|
||||
cardLock.setAvailableCards(new ArrayList<>(cardLock.getInitialCards()));
|
||||
cardLock.setOpenPicks(0);
|
||||
cardLock.setNextCardIn(now.plusMinutes(cardLock.getPickEveryMinute()));
|
||||
if (cardLock.getHygineOpeningEveryMinites() != null) {
|
||||
cardLock.setLastHygineOpening(now);
|
||||
}
|
||||
cardlockRepository.save(cardLock);
|
||||
lockName = cardLock.getName() != null && !cardLock.getName().isBlank() ? cardLock.getName() : "Unbenanntes Lock";
|
||||
} else if (lock instanceof TimeLockEntity timeLock) {
|
||||
unlockCode = CodeCreator.createNumeric(codeLines);
|
||||
int unlockMinutes = randomBetween(timeLock.getMinTimeInMinutes(), timeLock.getMaxTimeInMinutes());
|
||||
timeLock.setStartTime(now);
|
||||
timeLock.setUnlockTime(now.plusMinutes(unlockMinutes));
|
||||
timeLock.setUnlockCode(unlockCode);
|
||||
timeLock.setUnlockCodeLength(codeLines);
|
||||
if (timeLock.getHygineOpeningEveryMinites() != null) {
|
||||
timeLock.setLastHygineOpening(now);
|
||||
}
|
||||
timeLockRepository.save(timeLock);
|
||||
lockName = timeLock.getName() != null && !timeLock.getName().isBlank() ? timeLock.getName() : "Unbenanntes Lock";
|
||||
} else {
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
cardlockRepository.save(lock);
|
||||
|
||||
lockeeInvitationRepository.delete(inv);
|
||||
|
||||
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
|
||||
sendMessage(myId, inv.getKeyholderUserId(),
|
||||
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.",
|
||||
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
@@ -254,12 +294,12 @@ public class LockeeInvitationController {
|
||||
var inv = invOpt.get();
|
||||
if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var lockOpt = cardlockRepository.findById(inv.getLockId());
|
||||
var lockOpt = baseLockRepository.findById(inv.getLockId());
|
||||
lockeeInvitationRepository.delete(inv);
|
||||
|
||||
if (lockOpt.isPresent()) {
|
||||
var lock = lockOpt.get();
|
||||
cardlockRepository.delete(lock);
|
||||
baseLockRepository.delete(lock);
|
||||
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
|
||||
sendMessage(myId, inv.getKeyholderUserId(),
|
||||
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.pillory;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "pillory")
|
||||
public class PilloryEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
@Setter(lombok.AccessLevel.NONE)
|
||||
private UUID pilloryId;
|
||||
@Column(nullable = false)
|
||||
private UUID lockId;
|
||||
@Column(nullable = false)
|
||||
private UUID lockeeUserId;
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
@Column
|
||||
private PilloryReason reason;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.pillory;
|
||||
|
||||
public enum PilloryReason {
|
||||
|
||||
HYGIENE_OPENING_EXEEDED,
|
||||
KEYHOLDER_DESCESSION;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.pillory;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PilloryRepository extends JpaRepository<PilloryEntity, UUID> {
|
||||
|
||||
}
|
||||
@@ -11,16 +11,6 @@ public class Task {
|
||||
private String description;
|
||||
private Integer minutes;
|
||||
|
||||
/** @deprecated Backward-Compat – alte Einträge ohne title/description. Nur lesen, nicht setzen. */
|
||||
private String text;
|
||||
|
||||
/** Gibt den anzeigbaren Titel zurück – fällt auf altes text-Feld zurück. */
|
||||
public String resolveTitle() {
|
||||
if (title != null && !title.isBlank()) return title;
|
||||
if (text != null && !text.isBlank()) return text;
|
||||
return "Aufgabe";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Task[title=" + title + ", minutes=" + minutes + "]";
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
package de.oaa.xxx.games.chastity.timelock;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteEntity;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationEntity;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationRepository;
|
||||
import de.oaa.xxx.games.chastity.lockcontroll.LockControllType;
|
||||
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationEntity;
|
||||
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationRepository;
|
||||
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry;
|
||||
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/keyholder")
|
||||
public class TimeLockController {
|
||||
|
||||
private final TimeLockRepository timeLockRepository;
|
||||
private final TimeLockTemplateRepository templateRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final KeyholderInvitationRepository invitationRepository;
|
||||
private final LockeeInvitationRepository lockeeInvitationRepository;
|
||||
private final SystemMessageService systemMessageService;
|
||||
private final TimeLockServiceFactory timeLockServiceFactory;
|
||||
private final CommunityVerificationRepository verificationRepository;
|
||||
private final CommunityVerificationVoteRepository verificationVoteRepository;
|
||||
|
||||
public TimeLockController(TimeLockRepository timeLockRepository,
|
||||
TimeLockTemplateRepository templateRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderInvitationRepository invitationRepository,
|
||||
LockeeInvitationRepository lockeeInvitationRepository,
|
||||
SystemMessageService systemMessageService,
|
||||
TimeLockServiceFactory timeLockServiceFactory,
|
||||
CommunityVerificationRepository verificationRepository,
|
||||
CommunityVerificationVoteRepository verificationVoteRepository) {
|
||||
this.timeLockRepository = timeLockRepository;
|
||||
this.templateRepository = templateRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.invitationRepository = invitationRepository;
|
||||
this.lockeeInvitationRepository = lockeeInvitationRepository;
|
||||
this.systemMessageService = systemMessageService;
|
||||
this.timeLockServiceFactory = timeLockServiceFactory;
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
}
|
||||
|
||||
// ── Erstellen ────────────────────────────────────────────────────────────────
|
||||
|
||||
record CreateTimeLockRequest(
|
||||
UUID templateId,
|
||||
UUID lockeeUserId,
|
||||
boolean lockeeDetailsVisible,
|
||||
UUID keyholder,
|
||||
boolean testLock,
|
||||
Integer unlockCodeLength,
|
||||
LockControllType controllType
|
||||
) {}
|
||||
|
||||
@PostMapping("/timelock")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> createTimeLock(
|
||||
@RequestBody CreateTimeLockRequest req, Principal principal) {
|
||||
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var me = meOpt.get();
|
||||
UUID myId = me.getUserId();
|
||||
|
||||
var templateOpt = templateRepository.findById(req.templateId());
|
||||
if (templateOpt.isEmpty()) return ResponseEntity.badRequest().build();
|
||||
var template = templateOpt.get();
|
||||
|
||||
int codeLen = (req.unlockCodeLength() != null && req.unlockCodeLength() >= 1) ? req.unlockCodeLength() : 5;
|
||||
boolean friendLockee = req.lockeeUserId() != null && !req.lockeeUserId().equals(myId);
|
||||
|
||||
if (friendLockee) {
|
||||
var lockeeOpt = userRepository.findById(req.lockeeUserId());
|
||||
if (lockeeOpt.isEmpty()) return ResponseEntity.badRequest().build();
|
||||
var lockee = lockeeOpt.get();
|
||||
|
||||
TimeLockEntity lock = buildBaseEntity(template, myId, req.lockeeUserId(), false);
|
||||
lock.setStartTime(null);
|
||||
lock.setUnlockTime(null);
|
||||
timeLockRepository.save(lock);
|
||||
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
LockeeInvitationEntity inv = new LockeeInvitationEntity();
|
||||
inv.setLockId(lock.getLockId());
|
||||
inv.setLockeeUserId(lockee.getUserId());
|
||||
inv.setKeyholderUserId(myId);
|
||||
inv.setToken(token);
|
||||
inv.setCreatedAt(LocalDateTime.now());
|
||||
inv.setDetailsVisible(req.lockeeDetailsVisible());
|
||||
lockeeInvitationRepository.save(inv);
|
||||
|
||||
String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock";
|
||||
systemMessageService.send(myId, lockee.getUserId(),
|
||||
me.getName() + " hat dich als Lockee für das Lock „" + lockName + "\" eingeladen.",
|
||||
"/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"lockId", lock.getLockId().toString(),
|
||||
"lockeeInvitationSent", true));
|
||||
}
|
||||
|
||||
if (timeLockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(myId))
|
||||
return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
|
||||
|
||||
TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings(
|
||||
req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE,
|
||||
myId, req.keyholder(), req.testLock(), codeLen);
|
||||
TimeLockEntity lock = new TimeLockEntity();
|
||||
timeLockServiceFactory.create(lock).init(template, settings);
|
||||
timeLockRepository.save(lock); // Sicherstellen dass auch TRUST-Locks persistiert sind
|
||||
|
||||
boolean keyholderPending = false;
|
||||
if (req.keyholder() != null) {
|
||||
var khOpt = userRepository.findById(req.keyholder());
|
||||
if (khOpt.isPresent()) {
|
||||
var kh = khOpt.get();
|
||||
KeyholderInvitationEntity inv = new KeyholderInvitationEntity();
|
||||
inv.setLockId(lock.getLockId());
|
||||
inv.setKeyholderUserId(kh.getUserId());
|
||||
inv.setLockeeUserId(myId);
|
||||
inv.setToken(UUID.randomUUID().toString().replace("-", ""));
|
||||
inv.setCreatedAt(LocalDateTime.now());
|
||||
invitationRepository.save(inv);
|
||||
|
||||
String lockName = template.getName() != null ? template.getName() : "Unbenanntes Lock";
|
||||
systemMessageService.send(myId, kh.getUserId(),
|
||||
me.getName() + " hat dich als Keyholder*In für das Lock „" + lockName + "\" eingeladen.",
|
||||
"/einladungen.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
|
||||
|
||||
keyholderPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"lockId", lock.getLockId().toString(),
|
||||
"unlockCode", lock.getUnlockCode() != null ? lock.getUnlockCode() : "",
|
||||
"keyholderPending", keyholderPending));
|
||||
}
|
||||
|
||||
// ── State abrufen ────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/timelock/{lockId}")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> getTimeLock(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// Periodic check (daily violations)
|
||||
TimeLockService service = timeLockServiceFactory.create(l);
|
||||
service.check();
|
||||
|
||||
// Auto-unfreeze when frozenUntil has passed
|
||||
if (l.getFrozenUntil() != null && l.getFrozenUntil().isBefore(now) && l.getFrozenFrom() != null) {
|
||||
service.unfreeze();
|
||||
l.setFrozenFrom(null);
|
||||
l.setFrozenUntil(null);
|
||||
timeLockRepository.save(l);
|
||||
}
|
||||
|
||||
// Emergency auto-unlock after 1h
|
||||
if (l.getEmergencyUnlockRequestedAt() != null && !l.isKeyholderRequestedUnlock()
|
||||
&& l.getEmergencyUnlockRequestedAt().isBefore(now.minusHours(1))) {
|
||||
l.setEmergencyAutoUnlocked(true);
|
||||
l.setKeyholderRequestedUnlock(true);
|
||||
timeLockRepository.save(l);
|
||||
}
|
||||
|
||||
boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now);
|
||||
boolean isFrozen = l.getFrozenFrom() != null
|
||||
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
||||
|
||||
// Hygiene state
|
||||
boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null;
|
||||
boolean hygieneOpeningDue = false;
|
||||
long hygieneSecondsRemaining = 0;
|
||||
boolean hygieneOpeningActive = l.getTempOpeningTime() != null
|
||||
&& TempOpeningReason.HYGIENE == l.getTempOpeningReason();
|
||||
if (hygieneEnabled && !hygieneOpeningActive) {
|
||||
LocalDateTime lastH = l.getLastHygineOpening();
|
||||
if (lastH == null) {
|
||||
hygieneOpeningDue = true;
|
||||
} else {
|
||||
LocalDateTime nextH = lastH.plusMinutes(l.getHygineOpeningEveryMinites());
|
||||
long secs = ChronoUnit.SECONDS.between(now, nextH);
|
||||
if (secs <= 0) hygieneOpeningDue = true;
|
||||
else hygieneSecondsRemaining = secs;
|
||||
}
|
||||
}
|
||||
|
||||
// Spin wheel state
|
||||
boolean spinEnabled = l.getSpinsEveryMinutes() != null
|
||||
&& l.getSpinningWheelEntries() != null && !l.getSpinningWheelEntries().isEmpty();
|
||||
boolean spinDue = false;
|
||||
String nextSpinIn = null;
|
||||
if (spinEnabled) {
|
||||
List<LocalDateTime> times = l.getSpinningWheelTimes();
|
||||
if (times == null || times.isEmpty()) {
|
||||
spinDue = true;
|
||||
} else {
|
||||
LocalDateTime next = times.get(times.size() - 1).plusMinutes(l.getSpinsEveryMinutes());
|
||||
if (next.isBefore(now)) spinDue = true;
|
||||
else nextSpinIn = next.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Task timing state
|
||||
boolean taskTimingEnabled = l.getTaskEveryMinutes() != null;
|
||||
String nextTaskIn = null;
|
||||
if (taskTimingEnabled && l.getCurrentTask() == null) {
|
||||
List<LocalDateTime> times = l.getTaskTimes();
|
||||
LocalDateTime next;
|
||||
if (times == null || times.isEmpty()) {
|
||||
next = l.getStartTime() != null
|
||||
? l.getStartTime().plusMinutes(l.getTaskEveryMinutes()) : null;
|
||||
} else {
|
||||
next = times.get(times.size() - 1).plusMinutes(l.getTaskEveryMinutes());
|
||||
}
|
||||
if (next != null && next.isAfter(now)) nextTaskIn = next.toString();
|
||||
}
|
||||
|
||||
// Keyholder info
|
||||
boolean keyholderInvitationPending =
|
||||
l.getKeyholder() == null && !invitationRepository.findByLockId(lockId).isEmpty();
|
||||
|
||||
// Verification state
|
||||
boolean verificationDue = false;
|
||||
String verificationPendingId = null;
|
||||
String verificationPendingCode = null;
|
||||
long verificationUpvotes = 0;
|
||||
long verificationDownvotes = 0;
|
||||
if (l.isRequiresVerification()) {
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
var completed = verificationRepository
|
||||
.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId, todayStart, todayEnd);
|
||||
if (!completed.isEmpty()) {
|
||||
var todayV = completed.get(0);
|
||||
var votes = verificationVoteRepository.findAllByVerificationId(todayV.getDisplayId());
|
||||
verificationUpvotes = votes.stream().filter(CommunityVerificationVoteEntity::isUpvote).count();
|
||||
verificationDownvotes = votes.stream().filter(v -> !v.isUpvote()).count();
|
||||
} else {
|
||||
verificationDue = true;
|
||||
var pending = verificationRepository
|
||||
.findByLockIdAndCreatedAtBetweenAndImageIsNull(lockId, todayStart, todayEnd);
|
||||
if (!pending.isEmpty()) {
|
||||
verificationPendingId = pending.get(0).getDisplayId().toString();
|
||||
verificationPendingCode = pending.get(0).getCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("lockId", l.getLockId().toString());
|
||||
result.put("name", l.getName() != null ? l.getName() : "");
|
||||
result.put("testLock", l.isTestLock());
|
||||
result.put("startTime", l.getStartTime() != null ? l.getStartTime().toString() : null);
|
||||
result.put("endTimeVisible", l.isEndTimeVisible());
|
||||
result.put("timeUp", timeUp);
|
||||
result.put("isFrozen", isFrozen);
|
||||
result.put("frozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
|
||||
|
||||
// Only expose unlock time if end time is visible OR time is up
|
||||
if (l.isEndTimeVisible() || timeUp) {
|
||||
result.put("unlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null);
|
||||
} else {
|
||||
result.put("unlockTime", null);
|
||||
}
|
||||
|
||||
result.put("currentTask", l.getCurrentTask());
|
||||
result.put("currentTaskDescription", l.getCurrentTaskDescription());
|
||||
result.put("taskUntil", l.getTaskUntil() != null ? l.getTaskUntil().toString() : null);
|
||||
|
||||
result.put("spinEnabled", spinEnabled);
|
||||
result.put("spinDue", spinDue);
|
||||
result.put("nextSpinIn", nextSpinIn);
|
||||
|
||||
result.put("taskTimingEnabled", taskTimingEnabled);
|
||||
result.put("nextTaskIn", nextTaskIn);
|
||||
|
||||
result.put("hygieneEnabled", hygieneEnabled);
|
||||
result.put("hygieneOpeningDue", hygieneOpeningDue);
|
||||
result.put("hygieneSecondsRemaining", hygieneSecondsRemaining);
|
||||
result.put("hygieneOpeningActive", hygieneOpeningActive);
|
||||
result.put("hygieneOpeningStarted", l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null);
|
||||
result.put("hygieneDurationMinutes", l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0);
|
||||
|
||||
result.put("verificationRequired", l.isRequiresVerification());
|
||||
result.put("verificationDue", verificationDue);
|
||||
result.put("verificationPendingId", verificationPendingId);
|
||||
result.put("verificationPendingCode", verificationPendingCode);
|
||||
result.put("verificationUpvotes", verificationUpvotes);
|
||||
result.put("verificationDownvotes", verificationDownvotes);
|
||||
|
||||
result.put("hasKeyholder", l.getKeyholder() != null);
|
||||
result.put("keyholderInvitationPending", keyholderInvitationPending);
|
||||
if (l.getKeyholder() != null) {
|
||||
userRepository.findById(l.getKeyholder()).ifPresent(kh -> {
|
||||
result.put("keyholderName", kh.getName());
|
||||
result.put("keyholderUserId", kh.getUserId().toString());
|
||||
result.put("keyholderProfilePic", kh.getProfilePicture());
|
||||
});
|
||||
}
|
||||
|
||||
result.put("keyholderRequestedUnlock", l.isKeyholderRequestedUnlock());
|
||||
if (l.isKeyholderRequestedUnlock() || l.isTestLock()) {
|
||||
result.put("unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "");
|
||||
}
|
||||
result.put("emergencyUnlockRequested", l.getEmergencyUnlockRequestedAt() != null);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── Glücksrad drehen ─────────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/timelock/{lockId}/spin")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> spin(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
if (l.getUnlockTime() == null) return ResponseEntity.status(409).build(); // not started
|
||||
if (l.getSpinningWheelEntries() == null || l.getSpinningWheelEntries().isEmpty())
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
boolean isFrozen = l.getFrozenFrom() != null
|
||||
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
||||
if (isFrozen) return ResponseEntity.status(409).body(Map.of("error", "frozen"));
|
||||
if (TempOpeningReason.HYGIENE == l.getTempOpeningReason())
|
||||
return ResponseEntity.status(409).body(Map.of("error", "hygiene_opening"));
|
||||
|
||||
// Check spin is due
|
||||
List<LocalDateTime> spinTimes = l.getSpinningWheelTimes();
|
||||
if (spinTimes != null && !spinTimes.isEmpty() && l.getSpinsEveryMinutes() != null) {
|
||||
LocalDateTime next = spinTimes.get(spinTimes.size() - 1).plusMinutes(l.getSpinsEveryMinutes());
|
||||
if (next.isAfter(now)) return ResponseEntity.status(409).body(Map.of("error", "not_due"));
|
||||
}
|
||||
|
||||
TimeLockService service = timeLockServiceFactory.create(l);
|
||||
SpinningWheelEntry entry = service.spinWheel();
|
||||
if (entry == null) return ResponseEntity.status(409).body(Map.of("error", "no_entry"));
|
||||
|
||||
// Record spin time
|
||||
if (l.getSpinningWheelTimes() == null) l.setSpinningWheelTimes(new ArrayList<>());
|
||||
l.getSpinningWheelTimes().add(now);
|
||||
timeLockRepository.save(l);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("type", entry.getType().name());
|
||||
result.put("intVal", entry.getIntVal());
|
||||
result.put("stringVal", entry.getStringVal());
|
||||
// Include updated lock time fields
|
||||
result.put("newUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : null);
|
||||
result.put("newFrozenUntil", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
|
||||
result.put("isFrozen", l.getFrozenFrom() != null);
|
||||
result.put("currentTask", l.getCurrentTask());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── Aufgabe erledigt ──────────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/timelock/{lockId}/task/done")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> taskDone(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
if (l.getCurrentTask() == null) return ResponseEntity.status(409).build();
|
||||
|
||||
// Check task timer hasn't expired (still locked)
|
||||
if (l.getTaskUntil() != null && l.getTaskUntil().isAfter(LocalDateTime.now()))
|
||||
return ResponseEntity.status(409).body(null);
|
||||
|
||||
timeLockServiceFactory.create(l).clearTask();
|
||||
timeLockRepository.save(l);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Hygiene-Öffnung starten ───────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/timelock/{lockId}/hygiene/start")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> startHygiene(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
if (l.getHygineOpeningEveryMinites() == null) return ResponseEntity.status(409).build();
|
||||
if (l.getTempOpeningTime() != null) return ResponseEntity.status(409).body(Map.of("error", "already_open"));
|
||||
|
||||
TimeLockService service = timeLockServiceFactory.create(l);
|
||||
service.startHygieneOpening();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "",
|
||||
"durationMinutes", l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0,
|
||||
"openedAt", l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : ""));
|
||||
}
|
||||
|
||||
// ── Hygiene-Öffnung beenden ───────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/timelock/{lockId}/hygiene/end")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> endHygiene(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
if (l.getTempOpeningTime() == null || TempOpeningReason.HYGIENE != l.getTempOpeningReason())
|
||||
return ResponseEntity.status(409).build();
|
||||
|
||||
TimeLockService service = timeLockServiceFactory.create(l);
|
||||
String newCode = service.endHygieneOpening();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"newUnlockCode", newCode,
|
||||
"newUnlockTime", l.getUnlockTime() != null ? l.getUnlockTime().toString() : ""));
|
||||
}
|
||||
|
||||
// ── Verifikation starten ─────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/timelock/{lockId}/verification/start")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> startVerification(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var dto = timeLockServiceFactory.create(l).startVerification();
|
||||
return ResponseEntity.ok(Map.of("verificationId", dto.verficationId().toString(), "code", dto.code()));
|
||||
}
|
||||
|
||||
// ── Verifikation abschließen ──────────────────────────────────────────────────
|
||||
|
||||
@PostMapping(value = "/timelock/{lockId}/verification/{verificationId}/complete",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Transactional
|
||||
public ResponseEntity<Void> completeVerification(
|
||||
@PathVariable UUID lockId,
|
||||
@PathVariable UUID verificationId,
|
||||
@RequestParam MultipartFile image,
|
||||
Principal principal) throws IOException {
|
||||
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
boolean found = timeLockServiceFactory.create(l).completeVerification(verificationId, scaleImage(image.getBytes(), 1024));
|
||||
if (!found) return ResponseEntity.notFound().build();
|
||||
|
||||
if (l.getKeyholder() != null) {
|
||||
systemMessageService.send(myId, l.getKeyholder(),
|
||||
"📸 " + meOpt.get().getName() + " hat eine Verifikation eingereicht.",
|
||||
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Lock beenden (Zeit abgelaufen / Test-Lock) ────────────────────────────────
|
||||
|
||||
@DeleteMapping("/timelock/{lockId}")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> endLock(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
boolean timeUp = l.getUnlockTime() != null && l.getUnlockTime().isBefore(now);
|
||||
boolean isFrozen = l.getFrozenFrom() != null
|
||||
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
|
||||
|
||||
if (!l.isTestLock() && !timeUp && !l.isKeyholderRequestedUnlock()) {
|
||||
return ResponseEntity.status(409).build(); // Not yet unlockable
|
||||
}
|
||||
if (isFrozen && !l.isTestLock()) {
|
||||
return ResponseEntity.status(409).body(null); // Frozen
|
||||
}
|
||||
|
||||
TimeLockService service = timeLockServiceFactory.create(l);
|
||||
service.unlock(l.getUnlockCode());
|
||||
|
||||
// Clean up verifications
|
||||
var verifications = verificationRepository.findByLockId(lockId);
|
||||
verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getDisplayId()));
|
||||
verificationRepository.deleteAll(verifications);
|
||||
invitationRepository.deleteByLockId(lockId);
|
||||
timeLockRepository.deleteById(lockId);
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Notfall-Entsperrung ───────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/timelock/{lockId}/emergency-unlock")
|
||||
@Transactional
|
||||
public ResponseEntity<?> requestEmergencyUnlock(@PathVariable UUID lockId, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
var me = meOpt.get();
|
||||
UUID myId = me.getUserId();
|
||||
|
||||
var lockOpt = timeLockRepository.findById(lockId);
|
||||
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
var l = lockOpt.get();
|
||||
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
|
||||
if (l.isTestLock()) return ResponseEntity.badRequest().build();
|
||||
if (l.getEmergencyUnlockRequestedAt() != null) return ResponseEntity.status(409).build();
|
||||
|
||||
l.setEmergencyUnlockRequestedAt(LocalDateTime.now());
|
||||
if (l.getKeyholder() == null) {
|
||||
l.setEmergencyAutoUnlocked(true);
|
||||
l.setKeyholderRequestedUnlock(true);
|
||||
} else {
|
||||
systemMessageService.send(myId, l.getKeyholder(),
|
||||
"⚠️ NOTFALL: " + me.getName() + " bittet dringend um Freigabe des Locks. Bitte reagiere innerhalb einer Stunde.",
|
||||
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.EMERGENCY);
|
||||
}
|
||||
timeLockRepository.save(l);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────────
|
||||
|
||||
private TimeLockEntity buildBaseEntity(TimeLockTemplateEntity template, UUID keyholder, UUID lockee, boolean testLock) {
|
||||
TimeLockEntity lock = new TimeLockEntity();
|
||||
lock.setName(template.getName());
|
||||
lock.setLockee(lockee);
|
||||
lock.setKeyholder(keyholder);
|
||||
lock.setRequiresVerification(template.isRequiresVerification());
|
||||
lock.setTestLock(testLock);
|
||||
lock.setEndTimeVisible(template.isEndTimeVisible());
|
||||
lock.setTasks(template.getTasks());
|
||||
lock.setTaskMode(template.getTaskCardMode());
|
||||
lock.setTaskEveryMinutes(template.getTaskEveryMinutes());
|
||||
lock.setMinTasksPerDay(template.getMinTasksPerDay());
|
||||
lock.setSpinningWheelEntries(template.getSpinningWheelEntries());
|
||||
lock.setSpinsEveryMinutes(template.getSpinsEveryMinutes());
|
||||
lock.setMinSpinsPerDay(template.getMinSpinsPerDay());
|
||||
lock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes());
|
||||
lock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites());
|
||||
lock.setPenaltyType(template.getPenaltyType());
|
||||
lock.setPenaltyValue(template.getPenaltyValue());
|
||||
lock.setMinTimeInMinutes(template.getMinTimeInMinutes());
|
||||
lock.setMaxTimeInMinutes(template.getMaxTimeInMinutes());
|
||||
return lock;
|
||||
}
|
||||
|
||||
|
||||
private byte[] scaleImage(byte[] input, int maxSize) throws IOException {
|
||||
BufferedImage original = ImageIO.read(new ByteArrayInputStream(input));
|
||||
if (original == null) return input;
|
||||
int w = original.getWidth(), h = original.getHeight();
|
||||
if (w <= maxSize && h <= maxSize) return input;
|
||||
double scale = (double) maxSize / Math.max(w, h);
|
||||
int newW = (int) (w * scale), newH = (int) (h * scale);
|
||||
BufferedImage scaled = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = scaled.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(original, 0, 0, newW, newH, null);
|
||||
g.dispose();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ImageIO.write(scaled, "jpeg", out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -3,115 +3,60 @@ package de.oaa.xxx.games.chastity.timelock;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockEntity;
|
||||
import de.oaa.xxx.games.chastity.common.PenaltyType;
|
||||
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelConverter;
|
||||
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.DiscriminatorValue;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "time_lock")
|
||||
public class TimeLockEntity {
|
||||
@DiscriminatorValue("TIMELOCK")
|
||||
public class TimeLockEntity extends BaseLockEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID lockId;
|
||||
@Column
|
||||
private String name;
|
||||
// Settings
|
||||
@Column(nullable = false)
|
||||
private UUID lockee;
|
||||
@Column
|
||||
private UUID keyholder;
|
||||
@Column
|
||||
private LocalDateTime startTime;
|
||||
@Column
|
||||
private LocalDateTime unlockTime;
|
||||
@Column
|
||||
private boolean endTimeVisible;
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasks;
|
||||
@Column
|
||||
private Integer minTimeInMinutes;
|
||||
@Column
|
||||
private Integer maxTimeInMinutes;
|
||||
|
||||
@Column
|
||||
private Integer taskEveryMinutes;
|
||||
@Column
|
||||
private Integer minTasksPerDay;
|
||||
|
||||
@Convert(converter = SpinningWheelConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<SpinningWheelEntry> spinningWheelEntries;
|
||||
@Column
|
||||
private Integer hygineOpeningDurationMinutes;
|
||||
@Column
|
||||
private Integer hygineOpeningEveryMinites;
|
||||
@Column
|
||||
private boolean requiresVerification;
|
||||
@Column
|
||||
private boolean testLock;
|
||||
@Column
|
||||
private Integer unlockCodeLength;
|
||||
@Column(nullable = false)
|
||||
private TaskMode taskMode = TaskMode.RANDOM;
|
||||
@Column
|
||||
private Integer spinsEveryMinutes;
|
||||
@Column
|
||||
private Integer minSpinsPerDay;
|
||||
|
||||
@Column
|
||||
private PenaltyType penaltyType;
|
||||
@Column
|
||||
private Integer penaltyValue;
|
||||
|
||||
@Column
|
||||
private LocalDateTime lastHygineOpening;
|
||||
@Column
|
||||
private LocalDateTime tempOpeningTime; // If null, not while hygine opening
|
||||
@Column
|
||||
private Integer tempOpeningDuration;
|
||||
@Column
|
||||
private TempOpeningReason tempOpeningReason;
|
||||
|
||||
@Column
|
||||
private LocalDateTime frozenFrom;
|
||||
@Column
|
||||
private LocalDateTime frozenUntil;
|
||||
@Column
|
||||
private String currentTask;
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String currentTaskDescription;
|
||||
@Column
|
||||
private LocalDateTime taskUntil;
|
||||
@Column
|
||||
private String unlockCode;
|
||||
/** Keyholder hat Unlock angefordert – nächste Aktion der Lockee zeigt grüne Karte */
|
||||
@Column(nullable = false)
|
||||
private boolean keyholderRequestedUnlock = false;
|
||||
/** Lockee hat Notfall-Entsperrung angefordert */
|
||||
@Column
|
||||
private java.time.LocalDateTime emergencyUnlockRequestedAt;
|
||||
/** true = System hat automatisch entsperrt (Keyholderin nicht reagiert) */
|
||||
@Column(nullable = false)
|
||||
private boolean emergencyAutoUnlocked = false;
|
||||
|
||||
@Column
|
||||
private List<LocalDateTime> taskTimes;
|
||||
@Column
|
||||
private List<LocalDateTime> spinningWheelTimes;
|
||||
@Column
|
||||
private LocalDate lastCheck;
|
||||
|
||||
public TaskMode getTaskMode() { return taskMode != null ? taskMode : TaskMode.RANDOM; }
|
||||
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TimeLockRepository extends JpaRepository<TimeLockEntity, UUID> {
|
||||
|
||||
boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,381 +1,372 @@
|
||||
package de.oaa.xxx.games.chastity.timelock;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.AbstractLockService;
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockEntity;
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockService;
|
||||
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity;
|
||||
import de.oaa.xxx.games.chastity.common.VerificationCommonDTO;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityPilloryEntity;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityPilloryReason;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityPilloryRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationEntity;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationEntity;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.lockcontroll.LockControl;
|
||||
import de.oaa.xxx.games.chastity.lockcontroll.LockControlCallback;
|
||||
import de.oaa.xxx.games.chastity.lockcontroll.TTLockControl;
|
||||
import de.oaa.xxx.games.chastity.lockcontroll.TrustLockControl;
|
||||
import de.oaa.xxx.games.chastity.lockcontroll.UnlockcodeLockControl;
|
||||
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
|
||||
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
|
||||
import de.oaa.xxx.games.history.GameHistoryEntity;
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.games.history.GameType;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
public class TimeLockService extends AbstractLockService implements LockControlCallback {
|
||||
public class TimeLockService extends BaseLockService implements LockControlCallback {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TimeLockService.class);
|
||||
private final TimeLockEntity lock;
|
||||
private final TimeLockRepository timeLockRepository;
|
||||
private final VerificationRepository verificationRepository;
|
||||
private final GameHistoryRepository gameHistoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final KeyholderNotificationRepository keyholderNotificationRepository;
|
||||
private final UnlockCodeHistoryService unlockCodeHistoryService;
|
||||
|
||||
private LockControl lockControl;
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TimeLockService.class);
|
||||
private final TimeLockEntity lock;
|
||||
private final TimeLockRepository timeLockRepository;
|
||||
private final CommunityPilloryRepository pilloryRepository;
|
||||
|
||||
public TimeLockService(TimeLockEntity lock, VerificationRepository verificationRepository,
|
||||
VerificationVoteRepository verificationVoteRepository,
|
||||
private LockControl lockControl;
|
||||
|
||||
public TimeLockService(TimeLockEntity lock,
|
||||
CommunityVerificationRepository verificationRepository,
|
||||
CommunityVerificationVoteRepository verificationVoteRepository,
|
||||
TimeLockRepository timeLockRepository,
|
||||
GameHistoryRepository gameHistoryRepository,
|
||||
UserRepository userRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService) {
|
||||
super(verificationVoteRepository);
|
||||
this.lock = lock;
|
||||
this.timeLockRepository = timeLockRepository;
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.keyholderNotificationRepository = keyholderNotificationRepository;
|
||||
this.unlockCodeHistoryService = unlockCodeHistoryService;
|
||||
}
|
||||
|
||||
public void init(TimeLockTemplate template, TimeLockAdditionalSettings settings) {
|
||||
switch (settings.controllType()) {
|
||||
case TTLOCK -> lockControl = new TTLockControl();
|
||||
case TRUST -> lockControl = new TrustLockControl();
|
||||
case UNLOCK_CODE -> lockControl = new UnlockcodeLockControl(this);
|
||||
}
|
||||
|
||||
lock.setLockee(UUID.randomUUID());
|
||||
lock.setName(template.name());
|
||||
lock.setLockee(settings.lockee());
|
||||
lock.setKeyholder(settings.keyholder());
|
||||
lock.setRequiresVerification(template.requiresVerification());
|
||||
lock.setTestLock(settings.testlock());
|
||||
lock.setUnlockCodeLength(settings.unlockCodeLength());
|
||||
|
||||
lock.setStartTime(LocalDateTime.now());
|
||||
Integer unlockTimeMinutes = template.maxTimeInMinutes();
|
||||
if (template.minTimeInMinutes() != null) {
|
||||
unlockTimeMinutes = new Random().nextInt(template.minTimeInMinutes(), template.maxTimeInMinutes());
|
||||
}
|
||||
lock.setUnlockTime(LocalDateTime.now().plusMinutes(unlockTimeMinutes));
|
||||
lock.setEndTimeVisible(template.endTimeVisible());
|
||||
|
||||
lock.setTasks(template.tasks());
|
||||
lock.setTaskEveryMinutes(template.taskEveryMinutes());
|
||||
lock.setMinTasksPerDay(template.minTasksPerDay());
|
||||
|
||||
lock.setSpinningWheelEntries(template.spinningWheelEntries());
|
||||
lock.setSpinsEveryMinutes(template.spinsEveryMinutes());
|
||||
lock.setMinSpinsPerDay(template.minSpinsPerDay());
|
||||
|
||||
lock.setHygineOpeningDurationMinutes(template.hygineOpeningDurationMinutes());
|
||||
lock.setHygineOpeningEveryMinites(template.hygineOpeningEveryMinites());
|
||||
|
||||
lock.setTaskMode(template.taskMode());
|
||||
|
||||
lockControl.lock();
|
||||
}
|
||||
|
||||
public SpinningWheelEntry spinWheel() {
|
||||
if (TempOpeningReason.HYGIENE != lock.getTempOpeningReason() ) {
|
||||
var entries = lock.getSpinningWheelEntries();
|
||||
var entry = entries.get(new Random().nextInt(entries.size()));
|
||||
entry.getType().apply(this, entry.getIntVal(), entry.getStringVal());
|
||||
return entry;
|
||||
}
|
||||
// Nicht während der Hyhiene Öffnung
|
||||
return null;
|
||||
}
|
||||
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||
KeyholderVerificationRepository keyholderVerificationRepository,
|
||||
CommunityTaskVoteRepository communityTaskVoteRepository,
|
||||
CommunityPilloryRepository pilloryRepository,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService,
|
||||
SystemMessageService systemMessageService) {
|
||||
super(verificationVoteRepository, verificationRepository, keyholderVerificationRepository,
|
||||
gameHistoryRepository, userRepository, keyholderNotificationRepository,
|
||||
systemMessageService, unlockCodeHistoryService,
|
||||
keyholderTaskChoiceRepository, communityTaskVoteRepository);
|
||||
this.lock = lock;
|
||||
this.timeLockRepository = timeLockRepository;
|
||||
this.pilloryRepository = pilloryRepository;
|
||||
}
|
||||
|
||||
public void addTime(Integer intVal) {
|
||||
LOGGER.debug("Lock addTime: %s minutes", intVal);
|
||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(intVal));
|
||||
}
|
||||
// ── Abstract method implementations ──────────────────────────────────────
|
||||
|
||||
public void removeTime(Integer intVal) {
|
||||
LOGGER.debug("Lock removeTime: %s minutes", intVal);
|
||||
lock.setUnlockTime(lock.getUnlockTime().minusMinutes(intVal));
|
||||
}
|
||||
@Override
|
||||
protected BaseLockEntity getLock() {
|
||||
return lock;
|
||||
}
|
||||
|
||||
public void freeze(Integer intVal) {
|
||||
LOGGER.debug("Lock frozen for %s minutes", intVal);
|
||||
lock.setFrozenFrom(LocalDateTime.now());
|
||||
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(intVal));
|
||||
}
|
||||
@Override
|
||||
protected void saveLock() {
|
||||
timeLockRepository.save(lock);
|
||||
}
|
||||
|
||||
public void freeze() {
|
||||
LOGGER.debug("Lock frozen");
|
||||
lock.setFrozenFrom(LocalDateTime.now());
|
||||
}
|
||||
@Override
|
||||
protected GameType getGameType() {
|
||||
return GameType.TIMELOCK;
|
||||
}
|
||||
|
||||
public void unfreeze() {
|
||||
if (lock.getFrozenFrom() != null) {
|
||||
var unfreeTime = lock.getFrozenUntil() != null ? lock.getFrozenUntil() : LocalDateTime.now();
|
||||
var diff = ChronoUnit.MINUTES.between(lock.getFrozenFrom(), unfreeTime);
|
||||
LOGGER.debug("Lock unfrozen - adding %s minutes to the lock", diff);
|
||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(diff));
|
||||
} else {
|
||||
LOGGER.debug("Lock not frozen - ignore Call");
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected void applyHygieneOvertime(Long overtime) {
|
||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(overtime * 4));
|
||||
}
|
||||
|
||||
public void task() {
|
||||
if (TempOpeningReason.HYGIENE != lock.getTempOpeningReason() ) {
|
||||
switch (lock.getTaskMode()) {
|
||||
case TaskMode.RANDOM -> applyRandomTask();
|
||||
case TaskMode.KEYHOLDER -> startKeyHolderVote();
|
||||
case TaskMode.COMMUNITY -> startCommunityVode();
|
||||
}
|
||||
}
|
||||
// Nicht während der Hyhiene Öffnung
|
||||
}
|
||||
// ── Hook overrides ────────────────────────────────────────────────────────
|
||||
|
||||
private void startKeyHolderVote() {
|
||||
// Keyholder Vote starten
|
||||
}
|
||||
@Override
|
||||
protected void beforePhysicalUnlock() {
|
||||
lockControl.unlock();
|
||||
}
|
||||
|
||||
private void startCommunityVode() {
|
||||
// Community Vote starten
|
||||
}
|
||||
@Override
|
||||
protected void afterHygieneClosing() {
|
||||
lockControl.lock();
|
||||
}
|
||||
|
||||
public void applyRandomTask() {
|
||||
LOGGER.debug("Apply random task");
|
||||
var tasks = lock.getTasks();
|
||||
if (!tasks.isEmpty()) {
|
||||
task(tasks.get(new Random().nextInt(tasks.size())));
|
||||
}
|
||||
}
|
||||
// ── Initialisation ────────────────────────────────────────────────────────
|
||||
|
||||
public void task(Task task) {
|
||||
LOGGER.debug("Apply task {}", task);
|
||||
lock.setCurrentTask(task.resolveTitle());
|
||||
lock.setCurrentTaskDescription(task.getDescription());
|
||||
if (task.getMinutes() != null && task.getMinutes() > 0) {
|
||||
lock.setTaskUntil(LocalDateTime.now().plusMinutes(task.getMinutes()));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Initialisiert ein neues Lock anhand eines Template und Laufzeit-Einstellungen.
|
||||
* Ruft am Ende lockControl.lock() auf – bei UNLOCK_CODE wird dabei der Entsperrcode
|
||||
* generiert und das Lock bereits persistiert.
|
||||
*/
|
||||
public void init(TimeLockTemplateEntity template, TimeLockAdditionalSettings settings) {
|
||||
switch (settings.controllType()) {
|
||||
case TTLOCK -> lockControl = new TTLockControl();
|
||||
case TRUST -> lockControl = new TrustLockControl();
|
||||
case UNLOCK_CODE -> lockControl = new UnlockcodeLockControl(this);
|
||||
}
|
||||
|
||||
public void text(Integer intVal, String stringVal) {
|
||||
LOGGER.debug("Apply text {}", stringVal);
|
||||
lock.setCurrentTask(stringVal);
|
||||
if (intVal != null && intVal > 0) {
|
||||
lock.setTaskUntil(LocalDateTime.now().plusMinutes(intVal));
|
||||
}
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
lock.setStartTime(now);
|
||||
lock.setName(template.getName());
|
||||
lock.setLockee(settings.lockee());
|
||||
lock.setKeyholder(settings.keyholder());
|
||||
lock.setRequiresVerification(template.isRequiresVerification());
|
||||
lock.setTestLock(settings.testlock());
|
||||
lock.setUnlockCodeLength(settings.unlockCodeLength() != null ? settings.unlockCodeLength() : 5);
|
||||
|
||||
public String clearTask() {
|
||||
LOGGER.debug("Clear task");
|
||||
lock.setCurrentTask(null);
|
||||
lock.setCurrentTaskDescription(null);
|
||||
lock.setTaskUntil(null);
|
||||
return "";
|
||||
}
|
||||
Integer minMinutes = template.getMinTimeInMinutes();
|
||||
Integer maxMinutes = template.getMaxTimeInMinutes() != null ? template.getMaxTimeInMinutes() : 60;
|
||||
int unlockTimeMinutes = (minMinutes != null && minMinutes < maxMinutes)
|
||||
? minMinutes + new Random().nextInt(maxMinutes - minMinutes)
|
||||
: maxMinutes;
|
||||
lock.setUnlockTime(now.plusMinutes(unlockTimeMinutes));
|
||||
lock.setEndTimeVisible(template.isEndTimeVisible());
|
||||
|
||||
public void testUnfreeze() {
|
||||
if (lock.getFrozenUntil().isAfter(LocalDateTime.now())) {
|
||||
unfreeze();
|
||||
}
|
||||
}
|
||||
lock.setTasks(template.getTasks());
|
||||
lock.setTaskEveryMinutes(template.getTaskEveryMinutes());
|
||||
lock.setMinTasksPerDay(template.getMinTasksPerDay());
|
||||
|
||||
public void unlock(String unlockCode) {
|
||||
lockControl.unlock();
|
||||
this.lock.setUnlockTime(LocalDateTime.now());
|
||||
boolean valid = true;
|
||||
if (lock.isEmergencyAutoUnlocked()) {
|
||||
valid = false;
|
||||
LOGGER.debug("Lock invalid - Emergency Auto-Unlock (1h timer)");
|
||||
}
|
||||
if (lock.isTestLock()) {
|
||||
valid = false;
|
||||
} else if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) {
|
||||
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream()
|
||||
.filter(verification -> isValid(verification))
|
||||
.map(verification -> verification.getVerificationTime().toLocalDate()).collect(Collectors.toSet());
|
||||
lock.setSpinningWheelEntries(template.getSpinningWheelEntries());
|
||||
lock.setSpinsEveryMinutes(template.getSpinsEveryMinutes());
|
||||
lock.setMinSpinsPerDay(template.getMinSpinsPerDay());
|
||||
|
||||
LocalDate current = this.lock.getStartTime().toLocalDate();
|
||||
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1);
|
||||
lock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes());
|
||||
lock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites());
|
||||
if (template.getHygineOpeningEveryMinites() != null) {
|
||||
lock.setLastHygineOpening(now);
|
||||
}
|
||||
|
||||
while (!current.isAfter(last)) {
|
||||
if (!verifications.contains(current)) {
|
||||
valid = false;
|
||||
LOGGER.debug("Lock invalid - no daily verification on %s", current.toString());
|
||||
break;
|
||||
}
|
||||
current = current.plusDays(1);
|
||||
}
|
||||
}
|
||||
lock.setTaskMode(template.getTaskCardMode());
|
||||
lock.setPenaltyType(template.getPenaltyType());
|
||||
lock.setPenaltyValue(template.getPenaltyValue());
|
||||
lock.setMinTimeInMinutes(template.getMinTimeInMinutes());
|
||||
lock.setMaxTimeInMinutes(template.getMaxTimeInMinutes());
|
||||
|
||||
lock.setUnlockTime(LocalDateTime.now());
|
||||
LOGGER.debug("Unlocked at {}", lock.getUnlockTime());
|
||||
timeLockRepository.save(lock);
|
||||
lockControl.lock();
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes();
|
||||
// ── Spinning wheel ────────────────────────────────────────────────────────
|
||||
|
||||
// Gemeinsamer History-Eintrag mit Teilnehmerliste
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameType(de.oaa.xxx.games.history.GameType.CARDLOCK);
|
||||
entry.setGameName(lock.getName());
|
||||
entry.setStartTime(lock.getStartTime());
|
||||
entry.setEndTime(lock.getUnlockTime());
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(lock.getLockee(), de.oaa.xxx.games.history.GameRole.LOCKEE);
|
||||
if (lock.getKeyholder() != null) {
|
||||
entry.addParticipant(lock.getKeyholder(), de.oaa.xxx.games.history.GameRole.KEYHOLDER);
|
||||
}
|
||||
gameHistoryRepository.save(entry);
|
||||
public SpinningWheelEntry spinWheel() {
|
||||
if (TempOpeningReason.HYGIENE != lock.getTempOpeningReason()) {
|
||||
var entries = lock.getSpinningWheelEntries();
|
||||
var entry = entries.get(new Random().nextInt(entries.size()));
|
||||
entry.getType().apply(this, entry.getIntVal(), entry.getStringVal());
|
||||
return entry;
|
||||
}
|
||||
// Nicht während der Hygiene-Öffnung
|
||||
return null;
|
||||
}
|
||||
|
||||
int minutes = (int) durationMinutes;
|
||||
userRepository.findById(lock.getLockee()).ifPresent(u -> {
|
||||
u.setLockeeXp(u.getLockeeXp() + minutes);
|
||||
userRepository.save(u);
|
||||
});
|
||||
if (lock.getKeyholder() != null) {
|
||||
userRepository.findById(lock.getKeyholder()).ifPresent(u -> {
|
||||
u.setKeyholderXp(u.getKeyholderXp() + minutes);
|
||||
userRepository.save(u);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── Time controls ─────────────────────────────────────────────────────────
|
||||
|
||||
public void applyPenalty() {
|
||||
if (lock.getPenaltyType() != null) {
|
||||
switch (lock.getPenaltyType()) {
|
||||
case ADD -> addTime(lock.getPenaltyValue());
|
||||
case FREEZE -> freeze();
|
||||
case PILLORY -> pillory();
|
||||
}
|
||||
}
|
||||
}
|
||||
public void addTime(Integer intVal) {
|
||||
LOGGER.debug("Lock addTime: %s minutes", intVal);
|
||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(intVal));
|
||||
}
|
||||
|
||||
public void check() {
|
||||
LocalDate today = LocalDate.now();
|
||||
if (!lock.getStartTime().toLocalDate().equals(today)) {
|
||||
if (lock.getLastCheck() != null || today.isAfter(lock.getLastCheck())) {
|
||||
LOGGER.info("Check the day before for violations");
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
boolean violation = false;
|
||||
if (lock.getMinTasksPerDay() != null) {
|
||||
if (lock.getMinTasksPerDay() > lock.getTaskTimes().stream().map(LocalDateTime::toLocalDate)
|
||||
.filter(yesterday::equals).count()) {
|
||||
violation = true;
|
||||
}
|
||||
}
|
||||
if (lock.getMinSpinsPerDay() != null) {
|
||||
if (lock.getMinSpinsPerDay() > lock.getSpinningWheelTimes().stream().map(LocalDateTime::toLocalDate)
|
||||
.filter(yesterday::equals).count()) {
|
||||
violation = true;
|
||||
}
|
||||
}
|
||||
if (violation) {
|
||||
applyPenalty();
|
||||
}
|
||||
lock.setLastCheck(today);
|
||||
timeLockRepository.save(lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void removeTime(Integer intVal) {
|
||||
LOGGER.debug("Lock removeTime: %s minutes", intVal);
|
||||
lock.setUnlockTime(lock.getUnlockTime().minusMinutes(intVal));
|
||||
}
|
||||
|
||||
public void pillory() {
|
||||
// TODO an den Pranger stellen
|
||||
}
|
||||
public void freeze(Integer intVal) {
|
||||
LOGGER.debug("Lock frozen for %s minutes", intVal);
|
||||
lock.setFrozenFrom(LocalDateTime.now());
|
||||
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(intVal));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUnlockCode(String code) {
|
||||
lock.setUnlockCode(code);
|
||||
timeLockRepository.save(lock);
|
||||
}
|
||||
public void freeze() {
|
||||
LOGGER.debug("Lock frozen");
|
||||
lock.setFrozenFrom(LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUnlockcodeLenght() {
|
||||
return lock.getUnlockCodeLength();
|
||||
}
|
||||
|
||||
|
||||
public void startHygieneOpening() {
|
||||
tempOperning(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
|
||||
}
|
||||
|
||||
private Long calcOvertime() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Long overtime = null;
|
||||
if (lock.getTempOpeningTime() != null && lock.getTempOpeningDuration() != null) {
|
||||
LocalDateTime dueTime = lock.getTempOpeningTime().plusMinutes(lock.getTempOpeningDuration());
|
||||
if (LocalDateTime.now().isAfter(dueTime)) {
|
||||
overtime = ChronoUnit.MINUTES.between(dueTime, now);
|
||||
}
|
||||
}
|
||||
return overtime;
|
||||
}
|
||||
|
||||
public String endHygieneOpening() {
|
||||
lockControl.lock();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
public void unfreeze() {
|
||||
if (lock.getFrozenFrom() != null) {
|
||||
var unfreeTime = lock.getFrozenUntil() != null ? lock.getFrozenUntil() : LocalDateTime.now();
|
||||
var diff = ChronoUnit.MINUTES.between(lock.getFrozenFrom(), unfreeTime);
|
||||
LOGGER.debug("Lock unfrozen - adding %s minutes to the lock", diff);
|
||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(diff));
|
||||
} else {
|
||||
LOGGER.debug("Lock not frozen - ignore Call");
|
||||
}
|
||||
}
|
||||
|
||||
Long overtime = calcOvertime();
|
||||
if (overtime != null) {
|
||||
if (lock.getKeyholder() != null) {
|
||||
reportKeyholder(overtime);
|
||||
}
|
||||
addOvertime(overtime);
|
||||
}
|
||||
lock.setLastHygineOpening(now);
|
||||
lock.setTempOpeningDuration(null);
|
||||
lock.setTempOpeningTime(null);
|
||||
|
||||
var code = CodeCreator.createAlphanumericCode(lock.getUnlockCodeLength());
|
||||
lock.setUnlockCode(code);
|
||||
timeLockRepository.save(lock);
|
||||
return code;
|
||||
}
|
||||
public void testUnfreeze() {
|
||||
if (lock.getFrozenUntil().isAfter(LocalDateTime.now())) {
|
||||
unfreeze();
|
||||
}
|
||||
}
|
||||
|
||||
private void reportKeyholder(Long overtime) {
|
||||
KeyholderNotificationEntity notification = new KeyholderNotificationEntity();
|
||||
notification.setLockId(lock.getLockId());
|
||||
notification.setLockeeId(lock.getLockee());
|
||||
notification.setKeyholderUserId(lock.getKeyholder());
|
||||
notification.setViolationTime(LocalDateTime.now());
|
||||
notification.setOvertimeMinutes(overtime);
|
||||
keyholderNotificationRepository.save(notification);
|
||||
}
|
||||
// ── Tasks ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private void addOvertime(Long overtime) {
|
||||
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(overtime * 4));
|
||||
}
|
||||
|
||||
private void tempOperning(TempOpeningReason reason, Integer duration) {
|
||||
assert duration != null;
|
||||
lockControl.unlock();
|
||||
lock.setTempOpeningReason(reason);
|
||||
lock.setTempOpeningTime(LocalDateTime.now());;
|
||||
lock.setTempOpeningDuration(duration);
|
||||
timeLockRepository.save(lock);
|
||||
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), reason.toString());
|
||||
}
|
||||
public void task() {
|
||||
if (TempOpeningReason.HYGIENE != lock.getTempOpeningReason()) {
|
||||
switch (lock.getTaskMode()) {
|
||||
case TaskMode.RANDOM -> applyRandomTask();
|
||||
case TaskMode.KEYHOLDER -> startKeyholderVote();
|
||||
case TaskMode.COMMUNITY -> {
|
||||
if (lock.isTestLock()) applyRandomTask();
|
||||
else startCommunityVote();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Nicht während der Hygiene-Öffnung
|
||||
}
|
||||
|
||||
public void text(Integer intVal, String stringVal) {
|
||||
LOGGER.debug("Apply text {}", stringVal);
|
||||
lock.setCurrentTask(stringVal);
|
||||
if (intVal != null && intVal > 0) {
|
||||
lock.setTaskUntil(LocalDateTime.now().plusMinutes(intVal));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Verification ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt eine bestehende Verifikation für heute zurück (Idempotenz) oder legt eine neue an.
|
||||
* Erstellt je nach Keyholder-Präsenz eine KeyholderVerification oder CommunityVerification.
|
||||
*/
|
||||
public VerificationCommonDTO startVerification() {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDateTime todayStart = today.atStartOfDay();
|
||||
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||
|
||||
if (lock.getKeyholder() != null) {
|
||||
var existing = keyholderVerificationRepository.findByLockId(lock.getLockId()).stream()
|
||||
.filter(v -> v.getCreatedAt().toLocalDate().equals(today))
|
||||
.findFirst();
|
||||
if (existing.isPresent()) return existing.get().toCommonVerification();
|
||||
|
||||
KeyholderVerificationEntity v = new KeyholderVerificationEntity();
|
||||
v.setId(UUID.randomUUID());
|
||||
v.setLockId(lock.getLockId());
|
||||
v.setLockeeId(lock.getLockee());
|
||||
v.setKeyholderId(lock.getKeyholder());
|
||||
v.setCode(CodeCreator.createAlphanumeric(6));
|
||||
v.setCreatedAt(LocalDateTime.now());
|
||||
keyholderVerificationRepository.save(v);
|
||||
return v.toCommonVerification();
|
||||
} else {
|
||||
var pending = communityVerificationRepository
|
||||
.findByLockIdAndCreatedAtBetweenAndImageIsNull(lock.getLockId(), todayStart, todayEnd);
|
||||
if (!pending.isEmpty()) return pending.get(0).toCommonVerification();
|
||||
var completed = communityVerificationRepository
|
||||
.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lock.getLockId(), todayStart, todayEnd);
|
||||
if (!completed.isEmpty()) return completed.get(0).toCommonVerification();
|
||||
|
||||
CommunityVerificationEntity v = new CommunityVerificationEntity();
|
||||
v.setId(UUID.randomUUID());
|
||||
v.setLockId(lock.getLockId());
|
||||
v.setLockeeId(lock.getLockee());
|
||||
v.setCode(CodeCreator.createAlphanumeric(6));
|
||||
v.setCreatedAt(LocalDateTime.now());
|
||||
communityVerificationRepository.save(v);
|
||||
return v.toCommonVerification();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert das Verifikationsbild auf der richtigen Entity (Keyholder oder Community).
|
||||
* Gibt false zurück wenn die Verifikation nicht gefunden wurde oder nicht zu diesem Lock gehört.
|
||||
*/
|
||||
public boolean completeVerification(UUID verificationId, byte[] image) {
|
||||
if (lock.getKeyholder() != null) {
|
||||
var vOpt = keyholderVerificationRepository.findById(verificationId);
|
||||
if (vOpt.isEmpty() || !vOpt.get().getLockId().equals(lock.getLockId())) return false;
|
||||
var v = vOpt.get();
|
||||
v.setImage(image);
|
||||
keyholderVerificationRepository.save(v);
|
||||
} else {
|
||||
var vOpt = communityVerificationRepository.findById(verificationId);
|
||||
if (vOpt.isEmpty() || !vOpt.get().getLockId().equals(lock.getLockId())) return false;
|
||||
var v = vOpt.get();
|
||||
v.setImage(image);
|
||||
communityVerificationRepository.save(v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Penalty & check ───────────────────────────────────────────────────────
|
||||
|
||||
public void applyPenalty() {
|
||||
if (lock.getPenaltyType() != null) {
|
||||
switch (lock.getPenaltyType()) {
|
||||
case ADD -> addTime(lock.getPenaltyValue());
|
||||
case FREEZE -> freeze();
|
||||
case PILLORY -> pillory(CommunityPilloryReason.HYGIENE_OPENING_EXEEDED, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void check() {
|
||||
LocalDate today = LocalDate.now();
|
||||
if (!lock.getStartTime().toLocalDate().equals(today)) {
|
||||
if (lock.getLastCheck() != null || today.isAfter(lock.getLastCheck())) {
|
||||
LOGGER.info("Check the day before for violations");
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
boolean violation = false;
|
||||
if (lock.getMinTasksPerDay() != null) {
|
||||
if (lock.getMinTasksPerDay() > lock.getTaskTimes().stream().map(LocalDateTime::toLocalDate)
|
||||
.filter(yesterday::equals).count()) {
|
||||
violation = true;
|
||||
}
|
||||
}
|
||||
if (lock.getMinSpinsPerDay() != null) {
|
||||
if (lock.getMinSpinsPerDay() > lock.getSpinningWheelTimes().stream()
|
||||
.map(LocalDateTime::toLocalDate).filter(yesterday::equals).count()) {
|
||||
violation = true;
|
||||
}
|
||||
}
|
||||
if (violation) {
|
||||
applyPenalty();
|
||||
}
|
||||
lock.setLastCheck(today);
|
||||
timeLockRepository.save(lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void pillory(CommunityPilloryReason reason, UUID keyholderId) {
|
||||
CommunityPilloryEntity pillory = new CommunityPilloryEntity();
|
||||
pillory.setCreatedAt(LocalDateTime.now());
|
||||
pillory.setLockeeId(lock.getLockee());
|
||||
pillory.setLockId(lock.getLockId());
|
||||
pillory.setReason(reason);
|
||||
pillory.setKeyholderId(keyholderId);
|
||||
pilloryRepository.save(pillory);
|
||||
}
|
||||
|
||||
// ── Hygiene opening ───────────────────────────────────────────────────────
|
||||
|
||||
public void startHygieneOpening() {
|
||||
lockControl.unlock();
|
||||
startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
|
||||
}
|
||||
|
||||
// ── LockControlCallback ───────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void setUnlockCode(String code) {
|
||||
lock.setUnlockCode(code);
|
||||
timeLockRepository.save(lock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUnlockcodeLenght() {
|
||||
return lock.getUnlockCodeLength();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,53 +2,64 @@ package de.oaa.xxx.games.chastity.timelock;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import de.oaa.xxx.games.chastity.community.CommunityPilloryRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
|
||||
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
|
||||
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@Service
|
||||
public class TimeLockServiceFactory {
|
||||
|
||||
private final TimeLockRepository timeLockRepository;
|
||||
private final CommunityVerificationRepository communityVerificationRepository;
|
||||
private final GameHistoryRepository gameHistoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final KeyholderNotificationRepository keyholderNotificationRepository;
|
||||
private final CommunityPilloryRepository pilloryRepository;
|
||||
private final CommunityTaskVoteRepository communityTaskVoteRepository;
|
||||
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
||||
private final KeyholderVerificationRepository keyholderVerificationRepository;
|
||||
|
||||
private final VerificationRepository verificationRepository;
|
||||
private final VerificationVoteRepository verificationVoteRepository;
|
||||
private final TimeLockRepository timeLockRepository;
|
||||
private final GameHistoryRepository gameHistoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
private KeyholderNotificationRepository keyholderNotificationRepository;
|
||||
private final UnlockCodeHistoryService unlockCodeHistoryService;
|
||||
private final UnlockCodeHistoryService unlockCodeHistoryService;
|
||||
private final SystemMessageService systemMessageService;
|
||||
private CommunityVerificationVoteRepository communityVerificationVoteRepository;
|
||||
|
||||
public TimeLockServiceFactory(VerificationRepository verificationRepository,
|
||||
VerificationVoteRepository verificationVoteRepository,
|
||||
TimeLockRepository timeLockRepository,
|
||||
GameHistoryRepository gameHistoryRepository,
|
||||
UserRepository userRepository,
|
||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService) {
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
this.timeLockRepository = timeLockRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
public TimeLockServiceFactory(CommunityVerificationRepository verificationRepository,
|
||||
CommunityVerificationVoteRepository verificationVoteRepository, TimeLockRepository timeLockRepository,
|
||||
GameHistoryRepository gameHistoryRepository, UserRepository userRepository,
|
||||
KeyholderNotificationRepository keyholderNotificationRepository,
|
||||
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
|
||||
KeyholderVerificationRepository keyholderVerificationRepository,
|
||||
CommunityTaskVoteRepository communityTaskVoteRepository, CommunityPilloryRepository pilloryRepository,
|
||||
UnlockCodeHistoryService unlockCodeHistoryService, SystemMessageService systemMessageService) {
|
||||
this.communityVerificationVoteRepository = verificationVoteRepository;
|
||||
this.timeLockRepository = timeLockRepository;
|
||||
this.communityVerificationRepository = verificationRepository;
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.keyholderNotificationRepository = keyholderNotificationRepository;
|
||||
this.unlockCodeHistoryService = unlockCodeHistoryService;
|
||||
}
|
||||
this.pilloryRepository = pilloryRepository;
|
||||
this.unlockCodeHistoryService = unlockCodeHistoryService;
|
||||
this.systemMessageService = systemMessageService;
|
||||
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
|
||||
this.communityTaskVoteRepository = communityTaskVoteRepository;
|
||||
this.keyholderVerificationRepository = keyholderVerificationRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue CardLockService-Instanz für das gegebene Lock.
|
||||
*/
|
||||
public TimeLockService create(TimeLockEntity lock) {
|
||||
return new TimeLockService(
|
||||
lock,
|
||||
verificationRepository,
|
||||
verificationVoteRepository,
|
||||
timeLockRepository,
|
||||
gameHistoryRepository,
|
||||
userRepository,
|
||||
keyholderNotificationRepository,
|
||||
unlockCodeHistoryService
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Erstellt eine neue CardLockService-Instanz für das gegebene Lock.
|
||||
*/
|
||||
public TimeLockService create(TimeLockEntity lock) {
|
||||
return new TimeLockService(lock, communityVerificationRepository, communityVerificationVoteRepository,
|
||||
timeLockRepository, gameHistoryRepository, userRepository, keyholderNotificationRepository,
|
||||
keyholderTaskChoiceRepository, keyholderVerificationRepository, communityTaskVoteRepository,
|
||||
pilloryRepository, unlockCodeHistoryService, systemMessageService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package de.oaa.xxx.games.chastity.timelock;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.PenaltyType;
|
||||
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/timelock/templates")
|
||||
public class TimeLockTemplateController {
|
||||
|
||||
private final TimeLockTemplateRepository templateRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public TimeLockTemplateController(TimeLockTemplateRepository templateRepository,
|
||||
UserRepository userRepository) {
|
||||
this.templateRepository = templateRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record TemplateRequest(
|
||||
String name,
|
||||
Integer minTimeInMinutes,
|
||||
Integer maxTimeInMinutes,
|
||||
boolean endTimeVisible,
|
||||
Integer hygineOpeningDurationMinutes,
|
||||
Integer hygineOpeningEveryMinites,
|
||||
List<Task> tasks,
|
||||
Integer taskEveryMinutes,
|
||||
Integer minTasksPerDay,
|
||||
List<SpinningWheelEntry> spinningWheelEntries,
|
||||
Integer spinsEveryMinutes,
|
||||
Integer minSpinsPerDay,
|
||||
boolean requiresVerification,
|
||||
TaskMode taskMode,
|
||||
PenaltyType penaltyType,
|
||||
Integer penaltyValue
|
||||
) {}
|
||||
|
||||
private Map<String, Object> toDto(TimeLockTemplateEntity t) {
|
||||
Map<String, Object> dto = new LinkedHashMap<>();
|
||||
dto.put("templateId", t.getTemplateId());
|
||||
dto.put("name", t.getName());
|
||||
dto.put("minTimeInMinutes", t.getMinTimeInMinutes());
|
||||
dto.put("maxTimeInMinutes", t.getMaxTimeInMinutes());
|
||||
dto.put("endTimeVisible", t.isEndTimeVisible());
|
||||
dto.put("hygineOpeningEveryMinites", t.getHygineOpeningEveryMinites());
|
||||
dto.put("hygineOpeningDurationMinutes", t.getHygineOpeningDurationMinutes());
|
||||
dto.put("tasks", t.getTasks() != null ? t.getTasks() : List.of());
|
||||
dto.put("taskEveryMinutes", t.getTaskEveryMinutes());
|
||||
dto.put("minTasksPerDay", t.getMinTasksPerDay());
|
||||
dto.put("spinningWheelEntries", t.getSpinningWheelEntries() != null ? t.getSpinningWheelEntries() : List.of());
|
||||
dto.put("spinsEveryMinutes", t.getSpinsEveryMinutes());
|
||||
dto.put("minSpinsPerDay", t.getMinSpinsPerDay());
|
||||
dto.put("requiresVerification", t.isRequiresVerification());
|
||||
dto.put("taskMode", t.getTaskCardMode());
|
||||
dto.put("penaltyType", t.getPenaltyType());
|
||||
dto.put("penaltyValue", t.getPenaltyValue());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<Map<String, Object>>> list(Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
List<Map<String, Object>> result = templateRepository.findByOwner(myId)
|
||||
.stream().map(this::toDto).collect(Collectors.toList());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> create(@RequestBody TemplateRequest req, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();
|
||||
if (req.maxTimeInMinutes() == null || req.maxTimeInMinutes() < 1) return ResponseEntity.badRequest().build();
|
||||
|
||||
TimeLockTemplateEntity t = new TimeLockTemplateEntity();
|
||||
t.setOwner(myId);
|
||||
applyRequest(t, req);
|
||||
templateRepository.save(t);
|
||||
return ResponseEntity.ok(toDto(t));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> update(@PathVariable UUID id,
|
||||
@RequestBody TemplateRequest req,
|
||||
Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var opt = templateRepository.findById(id);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
TimeLockTemplateEntity t = opt.get();
|
||||
if (!t.getOwner().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();
|
||||
if (req.maxTimeInMinutes() == null || req.maxTimeInMinutes() < 1) return ResponseEntity.badRequest().build();
|
||||
|
||||
applyRequest(t, req);
|
||||
templateRepository.save(t);
|
||||
return ResponseEntity.ok(toDto(t));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID id, Principal principal) {
|
||||
var meOpt = userRepository.findByEmail(principal.getName());
|
||||
if (meOpt.isEmpty()) return ResponseEntity.status(401).build();
|
||||
UUID myId = meOpt.get().getUserId();
|
||||
|
||||
var opt = templateRepository.findById(id);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!opt.get().getOwner().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
templateRepository.deleteById(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private void applyRequest(TimeLockTemplateEntity t, TemplateRequest req) {
|
||||
t.setName(req.name());
|
||||
t.setMinTimeInMinutes(req.minTimeInMinutes());
|
||||
t.setMaxTimeInMinutes(req.maxTimeInMinutes());
|
||||
t.setEndTimeVisible(req.endTimeVisible());
|
||||
t.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites());
|
||||
t.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes());
|
||||
t.setTasks(req.tasks() != null ? req.tasks() : List.of());
|
||||
t.setTaskEveryMinutes(req.taskEveryMinutes());
|
||||
t.setMinTasksPerDay(req.minTasksPerDay());
|
||||
t.setSpinningWheelEntries(req.spinningWheelEntries() != null ? req.spinningWheelEntries() : List.of());
|
||||
t.setSpinsEveryMinutes(req.spinsEveryMinutes());
|
||||
t.setMinSpinsPerDay(req.minSpinsPerDay());
|
||||
t.setRequiresVerification(req.requiresVerification());
|
||||
t.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
|
||||
t.setPenaltyType(req.penaltyType());
|
||||
t.setPenaltyValue(req.penaltyValue());
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,25 @@
|
||||
package de.oaa.xxx.games.chastity.timelock;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.BaseLockTemplateEntity;
|
||||
import de.oaa.xxx.games.chastity.common.PenaltyType;
|
||||
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelConverter;
|
||||
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry;
|
||||
import de.oaa.xxx.games.chastity.tasks.Task;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
|
||||
import de.oaa.xxx.games.chastity.tasks.TaskMode;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.DiscriminatorValue;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "timelock_template")
|
||||
public class TimeLockTemplateEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column
|
||||
private UUID templateId;
|
||||
@Column(nullable = false)
|
||||
private UUID owner;
|
||||
@Column
|
||||
private String name;
|
||||
@DiscriminatorValue("TIMELOCK")
|
||||
public class TimeLockTemplateEntity extends BaseLockTemplateEntity {
|
||||
|
||||
@Column
|
||||
private Integer minTimeInMinutes;
|
||||
@Column
|
||||
@@ -40,18 +27,9 @@ public class TimeLockTemplateEntity {
|
||||
@Column
|
||||
private boolean endTimeVisible;
|
||||
@Column
|
||||
private Integer hygineOpeningDurationMinutes;
|
||||
@Column
|
||||
private Integer hygineOpeningEveryMinites;
|
||||
|
||||
@Convert(converter = TaskListConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<Task> tasks;
|
||||
@Column
|
||||
private Integer taskEveryMinutes;
|
||||
@Column
|
||||
private Integer minTasksPerDay;
|
||||
|
||||
@Convert(converter = SpinningWheelConverter.class)
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private List<SpinningWheelEntry> spinningWheelEntries;
|
||||
@@ -59,24 +37,15 @@ public class TimeLockTemplateEntity {
|
||||
private Integer spinsEveryMinutes;
|
||||
@Column
|
||||
private Integer minSpinsPerDay;
|
||||
|
||||
@Column
|
||||
private boolean requiresVerification;
|
||||
@Column(nullable = false)
|
||||
private TaskMode taskMode = TaskMode.RANDOM;
|
||||
@Column
|
||||
private PenaltyType penaltyType;
|
||||
@Column
|
||||
private Integer penaltyValue;
|
||||
|
||||
public TaskMode getTaskCardMode() {
|
||||
return taskMode != null ? taskMode : TaskMode.RANDOM;
|
||||
}
|
||||
|
||||
public TimeLockTemplate toTimeLockTemplate() {
|
||||
return new TimeLockTemplate(templateId, owner, name, minTimeInMinutes, maxTimeInMinutes, endTimeVisible,
|
||||
hygineOpeningDurationMinutes, hygineOpeningEveryMinites, tasks, taskEveryMinutes, minTasksPerDay,
|
||||
spinningWheelEntries, spinsEveryMinutes, minSpinsPerDay, requiresVerification, taskMode, penaltyType,
|
||||
return new TimeLockTemplate(getTemplateId(), getOwner(), getName(), minTimeInMinutes, maxTimeInMinutes, endTimeVisible,
|
||||
getHygineOpeningDurationMinutes(), getHygineOpeningEveryMinites(), getTasks(), taskEveryMinutes, minTasksPerDay,
|
||||
spinningWheelEntries, spinsEveryMinutes, minSpinsPerDay, isRequiresVerification(), getTaskMode(), penaltyType,
|
||||
penaltyValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface TimelockTemplateRepository extends JpaRepository<TimeLockTemplateEntity, UUID> {
|
||||
public interface TimeLockTemplateRepository extends JpaRepository<TimeLockTemplateEntity, UUID> {
|
||||
List<TimeLockTemplateEntity> findByOwner(UUID owner);
|
||||
}
|
||||
@@ -13,9 +13,9 @@ import de.oaa.xxx.util.ValidationResult;
|
||||
|
||||
public class TimeLockTemplateService {
|
||||
|
||||
private TimelockTemplateRepository timelockTemplateRepository;
|
||||
private TimeLockTemplateRepository timelockTemplateRepository;
|
||||
|
||||
public TimeLockTemplateService(TimelockTemplateRepository timelockTemplateRepository) {
|
||||
public TimeLockTemplateService(TimeLockTemplateRepository timelockTemplateRepository) {
|
||||
this.timelockTemplateRepository = timelockTemplateRepository;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.verification;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.games.chastity.common.CodeCreator;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/verification")
|
||||
@Transactional
|
||||
public class VerificationController {
|
||||
|
||||
|
||||
|
||||
private final VerificationRepository verificationRepository;
|
||||
private final VerificationVoteRepository verificationVoteRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public VerificationController(VerificationRepository verificationRepository,
|
||||
VerificationVoteRepository verificationVoteRepository, UserRepository userRepository) {
|
||||
this.verificationRepository = verificationRepository;
|
||||
this.verificationVoteRepository = verificationVoteRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{verificationId}")
|
||||
public ResponseEntity<VerificationDTO> get(@PathVariable UUID verificationId) {
|
||||
var optional = verificationRepository.findById(verificationId);
|
||||
if (optional.isEmpty()) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
var dto = optional.get().toVerification();
|
||||
verificationVoteRepository.findAllByVerificationId(verificationId).stream()
|
||||
.map(VerificationVoteEntity::toVerificationVote)
|
||||
.forEach(dto.votes()::add);
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public ResponseEntity<List<VerificationDTO>> getAll(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
var paging = PageRequest.of(page, size, Sort.by("verificationTime").descending());
|
||||
Page<VerificationEntity> result = verificationRepository.findAllByImageIsNotNull(paging);
|
||||
return ResponseEntity.ok(result.stream().map(VerificationEntity::toVerification).toList());
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public ResponseEntity<VerificationDTO> createVerification() {
|
||||
var verification = new VerificationEntity();
|
||||
verification.setVerficationId(UUID.randomUUID());
|
||||
verification.setCode(CodeCreator.createAlphanumericCode(6));
|
||||
verification.setVerificationTime(LocalDateTime.now());
|
||||
verificationRepository.save(verification);
|
||||
return ResponseEntity.ok(verification.toVerification());
|
||||
}
|
||||
|
||||
@PutMapping("/{verificationId}")
|
||||
public ResponseEntity<Void> update(@PathVariable UUID verificationId, @RequestBody VerificationDTO dto,
|
||||
Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
var entity = verificationRepository.findById(verificationId).orElse(null);
|
||||
if (entity == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (entity.getVerificationTime().isBefore(LocalDateTime.now().minusHours(1))) {
|
||||
return ResponseEntity.status(HttpStatus.GONE).build();
|
||||
}
|
||||
if (dto.image() != null) {
|
||||
entity.setImage(dto.image());
|
||||
}
|
||||
verificationRepository.save(entity);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping("/community")
|
||||
public ResponseEntity<List<Map<String, Object>>> getCommunity(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
UUID myId = user.getUserId();
|
||||
|
||||
LocalDateTime since = LocalDateTime.now().minusHours(24);
|
||||
LocalDateTime until = LocalDateTime.now();
|
||||
var paging = PageRequest.of(page, 10, Sort.by("verificationTime").descending());
|
||||
Page<VerificationEntity> result = verificationRepository
|
||||
.findByKeyholderIsNullAndVerificationTimeBetweenAndImageIsNotNull(since, until, paging);
|
||||
|
||||
List<Map<String, Object>> items = result.getContent().stream().map(v -> {
|
||||
var votes = verificationVoteRepository.findAllByVerificationId(v.getVerficationId());
|
||||
long upvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count();
|
||||
long downvotes = votes.stream().filter(vt -> !vt.isUpvote()).count();
|
||||
var myVoteOpt = votes.stream().filter(vt -> myId.equals(vt.getUserId())).findFirst();
|
||||
boolean isOwn = myId.equals(v.getLockeeId());
|
||||
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("verificationId", v.getVerficationId().toString());
|
||||
item.put("verificationTime", v.getVerificationTime().toString());
|
||||
item.put("code", v.getCode());
|
||||
item.put("image", v.getImage() != null ? Base64.getEncoder().encodeToString(v.getImage()) : null);
|
||||
item.put("upvotes", upvotes);
|
||||
item.put("downvotes", downvotes);
|
||||
item.put("myVote", isOwn ? "own" : myVoteOpt.map(VerificationVoteEntity::isUpvote).orElse(null));
|
||||
item.put("hasMore", result.hasNext());
|
||||
return item;
|
||||
}).toList();
|
||||
|
||||
return ResponseEntity.ok(items);
|
||||
}
|
||||
|
||||
@PostMapping("/{verificationId}/vote/")
|
||||
public ResponseEntity<Void> addVote(@PathVariable UUID verificationId, @RequestBody VerificationVoteDTO dto,
|
||||
Principal principal) {
|
||||
var user = userRepository.findByEmail(principal.getName()).orElse(null);
|
||||
if (user == null) return ResponseEntity.status(401).build();
|
||||
if (!verificationRepository.existsById(verificationId)) return ResponseEntity.notFound().build();
|
||||
|
||||
var vEntity = verificationRepository.findById(verificationId).orElse(null);
|
||||
if (vEntity == null) return ResponseEntity.notFound().build();
|
||||
if (user.getUserId().equals(vEntity.getLockeeId())) return ResponseEntity.status(403).build();
|
||||
if (verificationVoteRepository.findByVerificationIdAndUserId(verificationId, user.getUserId()).isPresent()) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
var vote = new VerificationVoteEntity();
|
||||
vote.setVoteId(UUID.randomUUID());
|
||||
vote.setVerificationId(verificationId);
|
||||
vote.setUserId(user.getUserId());
|
||||
vote.setUpvote(dto.upvote());
|
||||
verificationVoteRepository.save(vote);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.verification;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record VerificationDTO(UUID verficationId, String code, LocalDateTime verificationTime, byte[] image, List<VerificationVoteDTO> votes) {}
|
||||
@@ -1,47 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.verification;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "verification")
|
||||
public class VerificationEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID verficationId;
|
||||
@Column(nullable = false)
|
||||
private UUID lockId;
|
||||
@Column(nullable = false)
|
||||
private String code;
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime verificationTime;
|
||||
@Column(columnDefinition = "MEDIUMBLOB")
|
||||
private byte[] image;
|
||||
@Column
|
||||
private UUID lockeeId;
|
||||
@Column
|
||||
private UUID keyholder;
|
||||
|
||||
public UUID getKeyholderId() {
|
||||
return keyholder;
|
||||
}
|
||||
|
||||
public void setKeyholderId(UUID keyholder) {
|
||||
this.keyholder = keyholder;
|
||||
}
|
||||
|
||||
public VerificationDTO toVerification() {
|
||||
return new VerificationDTO(verficationId, code, verificationTime, image, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.verification;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface VerificationRepository extends JpaRepository<VerificationEntity, UUID> {
|
||||
|
||||
org.springframework.data.domain.Page<VerificationEntity> findAllByImageIsNotNull(Pageable pageable);
|
||||
|
||||
java.util.List<VerificationEntity> findByLockId(UUID lockId);
|
||||
|
||||
java.util.List<VerificationEntity> findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(UUID lockId, java.time.LocalDateTime from, java.time.LocalDateTime to);
|
||||
|
||||
java.util.List<VerificationEntity> findByLockIdAndVerificationTimeBetweenAndImageIsNull(UUID lockId, java.time.LocalDateTime from, java.time.LocalDateTime to);
|
||||
|
||||
org.springframework.data.domain.Page<VerificationEntity> findByKeyholderIsNullAndVerificationTimeBetweenAndImageIsNotNull(
|
||||
java.time.LocalDateTime from, java.time.LocalDateTime to, org.springframework.data.domain.Pageable pageable);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.verification;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record VerificationVoteDTO (UUID voteId, UUID userId, boolean upvote) {}
|
||||
@@ -1,17 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.verification;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface VerificationVoteRepository extends JpaRepository<VerificationVoteEntity, UUID> {
|
||||
|
||||
List<VerificationVoteEntity> findAllByVerificationId(UUID verificationId);
|
||||
|
||||
java.util.Optional<VerificationVoteEntity> findByVerificationIdAndUserId(UUID verificationId, UUID userId);
|
||||
|
||||
void deleteAllByVerificationId(UUID verificationId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.vote;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "community_task_vote")
|
||||
public class CommunityTaskVoteEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID voteSessionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID lockId;
|
||||
|
||||
/** ACTIVE | COMPLETED */
|
||||
@Column(nullable = false)
|
||||
private String status = "ACTIVE";
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime expiresAt;
|
||||
|
||||
/** true = TestLock, nicht der Community zeigen */
|
||||
@Column(nullable = false)
|
||||
private boolean testLock = false;
|
||||
|
||||
/** null until completed */
|
||||
@Column
|
||||
private Integer winningTaskIndex;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.vote;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface CommunityTaskVoteEntryRepository extends JpaRepository<CommunityTaskVoteEntryEntity, UUID> {
|
||||
List<CommunityTaskVoteEntryEntity> findByVoteSessionId(UUID voteSessionId);
|
||||
boolean existsByVoteSessionIdAndVoterUserId(UUID voteSessionId, UUID voterUserId);
|
||||
Integer countByVoteSessionIdAndTaskIndex(UUID voteSessionId, int taskIndex);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.oaa.xxx.games.chastity.vote;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface CommunityTaskVoteRepository extends JpaRepository<CommunityTaskVoteEntity, UUID> {
|
||||
List<CommunityTaskVoteEntity> findByStatus(String status);
|
||||
List<CommunityTaskVoteEntity> findByStatusAndExpiresAtBefore(String status, LocalDateTime time);
|
||||
boolean existsByLockIdAndStatus(UUID lockId, String status);
|
||||
}
|
||||
@@ -777,6 +777,16 @@
|
||||
|
||||
async function loadLock() {
|
||||
const res = await fetch('/keyholder/cardlock/' + lockId);
|
||||
if (res.status === 404) {
|
||||
// Prüfen, ob es ein TimeLock ist
|
||||
const tlRes = await fetch('/keyholder/timelock/' + lockId);
|
||||
if (tlRes.ok) {
|
||||
window.location.replace('/activetimelock.html?lockId=' + lockId);
|
||||
return;
|
||||
}
|
||||
document.getElementById('lockContent').textContent = 'Lock nicht gefunden.';
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
document.getElementById('lockContent').textContent = 'Lock nicht gefunden.';
|
||||
return;
|
||||
|
||||
1091
xxxthegame/src/main/resources/static/activetimelock.html
Normal file
1091
xxxthegame/src/main/resources/static/activetimelock.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,6 @@
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* ── Unified feed ── */
|
||||
#feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -87,7 +86,7 @@
|
||||
color: var(--color-primary);
|
||||
background: none;
|
||||
}
|
||||
.vote-btn.voted-up { border-color: #2ecc71; color: #2ecc71; background: rgba(46,204,113,0.08); }
|
||||
.vote-btn.voted-up { border-color: #2ecc71; color: #2ecc71; background: rgba(46,204,113,0.08); }
|
||||
.vote-btn.voted-down { border-color: #e74c3c; color: #e74c3c; background: rgba(231,76,60,0.08); }
|
||||
.vote-btn:disabled { opacity: 0.55; cursor: not-allowed; pointer-events: none; }
|
||||
.vote-count { font-weight: 600; font-size: 0.88rem; }
|
||||
@@ -107,7 +106,7 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.task-vote-lockee { font-weight: 600; font-size: 0.92rem; }
|
||||
.task-vote-lockee { font-weight: 600; font-size: 0.92rem; }
|
||||
.task-vote-expires { font-size: 0.78rem; color: var(--color-muted); }
|
||||
.task-vote-options { display: flex; flex-direction: column; gap: 0.35rem; margin-top: 0.5rem; }
|
||||
.task-vote-btn {
|
||||
@@ -150,6 +149,30 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Pranger-Karte ── */
|
||||
.pillory-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid rgba(231,76,60,0.35);
|
||||
border-radius: 10px;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
.pillory-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.pillory-lockee { font-weight: 600; font-size: 0.92rem; }
|
||||
.pillory-date { font-size: 0.78rem; color: var(--color-muted); }
|
||||
.pillory-reason {
|
||||
font-size: 0.82rem;
|
||||
color: #e74c3c;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.pillory-message { font-size: 0.88rem; }
|
||||
|
||||
.empty-hint {
|
||||
color: var(--color-muted);
|
||||
font-size: 0.9rem;
|
||||
@@ -170,7 +193,7 @@
|
||||
<div class="content">
|
||||
|
||||
<div class="page-title">Community Votes</div>
|
||||
<div class="page-subtitle">Verifikationen & Aufgaben-Abstimmungen</div>
|
||||
<div class="page-subtitle">Verifikationen, Aufgaben-Abstimmungen & Pranger</div>
|
||||
|
||||
<div id="feed"></div>
|
||||
<div class="load-spinner" id="loadSpinner" style="display:none;">Lädt…</div>
|
||||
@@ -186,36 +209,84 @@
|
||||
function esc(s) {
|
||||
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function fmtDateTime(isoStr) {
|
||||
return new Date(isoStr).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||||
}
|
||||
|
||||
// ── Task vote card builder ─────────────────────────────────────────────────
|
||||
// ── Verifikations-Karte ────────────────────────────────────────────────────
|
||||
|
||||
function buildTaskVoteCard(vote) {
|
||||
const isOwn = vote.isOwnLock;
|
||||
const alreadyVoted = vote.myVote !== null && vote.myVote !== undefined;
|
||||
function buildVerCard(base, detail) {
|
||||
const voted = detail.isOwnLock || detail.myVote !== null && detail.myVote !== undefined;
|
||||
const votedUp = !detail.isOwnLock && detail.myVote === true;
|
||||
const votedDn = !detail.isOwnLock && detail.myVote === false;
|
||||
const id = base.displayId;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'vote-card';
|
||||
card.innerHTML = `
|
||||
<div class="vote-card-media">
|
||||
<img class="vote-card-img" src="data:image/jpeg;base64,${detail.image}" alt="Verifikationsbild">
|
||||
<div class="vote-card-code">${esc(detail.code)}</div>
|
||||
</div>
|
||||
<div class="vote-card-body">
|
||||
<div class="vote-meta">Verifikation · ${esc(base.lockeeName)} · ${fmtDateTime(base.createdAt)}</div>
|
||||
<div class="vote-actions">
|
||||
<button class="vote-btn ${votedUp ? 'voted-up' : ''}" id="up-${id}"
|
||||
${voted ? 'disabled' : ''}
|
||||
onclick="castVerVote('${id}', true)">
|
||||
👍 <span class="vote-count" id="upcount-${id}">${detail.upvotes}</span>
|
||||
</button>
|
||||
<button class="vote-btn ${votedDn ? 'voted-down' : ''}" id="dn-${id}"
|
||||
${voted ? 'disabled' : ''}
|
||||
onclick="castVerVote('${id}', false)">
|
||||
👎 <span class="vote-count" id="dncount-${id}">${detail.downvotes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
return card;
|
||||
}
|
||||
|
||||
async function castVerVote(displayId, upvote) {
|
||||
document.getElementById('up-' + displayId).disabled = true;
|
||||
document.getElementById('dn-' + displayId).disabled = true;
|
||||
|
||||
const res = await fetch(`/games/chastity/community/verification/${displayId}/vote/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ upvote })
|
||||
});
|
||||
if (res.ok || res.status === 202) {
|
||||
const countEl = document.getElementById(upvote ? 'upcount-' + displayId : 'dncount-' + displayId);
|
||||
countEl.textContent = parseInt(countEl.textContent) + 1;
|
||||
document.getElementById((upvote ? 'up-' : 'dn-') + displayId)
|
||||
.classList.add(upvote ? 'voted-up' : 'voted-down');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aufgaben-Abstimmungs-Karte ─────────────────────────────────────────────
|
||||
|
||||
function buildTaskVoteCard(base, detail) {
|
||||
const isOwn = detail.isOwnLock;
|
||||
const alreadyVoted = detail.entries.some(e => e.ownVote);
|
||||
const id = base.displayId;
|
||||
|
||||
let optionsHtml = '';
|
||||
(vote.tasks || []).forEach((t, i) => {
|
||||
const count = (vote.voteCounts || [])[i] || 0;
|
||||
const isMyVote = vote.myVote === i;
|
||||
const desc = t.description
|
||||
? `<div style="font-size:0.75rem;color:var(--color-muted);margin-top:0.1rem;">${esc(t.description)}</div>`
|
||||
(detail.entries || []).forEach((e, i) => {
|
||||
const desc = e.description
|
||||
? `<div style="font-size:0.75rem;color:var(--color-muted);margin-top:0.1rem;">${esc(e.description)}</div>`
|
||||
: '';
|
||||
const mins = t.minutes > 0
|
||||
? ` <span style="font-size:0.75rem;color:var(--color-muted);">⏱ ${t.minutes} Min.</span>`
|
||||
const mins = e.minutes > 0
|
||||
? ` <span style="font-size:0.75rem;color:var(--color-muted);">⏱ ${e.minutes} Min.</span>`
|
||||
: '';
|
||||
optionsHtml += `<button class="task-vote-btn ${isMyVote ? 'my-vote' : ''}"
|
||||
id="tvbtn-${vote.voteSessionId}-${i}"
|
||||
optionsHtml += `<button class="task-vote-btn ${e.ownVote ? 'my-vote' : ''}"
|
||||
id="tvbtn-${id}-${i}"
|
||||
${(alreadyVoted || isOwn) ? 'disabled' : ''}
|
||||
onclick="castTaskVote('${vote.voteSessionId}', ${i})">
|
||||
onclick="castTaskVote('${id}', ${i})">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:600;">${esc(t.title)}${mins}</div>
|
||||
<div style="font-weight:600;">${esc(e.title)}${mins}</div>
|
||||
${desc}
|
||||
</div>
|
||||
<span class="task-vote-count" id="tvcount-${vote.voteSessionId}-${i}">${count} Stimme${count !== 1 ? 'n' : ''}</span>
|
||||
<span class="task-vote-count" id="tvcount-${id}-${i}">${e.votes} Stimme${e.votes !== 1 ? 'n' : ''}</span>
|
||||
</button>`;
|
||||
});
|
||||
|
||||
@@ -225,181 +296,107 @@
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'task-vote-card';
|
||||
card.dataset.ts = vote.createdAt;
|
||||
card.innerHTML = `
|
||||
<div class="task-vote-header">
|
||||
<span class="task-vote-lockee">🃏 ${esc(vote.lockeeName)}</span>
|
||||
<span class="task-vote-expires">Endet: ${fmtDateTime(vote.expiresAt)}</span>
|
||||
<span class="task-vote-lockee">🃏 ${esc(base.lockeeName)}</span>
|
||||
<span class="task-vote-expires">Endet: ${fmtDateTime(detail.expiresAt)}</span>
|
||||
</div>
|
||||
<div class="task-vote-options">${optionsHtml}</div>
|
||||
${ownHint}`;
|
||||
return card;
|
||||
}
|
||||
|
||||
async function castTaskVote(voteSessionId, taskIndex) {
|
||||
document.querySelectorAll(`[id^="tvbtn-${voteSessionId}-"]`).forEach(btn => btn.disabled = true);
|
||||
async function castTaskVote(displayId, taskIndex) {
|
||||
document.querySelectorAll(`[id^="tvbtn-${displayId}-"]`).forEach(btn => btn.disabled = true);
|
||||
|
||||
const res = await fetch(`/task-card/community/votes/${voteSessionId}/vote/${taskIndex}`, { method: 'POST' });
|
||||
const res = await fetch(`/games/chastity/community/taskvote/${displayId}/vote/${taskIndex}`, { method: 'POST' });
|
||||
if (res.ok || res.status === 204) {
|
||||
const countEl = document.getElementById(`tvcount-${voteSessionId}-${taskIndex}`);
|
||||
const countEl = document.getElementById(`tvcount-${displayId}-${taskIndex}`);
|
||||
if (countEl) {
|
||||
const next = (parseInt(countEl.textContent) || 0) + 1;
|
||||
countEl.textContent = `${next} Stimme${next !== 1 ? 'n' : ''}`;
|
||||
}
|
||||
const btn = document.getElementById(`tvbtn-${voteSessionId}-${taskIndex}`);
|
||||
if (btn) btn.classList.add('my-vote');
|
||||
document.getElementById(`tvbtn-${displayId}-${taskIndex}`)?.classList.add('my-vote');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Verification card builder ──────────────────────────────────────────────
|
||||
// ── Pranger-Karte ──────────────────────────────────────────────────────────
|
||||
|
||||
function buildVerCard(item) {
|
||||
const isOwn = item.myVote === 'own';
|
||||
const voted = isOwn || (item.myVote !== null && item.myVote !== undefined);
|
||||
const votedUp = !isOwn && item.myVote === true;
|
||||
const votedDn = !isOwn && item.myVote === false;
|
||||
const PILLORY_LABELS = {
|
||||
HYGIENE_OPENING_EXEEDED: 'Hygiene-Öffnung überschritten',
|
||||
KEYHOLDER_DESCESSION: 'Keyholder hat aufgegeben'
|
||||
};
|
||||
|
||||
function buildPilloryCard(base, detail) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'vote-card';
|
||||
card.dataset.ts = item.verificationTime;
|
||||
card.className = 'pillory-card';
|
||||
card.innerHTML = `
|
||||
<div class="vote-card-media">
|
||||
<img class="vote-card-img" src="data:image/jpeg;base64,${item.image}" alt="Verifikationsbild">
|
||||
<div class="vote-card-code">${esc(item.code)}</div>
|
||||
<div class="pillory-header">
|
||||
<span class="pillory-lockee">🔒 ${esc(base.lockeeName)}</span>
|
||||
<span class="pillory-date">${fmtDateTime(base.createdAt)}</span>
|
||||
</div>
|
||||
<div class="vote-card-body">
|
||||
<div class="vote-meta">Verifikation · ${fmtDateTime(item.verificationTime)}</div>
|
||||
<div class="vote-actions">
|
||||
<button class="vote-btn ${votedUp ? 'voted-up' : ''}" id="up-${item.verificationId}"
|
||||
${voted ? 'disabled' : ''}
|
||||
onclick="castVerVote('${item.verificationId}', true)">
|
||||
👍 <span class="vote-count" id="upcount-${item.verificationId}">${item.upvotes}</span>
|
||||
</button>
|
||||
<button class="vote-btn ${votedDn ? 'voted-down' : ''}" id="dn-${item.verificationId}"
|
||||
${voted ? 'disabled' : ''}
|
||||
onclick="castVerVote('${item.verificationId}', false)">
|
||||
👎 <span class="vote-count" id="dncount-${item.verificationId}">${item.downvotes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
<div class="pillory-reason">⚠️ ${esc(PILLORY_LABELS[detail.reason] || detail.reason)}</div>
|
||||
${detail.message ? `<div class="pillory-message">${esc(detail.message)}</div>` : ''}`;
|
||||
return card;
|
||||
}
|
||||
|
||||
async function castVerVote(verificationId, upvote) {
|
||||
const upBtn = document.getElementById('up-' + verificationId);
|
||||
const dnBtn = document.getElementById('dn-' + verificationId);
|
||||
upBtn.disabled = true;
|
||||
dnBtn.disabled = true;
|
||||
// ── Unified feed mit Paging ────────────────────────────────────────────────
|
||||
|
||||
const res = await fetch('/verification/' + verificationId + '/vote/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ upvote })
|
||||
});
|
||||
if (res.ok || res.status === 202) {
|
||||
const countEl = document.getElementById(upvote ? 'upcount-' + verificationId : 'dncount-' + verificationId);
|
||||
countEl.textContent = parseInt(countEl.textContent) + 1;
|
||||
(upvote ? upBtn : dnBtn).classList.add(upvote ? 'voted-up' : 'voted-down');
|
||||
}
|
||||
let page = 0;
|
||||
let exhausted = false;
|
||||
let loading = false;
|
||||
let rendered = 0;
|
||||
|
||||
async function fetchDetail(base) {
|
||||
const urls = {
|
||||
VERIFICATION: `/games/chastity/community/verification/${base.displayId}`,
|
||||
TASK_VOTE: `/games/chastity/community/taskvote/${base.displayId}`,
|
||||
PILLORY: `/games/chastity/community/pillory/${base.displayId}`
|
||||
};
|
||||
const url = urls[base.type];
|
||||
if (!url) return null;
|
||||
try {
|
||||
const r = await fetch(url);
|
||||
return r.ok ? await r.json() : null;
|
||||
} catch(e) { return null; }
|
||||
}
|
||||
|
||||
// ── Unified feed ──────────────────────────────────────────────────────────
|
||||
// Strategy: task votes are loaded once (small set); verifications are paginated.
|
||||
// When each verification page loads, we interleave the pending task votes that
|
||||
// belong chronologically before the oldest verification on this page.
|
||||
|
||||
let verPage = 0;
|
||||
let verExhausted = false;
|
||||
let loading = false;
|
||||
// Task votes sorted newest-first; items are removed as they get placed in feed
|
||||
let pendingTaskVotes = [];
|
||||
let taskVotesLoaded = false;
|
||||
let totalRendered = 0;
|
||||
|
||||
function getTs(isoStr) { return new Date(isoStr).getTime(); }
|
||||
|
||||
// Append cards to feed in the correct order.
|
||||
// `items` must already be sorted newest-first.
|
||||
function appendItems(items) {
|
||||
const feed = document.getElementById('feed');
|
||||
items.forEach(card => feed.appendChild(card));
|
||||
totalRendered += items.length;
|
||||
}
|
||||
|
||||
// Merge verItems (sorted newest-first) with the front of pendingTaskVotes
|
||||
// (also sorted newest-first). Only consume task votes that are >= cutoffMs
|
||||
// so that older task votes wait for later verification pages.
|
||||
// Pass cutoffMs = -Infinity to flush all remaining task votes.
|
||||
function mergeWithTaskVotes(verItems, cutoffMs) {
|
||||
const result = [];
|
||||
let vIdx = 0;
|
||||
while (vIdx < verItems.length || (pendingTaskVotes.length > 0 && getTs(pendingTaskVotes[0].createdAt) >= cutoffMs)) {
|
||||
const verTs = vIdx < verItems.length ? getTs(verItems[vIdx].verificationTime) : -Infinity;
|
||||
const tvTs = pendingTaskVotes.length > 0 && getTs(pendingTaskVotes[0].createdAt) >= cutoffMs
|
||||
? getTs(pendingTaskVotes[0].createdAt)
|
||||
: -Infinity;
|
||||
|
||||
if (tvTs >= verTs) {
|
||||
result.push(buildTaskVoteCard(pendingTaskVotes.shift()));
|
||||
} else if (vIdx < verItems.length) {
|
||||
result.push(buildVerCard(verItems[vIdx++]));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Remaining verifications (task votes exhausted or below cutoff)
|
||||
while (vIdx < verItems.length) {
|
||||
result.push(buildVerCard(verItems[vIdx++]));
|
||||
}
|
||||
return result;
|
||||
function buildCard(base, detail) {
|
||||
if (!detail) return null;
|
||||
if (base.type === 'VERIFICATION') return buildVerCard(base, detail);
|
||||
if (base.type === 'TASK_VOTE') return buildTaskVoteCard(base, detail);
|
||||
if (base.type === 'PILLORY') return buildPilloryCard(base, detail);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || verExhausted) return;
|
||||
if (loading || exhausted) return;
|
||||
loading = true;
|
||||
document.getElementById('loadSpinner').style.display = '';
|
||||
|
||||
// Load task votes on first call
|
||||
if (!taskVotesLoaded) {
|
||||
try {
|
||||
const r = await fetch('/task-card/community/votes');
|
||||
if (r.ok) {
|
||||
const votes = await r.json();
|
||||
pendingTaskVotes = votes.sort((a, b) => getTs(b.createdAt) - getTs(a.createdAt));
|
||||
}
|
||||
} catch(e) {}
|
||||
taskVotesLoaded = true;
|
||||
}
|
||||
|
||||
let verItems = [];
|
||||
let pageData;
|
||||
try {
|
||||
const r = await fetch('/verification/community?page=' + verPage);
|
||||
if (r.ok) verItems = await r.json();
|
||||
} catch(e) {}
|
||||
const r = await fetch(`/games/chastity/community?page=${page}&sort=createdAt,desc`);
|
||||
if (!r.ok) { loading = false; document.getElementById('loadSpinner').style.display = 'none'; return; }
|
||||
pageData = await r.json();
|
||||
} catch(e) { loading = false; document.getElementById('loadSpinner').style.display = 'none'; return; }
|
||||
|
||||
const items = pageData.content || [];
|
||||
if (pageData.last) exhausted = true;
|
||||
page++;
|
||||
|
||||
const details = await Promise.all(items.map(fetchDetail));
|
||||
|
||||
document.getElementById('loadSpinner').style.display = 'none';
|
||||
loading = false;
|
||||
|
||||
let batch;
|
||||
if (verItems.length === 0) {
|
||||
verExhausted = true;
|
||||
batch = mergeWithTaskVotes([], -Infinity);
|
||||
} else {
|
||||
const oldestVerTs = getTs(verItems[verItems.length - 1].verificationTime);
|
||||
if (verItems.length < 10) {
|
||||
verExhausted = true;
|
||||
// Last page: flush all remaining task votes too
|
||||
batch = mergeWithTaskVotes(verItems, -Infinity);
|
||||
} else {
|
||||
verPage++;
|
||||
// Only include task votes newer than the oldest item in this page
|
||||
batch = mergeWithTaskVotes(verItems, oldestVerTs);
|
||||
}
|
||||
}
|
||||
const feed = document.getElementById('feed');
|
||||
items.forEach((base, i) => {
|
||||
const card = buildCard(base, details[i]);
|
||||
if (card) { feed.appendChild(card); rendered++; }
|
||||
});
|
||||
|
||||
if (batch.length > 0) appendItems(batch);
|
||||
|
||||
if (totalRendered === 0 && verExhausted) {
|
||||
if (rendered === 0 && exhausted) {
|
||||
document.getElementById('emptyHint').style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
BIN
xxxthegame/src/main/resources/static/img/card_cum.png
Normal file
BIN
xxxthegame/src/main/resources/static/img/card_cum.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 299 KiB |
BIN
xxxthegame/src/main/resources/static/img/card_cum_caged.png
Normal file
BIN
xxxthegame/src/main/resources/static/img/card_cum_caged.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 301 KiB |
@@ -62,6 +62,22 @@ const CARD_DEFS = [
|
||||
defMin: 0,
|
||||
defMax: 0,
|
||||
},
|
||||
{
|
||||
id: 'CUM',
|
||||
img: '/img/card_cum.png',
|
||||
name: 'Cum',
|
||||
desc: 'Spezielle Karte.',
|
||||
defMin: 0,
|
||||
defMax: 0,
|
||||
},
|
||||
{
|
||||
id: 'CUM_IN_CAGE',
|
||||
img: '/img/card_cum_caged.png',
|
||||
name: 'Cum in Cage',
|
||||
desc: 'Spezielle Karte.',
|
||||
defMin: 0,
|
||||
defMax: 0,
|
||||
},
|
||||
];
|
||||
|
||||
/** Lookup-Objekt für Konsumenten, die nach ID auf Name/Bild/Beschreibung zugreifen. */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neues Lock – XXX The Game</title>
|
||||
@@ -196,7 +196,8 @@
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Optionen</div>
|
||||
|
||||
<div class="form-row">
|
||||
<!-- CardLock: Längste Dauer -->
|
||||
<div class="form-row" id="rowMaxDuration">
|
||||
<label>Längste Dauer</label>
|
||||
<div class="time-picker">
|
||||
<div class="tp-seg">
|
||||
@@ -220,6 +221,13 @@
|
||||
<div class="form-hint">Das Lock öffnet spätestens nach dieser Zeit automatisch. 0 : 00 = keine Begrenzung.</div>
|
||||
</div>
|
||||
|
||||
<!-- TimeLock: Dauer-Info aus Vorlage -->
|
||||
<div class="form-row" id="rowTimeLockInfo" style="display:none;">
|
||||
<label>Sperrdauer</label>
|
||||
<div id="timeLockDurationText" style="font-size:0.9rem;color:var(--color-text);padding:0.3rem 0;"></div>
|
||||
<div class="form-hint">Die Dauer wird beim Lock-Start zufällig aus dem Bereich der Vorlage gewählt.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="rowUnlockCodeLines">
|
||||
<label for="unlockCodeLines">Anzahl Ziffern des Entsperrcodes</label>
|
||||
<div class="inline-number">
|
||||
@@ -272,11 +280,12 @@
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script src="/js/social-sidebar.js"></script>
|
||||
<script>
|
||||
let myUserId = null;
|
||||
let myUserName = null;
|
||||
let allFriends = [];
|
||||
let allTemplates = [];
|
||||
let comboActiveIdx = -1;
|
||||
let myUserId = null;
|
||||
let myUserName = null;
|
||||
let allFriends = [];
|
||||
let allTemplates = []; // combined; each entry has _type: 'cardlock'|'timelock'
|
||||
let selectedTemplate = null;
|
||||
let comboActiveIdx = -1;
|
||||
|
||||
// ── Boot ──
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
|
||||
@@ -284,9 +293,16 @@
|
||||
myUserId = user.userId;
|
||||
myUserName = user.name;
|
||||
|
||||
// Templates laden – Pflicht
|
||||
// Templates laden – Pflicht (beide Typen parallel)
|
||||
try {
|
||||
allTemplates = await fetch('/cardlock/templates').then(r => r.ok ? r.json() : []);
|
||||
const [cardTpls, timeTpls] = await Promise.all([
|
||||
fetch('/cardlock/templates').then(r => r.ok ? r.json() : []),
|
||||
fetch('/timelock/templates').then(r => r.ok ? r.json() : [])
|
||||
]);
|
||||
allTemplates = [
|
||||
...cardTpls.map(t => ({ ...t, _type: 'cardlock' })),
|
||||
...timeTpls.map(t => ({ ...t, _type: 'timelock' }))
|
||||
];
|
||||
} catch { allTemplates = []; }
|
||||
|
||||
if (allTemplates.length === 0) {
|
||||
@@ -333,16 +349,20 @@
|
||||
dropdown.innerHTML = `<div class="combo-empty">Keine Vorlagen gefunden.</div>`;
|
||||
} else {
|
||||
filtered.forEach(t => {
|
||||
const badge = t._type === 'timelock' ? '⏱' : '🃏';
|
||||
const label = (t.name || 'Unbenannte Vorlage');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'combo-option';
|
||||
div.dataset.id = t.templateId;
|
||||
div.textContent = t.name || 'Unbenannte Vorlage';
|
||||
div.innerHTML = `${badge} ${label}`;
|
||||
div.addEventListener('mousedown', e => {
|
||||
e.preventDefault();
|
||||
hidden.value = t.templateId;
|
||||
input.value = t.name || 'Unbenannte Vorlage';
|
||||
hidden.value = t.templateId;
|
||||
input.value = badge + ' ' + label;
|
||||
selectedTemplate = t;
|
||||
dropdown.classList.remove('open');
|
||||
clearFieldError('rowTemplate');
|
||||
onTemplateChanged(t);
|
||||
});
|
||||
dropdown.appendChild(div);
|
||||
});
|
||||
@@ -471,6 +491,30 @@
|
||||
input.addEventListener('blur', () => { setTimeout(() => { dropdown.classList.remove('open'); if (!hidden.value) input.value = ''; }, 150); });
|
||||
}
|
||||
|
||||
// ── Template-Typ: Sektionen umschalten ──
|
||||
function onTemplateChanged(t) {
|
||||
const isTimeLock = t._type === 'timelock';
|
||||
document.getElementById('rowMaxDuration').style.display = isTimeLock ? 'none' : '';
|
||||
document.getElementById('rowTimeLockInfo').style.display = isTimeLock ? '' : 'none';
|
||||
if (isTimeLock) {
|
||||
const minM = t.minTimeInMinutes || 0;
|
||||
const maxM = t.maxTimeInMinutes || 0;
|
||||
document.getElementById('timeLockDurationText').textContent =
|
||||
`${fmtMinutes(minM)} – ${fmtMinutes(maxM)}`;
|
||||
}
|
||||
}
|
||||
function fmtMinutes(m) {
|
||||
if (!m) return '0 Min.';
|
||||
const d = Math.floor(m / 1440);
|
||||
const h = Math.floor((m % 1440) / 60);
|
||||
const min = m % 60;
|
||||
const parts = [];
|
||||
if (d) parts.push(d + ' Tag' + (d !== 1 ? 'e' : ''));
|
||||
if (h) parts.push(h + ' Std');
|
||||
if (min) parts.push(min + ' Min.');
|
||||
return parts.join(' ') || '0 Min.';
|
||||
}
|
||||
|
||||
// ── Zeitpicker ──
|
||||
function tpChange(prefix, delta, seg) {
|
||||
let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
|
||||
@@ -542,39 +586,56 @@
|
||||
}
|
||||
clearFieldError('rowTemplate');
|
||||
|
||||
const t = allTemplates.find(x => x.templateId === templateId);
|
||||
const t = selectedTemplate || allTemplates.find(x => x.templateId === templateId);
|
||||
if (!t) { showError('Vorlage nicht gefunden.'); return; }
|
||||
|
||||
const lockeeVal = document.getElementById('lockeeValue').value;
|
||||
const keyholderVal = document.getElementById('keyholderValue').value;
|
||||
const isFriendLockee = lockeeVal && lockeeVal !== myUserId;
|
||||
const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5);
|
||||
const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked;
|
||||
|
||||
const initialCards = buildInitialCardsFromTemplate(t);
|
||||
if (initialCards.length === 0) {
|
||||
showError('Die gewählte Vorlage enthält keine Karten. Bitte Vorlage prüfen.');
|
||||
return;
|
||||
let endpoint, body;
|
||||
|
||||
if (t._type === 'timelock') {
|
||||
endpoint = '/keyholder/timelock';
|
||||
body = {
|
||||
templateId: t.templateId,
|
||||
lockeeUserId: isFriendLockee ? lockeeVal : null,
|
||||
lockeeDetailsVisible: isFriendLockee ? document.getElementById('lockeeDetailsVisible').checked : false,
|
||||
keyholder: isFriendLockee ? null : (keyholderVal || null),
|
||||
testLock: isTestLock,
|
||||
unlockCodeLength: unlockCodeLen,
|
||||
};
|
||||
} else {
|
||||
// CardLock
|
||||
const initialCards = buildInitialCardsFromTemplate(t);
|
||||
if (initialCards.length === 0) {
|
||||
showError('Die gewählte Vorlage enthält keine Karten. Bitte Vorlage prüfen.');
|
||||
return;
|
||||
}
|
||||
endpoint = '/keyholder/cardlock';
|
||||
body = {
|
||||
name: t.name,
|
||||
lockeeUserId: isFriendLockee ? lockeeVal : null,
|
||||
lockeeDetailsVisible: isFriendLockee ? document.getElementById('lockeeDetailsVisible').checked : false,
|
||||
keyholder: isFriendLockee ? null : (keyholderVal || null),
|
||||
initialCards,
|
||||
pickEveryMinute: t.pickEveryMinute,
|
||||
accumulatePicks: t.accumulatePicks,
|
||||
showRemainingCards: t.showRemainingCards,
|
||||
latestOpeningtime: durationToLatestOpening(),
|
||||
hygineOpeningEveryMinites: t.hygineOpeningEveryMinites || null,
|
||||
hygineOpeningDurationMinutes: t.hygineOpeningDurationMinutes || null,
|
||||
tasks: t.tasks || [],
|
||||
taskCardMode: t.taskCardMode || 'RANDOM',
|
||||
unlockCodeLines: unlockCodeLen,
|
||||
requiresVerification: t.requiresVerification,
|
||||
testLock: isTestLock,
|
||||
};
|
||||
}
|
||||
|
||||
const body = {
|
||||
name: t.name,
|
||||
lockeeUserId: isFriendLockee ? lockeeVal : null,
|
||||
lockeeDetailsVisible: isFriendLockee ? document.getElementById('lockeeDetailsVisible').checked : false,
|
||||
keyholder: isFriendLockee ? null : (keyholderVal || null),
|
||||
initialCards,
|
||||
pickEveryMinute: t.pickEveryMinute,
|
||||
accumulatePicks: t.accumulatePicks,
|
||||
showRemainingCards: t.showRemainingCards,
|
||||
latestOpeningtime: durationToLatestOpening(),
|
||||
hygineOpeningEveryMinites: t.hygineOpeningEveryMinites || null,
|
||||
hygineOpeningDurationMinutes: t.hygineOpeningDurationMinutes || null,
|
||||
tasks: t.tasks || [],
|
||||
taskCardMode: t.taskCardMode || 'RANDOM',
|
||||
unlockCodeLines: isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5),
|
||||
requiresVerification: t.requiresVerification,
|
||||
testLock: isFriendLockee ? false : document.getElementById('testLock').checked,
|
||||
};
|
||||
|
||||
const res = await fetch('/keyholder/cardlock', {
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
@@ -605,7 +666,9 @@
|
||||
function showUnlockCodeModal(code, lockId, keyholderPending) {
|
||||
document.getElementById('unlockCodeDisplay').textContent = code;
|
||||
if (keyholderPending) document.getElementById('unlockKeyholderHint').style.display = '';
|
||||
const url = '/activelock.html?lockId=' + lockId + (keyholderPending ? '&keyholderPending=1' : '');
|
||||
const isTimeLock = selectedTemplate && selectedTemplate._type === 'timelock';
|
||||
const targetPage = isTimeLock ? '/activetimelock.html' : '/activelock.html';
|
||||
const url = targetPage + '?lockId=' + lockId + (keyholderPending ? '&keyholderPending=1' : '');
|
||||
document.getElementById('unlockModalBtn').onclick = () => startCodeScramble(code, url);
|
||||
document.getElementById('unlockModal').classList.add('open');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user