großes refactoring

This commit is contained in:
2026-03-22 23:13:40 +01:00
parent 53e7bcbbcc
commit 409f003aec
99 changed files with 10124 additions and 4386 deletions

View File

@@ -1,5 +1,5 @@
#Sat Mar 21 08:14:24 CET 2026 #Sun Mar 22 19:48:43 CET 2026
display=\:0 display=\:0
host=Mario-Linux host=Mario-Linux
process-id=23243 process-id=106706
user=mario user=mario

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,7 @@
[ { [ {
"version" : "9.6.0-20260321035213+0000", "version" : "9.5.0-20260322013634+0000",
"buildTime" : "20260321035213+0000", "buildTime" : "20260322013634+0000",
"commitId" : "cc9b7c946cf24a57b2119d39f1a33cf5493ea930", "commitId" : "01db0eb99f616dd415a084ffcce4cb2c185d5a2a",
"current" : false,
"snapshot" : true,
"nightly" : true,
"releaseNightly" : false,
"activeRc" : false,
"rcFor" : "",
"milestoneFor" : "",
"broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260321035213+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260321035213+0000-bin.zip.sha256",
"checksum" : "e533696ad1e80c2878ed39c18b5252ac7e0bad6394ead2b93663656cd6591059",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260321035213+0000-wrapper.jar.sha256",
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
}, {
"version" : "9.5.0-20260321014114+0000",
"buildTime" : "20260321014114+0000",
"commitId" : "e7d13113f033cc35f5b8c7e0eeb88906259a1410",
"current" : false, "current" : false,
"snapshot" : true, "snapshot" : true,
"nightly" : false, "nightly" : false,
@@ -27,10 +10,27 @@
"rcFor" : "", "rcFor" : "",
"milestoneFor" : "", "milestoneFor" : "",
"broken" : false, "broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260321014114+0000-bin.zip", "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260321014114+0000-bin.zip.sha256", "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-bin.zip.sha256",
"checksum" : "eeb50f4468d73f74a68fe62a16d371ccc7c54088d7d2f672adb12c4bce4104d5", "checksum" : "3e8a6689594399f81087ad962b1c489e0ae57201af0c6c00ea63d9d07e48506e",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260321014114+0000-wrapper.jar.sha256", "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260322013634+0000-wrapper.jar.sha256",
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
}, {
"version" : "9.6.0-20260322000231+0000",
"buildTime" : "20260322000231+0000",
"commitId" : "d63be1b9cd4d937f4a9f5cf7ee78eec20fe5354e",
"current" : false,
"snapshot" : true,
"nightly" : true,
"releaseNightly" : false,
"activeRc" : false,
"rcFor" : "",
"milestoneFor" : "",
"broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260322000231+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260322000231+0000-bin.zip.sha256",
"checksum" : "80f3af587bc824675e2a5617c7f30a3b8e4888746d486bc8b3517ebf84f028a9",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260322000231+0000-wrapper.jar.sha256",
"wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a" "wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
}, { }, {
"version" : "9.4.1", "version" : "9.4.1",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,6 +11,7 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
134995224.index 134995224.index
4025319337.index 4025319337.index
900586112.index 900586112.index
9341915.index
2929476459.index 2929476459.index
2065500052.index 2065500052.index
3051047092.index 3051047092.index

View File

@@ -4,4 +4,13 @@
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.history{GameHistoryEntity.java[GameHistoryEntity" modifiers="1" timestamp="1773860770365"/> <typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.history{GameHistoryEntity.java[GameHistoryEntity" modifiers="1" timestamp="1773860770365"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.bdsm.controller{BdsmGameController.java[BdsmGameController" modifiers="1" timestamp="1774017499554"/> <typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.bdsm.controller{BdsmGameController.java[BdsmGameController" modifiers="1" timestamp="1774017499554"/>
<typeInfo handle="=xxxthegame/\/usr\/lib\/jvm\/java-21-openjdk-amd64\/lib\/jrt-fs.jar`java.base=/javadoc_location=/https:\/\/docs.oracle.com\/en\/java\/javase\/21\/docs\/api\/=/&lt;java.util(UUID.class[UUID" modifiers="49" timestamp="1769125611000"/> <typeInfo handle="=xxxthegame/\/usr\/lib\/jvm\/java-21-openjdk-amd64\/lib\/jrt-fs.jar`java.base=/javadoc_location=/https:\/\/docs.oracle.com\/en\/java\/javase\/21\/docs\/api\/=/&lt;java.util(UUID.class[UUID" modifiers="49" timestamp="1769125611000"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.cardlock{CardLockController.java[CardLockController" modifiers="1" timestamp="1774198561526"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.cardlock{CardLockEntity.java[CardLockEntity" modifiers="1" timestamp="1774171624571"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.timelock{TimeLockEntity.java[TimeLockEntity" modifiers="1" timestamp="1774174304909"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.timelock{TimeLockTemplateEntity.java[TimeLockTemplateEntity" modifiers="1" timestamp="1774174437363"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.tasks{Task.java[Task" modifiers="1" timestamp="1774181757140"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.keyholder{KeyholderVerificationRepository.java[KeyholderVerificationRepository" modifiers="513" timestamp="1774197695163"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.keyholder{KeyholderVerificationEntity.java[KeyholderVerificationEntity" modifiers="1" timestamp="1774198252535"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.community{CommunityVerificationEntity.java[CommunityVerificationEntity" modifiers="1" timestamp="1774198326209"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.chastity.common{Verification.java[Verification" modifiers="513" timestamp="1774198235758"/>
</typeInfoHistroy> </typeInfoHistroy>

View File

@@ -1,28 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<qualifiedTypeNameHistroy> <qualifiedTypeNameHistroy>
<fullyQualifiedTypeName name="java.util.List"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.lockcontroll.LockControl"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.verification.VerificationEntity"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.lockcontroll.TTLockControl"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.CodeCreator"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.lockcontroll.TrustLockControl"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.verification.VerificationRepository"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.lockcontroll.UnlockcodeLockControl"/>
<fullyQualifiedTypeName name="org.hibernate.grammars.hql.HqlParser.LocalDateTimeContext"/>
<fullyQualifiedTypeName name="java.util.stream.Collectors"/>
<fullyQualifiedTypeName name="java.time.LocalDate"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.CardDTO"/>
<fullyQualifiedTypeName name="java.lang.Enum"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.GreenCard"/>
<fullyQualifiedTypeName name="java.util.Random"/> <fullyQualifiedTypeName name="java.util.Random"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.CardLockService"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.TempOpeningReason"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.history.LockHistoryRepository"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.Test"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.cardlock.UnlockCodeHistoryService"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.KeyholderCardLock"/>
<fullyQualifiedTypeName name="lombok.Getter"/>
<fullyQualifiedTypeName name="lombok.Setter"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.timelock.TimeLockService"/>
<fullyQualifiedTypeName name="jakarta.persistence.Column"/>
<fullyQualifiedTypeName name="java.time.temporal.ChronoUnit"/>
<fullyQualifiedTypeName name="java.time.LocalDateTime"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.AbstractLockService"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.timelock.TimeLockEntity"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.timelock.TimeLockEntity"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.timelock.TimeLockRepository"/> <fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.timelock.TimeLockRepository"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.verification.VerificationVoteRepository"/> <fullyQualifiedTypeName name="jakarta.persistence.Entity"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.pillory.PilloryEntity"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.pillory.PilloryReason"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.pillory.PilloryRepository"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.timelock.TimeLockService"/>
<fullyQualifiedTypeName name="jakarta.persistence.Table"/>
<fullyQualifiedTypeName name="jakarta.persistence.Inheritance"/>
<fullyQualifiedTypeName name="jakarta.persistence.DiscriminatorColumn"/>
<fullyQualifiedTypeName name="jakarta.persistence.DiscriminatorType"/>
<fullyQualifiedTypeName name="jakarta.persistence.EnumType"/>
<fullyQualifiedTypeName name="org.springframework.stereotype.Controller"/>
<fullyQualifiedTypeName name="java.util.UUID"/>
<fullyQualifiedTypeName name="com.fasterxml.jackson.core.sym.Name3"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.tasks.TaskMode"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.BaseLockTemplateEntity"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.BaseCommunityDisplayDTO"/>
<fullyQualifiedTypeName name="org.springframework.data.domain.Page"/>
<fullyQualifiedTypeName name="org.springframework.data.domain.Sort.Direction"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityPilloryReason"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.BaseCommunityDisplayEntity"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityPilloryDTO"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityPilloryRepository"/>
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.RestController"/>
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.RequestMapping"/>
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.GetMapping"/>
<fullyQualifiedTypeName name="de.oaa.xxx.user.UserRepository"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityTaskVoteEntryRepository"/>
<fullyQualifiedTypeName name="org.springframework.web.bind.annotation.PathVariable"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityTaskVoteDTO"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityTaskVoteEntryDTO"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.BaseLockTemplateRepository"/>
<fullyQualifiedTypeName name="org.springframework.data.jpa.repository.JpaRepository"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.BaseLockEntity"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.BaseLockRepository"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.tasks.Task"/>
<fullyQualifiedTypeName name="java.util.ArrayList"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityTaskVoteDisplayEntryDTO"/>
<fullyQualifiedTypeName name="org.springframework.http.ResponseEntity"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityTaskVoteDisplayDTO"/>
<fullyQualifiedTypeName name="jakarta.persistence.Column"/>
<fullyQualifiedTypeName name="java.util.List"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.Verification"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationEntity"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.VerificationDTO"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.CodeCreator"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.common.VerificationCommonDTO"/>
<fullyQualifiedTypeName name="de.oaa.xxx.games.chastity.community.CommunityVerificationEntity"/>
<fullyQualifiedTypeName name="java.time.LocalDate"/>
<fullyQualifiedTypeName name="java.time.LocalDateTime"/>
</qualifiedTypeNameHistroy> </qualifiedTypeNameHistroy>

View File

@@ -90,4 +90,21 @@
<item key="DIALOG_HEIGHT" value="577"/> <item key="DIALOG_HEIGHT" value="577"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/> <item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/>
</section> </section>
<section name="NewInterfaceCreationWizard.dialogBounds">
<item key="DIALOG_X_ORIGIN" value="948"/>
<item key="DIALOG_Y_ORIGIN" value="274"/>
<item key="DIALOG_WIDTH" value="665"/>
<item key="DIALOG_HEIGHT" value="605"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/>
</section>
<section name="NewRecordCreationWizard.dialogBounds">
<item key="DIALOG_X_ORIGIN" value="965"/>
<item key="DIALOG_Y_ORIGIN" value="247"/>
<item key="DIALOG_WIDTH" value="631"/>
<item key="DIALOG_HEIGHT" value="646"/>
<item key="DIALOG_FONT_NAME" value="1|Ubuntu Sans|11.0|0|GTK|1|"/>
</section>
<section name="NewRecordWizardPage">
<item key="create_unimplemented" value="false"/>
</section>
</section> </section>

View File

@@ -35,3 +35,8 @@
2026-03-21 08:03:15,921 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update. 2026-03-21 08:03:15,921 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
2026-03-21 08:10:51,183 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-21 08:10:51,183 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-21 08:14:27,502 [Worker-8: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-21 08:14:27,502 [Worker-8: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-22 08:51:09,620 [Worker-8: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
2026-03-22 08:53:24,304 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-22 08:53:52,098 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-22 08:56:13,330 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-22 19:48:46,352 [Worker-8: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.

View File

@@ -1,3 +1,3 @@
#Sat Mar 21 08:14:24 CET 2026 #Sun Mar 22 19:48:43 CET 2026
org.eclipse.core.runtime=2 org.eclipse.core.runtime=2
org.eclipse.platform=4.39.0.v20260226-0420 org.eclipse.platform=4.39.0.v20260226-0420

View File

@@ -136,7 +136,7 @@ public class BdsmGameService {
newLock.setTasks(template.getTasks()); newLock.setTasks(template.getTasks());
newLock.setRequiresVerification(template.isRequiresVerification()); newLock.setRequiresVerification(template.isRequiresVerification());
newLock.setTestLock(false); newLock.setTestLock(false);
newLock.setTaskCardMode(template.getTaskCardMode()); newLock.setTaskMode(template.getTaskMode());
int codeLines = template.getUnlockCodeLength() != null ? template.getUnlockCodeLength() : 5; int codeLines = template.getUnlockCodeLength() != null ? template.getUnlockCodeLength() : 5;
newLock.setUnlockCodeLength(codeLines); newLock.setUnlockCodeLength(codeLines);

View File

@@ -37,25 +37,24 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import de.oaa.xxx.games.chastity.common.CodeCreator; 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.KeyholderInvitationEntity;
import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderInvitationRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; 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.KeyholderTaskChoiceRepository;
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationEntity; import de.oaa.xxx.games.chastity.lockee.LockeeInvitationEntity;
import de.oaa.xxx.games.chastity.lockee.LockeeInvitationRepository; import de.oaa.xxx.games.chastity.lockee.LockeeInvitationRepository;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity; import de.oaa.xxx.games.chastity.tasks.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository; import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task; 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.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryRepository; import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryRepository;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; 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.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
@@ -66,8 +65,8 @@ public class CardLockController {
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final KeyholderInvitationRepository invitationRepository; private final KeyholderInvitationRepository invitationRepository;
private final VerificationRepository verificationRepository; private final CommunityVerificationRepository verificationRepository;
private final VerificationVoteRepository verificationVoteRepository; private final CommunityVerificationVoteRepository verificationVoteRepository;
private final KeyholderNotificationRepository keyholderNotificationRepository; private final KeyholderNotificationRepository keyholderNotificationRepository;
private final LockeeInvitationRepository lockeeInvitationRepository; private final LockeeInvitationRepository lockeeInvitationRepository;
private final AssignedTaskRepository assignedTaskRepository; private final AssignedTaskRepository assignedTaskRepository;
@@ -81,16 +80,20 @@ public class CardLockController {
@Value("${app.base-url:http://localhost:8080}") @Value("${app.base-url:http://localhost:8080}")
private String baseUrl; private String baseUrl;
public CardLockController(CardlockRepository cardlockRepository, UserRepository userRepository, public CardLockController(CardlockRepository cardlockRepository,
KeyholderInvitationRepository invitationRepository, VerificationRepository verificationRepository, UserRepository userRepository,
VerificationVoteRepository verificationVoteRepository, KeyholderInvitationRepository invitationRepository,
CommunityVerificationRepository verificationRepository,
CommunityVerificationVoteRepository verificationVoteRepository,
KeyholderNotificationRepository keyholderNotificationRepository, KeyholderNotificationRepository keyholderNotificationRepository,
LockeeInvitationRepository lockeeInvitationRepository, AssignedTaskRepository assignedTaskRepository, LockeeInvitationRepository lockeeInvitationRepository,
AssignedTaskRepository assignedTaskRepository,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository, CommunityTaskVoteRepository communityTaskVoteRepository,
UnlockCodeHistoryRepository unlockCodeHistoryRepository, UnlockCodeHistoryService unlockCodeHistoryService, UnlockCodeHistoryRepository unlockCodeHistoryRepository,
UnlockCodeHistoryService unlockCodeHistoryService,
SystemMessageService systemMessageService, CardLockServiceFactory cardLockServiceFactory) { SystemMessageService systemMessageService,
CardLockServiceFactory cardLockServiceFactory) {
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.invitationRepository = invitationRepository; this.invitationRepository = invitationRepository;
@@ -111,7 +114,7 @@ public class CardLockController {
List<CardEnum> initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards, List<CardEnum> initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards,
LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites, LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites,
List<Task> tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines, List<Task> tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines,
String taskCardMode) { TaskMode taskMode) {
} }
private static final SecureRandom RNG = new SecureRandom(); private static final SecureRandom RNG = new SecureRandom();
@@ -161,7 +164,7 @@ public class CardLockController {
lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
lock.setRequiresVerification(req.requiresVerification()); lock.setRequiresVerification(req.requiresVerification());
lock.setTestLock(false); 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 // startTime, unlockCode, unlockCodeLines left null until lockee accepts
cardlockRepository.save(lock); cardlockRepository.save(lock);
@@ -204,7 +207,7 @@ public class CardLockController {
lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
lock.setRequiresVerification(req.requiresVerification()); lock.setRequiresVerification(req.requiresVerification());
lock.setTestLock(req.testLock()); 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.setUnlockCodeLength(codeLines);
lock.setUnlockCode(unlockCode); lock.setUnlockCode(unlockCode);
@@ -267,34 +270,7 @@ public class CardLockController {
if (dto == null) if (dto == null)
return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar")); return ResponseEntity.status(409).body(Map.of("error", "Keine Karte verfügbar"));
// Task-Karte in nicht-zufälligem Modus → Entscheidung delegieren String taskPending = (dto.card() == CardEnum.TASK) ? service.getPendingTaskMode() : null;
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";
}
}
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("card", dto.card().name()); result.put("card", dto.card().name());
@@ -455,10 +431,10 @@ public class CardLockController {
result.put("totalCards", totalCards); result.put("totalCards", totalCards);
result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0); result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0);
result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : ""); 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("currentTask", l.getCurrentTask() != null ? l.getCurrentTask() : null);
result.put("currentTaskDescription", l.getCurrentTaskDescription()); 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("hygieneEnabled", hygieneEnabled);
result.put("hygieneOpeningDue", hygieneOpeningDue); result.put("hygieneOpeningDue", hygieneOpeningDue);
result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); result.put("hygieneSecondsRemaining", hygieneSecondsRemaining);
@@ -489,19 +465,19 @@ public class CardLockController {
LocalDateTime todayStart = LocalDate.now().atStartOfDay(); LocalDateTime todayStart = LocalDate.now().atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1); LocalDateTime todayEnd = todayStart.plusDays(1);
var completed = verificationRepository var completed = verificationRepository
.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(l.getLockId(), todayStart, todayEnd); .findByLockIdAndCreatedAtBetweenAndImageIsNotNull(l.getLockId(), todayStart, todayEnd);
if (!completed.isEmpty()) { if (!completed.isEmpty()) {
var todayV = completed.get(0); var todayV = completed.get(0);
verificationTodayId = todayV.getVerficationId().toString(); verificationTodayId = todayV.getDisplayId().toString();
var votes = verificationVoteRepository.findAllByVerificationId(todayV.getVerficationId()); var votes = verificationVoteRepository.findAllByVerificationId(todayV.getDisplayId());
verificationUpvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count(); verificationUpvotes = votes.stream().filter(CommunityVerificationVoteEntity::isUpvote).count();
verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count(); verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count();
} else { } else {
verificationDue = true; verificationDue = true;
var pending = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNull(l.getLockId(), var pending = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNull(l.getLockId(),
todayStart, todayEnd); todayStart, todayEnd);
if (!pending.isEmpty()) { if (!pending.isEmpty()) {
verificationPendingId = pending.get(0).getVerficationId().toString(); verificationPendingId = pending.get(0).getDisplayId().toString();
verificationPendingCode = pending.get(0).getCode(); verificationPendingCode = pending.get(0).getCode();
} }
} }
@@ -515,20 +491,19 @@ public class CardLockController {
result.put("verificationPendingCode", verificationPendingCode); result.put("verificationPendingCode", verificationPendingCode);
// Abgelaufene Aufgaben prüfen und Strafe anwenden // Abgelaufene Aufgaben prüfen und Strafe anwenden
boolean lockDirty = false;
var expiredTasks = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING").stream() var expiredTasks = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING").stream()
.filter(t -> t.getAcceptDeadline().isBefore(LocalDateTime.now())).toList(); .filter(t -> t.getAcceptDeadline().isBefore(LocalDateTime.now())).toList();
for (var t : expiredTasks) { if (!expiredTasks.isEmpty()) {
t.setStatus("EXPIRED"); CardLockService penaltyService = cardLockServiceFactory.create(l);
applyAssignedTaskPenalty(l, t); for (var t : expiredTasks) {
assignedTaskRepository.save(t); t.setStatus("EXPIRED");
lockDirty = true; penaltyService.applyAssignedTaskPenalty(t);
sendMessage(l.getKeyholder(), l.getLockee(), assignedTaskRepository.save(t);
"Die dir gestellte Aufgabe ist abgelaufen, ohne dass du reagiert hast. Die Strafe wurde automatisch angewendet.", sendMessage(l.getKeyholder(), l.getLockee(),
"/activelock.html?lockId=" + l.getLockId(), de.oaa.xxx.social.entity.MessageCause.GAME_STATE); "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) // Ausstehende Keyholder-Aufgaben (ohne Aufgabentext)
var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING").stream() var pendingAssigned = assignedTaskRepository.findByLockIdAndStatus(l.getLockId(), "PENDING").stream()
@@ -546,20 +521,20 @@ public class CardLockController {
return m; return m;
}).toList(); }).toList();
result.put("assignedTasks", pendingAssigned); result.put("assignedTasks", pendingAssigned);
result.put("taskCardMode", l.getTaskCardMode()); result.put("taskMode", l.getTaskMode());
// Ausstehende Keyholder-Choices // Ausstehende Keyholder-Choices
boolean pendingKeyholderChoice = !keyholderTaskChoiceRepository.findByLockIdAndStatus(l.getLockId(), "PENDING") boolean pendingKeyholderChoice = !keyholderTaskChoiceRepository.findByLockIdAndActiveTrue(l.getLockId())
.isEmpty(); .isEmpty();
result.put("pendingKeyholderChoice", pendingKeyholderChoice); result.put("pendingKeyholderChoice", pendingKeyholderChoice);
// Aktive Community-Vote // Aktive Community-Vote
var activeVotes = communityTaskVoteRepository.findByStatus("ACTIVE").stream() var activeVotes = communityTaskVoteRepository.findByActiveTrue().stream()
.filter(v -> v.getLockId().equals(l.getLockId())).findFirst(); .filter(v -> v.getLockId().equals(l.getLockId())).findFirst();
if (activeVotes.isPresent()) { if (activeVotes.isPresent()) {
var v = activeVotes.get(); var v = activeVotes.get();
result.put("activeCommunityVote", 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 // Notfall-Entsperrung: nach 1 Stunde automatisch öffnen
@@ -608,30 +583,30 @@ public class CardLockController {
LocalDateTime todayEnd = todayStart.plusDays(1); LocalDateTime todayEnd = todayStart.plusDays(1);
// Existierende Verifikation für heute zurückgeben statt neue anlegen // Existierende Verifikation für heute zurückgeben statt neue anlegen
var existing = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNull(lockId, todayStart, var existing = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNull(lockId, todayStart,
todayEnd); todayEnd);
if (!existing.isEmpty()) { if (!existing.isEmpty()) {
var ev = existing.get(0); 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); todayStart, todayEnd);
if (!completed.isEmpty()) { if (!completed.isEmpty()) {
var cv = completed.get(0); 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(); CommunityVerificationEntity v = new CommunityVerificationEntity();
v.setVerficationId(UUID.randomUUID()); v.setDisplayId(UUID.randomUUID());
v.setLockId(lockId); v.setLockId(lockId);
v.setLockeeId(myId); v.setLockeeId(myId);
v.setCode(CodeCreator.createAlphanumericCode(6)); v.setCode(CodeCreator.createAlphanumeric(6));
v.setVerificationTime(LocalDateTime.now()); v.setCreatedAt(LocalDateTime.now());
if (l.getKeyholder() != null) if (l.getKeyholder() != null)
v.setKeyholderId(l.getKeyholder()); v.setKeyholderId(l.getKeyholder());
verificationRepository.save(v); 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) @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 todayStart = LocalDate.now().atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1); LocalDateTime todayEnd = todayStart.plusDays(1);
var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId, var completed = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId,
todayStart, todayEnd); todayStart, todayEnd);
for (var v : completed) { for (var v : completed) {
verificationVoteRepository.deleteAllByVerificationId(v.getVerficationId()); verificationVoteRepository.deleteAllByVerificationId(v.getDisplayId());
verificationRepository.delete(v); verificationRepository.delete(v);
} }
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
@@ -866,7 +841,7 @@ public class CardLockController {
item.put("lockeeProfilePic", lockee.getProfilePicture()); item.put("lockeeProfilePic", lockee.getProfilePicture());
item.put("totalCards", lock.getAvailableCards() != null ? lock.getAvailableCards().size() : 0); item.put("totalCards", lock.getAvailableCards() != null ? lock.getAvailableCards().size() : 0);
item.put("startTime", lock.getStartTime() != null ? lock.getStartTime().toString() : null); 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()); && (lock.getCurrentTask() == null || lock.getCurrentTask().isBlank());
item.put("isFrozenByKeyholder", frozenByKh); item.put("isFrozenByKeyholder", frozenByKh);
result.add(item); result.add(item);
@@ -919,16 +894,16 @@ public class CardLockController {
if (l.isRequiresVerification()) { if (l.isRequiresVerification()) {
LocalDateTime todayStart = LocalDate.now().atStartOfDay(); LocalDateTime todayStart = LocalDate.now().atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1); LocalDateTime todayEnd = todayStart.plusDays(1);
var completed = verificationRepository.findByLockIdAndVerificationTimeBetweenAndImageIsNotNull(lockId, var completed = verificationRepository.findByLockIdAndCreatedAtBetweenAndImageIsNotNull(lockId,
todayStart, todayEnd); todayStart, todayEnd);
if (!completed.isEmpty()) { if (!completed.isEmpty()) {
verificationDoneToday = true; verificationDoneToday = true;
var v = completed.get(0); var v = completed.get(0);
var votes = verificationVoteRepository.findAllByVerificationId(v.getVerficationId()); var votes = verificationVoteRepository.findAllByVerificationId(v.getDisplayId());
verificationUpvotes = votes.stream().filter(VerificationVoteEntity::isUpvote).count(); verificationUpvotes = votes.stream().filter(CommunityVerificationVoteEntity::isUpvote).count();
verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count(); verificationDownvotes = votes.stream().filter(v2 -> !v2.isUpvote()).count();
verificationTodayId = v.getVerficationId().toString(); verificationTodayId = v.getDisplayId().toString();
var myVoteOpt = verificationVoteRepository.findByVerificationIdAndUserId(v.getVerficationId(), myId); var myVoteOpt = verificationVoteRepository.findByVerificationIdAndUserId(v.getDisplayId(), myId);
if (myVoteOpt.isPresent()) { if (myVoteOpt.isPresent()) {
verificationMyVote = myVoteOpt.get().isUpvote() ? "upvote" : "downvote"; verificationMyVote = myVoteOpt.get().isUpvote() ? "upvote" : "downvote";
} else if (v.getImage() != null) { } else if (v.getImage() != null) {
@@ -954,9 +929,9 @@ public class CardLockController {
result.put("cardCounts", cardCounts); result.put("cardCounts", cardCounts);
result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0); result.put("openPicks", l.getOpenPicks() != null ? l.getOpenPicks() : 0);
result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : null); result.put("nextCardIn", l.getNextCardIn() != null ? l.getNextCardIn().toString() : null);
result.put("frozenUntill", l.getFrozenUntill() != null ? l.getFrozenUntill().toString() : null); result.put("frozenUntill", l.getFrozenUntil() != null ? l.getFrozenUntil().toString() : null);
result.put("taskFrozenUntil", l.getTaskFrozenUntil() != null ? l.getTaskFrozenUntil().toString() : null); result.put("taskFrozenUntil", l.getTaskUntil() != null ? l.getTaskUntil().toString() : null);
boolean isFrozenByKeyholder = l.getFrozenUntill() != null && l.getFrozenUntill().isAfter(LocalDateTime.now()); boolean isFrozenByKeyholder = l.getFrozenUntil() != null && l.getFrozenUntil().isAfter(LocalDateTime.now());
result.put("isFrozenByKeyholder", isFrozenByKeyholder); result.put("isFrozenByKeyholder", isFrozenByKeyholder);
result.put("currentTask", l.getCurrentTask()); result.put("currentTask", l.getCurrentTask());
result.put("currentTaskDescription", l.getCurrentTaskDescription()); result.put("currentTaskDescription", l.getCurrentTaskDescription());
@@ -978,7 +953,7 @@ public class CardLockController {
if (l.getTasks() != null) { if (l.getTasks() != null) {
var taskList = l.getTasks().stream().map(t -> { var taskList = l.getTasks().stream().map(t -> {
Map<String, Object> m = new LinkedHashMap<>(); 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("description", t.getDescription() != null ? t.getDescription() : "");
m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0); m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
return m; return m;
@@ -1002,7 +977,7 @@ public class CardLockController {
return m; return m;
}).toList(); }).toList();
result.put("pendingAssignedTasks", pendingAssigned); result.put("pendingAssignedTasks", pendingAssigned);
result.put("taskCardMode", l.getTaskCardMode()); result.put("taskMode", l.getTaskMode());
// Ausstehende Task-Karten-Choices (KEYHOLDER-Modus) // Ausstehende Task-Karten-Choices (KEYHOLDER-Modus)
List<Task> lockTasks = l.getTasks() != null ? l.getTasks() : List.of(); List<Task> lockTasks = l.getTasks() != null ? l.getTasks() : List.of();
@@ -1011,12 +986,12 @@ public class CardLockController {
Task t = lockTasks.get(i); Task t = lockTasks.get(i);
Map<String, Object> tm = new LinkedHashMap<>(); Map<String, Object> tm = new LinkedHashMap<>();
tm.put("index", i); tm.put("index", i);
tm.put("title", t.resolveTitle()); tm.put("title", t.getTitle());
tm.put("description", t.getDescription() != null ? t.getDescription() : ""); tm.put("description", t.getDescription() != null ? t.getDescription() : "");
tm.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0); tm.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
taskListForChoice.add(tm); 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<>(); Map<String, Object> cm = new LinkedHashMap<>();
cm.put("choiceId", c.getChoiceId().toString()); cm.put("choiceId", c.getChoiceId().toString());
cm.put("createdAt", c.getCreatedAt().toString()); cm.put("createdAt", c.getCreatedAt().toString());
@@ -1053,7 +1028,7 @@ public class CardLockController {
service.unlock(l.getUnlockCode()); service.unlock(l.getUnlockCode());
var verifications = verificationRepository.findByLockId(lockId); var verifications = verificationRepository.findByLockId(lockId);
verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getVerficationId())); verifications.forEach(v -> verificationVoteRepository.deleteAllByVerificationId(v.getDisplayId()));
verificationRepository.deleteAll(verifications); verificationRepository.deleteAll(verifications);
invitationRepository.deleteByLockId(lockId); invitationRepository.deleteByLockId(lockId);
cardlockRepository.deleteById(lockId); cardlockRepository.deleteById(lockId);
@@ -1180,24 +1155,6 @@ public class CardLockController {
// ── Hilfsmethoden ────────────────────────────────────────────────────────── // ── 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, private void sendMessage(UUID senderId, UUID receiverId, String text, String targetUrl,
de.oaa.xxx.social.entity.MessageCause cause) { de.oaa.xxx.social.entity.MessageCause cause) {
systemMessageService.send(senderId, receiverId, text, targetUrl, cause); systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
@@ -1263,9 +1220,9 @@ public class CardLockController {
Task task = tasks.get(req.taskIndex()); Task task = tasks.get(req.taskIndex());
AssignedTaskEntity assigned = new AssignedTaskEntity(); AssignedTaskEntity assigned = new AssignedTaskEntity();
assigned.setLockId(lockId); assigned.setLockId(lockId);
assigned.setTaskTitle(task.resolveTitle()); assigned.setTaskTitle(task.getTitle());
assigned.setTaskDescription(task.getDescription()); assigned.setTaskDescription(task.getDescription());
assigned.setTaskText(task.resolveTitle()); // Compat assigned.setTaskText(task.getTitle()); // Compat
assigned.setTaskMinutes(task.getMinutes()); assigned.setTaskMinutes(task.getMinutes());
assigned.setAssignedAt(LocalDateTime.now()); assigned.setAssignedAt(LocalDateTime.now());
assigned.setAcceptDeadline(LocalDateTime.now().plusMinutes(req.acceptDeadlineMinutes())); assigned.setAcceptDeadline(LocalDateTime.now().plusMinutes(req.acceptDeadlineMinutes()));
@@ -1309,14 +1266,13 @@ public class CardLockController {
if (task.getAcceptDeadline().isBefore(LocalDateTime.now())) { if (task.getAcceptDeadline().isBefore(LocalDateTime.now())) {
// Bereits abgelaufen Strafe anwenden // Bereits abgelaufen Strafe anwenden
task.setStatus("EXPIRED"); task.setStatus("EXPIRED");
applyAssignedTaskPenalty(l, task); cardLockServiceFactory.create(l).applyAssignedTaskPenalty(task);
assignedTaskRepository.save(task); assignedTaskRepository.save(task);
cardlockRepository.save(l);
return ResponseEntity.status(409) return ResponseEntity.status(409)
.body(Map.of("error", "Die Annahme-Frist ist abgelaufen. Die Strafe wurde angewendet.")); .body(Map.of("error", "Die Annahme-Frist ist abgelaufen. Die Strafe wurde angewendet."));
} }
boolean hasActiveTask = (l.getCurrentTask() != null && !l.getCurrentTask().isBlank()) 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) if (hasActiveTask)
return ResponseEntity.status(409).body(Map.of("error", "Du hast bereits eine laufende Aufgabe.")); 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.setCurrentTask(title);
l.setCurrentTaskDescription(task.getTaskDescription()); l.setCurrentTaskDescription(task.getTaskDescription());
if (task.getTaskMinutes() != null && task.getTaskMinutes() > 0) { 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 // Fälligkeit aller anderen offenen Aufgaben um die Task-Dauer verschieben
final int extraMinutes = task.getTaskMinutes(); 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.")); return ResponseEntity.status(409).body(Map.of("error", "Diese Aufgabe ist nicht mehr ausstehend."));
task.setStatus("DECLINED"); task.setStatus("DECLINED");
applyAssignedTaskPenalty(l, task); cardLockServiceFactory.create(l).applyAssignedTaskPenalty(task);
assignedTaskRepository.save(task); assignedTaskRepository.save(task);
cardlockRepository.save(l);
sendMessage(myId, l.getKeyholder(), sendMessage(myId, l.getKeyholder(),
meOpt.get().getName() + " hat die gestellte Aufgabe abgelehnt. Die Strafe wurde angewendet.", 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.")); return ResponseEntity.badRequest().body(Map.of("error", "Zeitpunkt muss in der Zukunft liegen."));
} }
l.setFrozenUntill(until); l.setFrozenUntil(until);
cardlockRepository.save(l); cardlockRepository.save(l);
sendMessage(myId, l.getLockee(), sendMessage(myId, l.getLockee(),
@@ -1476,7 +1431,7 @@ public class CardLockController {
"Das Lock ist durch eine Aufgabe eingefroren und kann nicht manuell entfroren werden.")); "Das Lock ist durch eine Aufgabe eingefroren und kann nicht manuell entfroren werden."));
} }
l.setFrozenUntill(null); l.setFrozenUntil(null);
cardlockRepository.save(l); cardlockRepository.save(l);
sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.", sendMessage(myId, l.getLockee(), me.getName() + " hat dein Lock wieder entfroren.",

View File

@@ -2,40 +2,24 @@ package de.oaa.xxx.games.chastity.cardlock;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; 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.Task;
import de.oaa.xxx.games.chastity.tasks.TaskListConverter; import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Convert; import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity; 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.Getter;
import lombok.Setter; import lombok.Setter;
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "card_lock") @DiscriminatorValue("CARDLOCK")
public class CardLockEntity { 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) @Convert(converter = CardEnumListConverter.class)
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private List<CardEnum> initialCards; private List<CardEnum> initialCards;
@@ -47,69 +31,17 @@ public class CardLockEntity {
private boolean showRemainingCards; private boolean showRemainingCards;
@Column @Column
private LocalDateTime latestOpeningtime; 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 // State
@Column @Column
private LocalDateTime startTime;
@Column
private LocalDateTime nextCardIn; private LocalDateTime nextCardIn;
@Column @Column
private Integer openPicks; private Integer openPicks;
@Convert(converter = CardEnumListConverter.class) @Convert(converter = CardEnumListConverter.class)
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private List<CardEnum> availableCards; 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) @Convert(converter = TaskListConverter.class)
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private List<Task> tasksInQueue; 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"; }
} }

View File

@@ -1,332 +1,261 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.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.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.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; 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.GameHistoryRepository;
import de.oaa.xxx.games.history.GameType;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository; 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 static final Logger LOGGER = LoggerFactory.getLogger(CardLockService.class);
private final CardLockEntity lock; private final CardLockEntity lock;
private CardLockRepository cardLockRepository; private final CardLockRepository cardLockRepository;
private VerificationRepository verificationRepository; private String pendingTaskMode;
private GameHistoryRepository gameHistoryRepository;
private UserRepository userRepository;
private UnlockCodeHistoryService unlockCodeHistoryService;
private KeyholderNotificationRepository keyholderNotificationRepository;
public CardLockService(CardLockEntity lock, VerificationRepository verificationRepository, public CardLockService(
VerificationVoteRepository verificationVoteRepository, CardLockRepository cardLockRepository, CardLockEntity lock,
GameHistoryRepository gameHistoryRepository, UserRepository userRepository, KeyholderNotificationRepository keyholderNotificationRepository,UnlockCodeHistoryService unlockCodeHistoryService) { CommunityVerificationVoteRepository communityVerificationVoteRepository,
super(verificationVoteRepository); CommunityVerificationRepository communityVerificationRepository,
this.lock = lock; KeyholderVerificationRepository keyholderVerificationRepository,
this.cardLockRepository = cardLockRepository; GameHistoryRepository gameHistoryRepository,
this.verificationRepository = verificationRepository; UserRepository userRepository,
this.gameHistoryRepository = gameHistoryRepository; KeyholderNotificationRepository keyholderNotificationRepository,
this.userRepository = userRepository; SystemMessageService systemMessageService,
this.keyholderNotificationRepository = keyholderNotificationRepository; UnlockCodeHistoryService unlockCodeHistoryService,
this.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() { // ── Abstract method implementations ──────────────────────────────────────
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;
}
private CardDTO getRandomCard() { @Override
var cards = lock.getAvailableCards(); protected BaseLockEntity getLock() {
if (!cards.isEmpty()) { return lock;
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();
}
private CardDTO getGreenCard() { @Override
return new CardDTO(CardEnum.GREEN, lock.getUnlockCode()); protected void saveLock() {
} cardLockRepository.save(lock);
}
public String doubleUp() { @Override
var cards = lock.getAvailableCards(); protected GameType getGameType() {
LOGGER.debug("Double up {} cards", cards.size()); return GameType.CARDLOCK;
lock.getAvailableCards().addAll(cards); }
LOGGER.debug("Now {} cards", lock.getAvailableCards().size());
return "";
}
public String reset() { @Override
LOGGER.debug("Reset to initial cards"); protected void applyHygieneOvertime(Long overtime) {
lock.setAvailableCards(lock.getInitialCards()); if (lock.getFrozenUntil() != null) {
return ""; lock.setFrozenUntil(lock.getFrozenUntil().plusMinutes(overtime * 4));
} } else {
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(overtime * 4));
}
}
public String green() { // ── Card drawing ──────────────────────────────────────────────────────────
LOGGER.debug("Green Card drafted");
return lock.getUnlockCode();
}
public void unlock(String unlockCode) { public CardDTO getNextCard() {
this.lock.setUnlockTime(LocalDateTime.now()); LOGGER.debug("New Card requested by user {}", lock.getLockee());
boolean valid = true; CardDTO card = null;
if (lock.isEmergencyAutoUnlocked()) { if (lock.isKeyholderRequestedUnlock()
valid = false; || (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
LOGGER.debug("Lock invalid - Emergency Auto-Unlock (1h timer)"); card = getGreenCard();
} } else if (lock.isAccumulatePicks()) {
if (lock.isTestLock()) { if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
valid = false; lock.setOpenPicks(lock.getOpenPicks() == null ? 1 : lock.getOpenPicks() + 1);
} else if (Duration.between(lock.getStartTime(), lock.getUnlockTime()).toHours() > 24) { }
Set<LocalDate> verifications = verificationRepository.findByLockId(this.lock.getLockId()).stream() if (lock.getOpenPicks() != null && lock.getOpenPicks() > 0) {
.filter(verification -> isValid(verification)) lock.setOpenPicks(lock.getOpenPicks() - 1);
.map(verification -> verification.getVerificationTime().toLocalDate()) card = getRandomCard();
.collect(Collectors.toSet()); }
} else {
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
lock.setNextCardIn(LocalDateTime.now().plusMinutes(lock.getPickEveryMinute()));
card = getRandomCard();
}
}
cardLockRepository.save(lock);
return card;
}
LocalDate current = this.lock.getStartTime().toLocalDate(); private CardDTO getRandomCard() {
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1); 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();
}
while (!current.isAfter(last)) { private CardDTO getGreenCard() {
if (!verifications.contains(current)) { return new CardDTO(CardEnum.GREEN, lock.getUnlockCode());
valid = false; }
LOGGER.debug("Lock invalid - no daily verification on %s", current.toString());
break;
}
current = current.plusDays(1);
}
}
lock.setUnlockTime(LocalDateTime.now()); // ── Card effects ──────────────────────────────────────────────────────────
LOGGER.debug("Unlocked at {}", lock.getUnlockTime());
cardLockRepository.save(lock);
if (valid) { public String doubleUp() {
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes(); var cards = lock.getAvailableCards();
LOGGER.debug("Double up {} cards", cards.size());
lock.getAvailableCards().addAll(cards);
LOGGER.debug("Now {} cards", lock.getAvailableCards().size());
return "";
}
// Gemeinsamer History-Eintrag mit Teilnehmerliste public String reset() {
GameHistoryEntity entry = new GameHistoryEntity(); LOGGER.debug("Reset to initial cards");
entry.setGameType(de.oaa.xxx.games.history.GameType.CARDLOCK); lock.setAvailableCards(lock.getInitialCards());
entry.setGameName(lock.getName()); return "";
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);
int minutes = (int) durationMinutes; public String green() {
userRepository.findById(lock.getLockee()).ifPresent(u -> { LOGGER.debug("Green Card drafted");
u.setLockeeXp(u.getLockeeXp() + minutes); return lock.getUnlockCode();
userRepository.save(u); }
});
if (lock.getKeyholder() != null) {
userRepository.findById(lock.getKeyholder()).ifPresent(u -> {
u.setKeyholderXp(u.getKeyholderXp() + minutes);
userRepository.save(u);
});
}
}
}
public void putBackGreen() { public String freeze() {
LOGGER.debug("Green Card was put Back"); var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0);
lock.getAvailableCards().add(CardEnum.GREEN); freeze(multiplier);
cardLockRepository.save(lock); return "";
} }
public String freeze() { private String freeze(double multiplier) {
var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0); LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
freeze(multiplier); lock.setFrozenUntil(frozenTill);
return ""; lock.setNextCardIn(frozenTill);
} LOGGER.debug("Frozen until {}", lock.getFrozenUntil());
return "";
}
private String freeze(double multiplier) { /** Called by TaskCard. Dispatches based on TaskMode and stores result for controller. */
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES); public String task() {
lock.setFrozenUntill(frozenTill); switch (lock.getTaskMode()) {
lock.setNextCardIn(frozenTill); case RANDOM -> applyRandomTask();
LOGGER.debug("Frozen until {}", lock.getFrozenUntill()); case KEYHOLDER -> {
return ""; if (lock.isTestLock()) applyRandomTask();
} else startKeyholderVote();
}
case COMMUNITY -> {
if (lock.isTestLock()) applyRandomTask();
else startCommunityVote();
}
}
pendingTaskMode = lock.getTaskMode().name();
return "";
}
public String task() { /** Returns the TaskMode that was triggered by the last task() call, or null if no task card was drawn. */
// Non-RANDOM modes are handled by the controller after the card is drawn public String getPendingTaskMode() {
if (!"RANDOM".equals(lock.getTaskCardMode())) { return pendingTaskMode;
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 task(Task task) { public String redCard() {
LOGGER.debug("Apply task {}", task); return "";
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 clearTask() { public String yellowCard() {
LOGGER.debug("Clear task"); Random random = new Random();
lock.setCurrentTask(null); if (random.nextBoolean()) {
lock.setCurrentTaskDescription(null); for (int i = 0; i < random.nextInt(1, 3); i++) {
lock.setTaskFrozenUntil(null); LOGGER.debug("Adding Red card");
return ""; 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 String redCard() { public void putBackGreen() {
return ""; LOGGER.debug("Green Card was put Back");
} lock.getAvailableCards().add(CardEnum.GREEN);
cardLockRepository.save(lock);
}
public String yellowCard() { // ── Hygiene opening ───────────────────────────────────────────────────────
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() { public void startHygieneOpening() {
tempOperning(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes()); startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
} }
private Long calcOvertime() { // ── Cum cards ─────────────────────────────────────────────────────────────
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() { public String cum(boolean tempUnlock) {
LocalDateTime now = LocalDateTime.now(); if (tempUnlock) {
startTempOpening(TempOpeningReason.CARD, 0);
}
return lock.getUnlockCode();
}
Long overtime = calcOvertime(); public String endCumming() {
if (overtime != null) { Long overtime = calcOvertime();
if (lock.getKeyholder() != null) { if (overtime != null) {
reportKeyholder(overtime); if (lock.getKeyholder() == null) {
} applyHygieneOvertime(overtime);
freezeForOvertime(overtime); } else {
reportKeyholder(overtime);
}
}
lock.setTempOpeningDuration(null);
lock.setTempOpeningTime(null);
lock.setTempOpeningReason(null);
} var code = CodeCreator.createNumeric(lock.getUnlockCodeLength());
lock.setLastHygineOpening(now); lock.setUnlockCode(code);
lock.setTempOpeningDuration(null); cardLockRepository.save(lock);
lock.setTempOpeningTime(null); return code;
}
var code = CodeCreator.createAlphanumericCode(lock.getUnlockCodeLength()); // ── Assigned task penalty ─────────────────────────────────────────────────
lock.setUnlockCode(code);
cardLockRepository.save(lock);
return code;
}
private void reportKeyholder(Long overtime) { public void applyAssignedTaskPenalty(AssignedTaskEntity task) {
KeyholderNotificationEntity notification = new KeyholderNotificationEntity(); if (task.getPenaltyFreezeMinutes() != null && task.getPenaltyFreezeMinutes() > 0) {
notification.setLockId(lock.getLockId()); LocalDateTime until = LocalDateTime.now().plusMinutes(task.getPenaltyFreezeMinutes());
notification.setLockeeId(lock.getLockee()); if (lock.getFrozenUntil() == null || until.isAfter(lock.getFrozenUntil())) {
notification.setKeyholderUserId(lock.getKeyholder()); lock.setFrozenUntil(until);
notification.setViolationTime(LocalDateTime.now()); lock.setNextCardIn(until);
notification.setOvertimeMinutes(overtime); }
keyholderNotificationRepository.save(notification); }
} if (task.getPenaltyRedCards() != null && task.getPenaltyRedCards() > 0) {
List<CardEnum> cards = new ArrayList<>(
private void freezeForOvertime(Long overtime) { lock.getAvailableCards() != null ? lock.getAvailableCards() : List.of());
if (lock.getFrozenUntill() != null) { for (int i = 0; i < task.getPenaltyRedCards(); i++) {
lock.setFrozenUntill(lock.getFrozenUntill().plusMinutes(overtime * 4)); cards.add(CardEnum.RED);
} else { }
lock.setFrozenUntill(LocalDateTime.now().plusMinutes(overtime * 4)); lock.setAvailableCards(cards);
} }
} cardLockRepository.save(lock);
}
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 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;
}
} }

View File

@@ -1,10 +1,14 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.history.GameHistoryRepository; 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.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.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.games.chastity.verification.VerificationRepository; import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -18,43 +22,50 @@ import org.springframework.stereotype.Service;
@Service @Service
public class CardLockServiceFactory { public class CardLockServiceFactory {
private final VerificationRepository verificationRepository;
private final VerificationVoteRepository verificationVoteRepository;
private final CardLockRepository cardLockRepository; private final CardLockRepository cardLockRepository;
private final CommunityVerificationRepository communityVerificationRepository;
private final CommunityVerificationVoteRepository communityVerificationVoteRepository;
private final GameHistoryRepository gameHistoryRepository; private final GameHistoryRepository gameHistoryRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private KeyholderNotificationRepository keyholderNotificationRepository;
private final UnlockCodeHistoryService unlockCodeHistoryService; 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, public CardLockServiceFactory(
VerificationVoteRepository verificationVoteRepository, CommunityVerificationRepository communityVerificationRepository,
CardLockRepository cardLockRepository, CommunityVerificationVoteRepository communityVerificationVoteRepository,
GameHistoryRepository gameHistoryRepository, CardLockRepository cardLockRepository,
UserRepository userRepository, GameHistoryRepository gameHistoryRepository,
KeyholderNotificationRepository keyholderNotificationRepository, UserRepository userRepository,
UnlockCodeHistoryService unlockCodeHistoryService) { KeyholderNotificationRepository keyholderNotificationRepository,
this.verificationRepository = verificationRepository; KeyholderVerificationRepository keyholderVerificationRepository,
this.verificationVoteRepository = verificationVoteRepository; UnlockCodeHistoryService unlockCodeHistoryService,
SystemMessageService systemMessageService,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository) {
this.cardLockRepository = cardLockRepository; this.cardLockRepository = cardLockRepository;
this.communityVerificationRepository = communityVerificationRepository;
this.communityVerificationVoteRepository = communityVerificationVoteRepository;
this.gameHistoryRepository = gameHistoryRepository; this.gameHistoryRepository = gameHistoryRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.keyholderNotificationRepository = keyholderNotificationRepository; this.keyholderNotificationRepository = keyholderNotificationRepository;
this.unlockCodeHistoryService = unlockCodeHistoryService; 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. * Erstellt eine neue CardLockService-Instanz für das gegebene Lock.
*/ */
public CardLockService create(CardLockEntity lock) { public CardLockService create(CardLockEntity lock) {
return new CardLockService( return new CardLockService(lock, communityVerificationVoteRepository, communityVerificationRepository,
lock, keyholderVerificationRepository, gameHistoryRepository, userRepository,
verificationRepository, keyholderNotificationRepository, systemMessageService, unlockCodeHistoryService,
verificationVoteRepository, keyholderTaskChoiceRepository, communityTaskVoteRepository, cardLockRepository);
cardLockRepository,
gameHistoryRepository,
userRepository,
keyholderNotificationRepository,
unlockCodeHistoryService
);
} }
} }

View File

@@ -1,6 +1,7 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.tasks.Task; import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.tasks.TaskMode;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -33,7 +34,7 @@ public class CardlockTemplateController {
Integer hygineOpeningEveryMinites, Integer hygineOpeningEveryMinites,
List<Task> tasks, List<Task> tasks,
boolean requiresVerification, boolean requiresVerification,
String taskCardMode TaskMode taskMode
) {} ) {}
private Map<String, Object> toDto(CardlockTemplateEntity t) { private Map<String, Object> toDto(CardlockTemplateEntity t) {
@@ -129,6 +130,6 @@ public class CardlockTemplateController {
t.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); t.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes());
t.setTasks(req.tasks() != null ? req.tasks() : List.of()); t.setTasks(req.tasks() != null ? req.tasks() : List.of());
t.setRequiresVerification(req.requiresVerification()); t.setRequiresVerification(req.requiresVerification());
t.setTaskCardMode(req.taskCardMode() != null ? req.taskCardMode() : "RANDOM"); t.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
} }
} }

View File

@@ -1,64 +1,34 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.chastity.tasks.Task; import java.util.Map;
import de.oaa.xxx.games.chastity.tasks.TaskListConverter;
import jakarta.persistence.*; 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.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "cardlock_template") @DiscriminatorValue("CARDLOCK")
public class CardlockTemplateEntity { public class CardlockTemplateEntity extends BaseLockTemplateEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID templateId;
@Column(nullable = false)
private UUID owner;
@Column
private String name;
@Convert(converter = CardCountMapConverter.class) @Convert(converter = CardCountMapConverter.class)
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private Map<String, Integer> cardCountsMin; private Map<String, Integer> cardCountsMin;
@Convert(converter = CardCountMapConverter.class) @Convert(converter = CardCountMapConverter.class)
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private Map<String, Integer> cardCountsMax; private Map<String, Integer> cardCountsMax;
@Column @Column
private Integer pickEveryMinute; private Integer pickEveryMinute;
@Column @Column
private boolean accumulatePicks; private boolean accumulatePicks;
@Column @Column
private boolean showRemainingCards; private boolean showRemainingCards;
@Column
private Integer hygineOpeningDurationMinutes;
@Column
private Integer hygineOpeningEveryMinites;
@Convert(converter = TaskListConverter.class)
@Column(columnDefinition = "TEXT")
private List<Task> tasks;
@Column @Column
private boolean requiresVerification; private boolean requiresVerification;
@Column(nullable = false)
private String taskCardMode = "RANDOM";
public String getTaskCardMode() { return taskCardMode != null ? taskCardMode : "RANDOM"; }
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>{
}

View File

@@ -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);
});
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -11,7 +11,7 @@ public class CodeCreator {
return create(digits, CHARS_N); return create(digits, CHARS_N);
} }
public static String createAlphanumericCode(int digits) { public static String createAlphanumeric(int digits) {
return create(digits, CHARS_AN); return create(digits, CHARS_AN);
} }

View File

@@ -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);
}

View File

@@ -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) {
}

View File

@@ -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);
}
}

View File

@@ -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) {
}

View File

@@ -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);
}
}

View File

@@ -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> {
}

View File

@@ -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();
}
}

View File

@@ -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) {
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package de.oaa.xxx.games.chastity.community;
public enum CommunityPilloryReason {
HYGIENE_OPENING_EXEEDED,
KEYHOLDER_DESCESSION;
}

View File

@@ -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> {
}

View File

@@ -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();
}
}

View File

@@ -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) {
}

View File

@@ -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) {
}

View File

@@ -0,0 +1,5 @@
package de.oaa.xxx.games.chastity.community;
public record CommunityTaskVoteDisplayEntryDTO(String title, String description, Integer minutes, Integer votes,
boolean ownVote) {
}

View File

@@ -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);
}
}

View File

@@ -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) {
}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.games.chastity.vote; package de.oaa.xxx.games.chastity.community;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.Getter; import lombok.Getter;
@@ -18,11 +18,15 @@ public class CommunityTaskVoteEntryEntity {
private UUID entryId; private UUID entryId;
@Column(nullable = false) @Column(nullable = false)
private UUID voteSessionId; private UUID displayId;
@Column(nullable = false) @Column(nullable = false)
private UUID voterUserId; private UUID userId;
@Column(nullable = false) @Column(nullable = false)
private int taskIndex; private int taskIndex;
public CommunityTaskVoteEntryDTO toCommunityTaskVoteEntry() {
return new CommunityTaskVoteEntryDTO(entryId, displayId, userId, taskIndex);
}
} }

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.community;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
@@ -13,25 +13,24 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; 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.AssignedTaskEntity;
import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository; import de.oaa.xxx.games.chastity.tasks.AssignedTaskRepository;
import de.oaa.xxx.games.chastity.tasks.Task; 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; import de.oaa.xxx.social.SystemMessageService;
@Component @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 CommunityTaskVoteRepository communityTaskVoteRepository;
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository; private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final AssignedTaskRepository assignedTaskRepository; private final AssignedTaskRepository assignedTaskRepository;
private final SystemMessageService systemMessageService; private SystemMessageService systemMessageService;
public TaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository, public CommunityTaskVoteScheduler(CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository, CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
CardlockRepository cardlockRepository, CardlockRepository cardlockRepository,
AssignedTaskRepository assignedTaskRepository, AssignedTaskRepository assignedTaskRepository,
@@ -47,27 +46,26 @@ public class TaskVoteScheduler {
@Transactional @Transactional
public void processExpiredVotes() { public void processExpiredVotes() {
var expired = communityTaskVoteRepository var expired = communityTaskVoteRepository
.findByStatusAndExpiresAtBefore("ACTIVE", LocalDateTime.now()); .findByActiveTrueAndExpiresAtBefore(LocalDateTime.now());
for (var vote : expired) { 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()); var lockOpt = cardlockRepository.findById(vote.getLockId());
if (lockOpt.isEmpty()) { if (lockOpt.isEmpty()) {
vote.setStatus("COMPLETED"); vote.setActive(false);
communityTaskVoteRepository.save(vote); communityTaskVoteRepository.save(vote);
continue; continue;
} }
var lock = lockOpt.get(); var lock = lockOpt.get();
List<Task> tasks = lock.getTasks(); List<Task> tasks = lock.getTasks();
if (tasks == null || tasks.isEmpty()) { if (tasks == null || tasks.isEmpty()) {
vote.setStatus("COMPLETED"); vote.setActive(false);
communityTaskVoteRepository.save(vote); communityTaskVoteRepository.save(vote);
continue; continue;
} }
// Stimmen auszählen var entries = communityTaskVoteEntryRepository.findByDisplayId(vote.getDisplayId());
var entries = communityTaskVoteEntryRepository.findByVoteSessionId(vote.getVoteSessionId());
int winnerIndex; int winnerIndex;
if (entries.isEmpty()) { if (entries.isEmpty()) {
winnerIndex = new Random().nextInt(tasks.size()); winnerIndex = new Random().nextInt(tasks.size());
@@ -80,7 +78,6 @@ public class TaskVoteScheduler {
} }
} }
int max = Arrays.stream(counts).max().getAsInt(); int max = Arrays.stream(counts).max().getAsInt();
// Alle Tasks mit Maximalstimmen sammeln zufällige Auswahl bei Gleichstand
List<Integer> winners = new ArrayList<>(); List<Integer> winners = new ArrayList<>();
for (int i = 0; i < counts.length; i++) { for (int i = 0; i < counts.length; i++) {
if (counts[i] == max) winners.add(i); if (counts[i] == max) winners.add(i);
@@ -92,22 +89,21 @@ public class TaskVoteScheduler {
Task task = tasks.get(winnerIndex); Task task = tasks.get(winnerIndex);
AssignedTaskEntity assigned = new AssignedTaskEntity(); AssignedTaskEntity assigned = new AssignedTaskEntity();
assigned.setLockId(lock.getLockId()); assigned.setLockId(lock.getLockId());
assigned.setTaskTitle(task.resolveTitle()); assigned.setTaskTitle(task.getTitle());
assigned.setTaskDescription(task.getDescription()); assigned.setTaskDescription(task.getDescription());
assigned.setTaskText(task.resolveTitle()); assigned.setTaskText(task.getTitle());
assigned.setTaskMinutes(task.getMinutes()); assigned.setTaskMinutes(task.getMinutes());
assigned.setAssignedAt(LocalDateTime.now()); assigned.setAssignedAt(LocalDateTime.now());
assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1)); assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1));
assigned.setStatus("PENDING"); assigned.setStatus("PENDING");
assignedTaskRepository.save(assigned); assignedTaskRepository.save(assigned);
vote.setStatus("COMPLETED"); vote.setActive(false);
vote.setWinningTaskIndex(winnerIndex); vote.setWinningTaskIndex(winnerIndex);
communityTaskVoteRepository.save(vote); communityTaskVoteRepository.save(vote);
// Lockee benachrichtigen
sendMessage(lock.getLockee(), 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()); "/activelock.html?lockId=" + lock.getLockId());
} }
} }

View File

@@ -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();
}
}

View File

@@ -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) {}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,5 @@
package de.oaa.xxx.games.chastity.community;
import java.util.UUID;
public record CommunityVerificationVoteDTO (UUID voteId, UUID userId, boolean upvote) {}

View File

@@ -1,4 +1,4 @@
package de.oaa.xxx.games.chastity.verification; package de.oaa.xxx.games.chastity.community;
import java.util.UUID; import java.util.UUID;
@@ -13,7 +13,7 @@ import lombok.Setter;
@Setter @Setter
@Entity @Entity
@Table(name="verification_vote") @Table(name="verification_vote")
public class VerificationVoteEntity { public class CommunityVerificationVoteEntity {
@Id @Id
@Column @Column
@@ -25,7 +25,7 @@ public class VerificationVoteEntity {
@Column(nullable = false) @Column(nullable = false)
private boolean upvote; private boolean upvote;
public VerificationVoteDTO toVerificationVote() { public CommunityVerificationVoteDTO toVerificationVote() {
return new VerificationVoteDTO(voteId, userId, upvote); return new CommunityVerificationVoteDTO(voteId, userId, upvote);
} }
} }

View File

@@ -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);
}

