Refactor complet : fusion de Conversation + Message en un module Messaging unique avec ContactPolicy (règles qui-peut-parler-à-qui) et MemberBlock (blocages utilisateur). - Migration V28 : tables conversations/conversation_participants/messages/ contact_policies/member_blocks - Nouvelles entités : ContactPolicy, ConversationParticipant, MemberBlock (Conversation/Message mises à jour avec relations) - Nouvelles repositories : ContactPolicyRepository, ConversationParticipantRepository, MemberBlockRepository - MessagingResource (nouveau) remplace ConversationResource + MessageResource - MessagingService (nouveau) remplace ConversationService + MessageService avec vérifications appartenance org + policies + blocages avant envoi - Anciens fichiers Conversation/Message Resource/Service/Tests supprimés
654 lines
29 KiB
Java
654 lines
29 KiB
Java
package dev.lions.unionflow.server.service;
|
|
|
|
import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest;
|
|
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest;
|
|
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest;
|
|
import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest;
|
|
import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest;
|
|
import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
|
|
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
|
|
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
|
|
import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
|
|
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
|
import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
|
|
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
|
import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
|
|
import dev.lions.unionflow.server.entity.ContactPolicy;
|
|
import dev.lions.unionflow.server.entity.Conversation;
|
|
import dev.lions.unionflow.server.entity.ConversationParticipant;
|
|
import dev.lions.unionflow.server.entity.MemberBlock;
|
|
import dev.lions.unionflow.server.entity.Membre;
|
|
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
|
import dev.lions.unionflow.server.entity.Message;
|
|
import dev.lions.unionflow.server.entity.Organisation;
|
|
import dev.lions.unionflow.server.repository.ContactPolicyRepository;
|
|
import dev.lions.unionflow.server.repository.ConversationParticipantRepository;
|
|
import dev.lions.unionflow.server.repository.ConversationRepository;
|
|
import dev.lions.unionflow.server.repository.MemberBlockRepository;
|
|
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
|
import dev.lions.unionflow.server.repository.MembreRepository;
|
|
import dev.lions.unionflow.server.repository.MessageRepository;
|
|
import dev.lions.unionflow.server.messaging.KafkaEventProducer;
|
|
import jakarta.enterprise.context.ApplicationScoped;
|
|
import jakarta.inject.Inject;
|
|
import jakarta.transaction.Transactional;
|
|
import jakarta.ws.rs.BadRequestException;
|
|
import jakarta.ws.rs.ForbiddenException;
|
|
import jakarta.ws.rs.NotFoundException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
import java.util.stream.Collectors;
|
|
import org.jboss.logging.Logger;
|
|
|
|
/**
|
|
* Service métier pour la messagerie instantanée.
|
|
*
|
|
* <p>Gère les conversations (directes et canaux-rôle), les messages (texte,
|
|
* vocal, image), les blocages et les politiques de communication.
|
|
*
|
|
* <p>Politique par appartenance : deux membres de la même organisation
|
|
* peuvent se contacter sans demande d'amitié préalable.
|
|
* L'adhésion est la relation de confiance.
|
|
*
|
|
* @author UnionFlow Team
|
|
* @version 4.0
|
|
* @since 2026-04-13
|
|
*/
|
|
@ApplicationScoped
|
|
public class MessagingService {
|
|
|
|
private static final Logger LOG = Logger.getLogger(MessagingService.class);
|
|
private static final int PAGE_SIZE_DEFAULT = 30;
|
|
|
|
@Inject ConversationRepository conversationRepository;
|
|
@Inject ConversationParticipantRepository participantRepository;
|
|
@Inject MessageRepository messageRepository;
|
|
@Inject ContactPolicyRepository contactPolicyRepository;
|
|
@Inject MemberBlockRepository memberBlockRepository;
|
|
@Inject MembreRepository membreRepository;
|
|
@Inject MembreOrganisationRepository membreOrganisationRepository;
|
|
@Inject KafkaEventProducer kafkaEventProducer;
|
|
@Inject io.quarkus.security.identity.SecurityIdentity securityIdentity;
|
|
|
|
// ── Conversations ─────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Démarre ou récupère une conversation directe 1-1.
|
|
* Idempotent : si la conversation existe déjà, elle est retournée.
|
|
*/
|
|
@Transactional
|
|
public ConversationResponse demarrerConversationDirecte(DemarrerConversationDirecteRequest request) {
|
|
Membre moi = getMembreConnecte();
|
|
Membre destinataire = membreRepository.findById(request.destinataireId());
|
|
if (destinataire == null) {
|
|
throw new NotFoundException("Membre destinataire non trouvé : " + request.destinataireId());
|
|
}
|
|
|
|
UUID orgId = request.organisationId();
|
|
verifierAppartenance(moi.getId(), orgId);
|
|
verifierAppartenance(request.destinataireId(), orgId);
|
|
verifierPolitique(moi.getId(), request.destinataireId(), orgId, false);
|
|
|
|
// Idempotence : chercher une conversation directe existante
|
|
return conversationRepository
|
|
.findConversationDirecte(moi.getId(), request.destinataireId(), orgId)
|
|
.map(c -> {
|
|
// Envoyer le message initial si fourni
|
|
if (request.contenuInitial() != null && !request.contenuInitial().isBlank()) {
|
|
envoyerMessageDansConversation(c, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
|
|
}
|
|
return toConversationResponse(c, moi.getId());
|
|
})
|
|
.orElseGet(() -> {
|
|
Organisation org = getOrganisation(orgId);
|
|
Conversation conv = Conversation.builder()
|
|
.organisation(org)
|
|
.typeConversation(TypeConversation.DIRECTE)
|
|
.statut(StatutConversation.ACTIVE)
|
|
.build();
|
|
conversationRepository.persist(conv);
|
|
|
|
ajouterParticipant(conv, moi, "INITIATEUR");
|
|
ajouterParticipant(conv, destinataire, "PARTICIPANT");
|
|
|
|
if (request.contenuInitial() != null && !request.contenuInitial().isBlank()) {
|
|
envoyerMessageDansConversation(conv, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
|
|
}
|
|
|
|
LOG.infof("Conversation directe créée: %s ↔ %s dans org %s",
|
|
moi.getEmail(), destinataire.getEmail(), orgId);
|
|
return toConversationResponse(conv, moi.getId());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Démarre ou récupère un canal de rôle.
|
|
* Le canal est partagé : tous les membres qui contactent "Le Trésorier"
|
|
* aboutissent dans le même canal.
|
|
*/
|
|
@Transactional
|
|
public ConversationResponse demarrerConversationRole(DemarrerConversationRoleRequest request) {
|
|
Membre moi = getMembreConnecte();
|
|
UUID orgId = request.organisationId();
|
|
verifierAppartenance(moi.getId(), orgId);
|
|
verifierPolitique(moi.getId(), null, orgId, true);
|
|
|
|
String roleCible = request.roleCible();
|
|
List<Membre> porteurs = trouverPorteursDuRole(orgId, roleCible);
|
|
if (porteurs.isEmpty()) {
|
|
throw new NotFoundException("Aucun membre avec le rôle " + roleCible + " dans cette organisation");
|
|
}
|
|
|
|
Organisation org = getOrganisation(orgId);
|
|
String titreCanal = libelleDuRole(roleCible);
|
|
|
|
return conversationRepository.findCanalRole(orgId, roleCible)
|
|
.map(c -> {
|
|
// Ajouter l'initiateur s'il n'est pas encore participant
|
|
if (!participantRepository.estParticipant(c.getId(), moi.getId())) {
|
|
ajouterParticipant(c, moi, "PARTICIPANT");
|
|
}
|
|
envoyerMessageDansConversation(c, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
|
|
return toConversationResponse(c, moi.getId());
|
|
})
|
|
.orElseGet(() -> {
|
|
Conversation conv = Conversation.builder()
|
|
.organisation(org)
|
|
.typeConversation(TypeConversation.ROLE_CANAL)
|
|
.roleCible(roleCible)
|
|
.titre(titreCanal)
|
|
.statut(StatutConversation.ACTIVE)
|
|
.build();
|
|
conversationRepository.persist(conv);
|
|
|
|
ajouterParticipant(conv, moi, "INITIATEUR");
|
|
porteurs.forEach(p -> ajouterParticipant(conv, p, "MODERATEUR"));
|
|
|
|
envoyerMessageDansConversation(conv, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
|
|
|
|
LOG.infof("Canal rôle créé: %s dans org %s", roleCible, orgId);
|
|
return toConversationResponse(conv, moi.getId());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retourne la liste des conversations du membre connecté.
|
|
*/
|
|
public List<ConversationSummaryResponse> getMesConversations() {
|
|
Membre moi = getMembreConnecte();
|
|
return conversationRepository.findByMembreId(moi.getId()).stream()
|
|
.map(c -> toConversationSummary(c, moi.getId()))
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Retourne le détail d'une conversation (avec les derniers messages).
|
|
*/
|
|
public ConversationResponse getConversation(UUID conversationId) {
|
|
Membre moi = getMembreConnecte();
|
|
Conversation conv = conversationRepository.findConversationById(conversationId)
|
|
.orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
|
|
verifierParticipant(conv, moi.getId());
|
|
return toConversationResponse(conv, moi.getId());
|
|
}
|
|
|
|
/**
|
|
* Archive une conversation.
|
|
*/
|
|
@Transactional
|
|
public ConversationResponse archiverConversation(UUID conversationId) {
|
|
Membre moi = getMembreConnecte();
|
|
Conversation conv = conversationRepository.findConversationById(conversationId)
|
|
.orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
|
|
verifierParticipant(conv, moi.getId());
|
|
conv.archiver();
|
|
return toConversationResponse(conv, moi.getId());
|
|
}
|
|
|
|
// ── Messages ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Envoie un message dans une conversation existante.
|
|
*/
|
|
@Transactional
|
|
public MessageResponse envoyerMessage(UUID conversationId, EnvoyerMessageRequest request) {
|
|
Membre moi = getMembreConnecte();
|
|
Conversation conv = conversationRepository.findConversationById(conversationId)
|
|
.orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
|
|
|
|
verifierParticipant(conv, moi.getId());
|
|
if (!conv.estActive()) {
|
|
throw new BadRequestException("Cette conversation est archivée");
|
|
}
|
|
|
|
TypeContenu type = parseTypeContenu(request.typeMessage());
|
|
validerContenuMessage(type, request);
|
|
|
|
Message message = envoyerMessageDansConversation(
|
|
conv, moi,
|
|
request.contenu(), type,
|
|
request.urlFichier(), request.dureeAudio(),
|
|
request.messageParentId()
|
|
);
|
|
return toMessageResponse(message);
|
|
}
|
|
|
|
/**
|
|
* Récupère les messages d'une conversation (paginés).
|
|
*/
|
|
public List<MessageResponse> getMessages(UUID conversationId, int page) {
|
|
Membre moi = getMembreConnecte();
|
|
Conversation conv = conversationRepository.findConversationById(conversationId)
|
|
.orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
|
|
verifierParticipant(conv, moi.getId());
|
|
|
|
return messageRepository.findByConversationPagine(conversationId, page, PAGE_SIZE_DEFAULT)
|
|
.stream()
|
|
.map(this::toMessageResponse)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Marque tous les messages d'une conversation comme lus.
|
|
*/
|
|
@Transactional
|
|
public void marquerConversationLue(UUID conversationId) {
|
|
Membre moi = getMembreConnecte();
|
|
participantRepository.findParticipant(conversationId, moi.getId())
|
|
.ifPresent(p -> {
|
|
p.marquerLu();
|
|
LOG.debugf("Conversation %s marquée lue par %s", conversationId, moi.getEmail());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Supprime un message (soft delete — contenu remplacé par "[Message supprimé]").
|
|
*/
|
|
@Transactional
|
|
public void supprimerMessage(UUID conversationId, UUID messageId) {
|
|
Membre moi = getMembreConnecte();
|
|
Message message = messageRepository.findMessageById(messageId)
|
|
.orElseThrow(() -> new NotFoundException("Message non trouvé : " + messageId));
|
|
|
|
if (!message.getConversation().getId().equals(conversationId)) {
|
|
throw new NotFoundException("Message non trouvé dans cette conversation");
|
|
}
|
|
if (!message.getExpediteur().getId().equals(moi.getId())) {
|
|
throw new ForbiddenException("Vous ne pouvez supprimer que vos propres messages");
|
|
}
|
|
message.supprimer();
|
|
}
|
|
|
|
// ── Blocages ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Bloque un membre dans une organisation.
|
|
*/
|
|
@Transactional
|
|
public void bloquerMembre(BloquerMembreRequest request) {
|
|
Membre moi = getMembreConnecte();
|
|
UUID membreABloquerId = request.membreABloquerId();
|
|
UUID orgId = request.organisationId();
|
|
|
|
if (moi.getId().equals(membreABloquerId)) {
|
|
throw new BadRequestException("Vous ne pouvez pas vous bloquer vous-même");
|
|
}
|
|
|
|
Membre aBloquer = membreRepository.findById(membreABloquerId);
|
|
if (aBloquer == null) {
|
|
throw new NotFoundException("Membre non trouvé : " + membreABloquerId);
|
|
}
|
|
|
|
if (memberBlockRepository.estBloque(moi.getId(), membreABloquerId, orgId)) {
|
|
throw new BadRequestException("Ce membre est déjà bloqué");
|
|
}
|
|
|
|
Organisation org = getOrganisation(orgId);
|
|
MemberBlock block = MemberBlock.builder()
|
|
.bloqueur(moi)
|
|
.bloque(aBloquer)
|
|
.organisation(org)
|
|
.build();
|
|
memberBlockRepository.persist(block);
|
|
LOG.infof("%s a bloqué %s dans org %s", moi.getEmail(), aBloquer.getEmail(), orgId);
|
|
}
|
|
|
|
/**
|
|
* Débloque un membre dans une organisation.
|
|
*/
|
|
@Transactional
|
|
public void debloquerMembre(UUID membreId, UUID organisationId) {
|
|
Membre moi = getMembreConnecte();
|
|
MemberBlock block = memberBlockRepository.findBlocage(moi.getId(), membreId, organisationId)
|
|
.orElseThrow(() -> new NotFoundException("Aucun blocage trouvé pour ce membre"));
|
|
block.setActif(false);
|
|
}
|
|
|
|
/**
|
|
* Retourne la liste des membres bloqués par le membre connecté.
|
|
*/
|
|
public List<MemberBlock> getMesBlocages() {
|
|
Membre moi = getMembreConnecte();
|
|
return memberBlockRepository.findByBloqueur(moi.getId());
|
|
}
|
|
|
|
// ── Politique de communication ────────────────────────────────────────────
|
|
|
|
/**
|
|
* Retourne la politique de communication d'une organisation.
|
|
* Crée une politique par défaut si elle n'existe pas encore.
|
|
*/
|
|
@Transactional
|
|
public ContactPolicyResponse getPolitique(UUID organisationId) {
|
|
ContactPolicy policy = contactPolicyRepository.findByOrganisationId(organisationId)
|
|
.orElseGet(() -> creerPolitiqueParDefaut(organisationId));
|
|
return toContactPolicyResponse(policy);
|
|
}
|
|
|
|
/**
|
|
* Met à jour la politique de communication.
|
|
* Réservé aux ADMIN et ADMIN_ORGANISATION.
|
|
*/
|
|
@Transactional
|
|
public ContactPolicyResponse mettreAJourPolitique(UUID organisationId, MettreAJourPolitiqueRequest request) {
|
|
ContactPolicy policy = contactPolicyRepository.findByOrganisationId(organisationId)
|
|
.orElseGet(() -> creerPolitiqueParDefaut(organisationId));
|
|
|
|
if (request.typePolitique() != null) {
|
|
policy.setTypePolitique(TypePolitiqueCommunication.valueOf(request.typePolitique()));
|
|
}
|
|
if (request.autoriserMembreVersMembre() != null) {
|
|
policy.setAutoriserMembreVersMembre(request.autoriserMembreVersMembre());
|
|
}
|
|
if (request.autoriserMembreVersRole() != null) {
|
|
policy.setAutoriserMembreVersRole(request.autoriserMembreVersRole());
|
|
}
|
|
if (request.autoriserNotesVocales() != null) {
|
|
policy.setAutoriserNotesVocales(request.autoriserNotesVocales());
|
|
}
|
|
return toContactPolicyResponse(policy);
|
|
}
|
|
|
|
// ── Méthodes privées ──────────────────────────────────────────────────────
|
|
|
|
private Message envoyerMessageDansConversation(
|
|
Conversation conv, Membre expediteur,
|
|
String contenu, TypeContenu type,
|
|
String urlFichier, Integer dureeAudio,
|
|
UUID messageParentId) {
|
|
|
|
Message.MessageBuilder builder = Message.builder()
|
|
.conversation(conv)
|
|
.expediteur(expediteur)
|
|
.typeMessage(type)
|
|
.contenu(contenu)
|
|
.urlFichier(urlFichier)
|
|
.dureeAudio(dureeAudio);
|
|
|
|
if (messageParentId != null) {
|
|
messageRepository.findMessageById(messageParentId)
|
|
.ifPresent(builder::messageParent);
|
|
}
|
|
|
|
Message message = builder.build();
|
|
messageRepository.persist(message);
|
|
conv.enregistrerNouveauMessage();
|
|
|
|
// Notifier via Kafka → WebSocket
|
|
try {
|
|
java.util.Map<String, Object> data = new java.util.HashMap<>();
|
|
data.put("conversationId", conv.getId().toString());
|
|
data.put("messageId", message.getId() != null ? message.getId().toString() : "");
|
|
data.put("expediteurId", expediteur.getId().toString());
|
|
data.put("typeMessage", type.name());
|
|
kafkaEventProducer.publishNouveauMessage(conv.getId(), conv.getOrganisation().getId().toString(), data);
|
|
} catch (Exception e) {
|
|
LOG.warnf("Impossible de publier l'event Kafka pour le message: %s", e.getMessage());
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
private void ajouterParticipant(Conversation conv, Membre membre, String role) {
|
|
if (!participantRepository.estParticipant(conv.getId(), membre.getId())) {
|
|
ConversationParticipant participant = ConversationParticipant.builder()
|
|
.conversation(conv)
|
|
.membre(membre)
|
|
.roleDansConversation(role)
|
|
.notifier(true)
|
|
.build();
|
|
participantRepository.persist(participant);
|
|
}
|
|
}
|
|
|
|
private void verifierAppartenance(UUID membreId, UUID organisationId) {
|
|
boolean appartient = membreOrganisationRepository
|
|
.count("membre.id = ?1 AND organisation.id = ?2 AND actif = true", membreId, organisationId) > 0;
|
|
if (!appartient) {
|
|
throw new ForbiddenException("Le membre n'appartient pas à cette organisation");
|
|
}
|
|
}
|
|
|
|
private void verifierPolitique(UUID expediteurId, UUID destinataireId, UUID orgId, boolean versRole) {
|
|
contactPolicyRepository.findByOrganisationId(orgId).ifPresent(policy -> {
|
|
if (versRole && !policy.getAutoriserMembreVersRole()) {
|
|
throw new ForbiddenException("La politique de cette organisation n'autorise pas les contacts vers les rôles");
|
|
}
|
|
if (!versRole && !policy.getAutoriserMembreVersMembre()) {
|
|
throw new ForbiddenException("La politique de cette organisation n'autorise pas les contacts entre membres");
|
|
}
|
|
});
|
|
|
|
// Vérifier le blocage
|
|
if (destinataireId != null && memberBlockRepository.estBloque(destinataireId, expediteurId, orgId)) {
|
|
throw new ForbiddenException("Vous ne pouvez pas contacter ce membre");
|
|
}
|
|
}
|
|
|
|
private void verifierParticipant(Conversation conv, UUID membreId) {
|
|
if (!participantRepository.estParticipant(conv.getId(), membreId)) {
|
|
throw new ForbiddenException("Vous n'êtes pas participant à cette conversation");
|
|
}
|
|
}
|
|
|
|
private List<Membre> trouverPorteursDuRole(UUID orgId, String role) {
|
|
List<MembreOrganisation> membresOrg = membreOrganisationRepository
|
|
.find("organisation.id = ?1 AND roleOrg = ?2 AND actif = true", orgId, role)
|
|
.list();
|
|
return membresOrg.stream()
|
|
.map(MembreOrganisation::getMembre)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
private ContactPolicy creerPolitiqueParDefaut(UUID organisationId) {
|
|
Organisation org = getOrganisation(organisationId);
|
|
ContactPolicy policy = ContactPolicy.builder()
|
|
.organisation(org)
|
|
.typePolitique(TypePolitiqueCommunication.OUVERT)
|
|
.autoriserMembreVersMembre(true)
|
|
.autoriserMembreVersRole(true)
|
|
.autoriserNotesVocales(true)
|
|
.build();
|
|
contactPolicyRepository.persist(policy);
|
|
return policy;
|
|
}
|
|
|
|
private Membre getMembreConnecte() {
|
|
String email = securityIdentity.getPrincipal().getName();
|
|
return membreRepository.find("email", email).firstResult();
|
|
}
|
|
|
|
private Organisation getOrganisation(UUID orgId) {
|
|
return (Organisation) dev.lions.unionflow.server.entity.Organisation.findById(orgId);
|
|
}
|
|
|
|
private TypeContenu parseTypeContenu(String type) {
|
|
if (type == null || type.isBlank()) return TypeContenu.TEXTE;
|
|
try {
|
|
return TypeContenu.valueOf(type.toUpperCase());
|
|
} catch (IllegalArgumentException e) {
|
|
return TypeContenu.TEXTE;
|
|
}
|
|
}
|
|
|
|
private void validerContenuMessage(TypeContenu type, EnvoyerMessageRequest req) {
|
|
switch (type) {
|
|
case TEXTE:
|
|
if (req.contenu() == null || req.contenu().isBlank()) {
|
|
throw new BadRequestException("Le contenu est obligatoire pour un message texte");
|
|
}
|
|
break;
|
|
case VOCAL:
|
|
if (req.urlFichier() == null || req.urlFichier().isBlank()) {
|
|
throw new BadRequestException("L'URL du fichier audio est obligatoire pour une note vocale");
|
|
}
|
|
if (req.dureeAudio() == null) {
|
|
throw new BadRequestException("La durée audio est obligatoire pour une note vocale");
|
|
}
|
|
break;
|
|
case IMAGE:
|
|
if (req.urlFichier() == null || req.urlFichier().isBlank()) {
|
|
throw new BadRequestException("L'URL de l'image est obligatoire");
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
private String libelleDuRole(String role) {
|
|
return switch (role) {
|
|
case "PRESIDENT" -> "Président";
|
|
case "TRESORIER" -> "Trésorier";
|
|
case "SECRETAIRE" -> "Secrétaire";
|
|
case "VICE_PRESIDENT" -> "Vice-Président";
|
|
case "ADMIN" -> "Administrateur";
|
|
case "ADMIN_ORGANISATION" -> "Administrateur";
|
|
default -> role;
|
|
};
|
|
}
|
|
|
|
// ── Conversions DTO ───────────────────────────────────────────────────────
|
|
|
|
private ConversationResponse toConversationResponse(Conversation conv, UUID membreConnecteId) {
|
|
List<MessageResponse> msgs = messageRepository
|
|
.findByConversationPagine(conv.getId(), 0, PAGE_SIZE_DEFAULT)
|
|
.stream().map(this::toMessageResponse).collect(Collectors.toList());
|
|
|
|
List<ConversationResponse.ParticipantResponse> parts =
|
|
participantRepository.findByConversation(conv.getId()).stream()
|
|
.map(p -> ConversationResponse.ParticipantResponse.builder()
|
|
.membreId(p.getMembre().getId())
|
|
.prenom(p.getMembre().getPrenom())
|
|
.nom(p.getMembre().getNom())
|
|
.roleDansConversation(p.getRoleDansConversation())
|
|
.luJusqua(p.getLuJusqua())
|
|
.build())
|
|
.collect(Collectors.toList());
|
|
|
|
long nonLus = messageRepository.countNonLus(conv.getId(), membreConnecteId);
|
|
|
|
return ConversationResponse.builder()
|
|
.id(conv.getId())
|
|
.typeConversation(conv.getTypeConversation().name())
|
|
.titre(resolverTitre(conv, membreConnecteId))
|
|
.statut(conv.getStatut().name())
|
|
.roleCible(conv.getRoleCible())
|
|
.organisationId(conv.getOrganisation().getId())
|
|
.organisationNom(conv.getOrganisation().getNom())
|
|
.dateCreation(conv.getDateCreation())
|
|
.dernierMessageAt(conv.getDernierMessageAt())
|
|
.nombreMessages(conv.getNombreMessages())
|
|
.participants(parts)
|
|
.messages(msgs)
|
|
.nonLus(nonLus)
|
|
.build();
|
|
}
|
|
|
|
private ConversationSummaryResponse toConversationSummary(Conversation conv, UUID membreConnecteId) {
|
|
String apercu = messageRepository.findDernierMessage(conv.getId())
|
|
.map(m -> {
|
|
if (TypeContenu.VOCAL.equals(m.getTypeMessage())) return "🎤 Note vocale";
|
|
if (TypeContenu.IMAGE.equals(m.getTypeMessage())) return "📷 Image";
|
|
String c = m.getContenu();
|
|
return c != null && c.length() > 100 ? c.substring(0, 97) + "..." : c;
|
|
})
|
|
.orElse(null);
|
|
|
|
String dernierType = messageRepository.findDernierMessage(conv.getId())
|
|
.map(m -> m.getTypeMessage().name()).orElse(null);
|
|
|
|
long nonLus = messageRepository.countNonLus(conv.getId(), membreConnecteId);
|
|
|
|
return ConversationSummaryResponse.builder()
|
|
.id(conv.getId())
|
|
.typeConversation(conv.getTypeConversation().name())
|
|
.titre(resolverTitre(conv, membreConnecteId))
|
|
.statut(conv.getStatut().name())
|
|
.dernierMessageApercu(apercu)
|
|
.dernierMessageType(dernierType)
|
|
.dernierMessageAt(conv.getDernierMessageAt())
|
|
.nonLus(nonLus)
|
|
.organisationId(conv.getOrganisation().getId())
|
|
.build();
|
|
}
|
|
|
|
private MessageResponse toMessageResponse(Message message) {
|
|
String contenuAffiche = message.estSupprime()
|
|
? "[Message supprimé]"
|
|
: message.getContenu();
|
|
|
|
String parentApercu = null;
|
|
UUID parentId = null;
|
|
if (message.getMessageParent() != null) {
|
|
parentId = message.getMessageParent().getId();
|
|
String pc = message.getMessageParent().getContenu();
|
|
parentApercu = pc != null && pc.length() > 100 ? pc.substring(0, 97) + "..." : pc;
|
|
}
|
|
|
|
return MessageResponse.builder()
|
|
.id(message.getId())
|
|
.typeMessage(message.getTypeMessage().name())
|
|
.contenu(contenuAffiche)
|
|
.urlFichier(message.estSupprime() ? null : message.getUrlFichier())
|
|
.dureeAudio(message.getDureeAudio())
|
|
.supprime(message.estSupprime())
|
|
.expediteurId(message.getExpediteur().getId())
|
|
.expediteurNom(message.getExpediteur().getNom())
|
|
.expediteurPrenom(message.getExpediteur().getPrenom())
|
|
.messageParentId(parentId)
|
|
.messageParentApercu(parentApercu)
|
|
.dateEnvoi(message.getDateCreation())
|
|
.build();
|
|
}
|
|
|
|
private ContactPolicyResponse toContactPolicyResponse(ContactPolicy policy) {
|
|
return ContactPolicyResponse.builder()
|
|
.id(policy.getId())
|
|
.organisationId(policy.getOrganisation().getId())
|
|
.typePolitique(policy.getTypePolitique().name())
|
|
.autoriserMembreVersMembre(Boolean.TRUE.equals(policy.getAutoriserMembreVersMembre()))
|
|
.autoriserMembreVersRole(Boolean.TRUE.equals(policy.getAutoriserMembreVersRole()))
|
|
.autoriserNotesVocales(Boolean.TRUE.equals(policy.getAutoriserNotesVocales()))
|
|
.build();
|
|
}
|
|
|
|
/**
|
|
* Résout le titre affiché pour une conversation.
|
|
* Pour DIRECTE : "Prénom Nom" de l'autre participant.
|
|
* Pour ROLE_CANAL : le titre du canal.
|
|
*/
|
|
private String resolverTitre(Conversation conv, UUID membreConnecteId) {
|
|
if (conv.getTitre() != null) return conv.getTitre();
|
|
if (TypeConversation.DIRECTE.equals(conv.getTypeConversation())) {
|
|
return participantRepository.findByConversation(conv.getId()).stream()
|
|
.filter(p -> !p.getMembre().getId().equals(membreConnecteId))
|
|
.findFirst()
|
|
.map(p -> p.getMembre().getPrenom() + " " + p.getMembre().getNom())
|
|
.orElse("Conversation");
|
|
}
|
|
return conv.getRoleCible();
|
|
}
|
|
}
|