Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 100 additions & 150 deletions src/main/java/org/synyx/matrix/bot/MatrixClient.java
Original file line number Diff line number Diff line change
@@ -1,160 +1,110 @@
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.MatrixEventNotifier;
import org.synyx.matrix.bot.internal.MatrixStateSynchronizer;
import org.synyx.matrix.bot.internal.api.MatrixApi;
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.MatrixClientImpl;

import java.util.Optional;

@Slf4j
public class MatrixClient {

private final MatrixAuthentication authentication;
private final ObjectMapper objectMapper;
private final MatrixApi api;
private MatrixState state;
private MatrixStateSynchronizer stateSynchronizer;
private MatrixPersistedState persistedState;
private MatrixEventNotifier eventNotifier;
private boolean interruptionRequested;

public MatrixClient(String hostname, 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;
/**
* An interface for a client connecting to a matrix server.
* Serves as the main method of communicating with the server.
*/
public interface MatrixClient {

/**
* 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) {

return new MatrixClientImpl(url, username, password);
}

public void setEventCallback(MatrixEventConsumer eventConsumer) {

this.eventNotifier = MatrixEventNotifier.from(objectMapper, eventConsumer).orElse(null);
}

public void setPersistedState(MatrixPersistedState persistedState) {

this.persistedState = persistedState;
}

public void requestStopOfSync() {

interruptionRequested = true;
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;
}

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) {
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);
}
}
}

interruptionRequested = false;
}

public boolean isConnected() {

return state != null;
}

public Optional<MatrixState> getState() {

return Optional.ofNullable(state);
}

public boolean sendMessage(MatrixRoomId roomId, String messageBody) {

return api.sendEvent(roomId.getFormatted(), "m.room.message", new MessageDto(messageBody, "m.text"));
}

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);
}

public boolean joinRoom(MatrixRoomId roomId) {

return api.joinRoom(roomId.getFormatted(), "hello there");
}

public boolean leaveRoom(MatrixRoomId roomId) {

return api.leaveRoom(roomId.getFormatted(), "bai");
}
/**
* 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.
*/
void setEventCallback(MatrixEventConsumer eventConsumer);

/**
* 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
*/
void setPersistedStateProvider(MatrixPersistedStateProvider persistedState);


/**
* 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
*/
void syncContinuous() throws InterruptedException;

/**
* Requests the matrix client to stop syncing and terminate.
* May be called from a different thread.
*/
void requestStopOfSync();


/**
* 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.
*/
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.
*/
Optional<MatrixState> getState();

/**
* 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.
*/
Optional<MatrixEventId> sendMessage(MatrixRoomId roomId, String messageBody);

/**
* 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.
*/
Optional<MatrixEventId> addReaction(MatrixRoomId roomId, MatrixEventId eventId, String reaction);

/**
* 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.
*/
boolean joinRoom(MatrixRoomId roomId);

/**
* 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.
*/
boolean leaveRoom(MatrixRoomId roomId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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) {

super(message);
}

public MatrixCommunicationException(String message, Throwable cause) {

super(message, cause);
}
}
70 changes: 58 additions & 12 deletions src/main/java/org/synyx/matrix/bot/MatrixEventConsumer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,76 @@
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.
* <p>
* Any reactions to events happening shall be performed using the appropriate {@link MatrixClientImpl} 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) {

}
}
}
10 changes: 0 additions & 10 deletions src/main/java/org/synyx/matrix/bot/MatrixPersistedState.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.synyx.matrix.bot;

import java.util.Optional;

public interface MatrixPersistedStateProvider {

Optional<String> getLastBatch();

void setLastBatch(String value);
}
Loading