package be.naaturel.boardmateapi.services; import be.naaturel.boardmateapi.common.exceptions.ServiceException; import be.naaturel.boardmateapi.common.helpers.Logger; import be.naaturel.boardmateapi.common.models.Client; import be.naaturel.boardmateapi.common.models.Message; import be.naaturel.boardmateapi.common.models.Room; import be.naaturel.boardmateapi.configurations.properties.NgrokProperties; import be.naaturel.boardmateapi.configurations.properties.WebexProperties; import be.naaturel.boardmateapi.repository.RoomRepo; import be.naaturel.boardmateapi.repository.dtos.RoomDto; import be.naaturel.boardmateapi.repository.dtos.WebexMessageDto; import be.naaturel.boardmateapi.repository.mappings.RoomMapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Map; @Service public class WebexService { private final String botToken; private final String clientToken; private final String sharedSecret; private final String ngrokUrl; private final RoomRepo repo; private final ObjectMapper mapper; @Autowired public WebexService( RoomRepo repo, WebexProperties webexProperties, NgrokProperties ngrokProperties) { this.repo = repo; this.botToken = webexProperties.botToken; this.clientToken = webexProperties.clientToken; this.sharedSecret = webexProperties.sharedSecret; this.ngrokUrl = ngrokProperties.url; this.mapper = new ObjectMapper(); } @PostConstruct public void initializeWebhooks() { try (HttpClient client = HttpClient.newHttpClient()) { List> rooms = fetchRooms(client); List> existingWebhooks = fetchWebhooks(client); String targetUrl = ngrokUrl + "/message/webhook"; for (Map room : rooms) { String roomId = (String) room.get("id"); String roomTitle = (String) room.get("title"); if (!webhookExists(existingWebhooks, roomId, targetUrl)) { createWebhook(client, roomId, targetUrl); Logger.displayInfo("Webhook created for room: " + roomTitle); } else { Logger.displayInfo("Webhook already exists for room: " + roomTitle + "(ID:" + roomId + ")" ); } } } catch (Exception e) { e.printStackTrace(); Logger.displayError("Failed to init webhooks: " + e.getMessage()); } } public void post(Message m) throws ServiceException { try (HttpClient client = HttpClient.newHttpClient()) { Room room = getRoomByClient(m.getClientId()); if (room == null) { room = createRoom(m.getClientId()); inviteMemberToRoom(room.getId(), "laurent.crema@student.hepl.be"); } Map body = Map.of( "roomId", room.getId(), "text", m.getContent() ); sendPostRequest(client, "https://webexapis.com/v1/messages", botToken, body); } catch (Exception e) { Logger.displayError(Arrays.toString(e.getStackTrace())); throw new ServiceException("Failed to post message"); } } public Message fetchMessage(String messageId) throws ServiceException { try (HttpClient client = HttpClient.newHttpClient()) { String response = sendGetRequest(client, "https://webexapis.com/v1/messages/" + messageId, clientToken); WebexMessageDto dto = mapper.readValue(response, WebexMessageDto.class); return new Message(dto.getId(), dto.getText(), dto.getPersonId(), dto.getCreated().getEpochSecond()); } catch (Exception e) { throw new ServiceException("Failed to fetch Webex message", e); } } public Room createRoom(String clientId) throws ServiceException { try (HttpClient client = HttpClient.newHttpClient()) { Map body = Map.of("title", "Support"); String response = sendPostRequest(client, "https://webexapis.com/v1/rooms", botToken, body); JsonNode jsonNode = mapper.readTree(response); String id = jsonNode.get("id").asText(); String title = jsonNode.get("title").asText(); RoomDto dto = new RoomDto(); dto.setRoomId(id); dto.setTitle(title); dto.setClientId(clientId); repo.save(dto); return new Room(id, title, clientId); } catch (Exception e) { Logger.displayError(Arrays.toString(e.getStackTrace())); throw new ServiceException("Failed to create room: " + e.getMessage(), e); } } public void inviteMemberToRoom(String roomId, String email) throws ServiceException { try (HttpClient client = HttpClient.newHttpClient()) { Map body = Map.of( "roomId", roomId, "personEmail", email ); sendPostRequest(client, "https://webexapis.com/v1/memberships", botToken, body); } catch (Exception e) { Logger.displayError(Arrays.toString(e.getStackTrace())); throw new ServiceException("Failed to invite member: " + e.getMessage(), e); } } public boolean verifySignature(String payload, String signature) { try { Mac mac = Mac.getInstance("HmacSHA1"); mac.init(new SecretKeySpec(sharedSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA1")); byte[] payloadBytes = payload.getBytes(StandardCharsets.UTF_8); byte[] rawHmac = mac.doFinal(payloadBytes); StringBuilder sb = new StringBuilder(); for (byte b : rawHmac) { sb.append(String.format("%02x", b)); } String expected = sb.toString(); return expected.equals(signature.trim()); } catch (Exception e) { Logger.displayError(e.getMessage()); return false; } } public Room getRoomByClient(String clientId) { return repo.findByClientId(clientId).map(RoomMapper::toModel).orElse(null); } public Room getRoomById(String roomId) { return repo.findByRoomId(roomId).map(RoomMapper::toModel).orElse(null); } private List> fetchRooms(HttpClient client) throws Exception { String response = sendGetRequest(client, "https://webexapis.com/v1/rooms", clientToken); JsonNode items = mapper.readTree(response).get("items"); return mapper.readValue(items.toString(), List.class); } private List> fetchWebhooks(HttpClient client) throws Exception { String response = sendGetRequest(client, "https://webexapis.com/v1/webhooks", clientToken); JsonNode items = mapper.readTree(response).get("items"); return mapper.readValue(items.toString(), List.class); } private void createWebhook(HttpClient client, String roomId, String targetUrl) throws Exception { Map body = Map.of( "name", "BoardMate Webhook", "targetUrl", targetUrl, "resource", "messages", "event", "created", "roomId", roomId, "secret", sharedSecret ); sendPostRequest(client, "https://webexapis.com/v1/webhooks", clientToken, body); } private boolean webhookExists(List> webhooks, String roomId, String targetUrl) { return webhooks.stream().anyMatch(w -> targetUrl.equals(w.get("targetUrl")) && "messages".equals(w.get("resource")) && "created".equals(w.get("event")) && roomId.equals(w.get("roomId")) ); } private String sendGetRequest(HttpClient client, String url, String token) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Authorization", "Bearer " + token) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 300) { throw new RuntimeException("Webex GET error " + response.statusCode() + " : " + response.body()); } return response.body(); } private String sendPostRequest(HttpClient client, String url, String token, Map body) throws Exception { String jsonBody = mapper.writeValueAsString(body); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Authorization", "Bearer " + token) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 300) { throw new RuntimeException("Webex POST error " + response.statusCode() + " : " + response.body()); } return response.body(); } }