View File

@@ -1,53 +1,52 @@
package de.oaa.xxx.games.chastity.cardlock; package de.oaa.xxx.games.chastity.keyholder;
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.*;
import java.security.Principal; import java.security.Principal;
import java.time.LocalDateTime; 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 @RestController
@RequestMapping("/task-card") @RequestMapping("/games/chastity/keyholder/choices")
public class TaskCardController { public class KeyholderTaskChoiceController {
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository; private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
private final CommunityTaskVoteRepository communityTaskVoteRepository;
private final CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository;
private final AssignedTaskRepository assignedTaskRepository; private final AssignedTaskRepository assignedTaskRepository;
private final SystemMessageService systemMessageService; private final SystemMessageService systemMessageService;
public TaskCardController(CardlockRepository cardlockRepository, public KeyholderTaskChoiceController(CardlockRepository cardlockRepository,
UserRepository userRepository, UserRepository userRepository,
KeyholderTaskChoiceRepository keyholderTaskChoiceRepository, KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
CommunityTaskVoteRepository communityTaskVoteRepository,
CommunityTaskVoteEntryRepository communityTaskVoteEntryRepository,
AssignedTaskRepository assignedTaskRepository, AssignedTaskRepository assignedTaskRepository,
SystemMessageService systemMessageService) { SystemMessageService systemMessageService) {
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository; this.keyholderTaskChoiceRepository = keyholderTaskChoiceRepository;
this.communityTaskVoteRepository = communityTaskVoteRepository;
this.communityTaskVoteEntryRepository = communityTaskVoteEntryRepository;
this.assignedTaskRepository = assignedTaskRepository; this.assignedTaskRepository = assignedTaskRepository;
this.systemMessageService = systemMessageService; this.systemMessageService = systemMessageService;
} }
// Keyholder: ausstehende Aufgaben-Karten-Entscheidungen // Keyholder: ausstehende Aufgaben-Karten-Entscheidungen
@GetMapping
@GetMapping("/keyholder/choices")
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<Map<String, Object>>> getPendingChoices(Principal principal) { public ResponseEntity<List<Map<String, Object>>> getPendingChoices(Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName()); var meOpt = userRepository.findByEmail(principal.getName());
@@ -59,7 +58,7 @@ public class TaskCardController {
List<Map<String, Object>> result = new ArrayList<>(); List<Map<String, Object>> result = new ArrayList<>();
for (var lock : locks) { for (var lock : locks) {
var pending = keyholderTaskChoiceRepository.findByLockIdAndStatus(lock.getLockId(), "PENDING"); var pending = keyholderTaskChoiceRepository.findByLockIdAndActiveTrue(lock.getLockId());
if (pending.isEmpty()) continue; if (pending.isEmpty()) continue;
var lockee = userRepository.findById(lock.getLockee()).orElse(null); var lockee = userRepository.findById(lock.getLockee()).orElse(null);
@@ -81,7 +80,7 @@ public class TaskCardController {
record PenaltyRequest(Integer penaltyFreezeMinutes, Integer penaltyRedCards) {} record PenaltyRequest(Integer penaltyFreezeMinutes, Integer penaltyRedCards) {}
@PostMapping("/keyholder/choices/{choiceId}/choose/{taskIndex}") @PostMapping("/{choiceId}/choose/{taskIndex}")
@Transactional @Transactional
public ResponseEntity<Void> chooseTask(@PathVariable UUID choiceId, public ResponseEntity<Void> chooseTask(@PathVariable UUID choiceId,
@PathVariable int taskIndex, @PathVariable int taskIndex,
@@ -100,7 +99,7 @@ public class TaskCardController {
var lock = lockOpt.get(); var lock = lockOpt.get();
if (!myId.equals(lock.getKeyholder())) return ResponseEntity.status(403).build(); 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(); List<Task> tasks = lock.getTasks();
if (tasks == null || taskIndex < 0 || taskIndex >= tasks.size()) if (tasks == null || taskIndex < 0 || taskIndex >= tasks.size())
@@ -109,9 +108,9 @@ public class TaskCardController {
Task task = tasks.get(taskIndex); Task task = tasks.get(taskIndex);
AssignedTaskEntity assigned = new AssignedTaskEntity(); AssignedTaskEntity assigned = new AssignedTaskEntity();
assigned.setLockId(lock.getLockId()); assigned.setLockId(lock.getLockId());
assigned.setTaskTitle(task.resolveTitle()); assigned.setTaskTitle(task.getTitle());
assigned.setTaskDescription(task.getDescription()); assigned.setTaskDescription(task.getDescription());
assigned.setTaskText(task.resolveTitle()); assigned.setTaskText(task.getTitle());
assigned.setTaskMinutes(task.getMinutes()); assigned.setTaskMinutes(task.getMinutes());
assigned.setAssignedAt(LocalDateTime.now()); assigned.setAssignedAt(LocalDateTime.now());
assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1)); assigned.setAcceptDeadline(LocalDateTime.now().plusHours(1));
@@ -122,7 +121,7 @@ public class TaskCardController {
} }
assignedTaskRepository.save(assigned); assignedTaskRepository.save(assigned);
choice.setStatus("CHOSEN"); choice.setActive(false);
keyholderTaskChoiceRepository.save(choice); keyholderTaskChoiceRepository.save(choice);
sendMessage(myId, lock.getLockee(), sendMessage(myId, lock.getLockee(),
@@ -132,94 +131,6 @@ public class TaskCardController {
return ResponseEntity.noContent().build(); 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 // Helpers
@@ -230,7 +141,7 @@ public class TaskCardController {
Task t = tasks.get(i); Task t = tasks.get(i);
Map<String, Object> m = new LinkedHashMap<>(); Map<String, Object> m = new LinkedHashMap<>();
m.put("index", i); m.put("index", i);
m.put("title", t.resolveTitle()); m.put("title", t.getTitle());
m.put("description", t.getDescription() != null ? t.getDescription() : ""); m.put("description", t.getDescription() != null ? t.getDescription() : "");
m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0); m.put("minutes", t.getMinutes() != null ? t.getMinutes() : 0);
list.add(m); list.add(m);

View File

@@ -20,10 +20,12 @@ public class KeyholderTaskChoiceEntity {
@Column(nullable = false) @Column(nullable = false)
private UUID lockId; private UUID lockId;
/** PENDING | CHOSEN */
@Column(nullable = false) @Column(nullable = false)
private String status = "PENDING"; private boolean active = true;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@Column
private LocalDateTime expiresAt;
} }

View File

@@ -1,9 +1,11 @@
package de.oaa.xxx.games.chastity.keyholder; package de.oaa.xxx.games.chastity.keyholder;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
public interface KeyholderTaskChoiceRepository extends JpaRepository<KeyholderTaskChoiceEntity, 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);
} }

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -2,12 +2,12 @@ package de.oaa.xxx.games.chastity.lockcontroll;
public class TTLockControl extends LockControl { public class TTLockControl extends LockControl {
// private static final String BASE_URL = "https://euapi.ttlock.com/";
public TTLockControl() { public TTLockControl() {
super(new NoInteractionCallback()); super(new NoInteractionCallback());
} }
private static final String BASE_URL = "https://euapi.ttlock.com/";
@Override @Override
public boolean init() { public boolean init() {
// TODO Auto-generated method stub // TODO Auto-generated method stub
@@ -26,7 +26,4 @@ public class TTLockControl extends LockControl {
return false; return false;
} }
} }

View File

@@ -20,7 +20,7 @@ public class UnlockcodeLockControl extends LockControl {
@Override @Override
public boolean lock() { public boolean lock() {
var code = CodeCreator.createAlphanumericCode(callback.getUnlockcodeLenght()); var code = CodeCreator.createNumeric(callback.getUnlockcodeLenght());
callback.setUnlockCode(code); callback.setUnlockCode(code);
return true; return true;
} }

View File

@@ -8,6 +8,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Value; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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.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.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
@@ -31,6 +37,8 @@ public class LockeeInvitationController {
private final LockeeInvitationRepository lockeeInvitationRepository; private final LockeeInvitationRepository lockeeInvitationRepository;
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final BaseLockRepository baseLockRepository;
private final TimeLockRepository timeLockRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final SystemMessageService systemMessageService; private final SystemMessageService systemMessageService;
@@ -41,10 +49,14 @@ public class LockeeInvitationController {
public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository, public LockeeInvitationController(LockeeInvitationRepository lockeeInvitationRepository,
CardlockRepository cardlockRepository, CardlockRepository cardlockRepository,
BaseLockRepository baseLockRepository,
TimeLockRepository timeLockRepository,
UserRepository userRepository, UserRepository userRepository,
SystemMessageService systemMessageService) { SystemMessageService systemMessageService) {
this.lockeeInvitationRepository = lockeeInvitationRepository; this.lockeeInvitationRepository = lockeeInvitationRepository;
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.baseLockRepository = baseLockRepository;
this.timeLockRepository = timeLockRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.systemMessageService = systemMessageService; this.systemMessageService = systemMessageService;
} }
@@ -59,6 +71,12 @@ public class LockeeInvitationController {
return sb.toString(); 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") @GetMapping("/invitations/mine")
public ResponseEntity<List<Map<String, Object>>> getMyInvitations(Principal principal) { public ResponseEntity<List<Map<String, Object>>> getMyInvitations(Principal principal) {
var meOpt = userRepository.findByEmail(principal.getName()); var meOpt = userRepository.findByEmail(principal.getName());
@@ -68,7 +86,7 @@ public class LockeeInvitationController {
var invitations = lockeeInvitationRepository.findByLockeeUserId(myId); var invitations = lockeeInvitationRepository.findByLockeeUserId(myId);
List<Map<String, Object>> result = new ArrayList<>(); List<Map<String, Object>> result = new ArrayList<>();
for (var inv : invitations) { for (var inv : invitations) {
var lockOpt = cardlockRepository.findById(inv.getLockId()); var lockOpt = baseLockRepository.findById(inv.getLockId());
if (lockOpt.isEmpty()) continue; if (lockOpt.isEmpty()) continue;
var lock = lockOpt.get(); var lock = lockOpt.get();
if (lock.getStartTime() != null) continue; // already accepted if (lock.getStartTime() != null) continue; // already accepted
@@ -97,7 +115,7 @@ public class LockeeInvitationController {
var invitations = lockeeInvitationRepository.findByKeyholderUserId(myId); var invitations = lockeeInvitationRepository.findByKeyholderUserId(myId);
List<Map<String, Object>> result = new ArrayList<>(); List<Map<String, Object>> result = new ArrayList<>();
for (var inv : invitations) { for (var inv : invitations) {
var lockOpt = cardlockRepository.findById(inv.getLockId()); var lockOpt = baseLockRepository.findById(inv.getLockId());
if (lockOpt.isEmpty()) continue; if (lockOpt.isEmpty()) continue;
var lock = lockOpt.get(); var lock = lockOpt.get();
if (lock.getStartTime() != null) continue; // already accepted if (lock.getStartTime() != null) continue; // already accepted
@@ -130,12 +148,12 @@ public class LockeeInvitationController {
var inv = invOpt.get(); var inv = invOpt.get();
if (!inv.getKeyholderUserId().equals(myId)) return ResponseEntity.status(403).build(); 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); lockeeInvitationRepository.delete(inv);
if (lockOpt.isPresent()) { if (lockOpt.isPresent()) {
var lock = lockOpt.get(); var lock = lockOpt.get();
cardlockRepository.delete(lock); baseLockRepository.delete(lock);
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getLockeeUserId(), sendMessage(myId, inv.getLockeeUserId(),
me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.", me.getName() + " hat die Lockee-Einladung für das Lock „" + lockName + "\" zurückgezogen.",
@@ -156,7 +174,7 @@ public class LockeeInvitationController {
var inv = invOpt.get(); var inv = invOpt.get();
if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build(); 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(); if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var lock = lockOpt.get(); var lock = lockOpt.get();
@@ -173,18 +191,18 @@ public class LockeeInvitationController {
result.put("createdAt", inv.getCreatedAt().toString()); result.put("createdAt", inv.getCreatedAt().toString());
result.put("detailsVisible", inv.isDetailsVisible()); result.put("detailsVisible", inv.isDetailsVisible());
if (inv.isDetailsVisible() && lock.getInitialCards() != null) { if (inv.isDetailsVisible() && lock instanceof CardLockEntity cardLock && cardLock.getInitialCards() != null) {
Map<String, Long> cardCounts = lock.getInitialCards().stream() Map<String, Long> cardCounts = cardLock.getInitialCards().stream()
.collect(java.util.stream.Collectors.groupingBy( .collect(java.util.stream.Collectors.groupingBy(
c -> c.name(), java.util.stream.Collectors.counting())); c -> c.name(), java.util.stream.Collectors.counting()));
result.put("cardCounts", cardCounts); result.put("cardCounts", cardCounts);
result.put("pickEveryMinute", lock.getPickEveryMinute()); result.put("pickEveryMinute", cardLock.getPickEveryMinute());
result.put("accumulatePicks", lock.isAccumulatePicks()); result.put("accumulatePicks", cardLock.isAccumulatePicks());
result.put("showRemainingCards", lock.isShowRemainingCards()); result.put("showRemainingCards", cardLock.isShowRemainingCards());
result.put("hygineOpeningEveryMinites", lock.getHygineOpeningEveryMinites()); result.put("hygineOpeningEveryMinites", cardLock.getHygineOpeningEveryMinites());
result.put("hygineOpeningDurationMinutes", lock.getHygineOpeningDurationMinutes()); result.put("hygineOpeningDurationMinutes", cardLock.getHygineOpeningDurationMinutes());
result.put("requiresVerification", lock.isRequiresVerification()); result.put("requiresVerification", cardLock.isRequiresVerification());
result.put("taskCount", lock.getTasks() != null ? lock.getTasks().size() : 0); result.put("taskCount", cardLock.getTasks() != null ? cardLock.getTasks().size() : 0);
} }
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
@@ -207,30 +225,52 @@ public class LockeeInvitationController {
var inv = invOpt.get(); var inv = invOpt.get();
if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build(); 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(); if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var lock = lockOpt.get(); var lock = lockOpt.get();
if (lock.getStartTime() != null) return ResponseEntity.status(409).body(Map.of("error", "already_accepted")); 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")); return ResponseEntity.status(409).body(Map.of("error", "active_lock_exists"));
int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) ? req.unlockCodeLines() : 5; int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) ? req.unlockCodeLines() : 5;
String unlockCode = generateUnlockCode(codeLines);
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
lock.setStartTime(now);
lock.setUnlockCode(unlockCode); String unlockCode;
lock.setUnlockCodeLength(codeLines); String lockName;
lock.setAvailableCards(new ArrayList<>(lock.getInitialCards()));
lock.setOpenPicks(0); if (lock instanceof CardLockEntity cardLock) {
lock.setNextCardIn(now.plusMinutes(lock.getPickEveryMinute())); unlockCode = generateUnlockCode(codeLines);
if (lock.getHygineOpeningEveryMinites() != null) { cardLock.setStartTime(now);
lock.setLastHygineOpening(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); lockeeInvitationRepository.delete(inv);
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getKeyholderUserId(), sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.", me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" angenommen.",
"/keyholder.html", de.oaa.xxx.social.entity.MessageCause.INVITATION); "/keyholder.html", de.oaa.xxx.social.entity.MessageCause.INVITATION);
@@ -254,12 +294,12 @@ public class LockeeInvitationController {
var inv = invOpt.get(); var inv = invOpt.get();
if (!inv.getLockeeUserId().equals(myId)) return ResponseEntity.status(403).build(); 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); lockeeInvitationRepository.delete(inv);
if (lockOpt.isPresent()) { if (lockOpt.isPresent()) {
var lock = lockOpt.get(); var lock = lockOpt.get();
cardlockRepository.delete(lock); baseLockRepository.delete(lock);
String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock"; String lockName = lock.getName() != null && !lock.getName().isBlank() ? lock.getName() : "Unbenanntes Lock";
sendMessage(myId, inv.getKeyholderUserId(), sendMessage(myId, inv.getKeyholderUserId(),
me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.", me.getName() + " hat die Einladung als Lockee für das Lock „" + lockName + "\" abgelehnt.",

View File

@@ -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;
}

View File

@@ -1,7 +0,0 @@
package de.oaa.xxx.games.chastity.pillory;
public enum PilloryReason {
HYGIENE_OPENING_EXEEDED,
KEYHOLDER_DESCESSION;
}

View File

@@ -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> {
}

View File

@@ -11,16 +11,6 @@ public class Task {
private String description; private String description;
private Integer minutes; 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 @Override
public String toString() { public String toString() {
return "Task[title=" + title + ", minutes=" + minutes + "]"; return "Task[title=" + title + ", minutes=" + minutes + "]";

View File

@@ -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();
}
}

View File

@@ -3,115 +3,60 @@ package de.oaa.xxx.games.chastity.timelock;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; 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.common.PenaltyType;
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelConverter; import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelConverter;
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry; 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.Column;
import jakarta.persistence.Convert; import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity; 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.Getter;
import lombok.Setter; import lombok.Setter;
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "time_lock") @DiscriminatorValue("TIMELOCK")
public class TimeLockEntity { 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 @Column
private LocalDateTime startTime; private LocalDateTime startTime;
@Column @Column
private LocalDateTime unlockTime; private LocalDateTime unlockTime;
@Column @Column
private boolean endTimeVisible; private boolean endTimeVisible;
@Convert(converter = TaskListConverter.class) @Column
@Column(columnDefinition = "TEXT") private Integer minTimeInMinutes;
private List<Task> tasks; @Column
private Integer maxTimeInMinutes;
@Column @Column
private Integer taskEveryMinutes; private Integer taskEveryMinutes;
@Column @Column
private Integer minTasksPerDay; private Integer minTasksPerDay;
@Convert(converter = SpinningWheelConverter.class) @Convert(converter = SpinningWheelConverter.class)
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private List<SpinningWheelEntry> spinningWheelEntries; 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 @Column
private Integer spinsEveryMinutes; private Integer spinsEveryMinutes;
@Column @Column
private Integer minSpinsPerDay; private Integer minSpinsPerDay;
@Column @Column
private PenaltyType penaltyType; private PenaltyType penaltyType;
@Column @Column
private Integer penaltyValue; 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 @Column
private LocalDateTime frozenFrom; 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 @Column
private List<LocalDateTime> taskTimes; private List<LocalDateTime> taskTimes;
@Column @Column
private List<LocalDateTime> spinningWheelTimes; private List<LocalDateTime> spinningWheelTimes;
@Column @Column
private LocalDate lastCheck; private LocalDate lastCheck;
public TaskMode getTaskMode() { return taskMode != null ? taskMode : TaskMode.RANDOM; }
} }

View File

@@ -6,4 +6,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
public interface TimeLockRepository extends JpaRepository<TimeLockEntity, UUID> { public interface TimeLockRepository extends JpaRepository<TimeLockEntity, UUID> {
boolean existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(UUID lockee);
} }

View File

@@ -1,381 +1,372 @@
package de.oaa.xxx.games.chastity.timelock; package de.oaa.xxx.games.chastity.timelock;
import java.time.Duration;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Random; import java.util.Random;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.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.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.LockControl;
import de.oaa.xxx.games.chastity.lockcontroll.LockControlCallback; import de.oaa.xxx.games.chastity.lockcontroll.LockControlCallback;
import de.oaa.xxx.games.chastity.lockcontroll.TTLockControl; import de.oaa.xxx.games.chastity.lockcontroll.TTLockControl;
import de.oaa.xxx.games.chastity.lockcontroll.TrustLockControl; import de.oaa.xxx.games.chastity.lockcontroll.TrustLockControl;
import de.oaa.xxx.games.chastity.lockcontroll.UnlockcodeLockControl; import de.oaa.xxx.games.chastity.lockcontroll.UnlockcodeLockControl;
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry; 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.tasks.TaskMode;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason; import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; 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.GameHistoryRepository;
import de.oaa.xxx.games.history.GameType;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository; 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 static final Logger LOGGER = LoggerFactory.getLogger(TimeLockService.class);
private final TimeLockEntity lock; private final TimeLockEntity lock;
private final TimeLockRepository timeLockRepository; private final TimeLockRepository timeLockRepository;
private final VerificationRepository verificationRepository; private final CommunityPilloryRepository pilloryRepository;
private final GameHistoryRepository gameHistoryRepository;
private final UserRepository userRepository;
private final KeyholderNotificationRepository keyholderNotificationRepository;
private final UnlockCodeHistoryService unlockCodeHistoryService;
private LockControl lockControl; private LockControl lockControl;
public TimeLockService(TimeLockEntity lock, VerificationRepository verificationRepository, public TimeLockService(TimeLockEntity lock,
VerificationVoteRepository verificationVoteRepository, CommunityVerificationRepository verificationRepository,
CommunityVerificationVoteRepository verificationVoteRepository,
TimeLockRepository timeLockRepository, TimeLockRepository timeLockRepository,
GameHistoryRepository gameHistoryRepository, GameHistoryRepository gameHistoryRepository,
UserRepository userRepository, UserRepository userRepository,
KeyholderNotificationRepository keyholderNotificationRepository, KeyholderNotificationRepository keyholderNotificationRepository,
UnlockCodeHistoryService unlockCodeHistoryService) { KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
super(verificationVoteRepository); KeyholderVerificationRepository keyholderVerificationRepository,
this.lock = lock; CommunityTaskVoteRepository communityTaskVoteRepository,
this.timeLockRepository = timeLockRepository; CommunityPilloryRepository pilloryRepository,
this.verificationRepository = verificationRepository; UnlockCodeHistoryService unlockCodeHistoryService,
this.gameHistoryRepository = gameHistoryRepository; SystemMessageService systemMessageService) {
this.userRepository = userRepository; super(verificationVoteRepository, verificationRepository, keyholderVerificationRepository,
this.keyholderNotificationRepository = keyholderNotificationRepository; gameHistoryRepository, userRepository, keyholderNotificationRepository,
this.unlockCodeHistoryService = unlockCodeHistoryService; systemMessageService, unlockCodeHistoryService,
} keyholderTaskChoiceRepository, communityTaskVoteRepository);
this.lock = lock;
this.timeLockRepository = timeLockRepository;
this.pilloryRepository = pilloryRepository;
}
public void init(TimeLockTemplate template, TimeLockAdditionalSettings settings) { // ── Abstract method implementations ──────────────────────────────────────
switch (settings.controllType()) {
case TTLOCK -> lockControl = new TTLockControl();
case TRUST -> lockControl = new TrustLockControl();
case UNLOCK_CODE -> lockControl = new UnlockcodeLockControl(this);
}
lock.setLockee(UUID.randomUUID()); @Override
lock.setName(template.name()); protected BaseLockEntity getLock() {
lock.setLockee(settings.lockee()); return lock;
lock.setKeyholder(settings.keyholder()); }
lock.setRequiresVerification(template.requiresVerification());
lock.setTestLock(settings.testlock());
lock.setUnlockCodeLength(settings.unlockCodeLength());
lock.setStartTime(LocalDateTime.now()); @Override
Integer unlockTimeMinutes = template.maxTimeInMinutes(); protected void saveLock() {
if (template.minTimeInMinutes() != null) { timeLockRepository.save(lock);
unlockTimeMinutes = new Random().nextInt(template.minTimeInMinutes(), template.maxTimeInMinutes()); }
}
lock.setUnlockTime(LocalDateTime.now().plusMinutes(unlockTimeMinutes));
lock.setEndTimeVisible(template.endTimeVisible());
lock.setTasks(template.tasks()); @Override
lock.setTaskEveryMinutes(template.taskEveryMinutes()); protected GameType getGameType() {
lock.setMinTasksPerDay(template.minTasksPerDay()); return GameType.TIMELOCK;
}
lock.setSpinningWheelEntries(template.spinningWheelEntries()); @Override
lock.setSpinsEveryMinutes(template.spinsEveryMinutes()); protected void applyHygieneOvertime(Long overtime) {
lock.setMinSpinsPerDay(template.minSpinsPerDay()); lock.setUnlockTime(lock.getUnlockTime().plusMinutes(overtime * 4));
}
lock.setHygineOpeningDurationMinutes(template.hygineOpeningDurationMinutes()); // ── Hook overrides ────────────────────────────────────────────────────────
lock.setHygineOpeningEveryMinites(template.hygineOpeningEveryMinites());
lock.setTaskMode(template.taskMode()); @Override
protected void beforePhysicalUnlock() {
lockControl.unlock();
}
lockControl.lock(); @Override
} protected void afterHygieneClosing() {
lockControl.lock();
}
public SpinningWheelEntry spinWheel() { // ── Initialisation ────────────────────────────────────────────────────────
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;
}
public void addTime(Integer intVal) { /**
LOGGER.debug("Lock addTime: %s minutes", intVal); * Initialisiert ein neues Lock anhand eines Template und Laufzeit-Einstellungen.
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(intVal)); * 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 removeTime(Integer intVal) { LocalDateTime now = LocalDateTime.now();
LOGGER.debug("Lock removeTime: %s minutes", intVal); lock.setStartTime(now);
lock.setUnlockTime(lock.getUnlockTime().minusMinutes(intVal)); 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 void freeze(Integer intVal) { Integer minMinutes = template.getMinTimeInMinutes();
LOGGER.debug("Lock frozen for %s minutes", intVal); Integer maxMinutes = template.getMaxTimeInMinutes() != null ? template.getMaxTimeInMinutes() : 60;
lock.setFrozenFrom(LocalDateTime.now()); int unlockTimeMinutes = (minMinutes != null && minMinutes < maxMinutes)
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(intVal)); ? minMinutes + new Random().nextInt(maxMinutes - minMinutes)
} : maxMinutes;
lock.setUnlockTime(now.plusMinutes(unlockTimeMinutes));
lock.setEndTimeVisible(template.isEndTimeVisible());
public void freeze() { lock.setTasks(template.getTasks());
LOGGER.debug("Lock frozen"); lock.setTaskEveryMinutes(template.getTaskEveryMinutes());
lock.setFrozenFrom(LocalDateTime.now()); lock.setMinTasksPerDay(template.getMinTasksPerDay());
}
public void unfreeze() { lock.setSpinningWheelEntries(template.getSpinningWheelEntries());
if (lock.getFrozenFrom() != null) { lock.setSpinsEveryMinutes(template.getSpinsEveryMinutes());
var unfreeTime = lock.getFrozenUntil() != null ? lock.getFrozenUntil() : LocalDateTime.now(); lock.setMinSpinsPerDay(template.getMinSpinsPerDay());
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");
}
}
public void task() { lock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes());
if (TempOpeningReason.HYGIENE != lock.getTempOpeningReason() ) { lock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites());
switch (lock.getTaskMode()) { if (template.getHygineOpeningEveryMinites() != null) {
case TaskMode.RANDOM -> applyRandomTask(); lock.setLastHygineOpening(now);
case TaskMode.KEYHOLDER -> startKeyHolderVote(); }
case TaskMode.COMMUNITY -> startCommunityVode();
}
}
// Nicht während der Hyhiene Öffnung
}
private void startKeyHolderVote() { lock.setTaskMode(template.getTaskCardMode());
// Keyholder Vote starten lock.setPenaltyType(template.getPenaltyType());
} lock.setPenaltyValue(template.getPenaltyValue());
lock.setMinTimeInMinutes(template.getMinTimeInMinutes());
lock.setMaxTimeInMinutes(template.getMaxTimeInMinutes());
private void startCommunityVode() { lockControl.lock();
// Community Vote starten }
}
public void applyRandomTask() { // ── Spinning wheel ────────────────────────────────────────────────────────
LOGGER.debug("Apply random task");
var tasks = lock.getTasks();
if (!tasks.isEmpty()) {
task(tasks.get(new Random().nextInt(tasks.size())));
}
}
public void task(Task task) { public SpinningWheelEntry spinWheel() {
LOGGER.debug("Apply task {}", task); if (TempOpeningReason.HYGIENE != lock.getTempOpeningReason()) {
lock.setCurrentTask(task.resolveTitle()); var entries = lock.getSpinningWheelEntries();
lock.setCurrentTaskDescription(task.getDescription()); var entry = entries.get(new Random().nextInt(entries.size()));
if (task.getMinutes() != null && task.getMinutes() > 0) { entry.getType().apply(this, entry.getIntVal(), entry.getStringVal());
lock.setTaskUntil(LocalDateTime.now().plusMinutes(task.getMinutes())); return entry;
} }
} // Nicht während der Hygiene-Öffnung
return null;
}
public void text(Integer intVal, String stringVal) { // ── Time controls ─────────────────────────────────────────────────────────
LOGGER.debug("Apply text {}", stringVal);
lock.setCurrentTask(stringVal);
if (intVal != null && intVal > 0) {
lock.setTaskUntil(LocalDateTime.now().plusMinutes(intVal));
}
}
public String clearTask() { public void addTime(Integer intVal) {
LOGGER.debug("Clear task"); LOGGER.debug("Lock addTime: %s minutes", intVal);
lock.setCurrentTask(null); lock.setUnlockTime(lock.getUnlockTime().plusMinutes(intVal));
lock.setCurrentTaskDescription(null); }
lock.setTaskUntil(null);
return "";
}
public void testUnfreeze() { public void removeTime(Integer intVal) {
if (lock.getFrozenUntil().isAfter(LocalDateTime.now())) { LOGGER.debug("Lock removeTime: %s minutes", intVal);
unfreeze(); lock.setUnlockTime(lock.getUnlockTime().minusMinutes(intVal));
} }
}
public void unlock(String unlockCode) { public void freeze(Integer intVal) {
lockControl.unlock(); LOGGER.debug("Lock frozen for %s minutes", intVal);
this.lock.setUnlockTime(LocalDateTime.now()); lock.setFrozenFrom(LocalDateTime.now());
boolean valid = true; lock.setFrozenUntil(LocalDateTime.now().plusMinutes(intVal));
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(); public void freeze() {
LocalDate last = this.lock.getUnlockTime().toLocalDate().minusDays(1); LOGGER.debug("Lock frozen");
lock.setFrozenFrom(LocalDateTime.now());
}
while (!current.isAfter(last)) { public void unfreeze() {
if (!verifications.contains(current)) { if (lock.getFrozenFrom() != null) {
valid = false; var unfreeTime = lock.getFrozenUntil() != null ? lock.getFrozenUntil() : LocalDateTime.now();
LOGGER.debug("Lock invalid - no daily verification on %s", current.toString()); var diff = ChronoUnit.MINUTES.between(lock.getFrozenFrom(), unfreeTime);
break; LOGGER.debug("Lock unfrozen - adding %s minutes to the lock", diff);
} lock.setUnlockTime(lock.getUnlockTime().plusMinutes(diff));
current = current.plusDays(1); } else {
} LOGGER.debug("Lock not frozen - ignore Call");
} }
}
lock.setUnlockTime(LocalDateTime.now()); public void testUnfreeze() {
LOGGER.debug("Unlocked at {}", lock.getUnlockTime()); if (lock.getFrozenUntil().isAfter(LocalDateTime.now())) {
timeLockRepository.save(lock); unfreeze();
}
}
if (valid) { // ── Tasks ─────────────────────────────────────────────────────────────────
long durationMinutes = Duration.between(lock.getStartTime(), lock.getUnlockTime()).toMinutes();
// Gemeinsamer History-Eintrag mit Teilnehmerliste public void task() {
GameHistoryEntity entry = new GameHistoryEntity(); if (TempOpeningReason.HYGIENE != lock.getTempOpeningReason()) {
entry.setGameType(de.oaa.xxx.games.history.GameType.CARDLOCK); switch (lock.getTaskMode()) {
entry.setGameName(lock.getName()); case TaskMode.RANDOM -> applyRandomTask();
entry.setStartTime(lock.getStartTime()); case TaskMode.KEYHOLDER -> startKeyholderVote();
entry.setEndTime(lock.getUnlockTime()); case TaskMode.COMMUNITY -> {
entry.setDurationMinutes(durationMinutes); if (lock.isTestLock()) applyRandomTask();
entry.addParticipant(lock.getLockee(), de.oaa.xxx.games.history.GameRole.LOCKEE); else startCommunityVote();
if (lock.getKeyholder() != null) { }
entry.addParticipant(lock.getKeyholder(), de.oaa.xxx.games.history.GameRole.KEYHOLDER); }
} }
gameHistoryRepository.save(entry); // Nicht während der Hygiene-Öffnung
}
int minutes = (int) durationMinutes; public void text(Integer intVal, String stringVal) {
userRepository.findById(lock.getLockee()).ifPresent(u -> { LOGGER.debug("Apply text {}", stringVal);
u.setLockeeXp(u.getLockeeXp() + minutes); lock.setCurrentTask(stringVal);
userRepository.save(u); if (intVal != null && intVal > 0) {
}); lock.setTaskUntil(LocalDateTime.now().plusMinutes(intVal));
if (lock.getKeyholder() != null) { }
userRepository.findById(lock.getKeyholder()).ifPresent(u -> { }
u.setKeyholderXp(u.getKeyholderXp() + minutes);
userRepository.save(u);
});
}
}
}
public void applyPenalty() { // ── Verification ──────────────────────────────────────────────────────────
if (lock.getPenaltyType() != null) {
switch (lock.getPenaltyType()) {
case ADD -> addTime(lock.getPenaltyValue());
case FREEZE -> freeze();
case PILLORY -> pillory();
}
}
}
public void check() { /**
LocalDate today = LocalDate.now(); * Gibt eine bestehende Verifikation für heute zurück (Idempotenz) oder legt eine neue an.
if (!lock.getStartTime().toLocalDate().equals(today)) { * Erstellt je nach Keyholder-Präsenz eine KeyholderVerification oder CommunityVerification.
if (lock.getLastCheck() != null || today.isAfter(lock.getLastCheck())) { */
LOGGER.info("Check the day before for violations"); public VerificationCommonDTO startVerification() {
LocalDate yesterday = today.minusDays(1); LocalDate today = LocalDate.now();
boolean violation = false; LocalDateTime todayStart = today.atStartOfDay();
if (lock.getMinTasksPerDay() != null) { LocalDateTime todayEnd = todayStart.plusDays(1);
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() { if (lock.getKeyholder() != null) {
// TODO an den Pranger stellen var existing = keyholderVerificationRepository.findByLockId(lock.getLockId()).stream()
} .filter(v -> v.getCreatedAt().toLocalDate().equals(today))
.findFirst();
if (existing.isPresent()) return existing.get().toCommonVerification();
@Override KeyholderVerificationEntity v = new KeyholderVerificationEntity();
public void setUnlockCode(String code) { v.setId(UUID.randomUUID());
lock.setUnlockCode(code); v.setLockId(lock.getLockId());
timeLockRepository.save(lock); 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();
@Override CommunityVerificationEntity v = new CommunityVerificationEntity();
public int getUnlockcodeLenght() { v.setId(UUID.randomUUID());
return lock.getUnlockCodeLength(); 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;
}
public void startHygieneOpening() { // ── Penalty & check ───────────────────────────────────────────────────────
tempOperning(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
}
private Long calcOvertime() { public void applyPenalty() {
LocalDateTime now = LocalDateTime.now(); if (lock.getPenaltyType() != null) {
Long overtime = null; switch (lock.getPenaltyType()) {
if (lock.getTempOpeningTime() != null && lock.getTempOpeningDuration() != null) { case ADD -> addTime(lock.getPenaltyValue());
LocalDateTime dueTime = lock.getTempOpeningTime().plusMinutes(lock.getTempOpeningDuration()); case FREEZE -> freeze();
if (LocalDateTime.now().isAfter(dueTime)) { case PILLORY -> pillory(CommunityPilloryReason.HYGIENE_OPENING_EXEEDED, null);
overtime = ChronoUnit.MINUTES.between(dueTime, now); }
} }
} }
return overtime;
}
public String endHygieneOpening() { public void check() {
lockControl.lock(); LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.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);
}
}
}
Long overtime = calcOvertime(); public void pillory(CommunityPilloryReason reason, UUID keyholderId) {
if (overtime != null) { CommunityPilloryEntity pillory = new CommunityPilloryEntity();
if (lock.getKeyholder() != null) { pillory.setCreatedAt(LocalDateTime.now());
reportKeyholder(overtime); pillory.setLockeeId(lock.getLockee());
} pillory.setLockId(lock.getLockId());
addOvertime(overtime); pillory.setReason(reason);
} pillory.setKeyholderId(keyholderId);
lock.setLastHygineOpening(now); pilloryRepository.save(pillory);
lock.setTempOpeningDuration(null); }
lock.setTempOpeningTime(null);
var code = CodeCreator.createAlphanumericCode(lock.getUnlockCodeLength()); // ── Hygiene opening ───────────────────────────────────────────────────────
lock.setUnlockCode(code);
timeLockRepository.save(lock);
return code;
}
private void reportKeyholder(Long overtime) { public void startHygieneOpening() {
KeyholderNotificationEntity notification = new KeyholderNotificationEntity(); lockControl.unlock();
notification.setLockId(lock.getLockId()); startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes());
notification.setLockeeId(lock.getLockee()); }
notification.setKeyholderUserId(lock.getKeyholder());
notification.setViolationTime(LocalDateTime.now());
notification.setOvertimeMinutes(overtime);
keyholderNotificationRepository.save(notification);
}
private void addOvertime(Long overtime) { // ── LockControlCallback ───────────────────────────────────────────────────
lock.setUnlockTime(lock.getUnlockTime().plusMinutes(overtime * 4));
}
private void tempOperning(TempOpeningReason reason, Integer duration) { @Override
assert duration != null; public void setUnlockCode(String code) {
lockControl.unlock(); lock.setUnlockCode(code);
lock.setTempOpeningReason(reason); timeLockRepository.save(lock);
lock.setTempOpeningTime(LocalDateTime.now());; }
lock.setTempOpeningDuration(duration);
timeLockRepository.save(lock); @Override
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), reason.toString()); public int getUnlockcodeLenght() {
} return lock.getUnlockCodeLength();
}
} }

View File

@@ -2,53 +2,64 @@ package de.oaa.xxx.games.chastity.timelock;
import org.springframework.stereotype.Service; 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.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.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.games.history.GameHistoryRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
@Service @Service
public class TimeLockServiceFactory { 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 UnlockCodeHistoryService unlockCodeHistoryService;
private final VerificationVoteRepository verificationVoteRepository; private final SystemMessageService systemMessageService;
private final TimeLockRepository timeLockRepository; private CommunityVerificationVoteRepository communityVerificationVoteRepository;
private final GameHistoryRepository gameHistoryRepository;
private final UserRepository userRepository;
private KeyholderNotificationRepository keyholderNotificationRepository;
private final UnlockCodeHistoryService unlockCodeHistoryService;
public TimeLockServiceFactory(VerificationRepository verificationRepository, public TimeLockServiceFactory(CommunityVerificationRepository verificationRepository,
VerificationVoteRepository verificationVoteRepository, CommunityVerificationVoteRepository verificationVoteRepository, TimeLockRepository timeLockRepository,
TimeLockRepository timeLockRepository, GameHistoryRepository gameHistoryRepository, UserRepository userRepository,
GameHistoryRepository gameHistoryRepository, KeyholderNotificationRepository keyholderNotificationRepository,
UserRepository userRepository, KeyholderTaskChoiceRepository keyholderTaskChoiceRepository,
KeyholderNotificationRepository keyholderNotificationRepository, KeyholderVerificationRepository keyholderVerificationRepository,
UnlockCodeHistoryService unlockCodeHistoryService) { CommunityTaskVoteRepository communityTaskVoteRepository, CommunityPilloryRepository pilloryRepository,
this.verificationRepository = verificationRepository; UnlockCodeHistoryService unlockCodeHistoryService, SystemMessageService systemMessageService) {
this.verificationVoteRepository = verificationVoteRepository; this.communityVerificationVoteRepository = verificationVoteRepository;
this.timeLockRepository = timeLockRepository; this.timeLockRepository = timeLockRepository;
this.gameHistoryRepository = gameHistoryRepository; this.communityVerificationRepository = verificationRepository;
this.userRepository = userRepository; this.gameHistoryRepository = gameHistoryRepository;
this.userRepository = userRepository;
this.keyholderNotificationRepository = keyholderNotificationRepository; 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. * Erstellt eine neue CardLockService-Instanz für das gegebene Lock.
*/ */
public TimeLockService create(TimeLockEntity lock) { public TimeLockService create(TimeLockEntity lock) {
return new TimeLockService( return new TimeLockService(lock, communityVerificationRepository, communityVerificationVoteRepository,
lock, timeLockRepository, gameHistoryRepository, userRepository, keyholderNotificationRepository,
verificationRepository, keyholderTaskChoiceRepository, keyholderVerificationRepository, communityTaskVoteRepository,
verificationVoteRepository, pilloryRepository, unlockCodeHistoryService, systemMessageService);
timeLockRepository, }
gameHistoryRepository,
userRepository,
keyholderNotificationRepository,
unlockCodeHistoryService
);
}
} }

View File

@@ -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());
}
}

View File

@@ -1,38 +1,25 @@
package de.oaa.xxx.games.chastity.timelock; package de.oaa.xxx.games.chastity.timelock;
import java.util.List; 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.common.PenaltyType;
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelConverter; import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelConverter;
import de.oaa.xxx.games.chastity.spinningwheel.SpinningWheelEntry; 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.tasks.TaskMode;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Convert; import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity; 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.Getter;
import lombok.Setter; import lombok.Setter;
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "timelock_template") @DiscriminatorValue("TIMELOCK")
public class TimeLockTemplateEntity { public class TimeLockTemplateEntity extends BaseLockTemplateEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column
private UUID templateId;
@Column(nullable = false)
private UUID owner;
@Column
private String name;
@Column @Column
private Integer minTimeInMinutes; private Integer minTimeInMinutes;
@Column @Column
@@ -40,18 +27,9 @@ public class TimeLockTemplateEntity {
@Column @Column
private boolean endTimeVisible; private boolean endTimeVisible;
@Column @Column
private Integer hygineOpeningDurationMinutes;
@Column
private Integer hygineOpeningEveryMinites;
@Convert(converter = TaskListConverter.class)
@Column(columnDefinition = "TEXT")
private List<Task> tasks;
@Column
private Integer taskEveryMinutes; private Integer taskEveryMinutes;
@Column @Column
private Integer minTasksPerDay; private Integer minTasksPerDay;
@Convert(converter = SpinningWheelConverter.class) @Convert(converter = SpinningWheelConverter.class)
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private List<SpinningWheelEntry> spinningWheelEntries; private List<SpinningWheelEntry> spinningWheelEntries;
@@ -59,24 +37,15 @@ public class TimeLockTemplateEntity {
private Integer spinsEveryMinutes; private Integer spinsEveryMinutes;
@Column @Column
private Integer minSpinsPerDay; private Integer minSpinsPerDay;
@Column
private boolean requiresVerification;
@Column(nullable = false)
private TaskMode taskMode = TaskMode.RANDOM;
@Column @Column
private PenaltyType penaltyType; private PenaltyType penaltyType;
@Column @Column
private Integer penaltyValue; private Integer penaltyValue;
public TaskMode getTaskCardMode() {
return taskMode != null ? taskMode : TaskMode.RANDOM;
}
public TimeLockTemplate toTimeLockTemplate() { public TimeLockTemplate toTimeLockTemplate() {
return new TimeLockTemplate(templateId, owner, name, minTimeInMinutes, maxTimeInMinutes, endTimeVisible, return new TimeLockTemplate(getTemplateId(), getOwner(), getName(), minTimeInMinutes, maxTimeInMinutes, endTimeVisible,
hygineOpeningDurationMinutes, hygineOpeningEveryMinites, tasks, taskEveryMinutes, minTasksPerDay, getHygineOpeningDurationMinutes(), getHygineOpeningEveryMinites(), getTasks(), taskEveryMinutes, minTasksPerDay,
spinningWheelEntries, spinsEveryMinutes, minSpinsPerDay, requiresVerification, taskMode, penaltyType, spinningWheelEntries, spinsEveryMinutes, minSpinsPerDay, isRequiresVerification(), getTaskMode(), penaltyType,
penaltyValue); penaltyValue);
} }

View File

@@ -5,6 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
public interface TimelockTemplateRepository extends JpaRepository<TimeLockTemplateEntity, UUID> { public interface TimeLockTemplateRepository extends JpaRepository<TimeLockTemplateEntity, UUID> {
List<TimeLockTemplateEntity> findByOwner(UUID owner); List<TimeLockTemplateEntity> findByOwner(UUID owner);
} }

View File

@@ -13,9 +13,9 @@ import de.oaa.xxx.util.ValidationResult;
public class TimeLockTemplateService { public class TimeLockTemplateService {
private TimelockTemplateRepository timelockTemplateRepository; private TimeLockTemplateRepository timelockTemplateRepository;
public TimeLockTemplateService(TimelockTemplateRepository timelockTemplateRepository) { public TimeLockTemplateService(TimeLockTemplateRepository timelockTemplateRepository) {
this.timelockTemplateRepository = timelockTemplateRepository; this.timelockTemplateRepository = timelockTemplateRepository;
} }

View File

@@ -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();
}
}

View File

@@ -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) {}

View File

@@ -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<>());
}
}

View File

@@ -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);
}

View File

@@ -1,5 +0,0 @@
package de.oaa.xxx.games.chastity.verification;
import java.util.UUID;
public record VerificationVoteDTO (UUID voteId, UUID userId, boolean upvote) {}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -777,6 +777,16 @@
async function loadLock() { async function loadLock() {
const res = await fetch('/keyholder/cardlock/' + lockId); 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) { if (!res.ok) {
document.getElementById('lockContent').textContent = 'Lock nicht gefunden.'; document.getElementById('lockContent').textContent = 'Lock nicht gefunden.';
return; return;

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,6 @@
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
/* ── Unified feed ── */
#feed { #feed {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -87,7 +86,7 @@
color: var(--color-primary); color: var(--color-primary);
background: none; 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.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-btn:disabled { opacity: 0.55; cursor: not-allowed; pointer-events: none; }
.vote-count { font-weight: 600; font-size: 0.88rem; } .vote-count { font-weight: 600; font-size: 0.88rem; }
@@ -107,7 +106,7 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.4rem; 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-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-options { display: flex; flex-direction: column; gap: 0.35rem; margin-top: 0.5rem; }
.task-vote-btn { .task-vote-btn {
@@ -150,6 +149,30 @@
text-align: center; 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 { .empty-hint {
color: var(--color-muted); color: var(--color-muted);
font-size: 0.9rem; font-size: 0.9rem;
@@ -170,7 +193,7 @@
<div class="content"> <div class="content">
<div class="page-title">Community Votes</div> <div class="page-title">Community Votes</div>
<div class="page-subtitle">Verifikationen &amp; Aufgaben-Abstimmungen</div> <div class="page-subtitle">Verifikationen, Aufgaben-Abstimmungen &amp; Pranger</div>
<div id="feed"></div> <div id="feed"></div>
<div class="load-spinner" id="loadSpinner" style="display:none;">Lädt…</div> <div class="load-spinner" id="loadSpinner" style="display:none;">Lädt…</div>
@@ -186,36 +209,84 @@
function esc(s) { function esc(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
} }
function fmtDateTime(isoStr) { function fmtDateTime(isoStr) {
return new Date(isoStr).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); 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) { function buildVerCard(base, detail) {
const isOwn = vote.isOwnLock; const voted = detail.isOwnLock || detail.myVote !== null && detail.myVote !== undefined;
const alreadyVoted = vote.myVote !== null && vote.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 = ''; let optionsHtml = '';
(vote.tasks || []).forEach((t, i) => { (detail.entries || []).forEach((e, i) => {
const count = (vote.voteCounts || [])[i] || 0; const desc = e.description
const isMyVote = vote.myVote === i; ? `<div style="font-size:0.75rem;color:var(--color-muted);margin-top:0.1rem;">${esc(e.description)}</div>`
const desc = t.description
? `<div style="font-size:0.75rem;color:var(--color-muted);margin-top:0.1rem;">${esc(t.description)}</div>`
: ''; : '';
const mins = t.minutes > 0 const mins = e.minutes > 0
? ` <span style="font-size:0.75rem;color:var(--color-muted);">⏱ ${t.minutes} Min.</span>` ? ` <span style="font-size:0.75rem;color:var(--color-muted);">⏱ ${e.minutes} Min.</span>`
: ''; : '';
optionsHtml += `<button class="task-vote-btn ${isMyVote ? 'my-vote' : ''}" optionsHtml += `<button class="task-vote-btn ${e.ownVote ? 'my-vote' : ''}"
id="tvbtn-${vote.voteSessionId}-${i}" id="tvbtn-${id}-${i}"
${(alreadyVoted || isOwn) ? 'disabled' : ''} ${(alreadyVoted || isOwn) ? 'disabled' : ''}
onclick="castTaskVote('${vote.voteSessionId}', ${i})"> onclick="castTaskVote('${id}', ${i})">
<div style="flex:1;min-width:0;"> <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} ${desc}
</div> </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>`; </button>`;
}); });
@@ -225,181 +296,107 @@
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'task-vote-card'; card.className = 'task-vote-card';
card.dataset.ts = vote.createdAt;
card.innerHTML = ` card.innerHTML = `
<div class="task-vote-header"> <div class="task-vote-header">
<span class="task-vote-lockee">🃏 ${esc(vote.lockeeName)}</span> <span class="task-vote-lockee">🃏 ${esc(base.lockeeName)}</span>
<span class="task-vote-expires">Endet: ${fmtDateTime(vote.expiresAt)}</span> <span class="task-vote-expires">Endet: ${fmtDateTime(detail.expiresAt)}</span>
</div> </div>
<div class="task-vote-options">${optionsHtml}</div> <div class="task-vote-options">${optionsHtml}</div>
${ownHint}`; ${ownHint}`;
return card; return card;
} }
async function castTaskVote(voteSessionId, taskIndex) { async function castTaskVote(displayId, taskIndex) {
document.querySelectorAll(`[id^="tvbtn-${voteSessionId}-"]`).forEach(btn => btn.disabled = true); 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) { if (res.ok || res.status === 204) {
const countEl = document.getElementById(`tvcount-${voteSessionId}-${taskIndex}`); const countEl = document.getElementById(`tvcount-${displayId}-${taskIndex}`);
if (countEl) { if (countEl) {
const next = (parseInt(countEl.textContent) || 0) + 1; const next = (parseInt(countEl.textContent) || 0) + 1;
countEl.textContent = `${next} Stimme${next !== 1 ? 'n' : ''}`; countEl.textContent = `${next} Stimme${next !== 1 ? 'n' : ''}`;
} }
const btn = document.getElementById(`tvbtn-${voteSessionId}-${taskIndex}`); document.getElementById(`tvbtn-${displayId}-${taskIndex}`)?.classList.add('my-vote');
if (btn) btn.classList.add('my-vote');
} }
} }
// ── Verification card builder ────────────────────────────────────────────── // ── Pranger-Karte ──────────────────────────────────────────────────────────
function buildVerCard(item) { const PILLORY_LABELS = {
const isOwn = item.myVote === 'own'; HYGIENE_OPENING_EXEEDED: 'Hygiene-Öffnung überschritten',
const voted = isOwn || (item.myVote !== null && item.myVote !== undefined); KEYHOLDER_DESCESSION: 'Keyholder hat aufgegeben'
const votedUp = !isOwn && item.myVote === true; };
const votedDn = !isOwn && item.myVote === false;
function buildPilloryCard(base, detail) {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'vote-card'; card.className = 'pillory-card';
card.dataset.ts = item.verificationTime;
card.innerHTML = ` card.innerHTML = `
<div class="vote-card-media"> <div class="pillory-header">
<img class="vote-card-img" src="data:image/jpeg;base64,${item.image}" alt="Verifikationsbild"> <span class="pillory-lockee">🔒 ${esc(base.lockeeName)}</span>
<div class="vote-card-code">${esc(item.code)}</div> <span class="pillory-date">${fmtDateTime(base.createdAt)}</span>
</div> </div>
<div class="vote-card-body"> <div class="pillory-reason">⚠️ ${esc(PILLORY_LABELS[detail.reason] || detail.reason)}</div>
<div class="vote-meta">Verifikation · ${fmtDateTime(item.verificationTime)}</div> ${detail.message ? `<div class="pillory-message">${esc(detail.message)}</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>`;
return card; return card;
} }
async function castVerVote(verificationId, upvote) { // ── Unified feed mit Paging ────────────────────────────────────────────────
const upBtn = document.getElementById('up-' + verificationId);
const dnBtn = document.getElementById('dn-' + verificationId);
upBtn.disabled = true;
dnBtn.disabled = true;
const res = await fetch('/verification/' + verificationId + '/vote/', { let page = 0;
method: 'POST', let exhausted = false;
headers: { 'Content-Type': 'application/json' }, let loading = false;
body: JSON.stringify({ upvote }) let rendered = 0;
});
if (res.ok || res.status === 202) { async function fetchDetail(base) {
const countEl = document.getElementById(upvote ? 'upcount-' + verificationId : 'dncount-' + verificationId); const urls = {
countEl.textContent = parseInt(countEl.textContent) + 1; VERIFICATION: `/games/chastity/community/verification/${base.displayId}`,
(upvote ? upBtn : dnBtn).classList.add(upvote ? 'voted-up' : 'voted-down'); 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 ────────────────────────────────────────────────────────── function buildCard(base, detail) {
// Strategy: task votes are loaded once (small set); verifications are paginated. if (!detail) return null;
// When each verification page loads, we interleave the pending task votes that if (base.type === 'VERIFICATION') return buildVerCard(base, detail);
// belong chronologically before the oldest verification on this page. if (base.type === 'TASK_VOTE') return buildTaskVoteCard(base, detail);
if (base.type === 'PILLORY') return buildPilloryCard(base, detail);
let verPage = 0; return null;
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;
} }
async function loadMore() { async function loadMore() {
if (loading || verExhausted) return; if (loading || exhausted) return;
loading = true; loading = true;
document.getElementById('loadSpinner').style.display = ''; document.getElementById('loadSpinner').style.display = '';
// Load task votes on first call let pageData;
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 = [];
try { try {
const r = await fetch('/verification/community?page=' + verPage); const r = await fetch(`/games/chastity/community?page=${page}&sort=createdAt,desc`);
if (r.ok) verItems = await r.json(); if (!r.ok) { loading = false; document.getElementById('loadSpinner').style.display = 'none'; return; }
} catch(e) {} 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'; document.getElementById('loadSpinner').style.display = 'none';
loading = false; loading = false;
let batch; const feed = document.getElementById('feed');
if (verItems.length === 0) { items.forEach((base, i) => {
verExhausted = true; const card = buildCard(base, details[i]);
batch = mergeWithTaskVotes([], -Infinity); if (card) { feed.appendChild(card); rendered++; }
} 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);
}
}
if (batch.length > 0) appendItems(batch); if (rendered === 0 && exhausted) {
if (totalRendered === 0 && verExhausted) {
document.getElementById('emptyHint').style.display = ''; document.getElementById('emptyHint').style.display = '';
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -62,6 +62,22 @@ const CARD_DEFS = [
defMin: 0, defMin: 0,
defMax: 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. */ /** Lookup-Objekt für Konsumenten, die nach ID auf Name/Bild/Beschreibung zugreifen. */

File diff suppressed because it is too large Load Diff

View File

@@ -196,7 +196,8 @@
<div class="form-section"> <div class="form-section">
<div class="form-section-title">Optionen</div> <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> <label>Längste Dauer</label>
<div class="time-picker"> <div class="time-picker">
<div class="tp-seg"> <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 class="form-hint">Das Lock öffnet spätestens nach dieser Zeit automatisch. 0 : 00 = keine Begrenzung.</div>
</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"> <div class="form-row" id="rowUnlockCodeLines">
<label for="unlockCodeLines">Anzahl Ziffern des Entsperrcodes</label> <label for="unlockCodeLines">Anzahl Ziffern des Entsperrcodes</label>
<div class="inline-number"> <div class="inline-number">
@@ -272,11 +280,12 @@
<script src="/js/sidebar.js"></script> <script src="/js/sidebar.js"></script>
<script src="/js/social-sidebar.js"></script> <script src="/js/social-sidebar.js"></script>
<script> <script>
let myUserId = null; let myUserId = null;
let myUserName = null; let myUserName = null;
let allFriends = []; let allFriends = [];
let allTemplates = []; let allTemplates = []; // combined; each entry has _type: 'cardlock'|'timelock'
let comboActiveIdx = -1; let selectedTemplate = null;
let comboActiveIdx = -1;
// ── Boot ── // ── Boot ──
fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => { fetch('/login/me').then(r => r.ok ? r.json() : null).then(async user => {
@@ -284,9 +293,16 @@
myUserId = user.userId; myUserId = user.userId;
myUserName = user.name; myUserName = user.name;
// Templates laden Pflicht // Templates laden Pflicht (beide Typen parallel)
try { 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 = []; } } catch { allTemplates = []; }
if (allTemplates.length === 0) { if (allTemplates.length === 0) {
@@ -333,16 +349,20 @@
dropdown.innerHTML = `<div class="combo-empty">Keine Vorlagen gefunden.</div>`; dropdown.innerHTML = `<div class="combo-empty">Keine Vorlagen gefunden.</div>`;
} else { } else {
filtered.forEach(t => { filtered.forEach(t => {
const badge = t._type === 'timelock' ? '⏱' : '🃏';
const label = (t.name || 'Unbenannte Vorlage');
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'combo-option'; div.className = 'combo-option';
div.dataset.id = t.templateId; div.dataset.id = t.templateId;
div.textContent = t.name || 'Unbenannte Vorlage'; div.innerHTML = `${badge} ${label}`;
div.addEventListener('mousedown', e => { div.addEventListener('mousedown', e => {
e.preventDefault(); e.preventDefault();
hidden.value = t.templateId; hidden.value = t.templateId;
input.value = t.name || 'Unbenannte Vorlage'; input.value = badge + ' ' + label;
selectedTemplate = t;
dropdown.classList.remove('open'); dropdown.classList.remove('open');
clearFieldError('rowTemplate'); clearFieldError('rowTemplate');
onTemplateChanged(t);
}); });
dropdown.appendChild(div); dropdown.appendChild(div);
}); });
@@ -471,6 +491,30 @@
input.addEventListener('blur', () => { setTimeout(() => { dropdown.classList.remove('open'); if (!hidden.value) input.value = ''; }, 150); }); 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 ── // ── Zeitpicker ──
function tpChange(prefix, delta, seg) { function tpChange(prefix, delta, seg) {
let d = parseInt(document.getElementById(prefix + '_d').value) || 0; let d = parseInt(document.getElementById(prefix + '_d').value) || 0;
@@ -542,39 +586,56 @@
} }
clearFieldError('rowTemplate'); 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; } if (!t) { showError('Vorlage nicht gefunden.'); return; }
const lockeeVal = document.getElementById('lockeeValue').value; const lockeeVal = document.getElementById('lockeeValue').value;
const keyholderVal = document.getElementById('keyholderValue').value; const keyholderVal = document.getElementById('keyholderValue').value;
const isFriendLockee = lockeeVal && lockeeVal !== myUserId; 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); let endpoint, body;
if (initialCards.length === 0) {
showError('Die gewählte Vorlage enthält keine Karten. Bitte Vorlage prüfen.'); if (t._type === 'timelock') {
return; 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 = { const res = await fetch(endpoint, {
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', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body) body: JSON.stringify(body)
@@ -605,7 +666,9 @@
function showUnlockCodeModal(code, lockId, keyholderPending) { function showUnlockCodeModal(code, lockId, keyholderPending) {
document.getElementById('unlockCodeDisplay').textContent = code; document.getElementById('unlockCodeDisplay').textContent = code;
if (keyholderPending) document.getElementById('unlockKeyholderHint').style.display = ''; 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('unlockModalBtn').onclick = () => startCodeScramble(code, url);
document.getElementById('unlockModal').classList.add('open'); document.getElementById('unlockModal').classList.add('open');
} }