310 lines
14 KiB
Java
310 lines
14 KiB
Java
package com.walkguide.service;
|
|
|
|
import com.walkguide.dto.response.PairingStatusResponse;
|
|
import com.walkguide.dto.response.PairingCodeResponse;
|
|
import com.walkguide.entity.*;
|
|
import com.walkguide.enums.*;
|
|
import com.walkguide.exception.PairingException;
|
|
import com.walkguide.exception.ResourceNotFoundException;
|
|
import com.walkguide.repository.*;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.security.SecureRandom;
|
|
import java.time.LocalDateTime;
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class PairingService {
|
|
|
|
private final PairingRelationRepository pairingRelationRepository;
|
|
private final UserRepository userRepository;
|
|
private final VoiceCommandConfigRepository voiceCommandConfigRepository;
|
|
private final HardwareShortcutRepository hardwareShortcutRepository;
|
|
private final AiConfigRepository aiConfigRepository;
|
|
private final ActivityLogService activityLogService;
|
|
private final FcmService fcmService;
|
|
|
|
private static final String CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
private static final int PAIRING_CODE_LENGTH = 8;
|
|
private static final int PAIRING_CODE_TTL_MINUTES = 15;
|
|
private static final SecureRandom RANDOM = new SecureRandom();
|
|
|
|
@Transactional
|
|
public PairingCodeResponse getOrCreatePairingCode(Long userId) {
|
|
User user = userRepository.findById(userId)
|
|
.orElseThrow(() -> new ResourceNotFoundException("User tidak ditemukan"));
|
|
if (!"ROLE_USER".equals(user.getRole())) {
|
|
throw new PairingException("Hanya akun User yang bisa membuat pairing code.");
|
|
}
|
|
|
|
LocalDateTime now = LocalDateTime.now();
|
|
if (user.getPairingCode() == null
|
|
|| user.getPairingCodeExpiresAt() == null
|
|
|| !user.getPairingCodeExpiresAt().isAfter(now)) {
|
|
assignNewPairingCode(user, now);
|
|
userRepository.save(user);
|
|
}
|
|
return buildPairingCodeResponse(user, now);
|
|
}
|
|
|
|
@Transactional
|
|
public PairingCodeResponse regeneratePairingCode(Long userId) {
|
|
User user = userRepository.findById(userId)
|
|
.orElseThrow(() -> new ResourceNotFoundException("User tidak ditemukan"));
|
|
if (!"ROLE_USER".equals(user.getRole())) {
|
|
throw new PairingException("Hanya akun User yang bisa membuat pairing code.");
|
|
}
|
|
LocalDateTime now = LocalDateTime.now();
|
|
assignNewPairingCode(user, now);
|
|
userRepository.save(user);
|
|
activityLogService.createLog(user, ActivityLogType.PAIRING_INVITE_SENT,
|
|
"User membuat pairing code baru", null);
|
|
return buildPairingCodeResponse(user, now);
|
|
}
|
|
|
|
@Transactional
|
|
public PairingStatusResponse inviteUser(Long guardianId, String submittedCode) {
|
|
// Guardian tidak boleh punya pairing ACTIVE atau PENDING
|
|
if (pairingRelationRepository.existsByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE)) {
|
|
throw new PairingException("Kamu sudah memiliki user yang dipair. Unpair dulu sebelum invite user baru.");
|
|
}
|
|
if (pairingRelationRepository.existsByGuardian_IdAndStatus(guardianId, PairingStatus.PENDING)) {
|
|
throw new PairingException("Kamu sudah punya invite yang menunggu. Tunggu user menerima atau tolak dulu.");
|
|
}
|
|
|
|
User guardian = userRepository.findById(guardianId)
|
|
.orElseThrow(() -> new ResourceNotFoundException("Guardian tidak ditemukan"));
|
|
User user = resolveUserByPairingCode(submittedCode);
|
|
|
|
if (!"ROLE_USER".equals(user.getRole())) {
|
|
throw new PairingException("ID tersebut bukan milik User. Pastikan kamu memasukkan ID yang benar.");
|
|
}
|
|
if (pairingRelationRepository.existsByUser_IdAndStatus(user.getId(), PairingStatus.ACTIVE)) {
|
|
throw new PairingException("User ini sudah dipair dengan Guardian lain.");
|
|
}
|
|
|
|
PairingRelation pairing = PairingRelation.builder()
|
|
.guardian(guardian)
|
|
.user(user)
|
|
.status(PairingStatus.PENDING)
|
|
.build();
|
|
pairing = pairingRelationRepository.save(pairing);
|
|
|
|
user.setPairingCode(null);
|
|
user.setPairingCodeExpiresAt(null);
|
|
userRepository.save(user);
|
|
|
|
// Kirim FCM ke user
|
|
fcmService.sendToToken(user.getFcmToken(),
|
|
"Pairing Request",
|
|
"Guardian " + guardian.getDisplayName() + " mengundang kamu untuk terhubung",
|
|
Map.of("type", "PAIRING_INVITE", "guardianName", guardian.getDisplayName()));
|
|
|
|
activityLogService.createLog(guardian, ActivityLogType.PAIRING_INVITE_SENT,
|
|
"Guardian mengirim invite ke " + user.getDisplayName(), null);
|
|
|
|
return buildStatus(pairing, guardian, user, "GUARDIAN");
|
|
}
|
|
|
|
@Transactional
|
|
public PairingStatusResponse respondToPairing(Long userId, Long pairingId, boolean accept) {
|
|
PairingRelation pairing = pairingRelationRepository.findById(pairingId)
|
|
.orElseThrow(() -> new ResourceNotFoundException("Pairing tidak ditemukan"));
|
|
|
|
if (!pairing.getUser().getId().equals(userId)) {
|
|
throw new PairingException("Kamu tidak berhak merespons pairing ini");
|
|
}
|
|
if (pairing.getStatus() != PairingStatus.PENDING) {
|
|
throw new PairingException("Pairing ini sudah direspons sebelumnya");
|
|
}
|
|
|
|
User user = pairing.getUser();
|
|
User guardian = pairing.getGuardian();
|
|
|
|
pairing.setStatus(accept ? PairingStatus.ACTIVE : PairingStatus.REJECTED);
|
|
pairing.setRespondedAt(LocalDateTime.now());
|
|
pairingRelationRepository.save(pairing);
|
|
|
|
if (accept) {
|
|
seedDefaults(guardian.getId(), user.getId());
|
|
activityLogService.createLog(user, ActivityLogType.PAIRING_ACCEPTED,
|
|
"User menerima pairing dengan Guardian " + guardian.getDisplayName(), null);
|
|
fcmService.sendToToken(guardian.getFcmToken(),
|
|
"Pairing Berhasil!",
|
|
user.getDisplayName() + " menerima undangan pairing kamu",
|
|
Map.of("type", "PAIRING_RESPONSE", "accepted", "true"));
|
|
} else {
|
|
activityLogService.createLog(user, ActivityLogType.PAIRING_REJECTED,
|
|
"User menolak pairing dengan Guardian " + guardian.getDisplayName(), null);
|
|
fcmService.sendToToken(guardian.getFcmToken(),
|
|
"Pairing Ditolak",
|
|
user.getDisplayName() + " menolak undangan pairing kamu",
|
|
Map.of("type", "PAIRING_RESPONSE", "accepted", "false"));
|
|
}
|
|
|
|
return buildStatus(pairing, guardian, user, "USER");
|
|
}
|
|
|
|
@Transactional
|
|
public void unpair(Long requesterId) {
|
|
var pairing = pairingRelationRepository.findByGuardian_Id(requesterId)
|
|
.or(() -> pairingRelationRepository.findByUser_Id(requesterId))
|
|
.orElseThrow(() -> new ResourceNotFoundException("Tidak ada pairing yang aktif"));
|
|
|
|
User guardian = pairing.getGuardian();
|
|
User user = pairing.getUser();
|
|
|
|
// Hapus semua konfigurasi yang terkait dengan pasangan ini
|
|
voiceCommandConfigRepository.deleteByUserId(user.getId());
|
|
hardwareShortcutRepository.deleteByUserId(user.getId());
|
|
aiConfigRepository.findByUserId(user.getId()).ifPresent(aiConfigRepository::delete);
|
|
|
|
pairingRelationRepository.delete(pairing);
|
|
|
|
// Beritahu pihak lain
|
|
boolean requesterIsGuardian = guardian.getId().equals(requesterId);
|
|
String otherFcmToken = requesterIsGuardian ? user.getFcmToken() : guardian.getFcmToken();
|
|
String requesterName = requesterIsGuardian ? guardian.getDisplayName() : user.getDisplayName();
|
|
|
|
fcmService.sendToToken(otherFcmToken,
|
|
"Pairing Diakhiri",
|
|
requesterName + " mengakhiri koneksi pairing",
|
|
Map.of("type", "PAIRING_DISSOLVED"));
|
|
|
|
activityLogService.createLog(guardian, ActivityLogType.PAIRING_DISSOLVED,
|
|
"Pairing antara " + guardian.getDisplayName() + " dan " + user.getDisplayName() + " diakhiri", null);
|
|
}
|
|
|
|
public PairingStatusResponse getStatus(Long userId, String role) {
|
|
if ("ROLE_GUARDIAN".equals(role)) {
|
|
return pairingRelationRepository.findByGuardian_Id(userId)
|
|
.map(p -> buildStatus(p, p.getGuardian(), p.getUser(), "GUARDIAN"))
|
|
.orElse(PairingStatusResponse.builder().status("NONE").build());
|
|
} else {
|
|
return pairingRelationRepository.findByUser_Id(userId)
|
|
.map(p -> buildStatus(p, p.getGuardian(), p.getUser(), "USER"))
|
|
.orElse(PairingStatusResponse.builder().status("NONE").build());
|
|
}
|
|
}
|
|
|
|
// ========== PRIVATE ==========
|
|
|
|
private void seedDefaults(Long guardianId, Long userId) {
|
|
// Voice commands default
|
|
List<VoiceCommandConfig> defaults = List.of(
|
|
vc(guardianId, userId, VoiceCommandKey.OPEN_WALKGUIDE, "Open Walkguide"),
|
|
vc(guardianId, userId, VoiceCommandKey.START_WALKGUIDE, "Start Walkguide"),
|
|
vc(guardianId, userId, VoiceCommandKey.STOP_WALKGUIDE, "Stop Walkguide"),
|
|
vc(guardianId, userId, VoiceCommandKey.CALL_GUARDIAN, "Call Guardian"),
|
|
vc(guardianId, userId, VoiceCommandKey.OPEN_NOTIFICATION, "Open Notifications"),
|
|
vc(guardianId, userId, VoiceCommandKey.READ_ALL_NOTIF, "Read All My Notifications"),
|
|
vc(guardianId, userId, VoiceCommandKey.OPEN_SOS, "Open SOS"),
|
|
vc(guardianId, userId, VoiceCommandKey.SEND_SOS, "Send SOS"),
|
|
vc(guardianId, userId, VoiceCommandKey.WHERE_AM_I, "Where Am I"),
|
|
vc(guardianId, userId, VoiceCommandKey.OPEN_ACTIVITY, "Open Activity Log"),
|
|
vc(guardianId, userId, VoiceCommandKey.OPEN_NAVIGATION, "Open Navigation"),
|
|
vc(guardianId, userId, VoiceCommandKey.OPEN_SETTINGS, "Open Settings"),
|
|
vc(guardianId, userId, VoiceCommandKey.REPEAT_LAST, "Repeat"),
|
|
vc(guardianId, userId, VoiceCommandKey.STOP_TTS, "Stop")
|
|
);
|
|
voiceCommandConfigRepository.saveAll(defaults);
|
|
|
|
// Hardware shortcuts default (Vol Up = Call Guardian, Vol Down = Start WalkGuide)
|
|
List<HardwareShortcut> shortcuts = List.of(
|
|
hs(guardianId, userId, HardwareShortcutKey.CALL_GUARDIAN, "Volume Up", 24),
|
|
hs(guardianId, userId, HardwareShortcutKey.START_WALKGUIDE, "Volume Down", 25),
|
|
hs(guardianId, userId, HardwareShortcutKey.SEND_SOS, null, null),
|
|
hs(guardianId, userId, HardwareShortcutKey.STOP_WALKGUIDE, null, null),
|
|
hs(guardianId, userId, HardwareShortcutKey.OPEN_NOTIFICATION, null, null)
|
|
);
|
|
hardwareShortcutRepository.saveAll(shortcuts);
|
|
|
|
// AI config default
|
|
aiConfigRepository.save(AiConfig.builder()
|
|
.guardianId(guardianId)
|
|
.userId(userId)
|
|
.build());
|
|
}
|
|
|
|
private VoiceCommandConfig vc(Long gId, Long uId, VoiceCommandKey key, String phrase) {
|
|
return VoiceCommandConfig.builder()
|
|
.guardianId(gId).userId(uId).commandKey(key).triggerPhrase(phrase).build();
|
|
}
|
|
|
|
private HardwareShortcut hs(Long gId, Long uId, HardwareShortcutKey key, String name, Integer code) {
|
|
return HardwareShortcut.builder()
|
|
.guardianId(gId).userId(uId).shortcutKey(key)
|
|
.buttonName(name).buttonCode(code)
|
|
.enabled(name != null).build();
|
|
}
|
|
|
|
private User resolveUserByPairingCode(String submittedCode) {
|
|
if (submittedCode == null || submittedCode.isBlank()) {
|
|
throw new PairingException("Pairing code tidak boleh kosong.");
|
|
}
|
|
String code = submittedCode.trim().toUpperCase();
|
|
User user = userRepository.findByPairingCode(code)
|
|
.orElseThrow(() -> new ResourceNotFoundException(
|
|
"Pairing code '" + code + "' tidak ditemukan. Minta User generate kode baru."));
|
|
if (user.getPairingCodeExpiresAt() == null
|
|
|| !user.getPairingCodeExpiresAt().isAfter(LocalDateTime.now())) {
|
|
user.setPairingCode(null);
|
|
user.setPairingCodeExpiresAt(null);
|
|
userRepository.save(user);
|
|
throw new PairingException("Pairing code sudah kadaluarsa. Minta User generate kode baru.");
|
|
}
|
|
return user;
|
|
}
|
|
|
|
private void assignNewPairingCode(User user, LocalDateTime now) {
|
|
String candidate;
|
|
do {
|
|
candidate = randomCode();
|
|
} while (userRepository.findByPairingCode(candidate).isPresent());
|
|
user.setPairingCode(candidate);
|
|
user.setPairingCodeExpiresAt(now.plusMinutes(PAIRING_CODE_TTL_MINUTES));
|
|
}
|
|
|
|
private String randomCode() {
|
|
StringBuilder sb = new StringBuilder(PAIRING_CODE_LENGTH);
|
|
for (int i = 0; i < PAIRING_CODE_LENGTH; i++) {
|
|
sb.append(CODE_CHARS.charAt(RANDOM.nextInt(CODE_CHARS.length())));
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
private PairingCodeResponse buildPairingCodeResponse(User user, LocalDateTime now) {
|
|
long seconds = Math.max(0,
|
|
ChronoUnit.SECONDS.between(now, user.getPairingCodeExpiresAt()));
|
|
return PairingCodeResponse.builder()
|
|
.pairingCode(user.getPairingCode())
|
|
.expiresAt(user.getPairingCodeExpiresAt())
|
|
.expiresInSeconds(seconds)
|
|
.build();
|
|
}
|
|
|
|
private PairingStatusResponse buildStatus(PairingRelation p, User guardian, User user, String viewerRole) {
|
|
String pairedWithName = "GUARDIAN".equals(viewerRole)
|
|
? user.getDisplayName() : guardian.getDisplayName();
|
|
String pairedWithEmail = "GUARDIAN".equals(viewerRole)
|
|
? user.getEmail() : guardian.getEmail();
|
|
return PairingStatusResponse.builder()
|
|
.pairingId(p.getId())
|
|
.status(p.getStatus().name())
|
|
.pairedWithId("GUARDIAN".equals(viewerRole) ? user.getId() : guardian.getId())
|
|
.pairedWithName(pairedWithName)
|
|
.pairedWithEmail(pairedWithEmail)
|
|
.uniqueUserId(user.getUniqueUserId())
|
|
.pairingCode(user.getPairingCode())
|
|
.pairingCodeExpiresAt(user.getPairingCodeExpiresAt())
|
|
.invitedAt(p.getInvitedAt())
|
|
.respondedAt(p.getRespondedAt())
|
|
.build();
|
|
}
|
|
}
|