io.github.ma1uta.matrix.bot.Bot Maven / Gradle / Ivy
The newest version!
/*
* Copyright Anatoliy Sablin [email protected]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.ma1uta.matrix.bot;
import io.github.ma1uta.matrix.client.MatrixClient;
import io.github.ma1uta.matrix.client.StandaloneClient;
import io.github.ma1uta.matrix.client.model.account.RegisterRequest;
import io.github.ma1uta.matrix.client.model.filter.FilterData;
import io.github.ma1uta.matrix.client.model.filter.RoomEventFilter;
import io.github.ma1uta.matrix.client.model.filter.RoomFilter;
import io.github.ma1uta.matrix.common.Id;
import io.github.ma1uta.matrix.event.Event;
import io.github.ma1uta.matrix.event.RoomEvent;
import io.github.ma1uta.matrix.event.RoomMember;
import io.github.ma1uta.matrix.event.RoomMessage;
import io.github.ma1uta.matrix.event.content.RoomMemberContent;
import io.github.ma1uta.matrix.event.content.RoomMessageContent;
import io.github.ma1uta.matrix.event.message.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
/**
* Matrix bot client.
*
* @param bot configuration.
* @param bot dao.
* @param service.
* @param extra data.
*/
public class Bot, S extends PersistentService, E> {
private static final Logger LOGGER = LoggerFactory.getLogger(Bot.class);
private final Map> commands;
private BiConsumer, D> initAction;
private Context context;
private final boolean exitOnEmptyRooms;
private final Set skipTimelineRooms = new HashSet<>();
public Bot(String asToken, boolean exitOnEmptyRooms, C config, S service,
List>> commandsClasses) {
this.context = init(asToken, config, service);
this.exitOnEmptyRooms = exitOnEmptyRooms;
this.commands = new HashMap<>(commandsClasses.size());
commandsClasses.forEach(cl -> {
try {
Command command = cl.getDeclaredConstructor().newInstance();
this.commands.put(command.name(), command);
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
LOGGER.error("Cannot create new instance of the command: " + cl.getCanonicalName(), e);
}
});
}
protected Context init(String asToken, C config, S service) {
String userId = config.getUserId();
MatrixClient matrixClient = new StandaloneClient.Builder()
.userId(Id.localPart(userId).orElseThrow(() -> new IllegalArgumentException("Wrong userId, missing localpart: " + userId)))
.domain(Id.serverName(userId).orElseThrow(() -> new IllegalArgumentException("Wrong userId, missing server name: " + userId)))
.accessToken(asToken).build();
Context context = new Context<>(matrixClient, service, this);
context.setConfig(config);
return context;
}
/**
* Run startup action.
*/
public void init() {
Context context = getContext();
C config = context.getConfig();
MatrixClient matrixClient = context.getMatrixClient();
if (matrixClient instanceof StandaloneClient) {
((StandaloneClient) matrixClient).authAsync().login(config.getUserId(), config.getPassword()).join();
}
if (getInitAction() != null) {
context.runInTransaction((ctx, dao) -> {
getInitAction().accept(ctx, dao);
});
}
}
public Map> getCommands() {
return commands;
}
public Context getContext() {
return context;
}
public BiConsumer, D> getInitAction() {
return initAction;
}
public boolean isExitOnEmptyRooms() {
return exitOnEmptyRooms;
}
public void setInitAction(BiConsumer, D> initAction) {
this.initAction = initAction;
}
public Set getSkipTimelineRooms() {
return skipTimelineRooms;
}
/**
* Register a new bot.
*
* After registration setup a filter to receive only message events.
*
* @return {@link LoopState#NEXT_STATE} always. Move to the next state.
*/
public LoopState newState() {
getContext().runInTransaction((context, dao) -> {
LOGGER.debug("Start registration.");
BotConfig config = context.getConfig();
RegisterRequest registerRequest = new RegisterRequest();
registerRequest
.setUsername(Id.localPart(config.getUserId()).orElseThrow(() -> new IllegalArgumentException("Missing bot id.")));
registerRequest.setInitialDeviceDisplayName(config.getDisplayName());
registerRequest.setDeviceId(config.getDeviceId());
MatrixClient matrixClient = context.getMatrixClient();
matrixClient.account().register(registerRequest);
LOGGER.debug("Set new display name: {}", config.getDisplayName());
matrixClient.profile().setDisplayName(config.getDisplayName());
RoomEventFilter roomEventFilter = new RoomEventFilter();
roomEventFilter.setTypes(Collections.singletonList(RoomMessage.TYPE));
RoomFilter roomFilter = new RoomFilter();
roomFilter.setTimeline(roomEventFilter);
FilterData filter = new FilterData();
filter.setRoom(roomFilter);
config.setFilterId(matrixClient.filter().uploadFilter(filter).getFilterId());
LOGGER.debug("Set new filter: {}", config.getFilterId());
config.setState(BotState.REGISTERED);
LOGGER.debug("Finish registration.");
});
return LoopState.NEXT_STATE;
}
protected LoopState registeredState(Map> eventMap) {
LOGGER.debug("Wait for invite");
if (!eventMap.isEmpty()) {
return joinRoom(eventMap) ? LoopState.NEXT_STATE : LoopState.RUN;
}
return LoopState.RUN;
}
/**
* Join to room.
*
* @param eventMap invited eventMap. Map <roomId> - <[event]> room_id to invite_state.
* @return true if bot joined else false.
*/
public boolean joinRoom(Map> eventMap) {
return getContext().runInTransaction((context, dao) -> {
LOGGER.debug("Start joining.");
boolean joined = false;
for (Map.Entry> eventEntry : eventMap.entrySet()) {
List inviteEvents = eventEntry.getValue().stream().peek(state -> {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Event type: {}", state.getType());
}
}).filter(state -> {
if (state instanceof RoomMember) {
RoomMember roomMember = (RoomMember) state;
String membership = roomMember.getContent().getMembership();
LOGGER.debug("Membership: {}", membership);
return RoomMemberContent.INVITE.equals(membership);
}
return false;
}).collect(Collectors.toList());
for (Event state : inviteEvents) {
if (state instanceof RoomEvent) {
String roomId = eventEntry.getKey();
LOGGER.debug("Join to room {}", roomId);
context.getMatrixClient().room().joinByIdOrAlias(roomId, null, null);
C config = context.getConfig();
config.setState(BotState.JOINED);
config.setOwner(((RoomEvent) state).getSender().toString());
LOGGER.debug("Finish joining");
joined = true;
}
}
}
return joined;
});
}
/**
* Delete bot.
*
* @return stop running.
*/
public LoopState deletedState() {
getContext().runInTransaction((context, dao) -> {
LOGGER.debug("Delete bot");
MatrixClient matrixClient = context.getMatrixClient();
if (matrixClient instanceof StandaloneClient) {
((StandaloneClient) matrixClient).account().deactivate(null);
}
dao.delete(context.getConfig());
});
return LoopState.EXIT;
}
/**
* Process commands.
*
* @param roomId room id.
* @param events events.
* @return next loop state.
*/
public LoopState processJoinedRoom(String roomId, List events) {
String lastEvent = null;
long lastOriginTs = 0;
MatrixClient matrixClient = getContext().getMatrixClient();
boolean invoked = false;
for (Event event : events) {
if (!getSkipTimelineRooms().contains(roomId)) {
LOGGER.debug("Process events");
invoked = processEvent(roomId, event);
} else {
LOGGER.debug("Skip timelines");
}
if (event instanceof RoomEvent) {
RoomEvent roomEvent = (RoomEvent) event;
if (roomEvent.getOriginServerTs() != null && roomEvent.getOriginServerTs() > lastOriginTs) {
lastOriginTs = roomEvent.getOriginServerTs();
lastEvent = roomEvent.getEventId();
}
}
}
C config = getContext().getConfig();
boolean read = config.getReceiptPolicy() == null || ReceiptPolicy.READ.equals(config.getReceiptPolicy());
boolean executed = config.getReceiptPolicy() != null && ReceiptPolicy.EXECUTED.equals(config.getReceiptPolicy()) && invoked;
LOGGER.debug("Read: {}", read);
LOGGER.debug("Executed: {}", executed);
LOGGER.debug("Last event: {}", lastEvent);
if (lastEvent != null && (read || executed)) {
LOGGER.debug("send receipt");
matrixClient.receipt().sendReceipt(roomId, lastEvent);
}
getSkipTimelineRooms().remove(roomId);
return LoopState.RUN;
}
/**
* Process an one event.
*
* @param roomId room id.
* @param event event.
* @return {@code true} if any command was invoked, else {@code false}.
*/
protected boolean processEvent(String roomId, Event event) {
MatrixClient matrixClient = getContext().getMatrixClient();
C config = getContext().getConfig();
boolean invoked = false;
if (event instanceof RoomMessage) {
RoomMessage roomMessage = (RoomMessage) event;
RoomMessageContent content = (RoomMessageContent) roomMessage.getContent();
String body = content.getBody().trim();
boolean permit = permit(event);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sender: {}", roomMessage.getSender());
LOGGER.debug("Msgtype: {}", content.getMsgtype());
LOGGER.debug("Permit: {}", permit);
}
boolean defaultCommand = config.getDefaultCommand() != null && !config.getDefaultCommand().trim().isEmpty();
if (!matrixClient.getUserId().equals(roomMessage.getSender())
&& content instanceof Text
&& permit
&& (body.startsWith(getPrefix()) || defaultCommand)) {
try {
invoked = getContext().runInTransaction((context, dao) -> {
return processAction(roomId, roomMessage, body);
});
} catch (Exception e) {
LOGGER.error(String.format("Cannot perform action '%s'", body), e);
}
}
}
return invoked;
}
/**
* Permission check.
*
* @param event event.
* @return {@code true}, if process event, else {@code false}.
*/
protected boolean permit(Event event) {
C config = getContext().getConfig();
return config.getPolicy() == null
|| AccessPolicy.ALL.equals(config.getPolicy())
|| (event instanceof RoomEvent && config.getOwner().equals(((RoomEvent) event).getSender().toString()));
}
/**
* Get bot's command prefix.
*
* @return command prefix.
*/
public String getPrefix() {
C config = getContext().getConfig();
String prefix = config.getPrefix();
return prefix == null ? "!" : prefix.replaceAll("\\{\\{display_name}}", config.getDisplayName());
}
/**
* Process action.
*
* @param roomId room id.
* @param event event.
* @param content command.
* @return {@code true} if invoked command, else {@code false}.
*/
protected boolean processAction(String roomId, RoomEvent event, String content) {
String contentWithoutPrefix = content.substring(getPrefix().length());
String[] arguments = contentWithoutPrefix.trim().split("\\s");
String commandName = arguments[0];
Command command = getCommands().get(commandName);
C config = getContext().getConfig();
String argument = Arrays.stream(arguments).skip(1).collect(Collectors.joining(" "));
String defaultCommand = config.getDefaultCommand();
if (command == null && defaultCommand != null && !defaultCommand.trim().isEmpty()) {
command = getCommands().get(defaultCommand);
argument = content;
}
if (command != null) {
LOGGER.debug("invoke command: {}", command.getClass());
return command.invoke(getContext(), roomId, event, argument);
} else {
getContext().getMatrixClient().event().sendNotice(roomId, "Unknown command: " + commandName);
return false;
}
}
}