diff --git a/api/compose.yaml b/api/compose.yaml index 59227093..c50bfbe7 100644 --- a/api/compose.yaml +++ b/api/compose.yaml @@ -8,13 +8,17 @@ services: - "8000:8080" - "5005:5005" environment: + SSL_KEYSTORE_PATH: "/certs/keystore.p12" BROKER_URL: "tcp://api-broker:1883" BROKER_SECURE_URL: "tcp://api-broker:8883" + NGROK_URL: "https://lipographic-angla-cistophoric.ngrok-free.dev" + MONGODB_URI: "mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db" BROKER_USERNAME: "board-mate-api" BROKER_PASSWORD: "hepl" JWT_SECRET: "enY3OWU4djFyMTByNTZhcG9uY3Z0djQ5cnY0eDhhNWM0bjg5OTRjNDhidA==" - SSL_KEYSTORE_PATH: "/certs/keystore.p12" - MONGODB_URI: "mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db" + WEBEX_BOT_TOKEN: "YmU0NTdkNDMtYTg1ZC00M2YyLTk3YzUtODA1MWFmOTk1NjA1ZmU3MTYxNGUtYWZm_P0A1_14a2639d-5e4d-48b4-9757-f4b8a23372de" + WEBEX_CLIENT_TOKEN: "N2U0ZGE1OWItZTBhOC00MmU5LWIwMzYtNjM0NDk3NGUwMmIyNjEyMmNiMTYtOTI3_P0A1_14a2639d-5e4d-48b4-9757-f4b8a23372de" + WEBEX_SHARED_SECRET: "cUxSc0ZjNVBZVG5oRmhqaVN0YUtMTEVZb0pIZW5EY2Rwa0hUaWdiVm9nWlJiY2t5aFdmTjhvWmQ5U3R3TDIxVE1CRTl4VGJldVI3TFdVa3lMbFVPZUVSMkZPSnBHTjk=" DATABASE_NAME: "board-mate-db" JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" depends_on: @@ -79,15 +83,21 @@ services: - ./mosquitto/log:/mosquitto/log - ./mosquitto/certs:/mosquitto/certs - #mongo-express: - # image: mongo-express:latest - # depends_on: - # - mongodb - # ports: - # - "8401:8081" - # environment: - # - ME_CONFIG_MONGODB_SERVER=mongodb - # - ME_CONFIG_MONGODB_PORT=27017 - # - ME_CONFIG_MONGODB_ADMINUSERNAME=root - # - ME_CONFIG_MONGODB_ADMINPASSWORD=secret - # - ME_CONFIG_MONGODB_AUTH_DATABASE=admin \ No newline at end of file + ngrok: + image: ngrok/ngrok:latest + command: http --log=stdout nginx:8600 + ports: + - "8500:4040" + environment: + NGROK_AUTHTOKEN: 37gBJ6KZQIK9nwcq0mWlfRHqtvX_49hdE8PVLmRTzUVaNRJqx + NGROK_PORT: nginx:8600 + + nginx: + image: nginx:alpine + container_name: nginx + ports: + - "8600:8080" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - boardmate-api \ No newline at end of file diff --git a/api/nginx/nginx.conf b/api/nginx/nginx.conf new file mode 100644 index 00000000..d43cc037 --- /dev/null +++ b/api/nginx/nginx.conf @@ -0,0 +1,16 @@ +events {} + +http { + server { + listen 8600; + + location / { + proxy_pass https://boardmate-api:8080; + proxy_ssl_verify off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file diff --git a/api/src/main/java/be/naaturel/boardmateapi/common/models/Message.java b/api/src/main/java/be/naaturel/boardmateapi/common/models/Message.java index 9350c2e3..02a4b923 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/common/models/Message.java +++ b/api/src/main/java/be/naaturel/boardmateapi/common/models/Message.java @@ -5,9 +5,9 @@ public class Message { private String id; private String content; private String clientId; - private int timeStamp; + private long timeStamp; - public Message(String id, String content, String clientId, int timeStamp){ + public Message(String id, String content, String clientId, long timeStamp){ this.id = id; this.content = content; this.clientId = clientId; @@ -38,7 +38,7 @@ public class Message { this.clientId = clientId; } - public int getTimeStamp() { + public long getTimeStamp() { return timeStamp; } diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/AppSecurityConfig.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/AppSecurityConfig.java index bdb92f81..988f86f4 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/AppSecurityConfig.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/AppSecurityConfig.java @@ -59,6 +59,7 @@ public class AppSecurityConfig { "/docs/**", "/v1/docs/**", "/swagger-ui.html", + "/message/webhook", "/authenticate", "/client/create").permitAll() .anyRequest().authenticated() diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/WebexConfig.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/WebexConfig.java index e627958f..cd43a3e9 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/WebexConfig.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/configurations/WebexConfig.java @@ -1,25 +1,45 @@ package be.naaturel.boardmateapi.configurations.configurations; +import be.naaturel.boardmateapi.common.helpers.Logger; +import be.naaturel.boardmateapi.configurations.properties.NgrokProperties; import be.naaturel.boardmateapi.configurations.properties.WebexProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + @Configuration public class WebexConfig { - private final WebexProperties properties; + private final WebexProperties webexProperties; + private final NgrokProperties ngrokProperties; - public WebexConfig(WebexProperties properties){ - this.properties = properties; + public WebexConfig(WebexProperties webexProperties, NgrokProperties ngrokProperties){ + this.webexProperties = webexProperties; + this.ngrokProperties = ngrokProperties; } @Bean(name = "clientToken") - public String clientToken(){ - return properties.clientToken; + public String clientToken() { + return webexProperties.clientToken; } @Bean(name = "botToken") - public String botToken(){ - return properties.botToken; + public String botToken() { + return this.webexProperties.botToken; } + + @Bean(name = "sharedSecret") + public String sharedSecret() { + return webexProperties.sharedSecret; + } + } diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/NgrokProperties.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/NgrokProperties.java new file mode 100644 index 00000000..ad4fca93 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/NgrokProperties.java @@ -0,0 +1,12 @@ +package be.naaturel.boardmateapi.configurations.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class NgrokProperties { + + @Value("${ngrok.url}") + public String url; + +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/WebexProperties.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/WebexProperties.java index 95b0171a..984e2363 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/WebexProperties.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/properties/WebexProperties.java @@ -12,4 +12,7 @@ public class WebexProperties { @Value("${webex.bot.token}") public String botToken; + @Value("${webex.shared-secret}") + public String sharedSecret; + } diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/ChatController.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/ChatController.java index 4c71e75d..3de4257f 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/controllers/ChatController.java +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/ChatController.java @@ -1,12 +1,18 @@ package be.naaturel.boardmateapi.controllers; import be.naaturel.boardmateapi.common.exceptions.ServiceException; +import be.naaturel.boardmateapi.common.helpers.Logger; import be.naaturel.boardmateapi.common.models.Message; +import be.naaturel.boardmateapi.common.models.Room; import be.naaturel.boardmateapi.controllers.dtos.MessageDto; import be.naaturel.boardmateapi.controllers.dtos.MessagePostRequestDto; import be.naaturel.boardmateapi.controllers.dtos.ResponseBody; +import be.naaturel.boardmateapi.controllers.dtos.WebexWebhook; import be.naaturel.boardmateapi.services.MessageService; +import be.naaturel.boardmateapi.services.MqttService; import be.naaturel.boardmateapi.services.WebexService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -19,11 +25,16 @@ public class ChatController { private final MessageService messageService; private final WebexService webexService; + private final MqttService mqttService; @Autowired - public ChatController(MessageService messageService, WebexService webexService){ + public ChatController( + MessageService messageService, + WebexService webexService, + MqttService mqttService){ this.messageService = messageService; this.webexService = webexService; + this.mqttService = mqttService; } @PostMapping("/message/send") @@ -81,4 +92,38 @@ public class ChatController { } } + @PostMapping("/message/webhook") + public ResponseEntity> handleWebhook( + @RequestHeader("X-Spark-Signature") String signature, + @RequestBody String rawPayload) { + + Logger.displayInfo("========================"); + ResponseBody result = ResponseBody.createEmpty(); + + if (!webexService.verifySignature(rawPayload, signature)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(result); + } + + try { + ObjectMapper mapper = new ObjectMapper(); + WebexWebhook payload = mapper.readValue(rawPayload, WebexWebhook.class); + + Room room = webexService.getByRoom(payload.getData().getRoomId()); + Message msg = webexService.fetchMessage(payload.getData().getId()); + this.messageService.save(msg); + + this.mqttService.publish("/chat/" + room.getClientId() + "/message", msg); + + Logger.displayInfo(msg.getContent()); + Logger.displayInfo(msg.getId()); + Logger.displayInfo(msg.getClientId()); + + result.setSuccess(true); + return ResponseEntity.ok(result); + + } catch (Exception e) { + result.setMessage("Unable to handle request"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); + } + } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/MessageDto.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/MessageDto.java index 40916ee2..04de6345 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/MessageDto.java +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/MessageDto.java @@ -5,7 +5,7 @@ public class MessageDto { private String content; - private int timestamp; + private long timestamp; public String getContent() { return content; @@ -15,11 +15,11 @@ public class MessageDto { this.content = content; } - public int getTimestamp() { + public long getTimestamp() { return timestamp; } - public void setTimestamp(int timestamp) { + public void setTimestamp(long timestamp) { this.timestamp = timestamp; } diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/WebexWebhook.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/WebexWebhook.java new file mode 100644 index 00000000..13918093 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/WebexWebhook.java @@ -0,0 +1,85 @@ +package be.naaturel.boardmateapi.controllers.dtos; + +public class WebexWebhook { + + private String id; + private String name; + private String targetUrl; + private String resource; + private String event; + private String orgId; + private String createdBy; + private String appId; + private String ownedBy; + private String status; + private String created; + private String actorId; + + private WebexData data; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getTargetUrl() { return targetUrl; } + public void setTargetUrl(String targetUrl) { this.targetUrl = targetUrl; } + + public String getResource() { return resource; } + public void setResource(String resource) { this.resource = resource; } + + public String getEvent() { return event; } + public void setEvent(String event) { this.event = event; } + + public String getOrgId() { return orgId; } + public void setOrgId(String orgId) { this.orgId = orgId; } + + public String getCreatedBy() { return createdBy; } + public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } + + public String getAppId() { return appId; } + public void setAppId(String appId) { this.appId = appId; } + + public String getOwnedBy() { return ownedBy; } + public void setOwnedBy(String ownedBy) { this.ownedBy = ownedBy; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getCreated() { return created; } + public void setCreated(String created) { this.created = created; } + + public String getActorId() { return actorId; } + public void setActorId(String actorId) { this.actorId = actorId; } + + public WebexData getData() { return data; } + public void setData(WebexData data) { this.data = data; } + + public static class WebexData { + private String id; + private String roomId; + private String roomType; + private String personId; + private String personEmail; + private String created; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getRoomId() { return roomId; } + public void setRoomId(String roomId) { this.roomId = roomId; } + + public String getRoomType() { return roomType; } + public void setRoomType(String roomType) { this.roomType = roomType; } + + public String getPersonId() { return personId; } + public void setPersonId(String personId) { this.personId = personId; } + + public String getPersonEmail() { return personEmail; } + public void setPersonEmail(String personEmail) { this.personEmail = personEmail; } + + public String getCreated() { return created; } + public void setCreated(String created) { this.created = created; } + } +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/repository/RoomRepo.java b/api/src/main/java/be/naaturel/boardmateapi/repository/RoomRepo.java index 0ec54408..3f5e03f6 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/repository/RoomRepo.java +++ b/api/src/main/java/be/naaturel/boardmateapi/repository/RoomRepo.java @@ -9,4 +9,6 @@ import java.util.Optional; public interface RoomRepo extends MongoRepository { Optional findByClientId(String clientId); + + Optional findByRoomId(String roomdId); } diff --git a/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/MessageDto.java b/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/MessageDto.java index f5fc8849..d1af81d4 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/MessageDto.java +++ b/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/MessageDto.java @@ -17,7 +17,7 @@ public class MessageDto { private String content; @Field("timestamp") - private int timestamp; + private long timestamp; public String getId() { return id; @@ -43,11 +43,11 @@ public class MessageDto { this.content = content; } - public int getTimestamp() { + public long getTimestamp() { return timestamp; } - public void setTimestamp(int timestamp) { + public void setTimestamp(long timestamp) { this.timestamp = timestamp; } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/WebexMessageDto.java b/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/WebexMessageDto.java new file mode 100644 index 00000000..e807027b --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/WebexMessageDto.java @@ -0,0 +1,76 @@ +package be.naaturel.boardmateapi.repository.dtos; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.Instant; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class WebexMessageDto { + + private String id; + private String roomId; + private String roomType; + private String text; + private String personId; + private String personEmail; + private Instant created; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRoomId() { + return roomId; + } + + public void setRoomId(String roomId) { + this.roomId = roomId; + } + + public String getRoomType() { + return roomType; + } + + public void setRoomType(String roomType) { + this.roomType = roomType; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getPersonId() { + return personId; + } + + public void setPersonId(String personId) { + this.personId = personId; + } + + public String getPersonEmail() { + return personEmail; + } + + public void setPersonEmail(String personEmail) { + this.personEmail = personEmail; + } + + public Instant getCreated() { + return created; + } + + @JsonProperty("created") + public void setCreated(String created) { + this.created = Instant.parse(created); + } +} \ No newline at end of file diff --git a/api/src/main/java/be/naaturel/boardmateapi/services/MqttService.java b/api/src/main/java/be/naaturel/boardmateapi/services/MqttService.java index a4421c1f..13731d26 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/services/MqttService.java +++ b/api/src/main/java/be/naaturel/boardmateapi/services/MqttService.java @@ -3,6 +3,7 @@ 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.MqttMessageWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.paho.client.mqttv3.*; import org.springframework.stereotype.Service; @@ -32,14 +33,23 @@ public class MqttService { this.onMessageReceived = consumer; } - public void publish(String topic, String payload) { + public void publish(String topic, Object data) throws ServiceException { + try { + String payload = new ObjectMapper().writeValueAsString(data); + publish(topic, payload); + } catch (Exception e){ + throw new ServiceException("Unable to serialize data", e); + } + } + + public void publish(String topic, String payload) throws ServiceException { try { connect(); MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); message.setQos(1); brokerClient.publish(topic, message); } catch (MqttException e) { - throw new RuntimeException(e); + throw new ServiceException("Failed to publish on broker", e); } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/services/WebexService.java b/api/src/main/java/be/naaturel/boardmateapi/services/WebexService.java index 22437959..bc95996a 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/services/WebexService.java +++ b/api/src/main/java/be/naaturel/boardmateapi/services/WebexService.java @@ -4,66 +4,93 @@ import be.naaturel.boardmateapi.common.exceptions.ServiceException; import be.naaturel.boardmateapi.common.helpers.Logger; 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.beans.factory.annotation.Qualifier; 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; -import java.util.Optional; @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, - @Qualifier("botToken") String botToken){ + WebexProperties webexProperties, + NgrokProperties ngrokProperties) { this.repo = repo; - this.botToken = botToken; + 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()) { + try (HttpClient client = HttpClient.newHttpClient()) { - Room room = getClientRoom(m.getClientId()); - if(room == null){ + Room room = getByClient(m.getClientId()); + if (room == null) { room = createRoom(m.getClientId()); + inviteMemberToRoom(room.getId(), "laurent.crema@student.hepl.be"); } - ObjectMapper mapper = new ObjectMapper(); - String jsonBody = mapper.writeValueAsString( - Map.of( + Map body = Map.of( "roomId", room.getId(), "text", m.getContent() - ) ); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://webexapis.com/v1/messages")) - .header("Authorization", "Bearer " + this.botToken) - .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 error " + response.statusCode() + " : " + response.body()); - } + sendPostRequest(client, "https://webexapis.com/v1/messages", botToken, body); } catch (Exception e) { Logger.displayError(Arrays.toString(e.getStackTrace())); @@ -71,44 +98,143 @@ public class WebexService { } } - public Room createRoom(String clientId) throws ServiceException { + public Message fetchMessage(String messageId) throws ServiceException { try (HttpClient client = HttpClient.newHttpClient()) { - - ObjectMapper mapper = new ObjectMapper(); - String jsonBody = mapper.writeValueAsString(Map.of("title", "Support")); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://webexapis.com/v1/rooms")) - .header("Authorization", "Bearer " + this.botToken) - .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 error " + response.statusCode() + " : " + response.body()); - } - - JsonNode jsonNode = mapper.readTree(response.body()); - String id = jsonNode.get("id").asText(); - String title = jsonNode.get("title").asText(); - RoomDto dto = new RoomDto(); - dto.setTitle(title); - dto.setClientId(clientId); - dto.setRoomId(id); - repo.save(dto); - - return new Room(id, title, clientId); + 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) { - Logger.displayError(Arrays.toString(e.getStackTrace())); - throw new ServiceException("Failed to create private room : " + e.getMessage(), e); + throw new ServiceException("Failed to fetch Webex message", e); } } - private Room getClientRoom(String clientId){ - Optional dto = repo.findByClientId(clientId); - return dto.map(RoomMapper::toModel).orElse(null); + 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 getByClient(String clientId) { + return repo.findByClientId(clientId).map(RoomMapper::toModel).orElse(null); + } + + public Room getByRoom(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(); + } +} \ No newline at end of file diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index a13c3ab7..53b6f9e4 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -27,8 +27,11 @@ mqtt.username=${BROKER_USERNAME} mqtt.password=${BROKER_PASSWORD} #=============WEBEX============= -webex.client.token=N2E0ODMyZDUtY2JmZi00YjlhLWFjZmEtOTU0MmFlNjY3ZDE2M2ZhYWYzNzAtNzFm_P0A1_14a2639d-5e4d-48b4-9757-f4b8a23372de -webex.bot.token=MGM4ZDYzYzctZTZiMi00MjNlLWI3YzEtOTFhNDlmOGM1YzVjYWJhYTk0NzctNjBj_P0A1_14a2639d-5e4d-48b4-9757-f4b8a23372de +webex.client.token=${WEBEX_CLIENT_TOKEN} +webex.bot.token=${WEBEX_BOT_TOKEN} +webex.shared-secret=${WEBEX_SHARED_SECRET} +#=============NGROK============= +ngrok.url=${NGROK_URL} #=============METRICS============= management.endpoint.health.show-details=always