From 6c9980f4ac9c963b4aa37e75119f023fa950bccd Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 30 Sep 2025 10:50:34 +0200 Subject: [PATCH 1/7] Add backoff for failed sync --- .../org/synyx/matrix/bot/MatrixClient.java | 136 +++++--- .../matrix/bot/internal/api/MatrixApi.java | 323 +++++++++--------- 2 files changed, 244 insertions(+), 215 deletions(-) diff --git a/src/main/java/org/synyx/matrix/bot/MatrixClient.java b/src/main/java/org/synyx/matrix/bot/MatrixClient.java index 6462d5b..65b24ec 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixClient.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixClient.java @@ -18,11 +18,15 @@ import org.synyx.matrix.bot.internal.api.dto.ReactionDto; import org.synyx.matrix.bot.internal.api.dto.ReactionRelatesToDto; +import java.io.IOException; import java.util.Optional; @Slf4j public class MatrixClient { + private static final long DEFAULT_BACKOFF_IN_SEC = 3; + private static final long BACKOFF_MAX_IN_SEC = 60; + private final MatrixAuthentication authentication; private final ObjectMapper objectMapper; private final MatrixApi api; @@ -31,6 +35,7 @@ public class MatrixClient { private MatrixPersistedState persistedState; private MatrixEventNotifier eventNotifier; private boolean interruptionRequested; + private long currentBackoffInSec; public MatrixClient(String hostname, String username, String password) { @@ -46,6 +51,7 @@ public MatrixClient(String hostname, String username, String password) { this.state = null; this.eventNotifier = null; this.interruptionRequested = false; + this.currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; } public void setEventCallback(MatrixEventConsumer eventConsumer) { @@ -64,67 +70,79 @@ public void requestStopOfSync() { api.terminateOpenConnections(); } - public void syncContinuous() { - - if (!authentication.isAuthenticated()) { - if (api.login()) { - log.info("Successfully logged in to matrix server as {}", - authentication.getUserId() - .map(MatrixUserId::toString) - .orElse("UNKNOWN") - ); - } else { - return; - } - } - - state = new MatrixState(authentication.getUserId().orElseThrow(IllegalStateException::new)); - stateSynchronizer = new MatrixStateSynchronizer(state, objectMapper); - - var maybeSyncResponse = api.syncFull(); - String lastBatch; - if (maybeSyncResponse.isPresent()) { - final var syncResponse = maybeSyncResponse.get(); - lastBatch = syncResponse.nextBatch(); - - stateSynchronizer.synchronizeState(syncResponse); - } else { - log.error("Failed to perform initial sync"); - return; - } + public void syncContinuous() throws InterruptedException { - if (eventNotifier != null) { - eventNotifier.getConsumer().onConnected(state); - } + while (!interruptionRequested) { + try { + if (!authentication.isAuthenticated()) { + if (!api.login()) { + log.error("Failed to login to matrix server!"); + return; + } + + log.info("Successfully logged in to matrix server as {}", + authentication.getUserId() + .map(MatrixUserId::toString) + .orElse("UNKNOWN") + ); + } - if (persistedState != null) { - final var maybePersistedLastBatch = persistedState.getLastBatch(); - if (maybePersistedLastBatch.isPresent()) { - lastBatch = maybePersistedLastBatch.get(); - } else { - persistedState.setLastBatch(lastBatch); - } - } + state = new MatrixState(authentication.getUserId().orElseThrow(IllegalStateException::new)); + stateSynchronizer = new MatrixStateSynchronizer(state, objectMapper); - while (!interruptionRequested) { - maybeSyncResponse = api.sync(lastBatch); - if (maybeSyncResponse.isPresent()) { - final var syncResponse = maybeSyncResponse.get(); - lastBatch = syncResponse.nextBatch(); + var maybeSyncResponse = api.syncFull(); + String lastBatch; + if (maybeSyncResponse.isPresent()) { + final var syncResponse = maybeSyncResponse.get(); + lastBatch = syncResponse.nextBatch(); - stateSynchronizer.synchronizeState(syncResponse); + stateSynchronizer.synchronizeState(syncResponse); + } else { + log.error("Failed to perform initial sync"); + return; + } if (eventNotifier != null) { - eventNotifier.notifyFromSynchronizationResponse(state, syncResponse); + eventNotifier.getConsumer().onConnected(state); } if (persistedState != null) { - persistedState.setLastBatch(lastBatch); + final var maybePersistedLastBatch = persistedState.getLastBatch(); + if (maybePersistedLastBatch.isPresent()) { + lastBatch = maybePersistedLastBatch.get(); + } else { + persistedState.setLastBatch(lastBatch); + } + } + + while (!interruptionRequested) { + maybeSyncResponse = api.sync(lastBatch); + if (maybeSyncResponse.isPresent()) { + final var syncResponse = maybeSyncResponse.get(); + lastBatch = syncResponse.nextBatch(); + + stateSynchronizer.synchronizeState(syncResponse); + + if (eventNotifier != null) { + eventNotifier.notifyFromSynchronizationResponse(state, syncResponse); + } + + if (persistedState != null) { + persistedState.setLastBatch(lastBatch); + } + } } + + } catch (IOException e) { + log.warn("Sync failed: {}, backing off for {}s", e.getClass().getName(), currentBackoffInSec); + + Thread.sleep(currentBackoffInSec * 1000); + currentBackoffInSec = Math.min(currentBackoffInSec * 2, BACKOFF_MAX_IN_SEC); } } interruptionRequested = false; + currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; } public boolean isConnected() { @@ -139,22 +157,38 @@ public Optional getState() { public boolean sendMessage(MatrixRoomId roomId, String messageBody) { - return api.sendEvent(roomId.getFormatted(), "m.room.message", new MessageDto(messageBody, "m.text")); + try { + return api.sendEvent(roomId.getFormatted(), "m.room.message", new MessageDto(messageBody, "m.text")); + } catch (InterruptedException e) { + return false; + } } public boolean addReaction(MatrixRoomId roomId, MatrixEventId eventId, String reaction) { final var reactionDto = new ReactionDto(new ReactionRelatesToDto(eventId.getFormatted(), reaction)); - return api.sendEvent(roomId.getFormatted(), "m.reaction", reactionDto); + try { + return api.sendEvent(roomId.getFormatted(), "m.reaction", reactionDto); + } catch (InterruptedException e) { + return false; + } } public boolean joinRoom(MatrixRoomId roomId) { - return api.joinRoom(roomId.getFormatted(), "hello there"); + try { + return api.joinRoom(roomId.getFormatted(), "hello there"); + } catch (InterruptedException e) { + return false; + } } public boolean leaveRoom(MatrixRoomId roomId) { - return api.leaveRoom(roomId.getFormatted(), "bai"); + try { + return api.leaveRoom(roomId.getFormatted(), "bai"); + } catch (InterruptedException e) { + return false; + } } } diff --git a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java index 582b84a..8fbec22 100644 --- a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java +++ b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java @@ -27,202 +27,197 @@ @Slf4j public class MatrixApi { - private static final int SYNC_TIMEOUT = 30_000; + private static final int SYNC_TIMEOUT = 30_000; - private final URI baseUri; - private final MatrixAuthentication authentication; - private final HttpClient httpClient; - private final ObjectMapper objectMapper; + private final URI baseUri; + private final MatrixAuthentication authentication; + private final HttpClient httpClient; + private final ObjectMapper objectMapper; - public MatrixApi(String hostname, MatrixAuthentication authentication, ObjectMapper objectMapper) { + public MatrixApi(String hostname, MatrixAuthentication authentication, ObjectMapper objectMapper) { - try { - this.baseUri = new URI(hostname); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - this.authentication = authentication; - this.httpClient = HttpClient.newHttpClient(); - this.objectMapper = objectMapper; + try { + this.baseUri = new URI(hostname); + } catch (URISyntaxException e) { + throw new RuntimeException(e); } - - public void terminateOpenConnections() { - - httpClient.shutdownNow(); + this.authentication = authentication; + this.httpClient = HttpClient.newHttpClient(); + this.objectMapper = objectMapper; + } + + public void terminateOpenConnections() { + + httpClient.shutdownNow(); + } + + public boolean login() throws IOException, InterruptedException { + + final var response = httpClient.send( + post( + "/_matrix/client/v3/login", + null, + new MatrixLoginDto( + new MatrixIdentifierDto("m.id.user", authentication.getUsername()), + authentication.getPassword(), + "m.login.password" + ) + ).build(), + jsonBodyHandler(MatrixLoginResponseDto.class) + ); + + final var maybeBody = Optional.ofNullable(response.body()); + maybeBody.ifPresent(body -> { + final var userId = MatrixUserId.from(body.userId()) + .orElseThrow(IllegalStateException::new); + authentication.setUserId(userId); + authentication.setBearerToken(body.accessToken()); + }); + + return maybeBody.isPresent(); + } + + public Optional sync(String since) throws InterruptedException { + + try { + final var response = httpClient.send( + get( + "/_matrix/client/v3/sync", + "timeout=%d&since=%s".formatted( + SYNC_TIMEOUT, + URLEncoder.encode(since, StandardCharsets.UTF_8) + ) + ).build(), + jsonBodyHandler(SyncResponseDto.class) + ); + + return Optional.ofNullable(response.body()); + } catch (IOException e) { + log.error("Failed to sync", e); } - public boolean login() { - - try { - final var response = httpClient.send( - post( - "/_matrix/client/v3/login", - null, - new MatrixLoginDto( - new MatrixIdentifierDto("m.id.user", authentication.getUsername()), - authentication.getPassword(), - "m.login.password" - ) - ).build(), - jsonBodyHandler(MatrixLoginResponseDto.class) - ); - - final var maybeBody = Optional.ofNullable(response.body()); - maybeBody.ifPresent(body -> { - final var userId = MatrixUserId.from(body.userId()) - .orElseThrow(IllegalStateException::new); - authentication.setUserId(userId); - authentication.setBearerToken(body.accessToken()); - }); - - return maybeBody.isPresent(); - } catch (IOException | InterruptedException e) { - log.error("Failed to login", e); - return false; - } - } + return Optional.empty(); + } - public Optional sync(String since) { - - try { - final var response = httpClient.send( - get( - "/_matrix/client/v3/sync", - "timeout=%d&since=%s".formatted( - SYNC_TIMEOUT, - URLEncoder.encode(since, StandardCharsets.UTF_8) - ) - ).build(), - jsonBodyHandler(SyncResponseDto.class) - ); - - return Optional.ofNullable(response.body()); - } catch (IOException | InterruptedException e) { - log.error("Failed to sync", e); - } + public Optional syncFull() throws InterruptedException { - return Optional.empty(); + try { + final var response = httpClient.send( + get("/_matrix/client/v3/sync", "timeout=0").build(), + jsonBodyHandler(SyncResponseDto.class) + ); + + return Optional.ofNullable(response.body()); + } catch (IOException e) { + log.error("Failed to sync", e); } - public Optional syncFull() { + return Optional.empty(); + } + + public boolean sendEvent(String roomId, String eventType, Object event) throws InterruptedException { + + final var uri = "/_matrix/client/v3/rooms/%s/send/%s/%s".formatted( + roomId, + eventType, + UUID.randomUUID() + ); + try { + final var response = httpClient.send( + put(uri, null, event).build(), + jsonBodyHandler(EventIdResponseDto.class) + ); + return response.statusCode() >= 200 && response.statusCode() < 300; + } catch (IOException e) { + log.error("Failed to send event", e); + } - try { - final var response = httpClient.send( - get("/_matrix/client/v3/sync", "timeout=0").build(), - jsonBodyHandler(SyncResponseDto.class) - ); + return false; + } - return Optional.ofNullable(response.body()); - } catch (IOException | InterruptedException e) { - log.error("Failed to sync", e); - } + public boolean joinRoom(String roomId, String reason) throws InterruptedException { - return Optional.empty(); + final var uri = "/_matrix/client/v3/rooms/%s/join".formatted(roomId); + try { + final var response = httpClient.send( + post(uri, null, new RoomJoinPayloadDto(reason)).build(), + HttpResponse.BodyHandlers.ofString() + ); + return response.statusCode() >= 200 && response.statusCode() < 300; + } catch (IOException e) { + log.error("Failed to join room", e); } - public boolean sendEvent(String roomId, String eventType, Object event) { - - final var uri = "/_matrix/client/v3/rooms/%s/send/%s/%s".formatted( - roomId, - eventType, - UUID.randomUUID() - ); - try { - final var response = httpClient.send( - put(uri, null, event).build(), - jsonBodyHandler(EventIdResponseDto.class) - ); - return response.statusCode() >= 200 && response.statusCode() < 300; - } catch (IOException | InterruptedException e) { - log.error("Failed to send event", e); - } + return false; + } + + public boolean leaveRoom(String roomId, String reason) throws InterruptedException { - return false; + final var uri = "/_matrix/client/v3/rooms/%s/leave".formatted(roomId); + try { + final var response = httpClient.send( + post(uri, null, new RoomLeavePayloadDto(reason)).build(), + HttpResponse.BodyHandlers.ofString() + ); + return response.statusCode() >= 200 && response.statusCode() < 300; + } catch (IOException e) { + log.error("Failed to leave room", e); } - public boolean joinRoom(String roomId, String reason) { - - final var uri = "/_matrix/client/v3/rooms/%s/join".formatted(roomId); - try { - final var response = httpClient.send( - post(uri, null, new RoomJoinPayloadDto(reason)).build(), - HttpResponse.BodyHandlers.ofString() - ); - return response.statusCode() >= 200 && response.statusCode() < 300; - } catch (IOException | InterruptedException e) { - log.error("Failed to join room", e); - } + return false; + } - return false; - } + private HttpRequest.Builder get(String url, String query) { - public boolean leaveRoom(String roomId, String reason) { - - final var uri = "/_matrix/client/v3/rooms/%s/leave".formatted(roomId); - try { - final var response = httpClient.send( - post(uri, null, new RoomLeavePayloadDto(reason)).build(), - HttpResponse.BodyHandlers.ofString() - ); - return response.statusCode() >= 200 && response.statusCode() < 300; - } catch (IOException | InterruptedException e) { - log.error("Failed to leave room", e); - } + return request(url, query).GET(); + } - return false; + private HttpRequest.Builder put(String url, String query, T body) { + + try { + return request(url, query) + .header("Content-Type", "application/json") + .PUT(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); } + } - private HttpRequest.Builder get(String url, String query) { + private HttpRequest.Builder post(String url, String query, T body) { - return request(url, query).GET(); + try { + return request(url, query) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); } + } - private HttpRequest.Builder put(String url, String query, T body) { + private HttpRequest.Builder request(String url, String query) { - try { - return request(url, query) - .header("Content-Type", "application/json") - .PUT(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + HttpRequest.Builder builder; + try { + builder = HttpRequest.newBuilder(new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), url, query, null)); + } catch (URISyntaxException e) { + throw new RuntimeException(e); } - private HttpRequest.Builder post(String url, String query, T body) { + authentication.getBearerToken().ifPresent(token -> builder.header("Authorization", "Bearer %s".formatted(token))); - try { - return request(url, query) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } + return builder; + } - private HttpRequest.Builder request(String url, String query) { + private HttpResponse.BodyHandler jsonBodyHandler(Class clazz) { - HttpRequest.Builder builder; - try { - builder = HttpRequest.newBuilder(new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), url, query, null)); - } catch (URISyntaxException e) { + return responseInfo -> HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofByteArray(), bytes -> { + try { + log.debug("sync: {}", new String(bytes, StandardCharsets.UTF_8)); + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { throw new RuntimeException(e); + } } - - authentication.getBearerToken().ifPresent(token -> builder.header("Authorization", "Bearer %s".formatted(token))); - - return builder; - } - - private HttpResponse.BodyHandler jsonBodyHandler(Class clazz) { - - return responseInfo -> HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofByteArray(), bytes -> { - try { - log.debug("sync: {}", new String(bytes, StandardCharsets.UTF_8)); - return objectMapper.readValue(bytes, clazz); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - ); - } + ); + } } From 3ae6e2617b845b8ea78ce66f591467828fdc3e2d Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 30 Sep 2025 11:09:14 +0200 Subject: [PATCH 2/7] Configure timeouts for all api requests --- .../synyx/matrix/bot/internal/api/MatrixApi.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java index 8fbec22..23deb0d 100644 --- a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java +++ b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java @@ -21,13 +21,17 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.UUID; @Slf4j public class MatrixApi { - private static final int SYNC_TIMEOUT = 30_000; + private static final Duration SYNC_TIMEOUT = Duration.of(30, ChronoUnit.SECONDS); + private static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.of(30, ChronoUnit.SECONDS); + private static final Duration SYNC_REQUEST_TIMEOUT = Duration.of((long) (SYNC_TIMEOUT.toMillis() * 1.5D), ChronoUnit.MILLIS); private final URI baseUri; private final MatrixAuthentication authentication; @@ -84,10 +88,10 @@ public Optional sync(String since) throws InterruptedException get( "/_matrix/client/v3/sync", "timeout=%d&since=%s".formatted( - SYNC_TIMEOUT, + SYNC_TIMEOUT.toMillis(), URLEncoder.encode(since, StandardCharsets.UTF_8) ) - ).build(), + ).timeout(SYNC_REQUEST_TIMEOUT).build(), jsonBodyHandler(SyncResponseDto.class) ); @@ -198,7 +202,8 @@ private HttpRequest.Builder request(String url, String query) { HttpRequest.Builder builder; try { - builder = HttpRequest.newBuilder(new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), url, query, null)); + builder = HttpRequest.newBuilder(new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), url, query, null)) + .timeout(DEFAULT_REQUEST_TIMEOUT); } catch (URISyntaxException e) { throw new RuntimeException(e); } From bbd3a384ca2d97c25b0895865a82ed67e3cc49cf Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 30 Sep 2025 11:58:49 +0200 Subject: [PATCH 3/7] Improve error handling for MatrixClient --- .../org/synyx/matrix/bot/MatrixClient.java | 92 +++++++---- .../bot/MatrixCommunicationException.java | 14 ++ .../matrix/bot/internal/api/MatrixApi.java | 146 ++++++++---------- .../bot/internal/api/MatrixApiException.java | 17 ++ 4 files changed, 161 insertions(+), 108 deletions(-) create mode 100644 src/main/java/org/synyx/matrix/bot/MatrixCommunicationException.java create mode 100644 src/main/java/org/synyx/matrix/bot/internal/api/MatrixApiException.java diff --git a/src/main/java/org/synyx/matrix/bot/MatrixClient.java b/src/main/java/org/synyx/matrix/bot/MatrixClient.java index 65b24ec..149f773 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixClient.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixClient.java @@ -14,9 +14,11 @@ import org.synyx.matrix.bot.internal.MatrixEventNotifier; import org.synyx.matrix.bot.internal.MatrixStateSynchronizer; import org.synyx.matrix.bot.internal.api.MatrixApi; +import org.synyx.matrix.bot.internal.api.MatrixApiException; import org.synyx.matrix.bot.internal.api.dto.MessageDto; import org.synyx.matrix.bot.internal.api.dto.ReactionDto; import org.synyx.matrix.bot.internal.api.dto.ReactionRelatesToDto; +import org.synyx.matrix.bot.internal.api.dto.SyncResponseDto; import java.io.IOException; import java.util.Optional; @@ -75,9 +77,10 @@ public void syncContinuous() throws InterruptedException { while (!interruptionRequested) { try { if (!authentication.isAuthenticated()) { - if (!api.login()) { - log.error("Failed to login to matrix server!"); - return; + try { + api.login(); + } catch (MatrixApiException e) { + throw new MatrixCommunicationException("Failed to login to matrix server!", e); } log.info("Successfully logged in to matrix server as {}", @@ -90,18 +93,17 @@ public void syncContinuous() throws InterruptedException { state = new MatrixState(authentication.getUserId().orElseThrow(IllegalStateException::new)); stateSynchronizer = new MatrixStateSynchronizer(state, objectMapper); - var maybeSyncResponse = api.syncFull(); - String lastBatch; - if (maybeSyncResponse.isPresent()) { - final var syncResponse = maybeSyncResponse.get(); - lastBatch = syncResponse.nextBatch(); - - stateSynchronizer.synchronizeState(syncResponse); - } else { - log.error("Failed to perform initial sync"); - return; + SyncResponseDto syncResponse; + try { + syncResponse = api.syncFull() + .orElseThrow(() -> new MatrixCommunicationException("No data in initial sync")); + } catch (MatrixApiException e) { + throw new MatrixCommunicationException("Failed to perform initial sync"); } + String lastBatch = syncResponse.nextBatch(); + stateSynchronizer.synchronizeState(syncResponse); + if (eventNotifier != null) { eventNotifier.getConsumer().onConnected(state); } @@ -116,9 +118,17 @@ public void syncContinuous() throws InterruptedException { } while (!interruptionRequested) { - maybeSyncResponse = api.sync(lastBatch); - if (maybeSyncResponse.isPresent()) { - final var syncResponse = maybeSyncResponse.get(); + Optional maybePartialSyncResponse; + + try { + maybePartialSyncResponse = api.sync(lastBatch); + } catch (MatrixApiException e) { + log.warn("Could not partial sync", e); + maybePartialSyncResponse = Optional.empty(); + } + + if (maybePartialSyncResponse.isPresent()) { + syncResponse = maybePartialSyncResponse.get(); lastBatch = syncResponse.nextBatch(); stateSynchronizer.synchronizeState(syncResponse); @@ -155,40 +165,62 @@ public Optional getState() { return Optional.ofNullable(state); } - public boolean sendMessage(MatrixRoomId roomId, String messageBody) { + public Optional sendMessage(MatrixRoomId roomId, String messageBody) { try { - return api.sendEvent(roomId.getFormatted(), "m.room.message", new MessageDto(messageBody, "m.text")); - } catch (InterruptedException e) { - return false; + return MatrixEventId.from( + api.sendEvent(roomId.getFormatted(), "m.room.message", new MessageDto(messageBody, "m.text")) + ); + } catch (InterruptedException | IOException e) { + log.error("Failed to send message", e); + } catch (MatrixApiException e) { + log.warn("Could not send message", e); } + + return Optional.empty(); } - public boolean addReaction(MatrixRoomId roomId, MatrixEventId eventId, String reaction) { + public Optional addReaction(MatrixRoomId roomId, MatrixEventId eventId, String reaction) { final var reactionDto = new ReactionDto(new ReactionRelatesToDto(eventId.getFormatted(), reaction)); try { - return api.sendEvent(roomId.getFormatted(), "m.reaction", reactionDto); - } catch (InterruptedException e) { - return false; + return MatrixEventId.from( + api.sendEvent(roomId.getFormatted(), "m.reaction", reactionDto) + ); + } catch (InterruptedException | IOException e) { + log.error("Failed to add reaction", e); + } catch (MatrixApiException e) { + log.warn("Could not add reaction", e); } + + return Optional.empty(); } public boolean joinRoom(MatrixRoomId roomId) { try { - return api.joinRoom(roomId.getFormatted(), "hello there"); - } catch (InterruptedException e) { - return false; + api.joinRoom(roomId.getFormatted(), "i'm a bot"); + return true; + } catch (InterruptedException | IOException e) { + log.error("Failed to join room", e); + } catch (MatrixApiException e) { + log.warn("Could not join room", e); } + + return false; } public boolean leaveRoom(MatrixRoomId roomId) { try { - return api.leaveRoom(roomId.getFormatted(), "bai"); - } catch (InterruptedException e) { - return false; + api.leaveRoom(roomId.getFormatted(), "i'm a bot"); + return true; + } catch (InterruptedException | IOException e) { + log.error("Failed to leave room", e); + } catch (MatrixApiException e) { + log.warn("Could not leave room", e); } + + return false; } } diff --git a/src/main/java/org/synyx/matrix/bot/MatrixCommunicationException.java b/src/main/java/org/synyx/matrix/bot/MatrixCommunicationException.java new file mode 100644 index 0000000..eb7a9e0 --- /dev/null +++ b/src/main/java/org/synyx/matrix/bot/MatrixCommunicationException.java @@ -0,0 +1,14 @@ +package org.synyx.matrix.bot; + +public class MatrixCommunicationException extends RuntimeException { + + public MatrixCommunicationException(String message) { + + super(message); + } + + public MatrixCommunicationException(String message, Throwable cause) { + + super(message, cause); + } +} diff --git a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java index 23deb0d..432b1e0 100644 --- a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java +++ b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; +import org.synyx.matrix.bot.MatrixCommunicationException; import org.synyx.matrix.bot.domain.MatrixUserId; import org.synyx.matrix.bot.internal.MatrixAuthentication; import org.synyx.matrix.bot.internal.api.dto.EventIdResponseDto; @@ -43,7 +44,7 @@ public MatrixApi(String hostname, MatrixAuthentication authentication, ObjectMap try { this.baseUri = new URI(hostname); } catch (URISyntaxException e) { - throw new RuntimeException(e); + throw new MatrixCommunicationException("Invalid matrix URI", e); } this.authentication = authentication; this.httpClient = HttpClient.newHttpClient(); @@ -55,7 +56,7 @@ public void terminateOpenConnections() { httpClient.shutdownNow(); } - public boolean login() throws IOException, InterruptedException { + public void login() throws IOException, InterruptedException, MatrixApiException { final var response = httpClient.send( post( @@ -70,105 +71,87 @@ public boolean login() throws IOException, InterruptedException { jsonBodyHandler(MatrixLoginResponseDto.class) ); - final var maybeBody = Optional.ofNullable(response.body()); - maybeBody.ifPresent(body -> { - final var userId = MatrixUserId.from(body.userId()) - .orElseThrow(IllegalStateException::new); - authentication.setUserId(userId); - authentication.setBearerToken(body.accessToken()); - }); + expected2xx("login", response); - return maybeBody.isPresent(); + final var body = response.body(); + if (body == null) { + throw new MatrixApiException("Received no login data", response); + } + + final var userId = MatrixUserId.from(body.userId()) + .orElseThrow(IllegalStateException::new); + authentication.setUserId(userId); + authentication.setBearerToken(body.accessToken()); } - public Optional sync(String since) throws InterruptedException { + public Optional sync(String since) throws IOException, InterruptedException, MatrixApiException { - try { - final var response = httpClient.send( - get( - "/_matrix/client/v3/sync", - "timeout=%d&since=%s".formatted( - SYNC_TIMEOUT.toMillis(), - URLEncoder.encode(since, StandardCharsets.UTF_8) - ) - ).timeout(SYNC_REQUEST_TIMEOUT).build(), - jsonBodyHandler(SyncResponseDto.class) - ); - - return Optional.ofNullable(response.body()); - } catch (IOException e) { - log.error("Failed to sync", e); - } + final var response = httpClient.send( + get( + "/_matrix/client/v3/sync", + "timeout=%d&since=%s".formatted( + SYNC_TIMEOUT.toMillis(), + URLEncoder.encode(since, StandardCharsets.UTF_8) + ) + ).timeout(SYNC_REQUEST_TIMEOUT).build(), + jsonBodyHandler(SyncResponseDto.class) + ); + + expected2xx("syncing", response); - return Optional.empty(); + return Optional.ofNullable(response.body()); } - public Optional syncFull() throws InterruptedException { + public Optional syncFull() throws IOException, InterruptedException, MatrixApiException { - try { - final var response = httpClient.send( - get("/_matrix/client/v3/sync", "timeout=0").build(), - jsonBodyHandler(SyncResponseDto.class) - ); - - return Optional.ofNullable(response.body()); - } catch (IOException e) { - log.error("Failed to sync", e); - } + final var response = httpClient.send( + get("/_matrix/client/v3/sync", "timeout=0").build(), + jsonBodyHandler(SyncResponseDto.class) + ); - return Optional.empty(); + expected2xx("full syncing", response); + + return Optional.ofNullable(response.body()); } - public boolean sendEvent(String roomId, String eventType, Object event) throws InterruptedException { + public String sendEvent(String roomId, String eventType, Object event) throws IOException, InterruptedException, MatrixApiException { final var uri = "/_matrix/client/v3/rooms/%s/send/%s/%s".formatted( roomId, eventType, UUID.randomUUID() ); - try { - final var response = httpClient.send( - put(uri, null, event).build(), - jsonBodyHandler(EventIdResponseDto.class) - ); - return response.statusCode() >= 200 && response.statusCode() < 300; - } catch (IOException e) { - log.error("Failed to send event", e); - } - return false; + final var response = httpClient.send( + put(uri, null, event).build(), + jsonBodyHandler(EventIdResponseDto.class) + ); + + expected2xx("sending event", response); + + return response.body().eventId(); } - public boolean joinRoom(String roomId, String reason) throws InterruptedException { + public void joinRoom(String roomId, String reason) throws IOException, InterruptedException, MatrixApiException { final var uri = "/_matrix/client/v3/rooms/%s/join".formatted(roomId); - try { - final var response = httpClient.send( - post(uri, null, new RoomJoinPayloadDto(reason)).build(), - HttpResponse.BodyHandlers.ofString() - ); - return response.statusCode() >= 200 && response.statusCode() < 300; - } catch (IOException e) { - log.error("Failed to join room", e); - } + final var response = httpClient.send( + post(uri, null, new RoomJoinPayloadDto(reason)).build(), + HttpResponse.BodyHandlers.ofString() + ); - return false; + expected2xx("joining room", response); } - public boolean leaveRoom(String roomId, String reason) throws InterruptedException { + public void leaveRoom(String roomId, String reason) throws IOException, InterruptedException, MatrixApiException { final var uri = "/_matrix/client/v3/rooms/%s/leave".formatted(roomId); - try { - final var response = httpClient.send( - post(uri, null, new RoomLeavePayloadDto(reason)).build(), - HttpResponse.BodyHandlers.ofString() - ); - return response.statusCode() >= 200 && response.statusCode() < 300; - } catch (IOException e) { - log.error("Failed to leave room", e); - } + final var response = httpClient.send( + post(uri, null, new RoomLeavePayloadDto(reason)).build(), + HttpResponse.BodyHandlers.ofString() + ); - return false; + expected2xx("leaving room", response); } private HttpRequest.Builder get(String url, String query) { @@ -183,7 +166,7 @@ private HttpRequest.Builder put(String url, String query, T body) { .header("Content-Type", "application/json") .PUT(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))); } catch (JsonProcessingException e) { - throw new RuntimeException(e); + throw new MatrixCommunicationException("Failed to parse JSON", e); } } @@ -194,7 +177,7 @@ private HttpRequest.Builder post(String url, String query, T body) { .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body))); } catch (JsonProcessingException e) { - throw new RuntimeException(e); + throw new MatrixCommunicationException("Failed to parse JSON", e); } } @@ -205,7 +188,7 @@ private HttpRequest.Builder request(String url, String query) { builder = HttpRequest.newBuilder(new URI(baseUri.getScheme(), null, baseUri.getHost(), baseUri.getPort(), url, query, null)) .timeout(DEFAULT_REQUEST_TIMEOUT); } catch (URISyntaxException e) { - throw new RuntimeException(e); + throw new MatrixCommunicationException("Invalid URI when trying to make API request", e); } authentication.getBearerToken().ifPresent(token -> builder.header("Authorization", "Bearer %s".formatted(token))); @@ -217,12 +200,19 @@ private HttpResponse.BodyHandler jsonBodyHandler(Class clazz) { return responseInfo -> HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofByteArray(), bytes -> { try { - log.debug("sync: {}", new String(bytes, StandardCharsets.UTF_8)); return objectMapper.readValue(bytes, clazz); } catch (IOException e) { - throw new RuntimeException(e); + throw new MatrixCommunicationException("Invalid URI when trying to make API request", e); } } ); } + + private void expected2xx(String performedAction, HttpResponse response) throws MatrixApiException { + + final var statusCode = response.statusCode(); + if (statusCode < 200 || statusCode >= 300) { + throw new MatrixApiException(performedAction, response); + } + } } diff --git a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApiException.java b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApiException.java new file mode 100644 index 0000000..9dceccc --- /dev/null +++ b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApiException.java @@ -0,0 +1,17 @@ +package org.synyx.matrix.bot.internal.api; + +import java.io.IOException; +import java.net.http.HttpResponse; + +public class MatrixApiException extends Exception { + + public MatrixApiException(String performedAction, HttpResponse response) { + + super("%s failed - %d".formatted(performedAction, response.statusCode())); + } + + public MatrixApiException(String performedAction, IOException ioException) { + + super("%s failed - %s".formatted(performedAction, ioException.getClass().getName()), ioException); + } +} From e8d1c7be6ad92f93f5193bee8a374ba28f2825bb Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 30 Sep 2025 12:01:45 +0200 Subject: [PATCH 4/7] Reset authentication on matrix backoff --- .../org/synyx/matrix/bot/MatrixClient.java | 3 ++ .../bot/internal/MatrixAuthentication.java | 50 +++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/synyx/matrix/bot/MatrixClient.java b/src/main/java/org/synyx/matrix/bot/MatrixClient.java index 149f773..421231d 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixClient.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixClient.java @@ -141,12 +141,15 @@ public void syncContinuous() throws InterruptedException { persistedState.setLastBatch(lastBatch); } } + + currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; } } catch (IOException e) { log.warn("Sync failed: {}, backing off for {}s", e.getClass().getName(), currentBackoffInSec); Thread.sleep(currentBackoffInSec * 1000); + authentication.clear(); currentBackoffInSec = Math.min(currentBackoffInSec * 2, BACKOFF_MAX_IN_SEC); } } diff --git a/src/main/java/org/synyx/matrix/bot/internal/MatrixAuthentication.java b/src/main/java/org/synyx/matrix/bot/internal/MatrixAuthentication.java index a59ee79..5c6d741 100644 --- a/src/main/java/org/synyx/matrix/bot/internal/MatrixAuthentication.java +++ b/src/main/java/org/synyx/matrix/bot/internal/MatrixAuthentication.java @@ -8,35 +8,41 @@ public class MatrixAuthentication { - @Getter - private final String username; - @Getter - private final String password; + @Getter + private final String username; + @Getter + private final String password; - @Setter - private MatrixUserId userId; - @Setter - private String bearerToken; + @Setter + private MatrixUserId userId; + @Setter + private String bearerToken; - public MatrixAuthentication(String username, String password) { + public MatrixAuthentication(String username, String password) { - this.username = username; - this.password = password; - this.bearerToken = null; - } + this.username = username; + this.password = password; + this.bearerToken = null; + } - public boolean isAuthenticated() { + public boolean isAuthenticated() { - return bearerToken != null; - } + return bearerToken != null; + } - public Optional getBearerToken() { + public void clear() { - return Optional.ofNullable(bearerToken); - } + bearerToken = null; + userId = null; + } - public Optional getUserId() { + public Optional getBearerToken() { - return Optional.ofNullable(userId); - } + return Optional.ofNullable(bearerToken); + } + + public Optional getUserId() { + + return Optional.ofNullable(userId); + } } From 35eff52542ee69b26ac08d01c1faa7ad399fca33 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 30 Sep 2025 12:15:52 +0200 Subject: [PATCH 5/7] Backoff when sync fails --- .../java/org/synyx/matrix/bot/MatrixClient.java | 16 +++++++++------- .../bot/internal/MatrixBackoffException.java | 9 +++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/synyx/matrix/bot/internal/MatrixBackoffException.java diff --git a/src/main/java/org/synyx/matrix/bot/MatrixClient.java b/src/main/java/org/synyx/matrix/bot/MatrixClient.java index 421231d..4ab977c 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixClient.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixClient.java @@ -11,6 +11,7 @@ import org.synyx.matrix.bot.domain.MatrixRoomId; import org.synyx.matrix.bot.domain.MatrixUserId; import org.synyx.matrix.bot.internal.MatrixAuthentication; +import org.synyx.matrix.bot.internal.MatrixBackoffException; import org.synyx.matrix.bot.internal.MatrixEventNotifier; import org.synyx.matrix.bot.internal.MatrixStateSynchronizer; import org.synyx.matrix.bot.internal.api.MatrixApi; @@ -79,6 +80,8 @@ public void syncContinuous() throws InterruptedException { if (!authentication.isAuthenticated()) { try { api.login(); + } catch (IOException e) { + throw new MatrixBackoffException("Failed to login to matrix server!", e); } catch (MatrixApiException e) { throw new MatrixCommunicationException("Failed to login to matrix server!", e); } @@ -97,8 +100,8 @@ public void syncContinuous() throws InterruptedException { try { syncResponse = api.syncFull() .orElseThrow(() -> new MatrixCommunicationException("No data in initial sync")); - } catch (MatrixApiException e) { - throw new MatrixCommunicationException("Failed to perform initial sync"); + } catch (MatrixApiException | IOException e) { + throw new MatrixBackoffException("Failed to perform initial sync", e); } String lastBatch = syncResponse.nextBatch(); @@ -122,9 +125,8 @@ public void syncContinuous() throws InterruptedException { try { maybePartialSyncResponse = api.sync(lastBatch); - } catch (MatrixApiException e) { - log.warn("Could not partial sync", e); - maybePartialSyncResponse = Optional.empty(); + } catch (MatrixApiException | IOException e) { + throw new MatrixBackoffException("Could not partial sync", e); } if (maybePartialSyncResponse.isPresent()) { @@ -145,8 +147,8 @@ public void syncContinuous() throws InterruptedException { currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; } - } catch (IOException e) { - log.warn("Sync failed: {}, backing off for {}s", e.getClass().getName(), currentBackoffInSec); + } catch (MatrixBackoffException e) { + log.warn("Sync failed: {}, backing off for {}s", e.getCause().getClass().getName(), currentBackoffInSec); Thread.sleep(currentBackoffInSec * 1000); authentication.clear(); diff --git a/src/main/java/org/synyx/matrix/bot/internal/MatrixBackoffException.java b/src/main/java/org/synyx/matrix/bot/internal/MatrixBackoffException.java new file mode 100644 index 0000000..e9bbcbe --- /dev/null +++ b/src/main/java/org/synyx/matrix/bot/internal/MatrixBackoffException.java @@ -0,0 +1,9 @@ +package org.synyx.matrix.bot.internal; + +public class MatrixBackoffException extends RuntimeException { + + public MatrixBackoffException(String message, Throwable e) { + + super(message, e); + } +} From fbe787a63d4bb33e2f182712246c87ef310911d0 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 30 Sep 2025 14:10:48 +0200 Subject: [PATCH 6/7] Add javadoc --- .../org/synyx/matrix/bot/MatrixClient.java | 74 ++++++++++++++++++- .../bot/MatrixCommunicationException.java | 3 + .../synyx/matrix/bot/MatrixEventConsumer.java | 69 ++++++++++++++--- .../matrix/bot/MatrixPersistedState.java | 10 --- .../bot/MatrixPersistedStateProvider.java | 10 +++ 5 files changed, 141 insertions(+), 25 deletions(-) delete mode 100644 src/main/java/org/synyx/matrix/bot/MatrixPersistedState.java create mode 100644 src/main/java/org/synyx/matrix/bot/MatrixPersistedStateProvider.java diff --git a/src/main/java/org/synyx/matrix/bot/MatrixClient.java b/src/main/java/org/synyx/matrix/bot/MatrixClient.java index 4ab977c..a47f665 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixClient.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixClient.java @@ -35,7 +35,7 @@ public class MatrixClient { private final MatrixApi api; private MatrixState state; private MatrixStateSynchronizer stateSynchronizer; - private MatrixPersistedState persistedState; + private MatrixPersistedStateProvider persistedState; private MatrixEventNotifier eventNotifier; private boolean interruptionRequested; private long currentBackoffInSec; @@ -57,22 +57,46 @@ public MatrixClient(String hostname, String username, String password) { this.currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; } + /** + * Sets a consumer object that gets called on events happening on the matrix server. + * Only one consumer can be set at any time. + * Calling this method again replaces any previous event callback. + * + * @param eventConsumer The consumer to call on events. + */ public void setEventCallback(MatrixEventConsumer eventConsumer) { this.eventNotifier = MatrixEventNotifier.from(objectMapper, eventConsumer).orElse(null); } - public void setPersistedState(MatrixPersistedState persistedState) { + /** + * Optionally provides an interface to provide the current state of the matrix client. + * If not provided, any startup will act like the first startup and will ignore any previously sent messages. + * Providing a persisted state will make the client be able to determine which events happened while offline. + * + * @param persistedState An interface for persisting the matrix client state + */ + public void setPersistedStateProvider(MatrixPersistedStateProvider persistedState) { this.persistedState = persistedState; } + /** + * Requests the matrix client to stop syncing and terminate. + * May be called from a different thread. + */ public void requestStopOfSync() { interruptionRequested = true; api.terminateOpenConnections(); } + /** + * The main matrix client event loop that continuously syncs all events happening on the matrix server to the client. + * This is a blocking call, so make sure to call it from a different thread if needed. + * + * @throws InterruptedException The sync has been interrupted + */ public void syncContinuous() throws InterruptedException { while (!interruptionRequested) { @@ -150,26 +174,50 @@ public void syncContinuous() throws InterruptedException { } catch (MatrixBackoffException e) { log.warn("Sync failed: {}, backing off for {}s", e.getCause().getClass().getName(), currentBackoffInSec); + clearSyncState(); Thread.sleep(currentBackoffInSec * 1000); - authentication.clear(); currentBackoffInSec = Math.min(currentBackoffInSec * 2, BACKOFF_MAX_IN_SEC); } } + clearSyncState(); interruptionRequested = false; currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; } + private void clearSyncState() { + + authentication.clear(); + state = null; + } + + /** + * Returns whether the matrix client is currently connected to the server or not. + * + * @return {@code true} if the client is currently connected to the server, {@code false} otherwise. + */ public boolean isConnected() { return state != null; } + /** + * Returns the current state of the matrix client. + * + * @return A {@link MatrixState} object if currently connected to a server, {@link Optional#empty()} otherwise. + */ public Optional getState() { return Optional.ofNullable(state); } + /** + * Attempts to send a message to the specified room. + * + * @param roomId The id of the room to send the message to. + * @param messageBody The body of the message to send. + * @return A {@link MatrixEventId} containing the id of the event that was sent or {@link Optional#empty()} if sending the message did not succeed. + */ public Optional sendMessage(MatrixRoomId roomId, String messageBody) { try { @@ -185,6 +233,14 @@ public Optional sendMessage(MatrixRoomId roomId, String messageBo return Optional.empty(); } + /** + * Attempts to add a reaction to an event (a message of the time). + * + * @param roomId The id of the room to send the message in. + * @param eventId The id of the event to react to. + * @param reaction The reaction to send. + * @return A {@link MatrixEventId} containing the id of the event that was sent or {@link Optional#empty()} if sending the reaction did not succeed. + */ public Optional addReaction(MatrixRoomId roomId, MatrixEventId eventId, String reaction) { final var reactionDto = new ReactionDto(new ReactionRelatesToDto(eventId.getFormatted(), reaction)); @@ -201,6 +257,12 @@ public Optional addReaction(MatrixRoomId roomId, MatrixEventId ev return Optional.empty(); } + /** + * Attempts to join a room. + * + * @param roomId The id of the room to join. + * @return {@code true} if joining the room was successful, {@code false} otherwise. + */ public boolean joinRoom(MatrixRoomId roomId) { try { @@ -215,6 +277,12 @@ public boolean joinRoom(MatrixRoomId roomId) { return false; } + /** + * Attempts to leave a room. + * + * @param roomId The id of the room to leave. + * @return {@code true} if leaving the room was successful, {@code false} otherwise. + */ public boolean leaveRoom(MatrixRoomId roomId) { try { diff --git a/src/main/java/org/synyx/matrix/bot/MatrixCommunicationException.java b/src/main/java/org/synyx/matrix/bot/MatrixCommunicationException.java index eb7a9e0..a036022 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixCommunicationException.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixCommunicationException.java @@ -1,5 +1,8 @@ package org.synyx.matrix.bot; +/** + * An exception that was not recoverable from by the matrix client itself occurred while communicating with the matrix server. + */ public class MatrixCommunicationException extends RuntimeException { public MatrixCommunicationException(String message) { diff --git a/src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java b/src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java index 3f2c28d..70f7226 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java @@ -6,29 +6,74 @@ import org.synyx.matrix.bot.domain.MatrixRoomInvite; import org.synyx.matrix.bot.domain.MatrixUserId; +/** + * An interface providing callbacks for things happening on the matrix server that were received by the client. + * All methods have a default implementation that does nothing, so implementing classes only need to override whatever + * they want to listen to. + *

+ * Any reactions to events happening shall be performed using the appropriate {@link MatrixClient} instance. + */ public interface MatrixEventConsumer { - default void onConnected(MatrixState state) { + /** + * The client successfully connected to the server. + * + * @param state The state after the initial synchronisation. + */ + default void onConnected(MatrixState state) { - } + } - default void onMessage(MatrixState state, MatrixRoom room, MatrixMessage message) { + /** + * A message event was received in a room that the client is part of. + * + * @param state The current client state. + * @param room The room the message was received in. + * @param message The message that was received. + */ + default void onMessage(MatrixState state, MatrixRoom room, MatrixMessage message) { - } + } - default void onInviteToRoom(MatrixState state, MatrixRoomInvite invite) { + /** + * An invitation to a room was received. + * + * @param state The current client state. + * @param invite The invite that was received. + */ + default void onInviteToRoom(MatrixState state, MatrixRoomInvite invite) { - } + } - default void onUserJoinRoom(MatrixState state, MatrixRoom room, MatrixUserId userId) { + /** + * A user joined a room that the client is part of. + * + * @param state The current client state. + * @param room The room that the user joined in. + * @param userId The id of the user that joined the room. + */ + default void onUserJoinRoom(MatrixState state, MatrixRoom room, MatrixUserId userId) { - } + } - default void onUserLeaveRoom(MatrixState state, MatrixRoom room, MatrixUserId userId) { + /** + * A user left a room that the client is part of. + * + * @param state The current client state. + * @param room The room that the user left from. + * @param userId The id of the user that left the room. + */ + default void onUserLeaveRoom(MatrixState state, MatrixRoom room, MatrixUserId userId) { - } + } - default void onSelfLeaveRoom(MatrixState state, MatrixRoomId roomId) { + /** + * The client left a room it was part of. May have been caused by external factors like kicks or bans. + * + * @param state The current client state. + * @param roomId The id of the room that the client left from. + */ + default void onSelfLeaveRoom(MatrixState state, MatrixRoomId roomId) { - } + } } diff --git a/src/main/java/org/synyx/matrix/bot/MatrixPersistedState.java b/src/main/java/org/synyx/matrix/bot/MatrixPersistedState.java deleted file mode 100644 index a36ee51..0000000 --- a/src/main/java/org/synyx/matrix/bot/MatrixPersistedState.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.synyx.matrix.bot; - -import java.util.Optional; - -public interface MatrixPersistedState { - - Optional getLastBatch(); - - void setLastBatch(String value); -} diff --git a/src/main/java/org/synyx/matrix/bot/MatrixPersistedStateProvider.java b/src/main/java/org/synyx/matrix/bot/MatrixPersistedStateProvider.java new file mode 100644 index 0000000..49bd9eb --- /dev/null +++ b/src/main/java/org/synyx/matrix/bot/MatrixPersistedStateProvider.java @@ -0,0 +1,10 @@ +package org.synyx.matrix.bot; + +import java.util.Optional; + +public interface MatrixPersistedStateProvider { + + Optional getLastBatch(); + + void setLastBatch(String value); +} From 08a13de7e27b6763571f2b2e14f7173dab630596 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Tue, 30 Sep 2025 14:20:31 +0200 Subject: [PATCH 7/7] Only expose interface for MatrixClient --- .../org/synyx/matrix/bot/MatrixClient.java | 249 +++--------------- .../synyx/matrix/bot/MatrixEventConsumer.java | 3 +- .../matrix/bot/internal/MatrixClientImpl.java | 249 ++++++++++++++++++ .../matrix/bot/internal/api/MatrixApi.java | 4 +- 4 files changed, 283 insertions(+), 222 deletions(-) create mode 100644 src/main/java/org/synyx/matrix/bot/internal/MatrixClientImpl.java diff --git a/src/main/java/org/synyx/matrix/bot/MatrixClient.java b/src/main/java/org/synyx/matrix/bot/MatrixClient.java index a47f665..01cb65d 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixClient.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixClient.java @@ -1,60 +1,28 @@ package org.synyx.matrix.bot; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import lombok.extern.slf4j.Slf4j; import org.synyx.matrix.bot.domain.MatrixEventId; import org.synyx.matrix.bot.domain.MatrixRoomId; -import org.synyx.matrix.bot.domain.MatrixUserId; -import org.synyx.matrix.bot.internal.MatrixAuthentication; -import org.synyx.matrix.bot.internal.MatrixBackoffException; -import org.synyx.matrix.bot.internal.MatrixEventNotifier; -import org.synyx.matrix.bot.internal.MatrixStateSynchronizer; -import org.synyx.matrix.bot.internal.api.MatrixApi; -import org.synyx.matrix.bot.internal.api.MatrixApiException; -import org.synyx.matrix.bot.internal.api.dto.MessageDto; -import org.synyx.matrix.bot.internal.api.dto.ReactionDto; -import org.synyx.matrix.bot.internal.api.dto.ReactionRelatesToDto; -import org.synyx.matrix.bot.internal.api.dto.SyncResponseDto; +import org.synyx.matrix.bot.internal.MatrixClientImpl; -import java.io.IOException; import java.util.Optional; -@Slf4j -public class MatrixClient { +/** + * An interface for a client connecting to a matrix server. + * Serves as the main method of communicating with the server. + */ +public interface MatrixClient { - private static final long DEFAULT_BACKOFF_IN_SEC = 3; - private static final long BACKOFF_MAX_IN_SEC = 60; - - private final MatrixAuthentication authentication; - private final ObjectMapper objectMapper; - private final MatrixApi api; - private MatrixState state; - private MatrixStateSynchronizer stateSynchronizer; - private MatrixPersistedStateProvider persistedState; - private MatrixEventNotifier eventNotifier; - private boolean interruptionRequested; - private long currentBackoffInSec; - - public MatrixClient(String hostname, String username, String password) { + /** + * Creates a new matrix client to connect to the specified server. + * + * @param url The url for connecting to the intended matrix server. Must start with http:// or https:// + * @param username The username for logging into the matrix server. + * @param password The password for logging into the matrix server. + * @return A {@link MatrixClient} implementation that connects to the specified matrix server. + */ + static MatrixClient create(String url, String username, String password) { - this.authentication = new MatrixAuthentication(username, password); - this.objectMapper = JsonMapper.builder() - .addModule(new Jdk8Module()) - .addModule(new JavaTimeModule()) - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true) - .build(); - this.api = new MatrixApi(hostname, authentication, objectMapper); - this.state = null; - this.eventNotifier = null; - this.interruptionRequested = false; - this.currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; + return new MatrixClientImpl(url, username, password); } /** @@ -64,10 +32,7 @@ public MatrixClient(String hostname, String username, String password) { * * @param eventConsumer The consumer to call on events. */ - public void setEventCallback(MatrixEventConsumer eventConsumer) { - - this.eventNotifier = MatrixEventNotifier.from(objectMapper, eventConsumer).orElse(null); - } + void setEventCallback(MatrixEventConsumer eventConsumer); /** * Optionally provides an interface to provide the current state of the matrix client. @@ -76,20 +41,8 @@ public void setEventCallback(MatrixEventConsumer eventConsumer) { * * @param persistedState An interface for persisting the matrix client state */ - public void setPersistedStateProvider(MatrixPersistedStateProvider persistedState) { + void setPersistedStateProvider(MatrixPersistedStateProvider persistedState); - this.persistedState = persistedState; - } - - /** - * Requests the matrix client to stop syncing and terminate. - * May be called from a different thread. - */ - public void requestStopOfSync() { - - interruptionRequested = true; - api.terminateOpenConnections(); - } /** * The main matrix client event loop that continuously syncs all events happening on the matrix server to the client. @@ -97,119 +50,28 @@ public void requestStopOfSync() { * * @throws InterruptedException The sync has been interrupted */ - public void syncContinuous() throws InterruptedException { - - while (!interruptionRequested) { - try { - if (!authentication.isAuthenticated()) { - try { - api.login(); - } catch (IOException e) { - throw new MatrixBackoffException("Failed to login to matrix server!", e); - } catch (MatrixApiException e) { - throw new MatrixCommunicationException("Failed to login to matrix server!", e); - } - - log.info("Successfully logged in to matrix server as {}", - authentication.getUserId() - .map(MatrixUserId::toString) - .orElse("UNKNOWN") - ); - } - - state = new MatrixState(authentication.getUserId().orElseThrow(IllegalStateException::new)); - stateSynchronizer = new MatrixStateSynchronizer(state, objectMapper); - - SyncResponseDto syncResponse; - try { - syncResponse = api.syncFull() - .orElseThrow(() -> new MatrixCommunicationException("No data in initial sync")); - } catch (MatrixApiException | IOException e) { - throw new MatrixBackoffException("Failed to perform initial sync", e); - } - - String lastBatch = syncResponse.nextBatch(); - stateSynchronizer.synchronizeState(syncResponse); - - if (eventNotifier != null) { - eventNotifier.getConsumer().onConnected(state); - } - - if (persistedState != null) { - final var maybePersistedLastBatch = persistedState.getLastBatch(); - if (maybePersistedLastBatch.isPresent()) { - lastBatch = maybePersistedLastBatch.get(); - } else { - persistedState.setLastBatch(lastBatch); - } - } - - while (!interruptionRequested) { - Optional maybePartialSyncResponse; - - try { - maybePartialSyncResponse = api.sync(lastBatch); - } catch (MatrixApiException | IOException e) { - throw new MatrixBackoffException("Could not partial sync", e); - } - - if (maybePartialSyncResponse.isPresent()) { - syncResponse = maybePartialSyncResponse.get(); - lastBatch = syncResponse.nextBatch(); - - stateSynchronizer.synchronizeState(syncResponse); - - if (eventNotifier != null) { - eventNotifier.notifyFromSynchronizationResponse(state, syncResponse); - } + void syncContinuous() throws InterruptedException; - if (persistedState != null) { - persistedState.setLastBatch(lastBatch); - } - } - - currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; - } - - } catch (MatrixBackoffException e) { - log.warn("Sync failed: {}, backing off for {}s", e.getCause().getClass().getName(), currentBackoffInSec); - - clearSyncState(); - Thread.sleep(currentBackoffInSec * 1000); - currentBackoffInSec = Math.min(currentBackoffInSec * 2, BACKOFF_MAX_IN_SEC); - } - } - - clearSyncState(); - interruptionRequested = false; - currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; - } - - private void clearSyncState() { + /** + * Requests the matrix client to stop syncing and terminate. + * May be called from a different thread. + */ + void requestStopOfSync(); - authentication.clear(); - state = null; - } /** * Returns whether the matrix client is currently connected to the server or not. * * @return {@code true} if the client is currently connected to the server, {@code false} otherwise. */ - public boolean isConnected() { - - return state != null; - } + boolean isConnected(); /** * Returns the current state of the matrix client. * * @return A {@link MatrixState} object if currently connected to a server, {@link Optional#empty()} otherwise. */ - public Optional getState() { - - return Optional.ofNullable(state); - } + Optional getState(); /** * Attempts to send a message to the specified room. @@ -218,20 +80,7 @@ public Optional getState() { * @param messageBody The body of the message to send. * @return A {@link MatrixEventId} containing the id of the event that was sent or {@link Optional#empty()} if sending the message did not succeed. */ - public Optional sendMessage(MatrixRoomId roomId, String messageBody) { - - try { - return MatrixEventId.from( - api.sendEvent(roomId.getFormatted(), "m.room.message", new MessageDto(messageBody, "m.text")) - ); - } catch (InterruptedException | IOException e) { - log.error("Failed to send message", e); - } catch (MatrixApiException e) { - log.warn("Could not send message", e); - } - - return Optional.empty(); - } + Optional sendMessage(MatrixRoomId roomId, String messageBody); /** * Attempts to add a reaction to an event (a message of the time). @@ -241,21 +90,7 @@ public Optional sendMessage(MatrixRoomId roomId, String messageBo * @param reaction The reaction to send. * @return A {@link MatrixEventId} containing the id of the event that was sent or {@link Optional#empty()} if sending the reaction did not succeed. */ - public Optional addReaction(MatrixRoomId roomId, MatrixEventId eventId, String reaction) { - - final var reactionDto = new ReactionDto(new ReactionRelatesToDto(eventId.getFormatted(), reaction)); - try { - return MatrixEventId.from( - api.sendEvent(roomId.getFormatted(), "m.reaction", reactionDto) - ); - } catch (InterruptedException | IOException e) { - log.error("Failed to add reaction", e); - } catch (MatrixApiException e) { - log.warn("Could not add reaction", e); - } - - return Optional.empty(); - } + Optional addReaction(MatrixRoomId roomId, MatrixEventId eventId, String reaction); /** * Attempts to join a room. @@ -263,19 +98,7 @@ public Optional addReaction(MatrixRoomId roomId, MatrixEventId ev * @param roomId The id of the room to join. * @return {@code true} if joining the room was successful, {@code false} otherwise. */ - public boolean joinRoom(MatrixRoomId roomId) { - - try { - api.joinRoom(roomId.getFormatted(), "i'm a bot"); - return true; - } catch (InterruptedException | IOException e) { - log.error("Failed to join room", e); - } catch (MatrixApiException e) { - log.warn("Could not join room", e); - } - - return false; - } + boolean joinRoom(MatrixRoomId roomId); /** * Attempts to leave a room. @@ -283,17 +106,5 @@ public boolean joinRoom(MatrixRoomId roomId) { * @param roomId The id of the room to leave. * @return {@code true} if leaving the room was successful, {@code false} otherwise. */ - public boolean leaveRoom(MatrixRoomId roomId) { - - try { - api.leaveRoom(roomId.getFormatted(), "i'm a bot"); - return true; - } catch (InterruptedException | IOException e) { - log.error("Failed to leave room", e); - } catch (MatrixApiException e) { - log.warn("Could not leave room", e); - } - - return false; - } + boolean leaveRoom(MatrixRoomId roomId); } diff --git a/src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java b/src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java index 70f7226..67a77f1 100644 --- a/src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java +++ b/src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java @@ -5,13 +5,14 @@ import org.synyx.matrix.bot.domain.MatrixRoomId; import org.synyx.matrix.bot.domain.MatrixRoomInvite; import org.synyx.matrix.bot.domain.MatrixUserId; +import org.synyx.matrix.bot.internal.MatrixClientImpl; /** * An interface providing callbacks for things happening on the matrix server that were received by the client. * All methods have a default implementation that does nothing, so implementing classes only need to override whatever * they want to listen to. *

- * Any reactions to events happening shall be performed using the appropriate {@link MatrixClient} instance. + * Any reactions to events happening shall be performed using the appropriate {@link MatrixClientImpl} instance. */ public interface MatrixEventConsumer { diff --git a/src/main/java/org/synyx/matrix/bot/internal/MatrixClientImpl.java b/src/main/java/org/synyx/matrix/bot/internal/MatrixClientImpl.java new file mode 100644 index 0000000..a4299b3 --- /dev/null +++ b/src/main/java/org/synyx/matrix/bot/internal/MatrixClientImpl.java @@ -0,0 +1,249 @@ +package org.synyx.matrix.bot.internal; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.synyx.matrix.bot.MatrixClient; +import org.synyx.matrix.bot.MatrixCommunicationException; +import org.synyx.matrix.bot.MatrixEventConsumer; +import org.synyx.matrix.bot.MatrixPersistedStateProvider; +import org.synyx.matrix.bot.MatrixState; +import org.synyx.matrix.bot.domain.MatrixEventId; +import org.synyx.matrix.bot.domain.MatrixRoomId; +import org.synyx.matrix.bot.domain.MatrixUserId; +import org.synyx.matrix.bot.internal.api.MatrixApi; +import org.synyx.matrix.bot.internal.api.MatrixApiException; +import org.synyx.matrix.bot.internal.api.dto.MessageDto; +import org.synyx.matrix.bot.internal.api.dto.ReactionDto; +import org.synyx.matrix.bot.internal.api.dto.ReactionRelatesToDto; +import org.synyx.matrix.bot.internal.api.dto.SyncResponseDto; + +import java.io.IOException; +import java.util.Optional; + +@Slf4j +public class MatrixClientImpl implements MatrixClient { + + private static final long DEFAULT_BACKOFF_IN_SEC = 3; + private static final long BACKOFF_MAX_IN_SEC = 60; + + private final MatrixAuthentication authentication; + private final ObjectMapper objectMapper; + private final MatrixApi api; + private MatrixState state; + private MatrixStateSynchronizer stateSynchronizer; + private MatrixPersistedStateProvider persistedState; + private MatrixEventNotifier eventNotifier; + private boolean interruptionRequested; + private long currentBackoffInSec; + + public MatrixClientImpl(String url, String username, String password) { + + this.authentication = new MatrixAuthentication(username, password); + this.objectMapper = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true) + .build(); + this.api = new MatrixApi(url, authentication, objectMapper); + this.state = null; + this.eventNotifier = null; + this.interruptionRequested = false; + this.currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; + } + + @Override + public void setEventCallback(MatrixEventConsumer eventConsumer) { + + this.eventNotifier = MatrixEventNotifier.from(objectMapper, eventConsumer).orElse(null); + } + + @Override + public void setPersistedStateProvider(MatrixPersistedStateProvider persistedState) { + + this.persistedState = persistedState; + } + + @Override + public void syncContinuous() throws InterruptedException { + + while (!interruptionRequested) { + try { + if (!authentication.isAuthenticated()) { + try { + api.login(); + } catch (IOException e) { + throw new MatrixBackoffException("Failed to login to matrix server!", e); + } catch (MatrixApiException e) { + throw new MatrixCommunicationException("Failed to login to matrix server!", e); + } + + log.info("Successfully logged in to matrix server as {}", + authentication.getUserId() + .map(MatrixUserId::toString) + .orElse("UNKNOWN") + ); + } + + state = new MatrixState(authentication.getUserId().orElseThrow(IllegalStateException::new)); + stateSynchronizer = new MatrixStateSynchronizer(state, objectMapper); + + SyncResponseDto syncResponse; + try { + syncResponse = api.syncFull() + .orElseThrow(() -> new MatrixCommunicationException("No data in initial sync")); + } catch (MatrixApiException | IOException e) { + throw new MatrixBackoffException("Failed to perform initial sync", e); + } + + String lastBatch = syncResponse.nextBatch(); + stateSynchronizer.synchronizeState(syncResponse); + + if (eventNotifier != null) { + eventNotifier.getConsumer().onConnected(state); + } + + if (persistedState != null) { + final var maybePersistedLastBatch = persistedState.getLastBatch(); + if (maybePersistedLastBatch.isPresent()) { + lastBatch = maybePersistedLastBatch.get(); + } else { + persistedState.setLastBatch(lastBatch); + } + } + + while (!interruptionRequested) { + Optional maybePartialSyncResponse; + + try { + maybePartialSyncResponse = api.sync(lastBatch); + } catch (MatrixApiException | IOException e) { + throw new MatrixBackoffException("Could not partial sync", e); + } + + if (maybePartialSyncResponse.isPresent()) { + syncResponse = maybePartialSyncResponse.get(); + lastBatch = syncResponse.nextBatch(); + + stateSynchronizer.synchronizeState(syncResponse); + + if (eventNotifier != null) { + eventNotifier.notifyFromSynchronizationResponse(state, syncResponse); + } + + if (persistedState != null) { + persistedState.setLastBatch(lastBatch); + } + } + + currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; + } + + } catch (MatrixBackoffException e) { + log.warn("Sync failed: {}, backing off for {}s", e.getCause().getClass().getName(), currentBackoffInSec); + + clearSyncState(); + Thread.sleep(currentBackoffInSec * 1000); + currentBackoffInSec = Math.min(currentBackoffInSec * 2, BACKOFF_MAX_IN_SEC); + } + } + + clearSyncState(); + interruptionRequested = false; + currentBackoffInSec = DEFAULT_BACKOFF_IN_SEC; + } + + @Override + public void requestStopOfSync() { + + interruptionRequested = true; + api.terminateOpenConnections(); + } + + private void clearSyncState() { + + authentication.clear(); + state = null; + } + + @Override + public boolean isConnected() { + + return state != null; + } + + @Override + public Optional getState() { + + return Optional.ofNullable(state); + } + + @Override + public Optional sendMessage(MatrixRoomId roomId, String messageBody) { + + try { + return MatrixEventId.from( + api.sendEvent(roomId.getFormatted(), "m.room.message", new MessageDto(messageBody, "m.text")) + ); + } catch (InterruptedException | IOException e) { + log.error("Failed to send message", e); + } catch (MatrixApiException e) { + log.warn("Could not send message", e); + } + + return Optional.empty(); + } + + @Override + public Optional addReaction(MatrixRoomId roomId, MatrixEventId eventId, String reaction) { + + final var reactionDto = new ReactionDto(new ReactionRelatesToDto(eventId.getFormatted(), reaction)); + try { + return MatrixEventId.from( + api.sendEvent(roomId.getFormatted(), "m.reaction", reactionDto) + ); + } catch (InterruptedException | IOException e) { + log.error("Failed to add reaction", e); + } catch (MatrixApiException e) { + log.warn("Could not add reaction", e); + } + + return Optional.empty(); + } + + @Override + public boolean joinRoom(MatrixRoomId roomId) { + + try { + api.joinRoom(roomId.getFormatted(), "i'm a bot"); + return true; + } catch (InterruptedException | IOException e) { + log.error("Failed to join room", e); + } catch (MatrixApiException e) { + log.warn("Could not join room", e); + } + + return false; + } + + @Override + public boolean leaveRoom(MatrixRoomId roomId) { + + try { + api.leaveRoom(roomId.getFormatted(), "i'm a bot"); + return true; + } catch (InterruptedException | IOException e) { + log.error("Failed to leave room", e); + } catch (MatrixApiException e) { + log.warn("Could not leave room", e); + } + + return false; + } +} diff --git a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java index 432b1e0..c11267a 100644 --- a/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java +++ b/src/main/java/org/synyx/matrix/bot/internal/api/MatrixApi.java @@ -39,10 +39,10 @@ public class MatrixApi { private final HttpClient httpClient; private final ObjectMapper objectMapper; - public MatrixApi(String hostname, MatrixAuthentication authentication, ObjectMapper objectMapper) { + public MatrixApi(String url, MatrixAuthentication authentication, ObjectMapper objectMapper) { try { - this.baseUri = new URI(hostname); + this.baseUri = new URI(url); } catch (URISyntaxException e) { throw new MatrixCommunicationException("Invalid matrix URI", e); }