diff --git a/src/main/kotlin/org/fuchss/matrix/yarb/Helper.kt b/src/main/kotlin/org/fuchss/matrix/yarb/Helper.kt new file mode 100644 index 0000000..47a4b5a --- /dev/null +++ b/src/main/kotlin/org/fuchss/matrix/yarb/Helper.kt @@ -0,0 +1,11 @@ +package org.fuchss.matrix.yarb + +import net.folivo.trixnity.client.room.RoomService +import net.folivo.trixnity.core.model.EventId +import org.fuchss.matrix.bots.firstWithTimeout + +suspend fun RoomService.getMessageId(transactionId: String): EventId? { + val outboxWithTransaction = this.getOutbox().firstWithTimeout { it[transactionId] != null } ?: return null + val transaction = outboxWithTransaction[transactionId] ?: return null + return transaction.firstWithTimeout { it?.eventId != null }?.eventId +} diff --git a/src/main/kotlin/org/fuchss/matrix/yarb/Main.kt b/src/main/kotlin/org/fuchss/matrix/yarb/Main.kt index fe46afd..d76a4b2 100644 --- a/src/main/kotlin/org/fuchss/matrix/yarb/Main.kt +++ b/src/main/kotlin/org/fuchss/matrix/yarb/Main.kt @@ -51,16 +51,6 @@ fun main() { matrixBot.subscribeContent { event -> handleCommand(commands, event, matrixBot, config, ReminderCommand.COMMAND_NAME) } matrixBot.subscribeContent { encEvent -> handleEncryptedCommand(commands, encEvent, matrixBot, config, ReminderCommand.COMMAND_NAME) } - // Listen to own messages (i.e., reminder messages) - matrixBot.subscribeContent(listenNonUsers = true, listenBotEvents = true) { eventId, sender, roomId, content -> - reminderCommand.handleBotMessageForReminder(matrixBot, eventId, sender, roomId, content) - } - matrixBot.subscribeContent(listenNonUsers = true, listenBotEvents = true) { encryptedEvent -> - decryptMessage(encryptedEvent, matrixBot) { eventId, userId, roomId, text -> - reminderCommand.handleBotMessageForReminder(matrixBot, eventId, userId, roomId, text) - } - } - // Listen for edits of user messages matrixBot.subscribeContent { eventId, sender, roomId, content -> reminderCommand.handleUserEditMessage(matrixBot, eventId, sender, roomId, content) @@ -73,9 +63,10 @@ fun main() { matrixBot.subscribeContent { event -> reminderCommand.handleUserDeleteMessage(matrixBot, event) } val loggedOut = matrixBot.startBlocking() - timer.cancel() // After Shutdown + timer.cancel() + if (loggedOut) { // Cleanup database val databaseFiles = listOf(File(config.dataDirectory + "/database.mv.db"), File(config.dataDirectory + "/database.trace.db")) diff --git a/src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt b/src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt index dabe0db..9339193 100644 --- a/src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt +++ b/src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt @@ -6,26 +6,15 @@ import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.registerKotlinModule -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeoutOrNull -import net.folivo.trixnity.client.room.RoomService +import net.folivo.trixnity.client.room.getTimelineEventReactionAggregation import net.folivo.trixnity.client.room.message.mentions import net.folivo.trixnity.client.room.message.reply import net.folivo.trixnity.client.room.message.text -import net.folivo.trixnity.client.store.eventId -import net.folivo.trixnity.client.store.relatesTo -import net.folivo.trixnity.client.store.sender import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId -import net.folivo.trixnity.core.model.events.m.RelatesTo -import net.folivo.trixnity.core.model.events.m.RelationType import org.fuchss.matrix.bots.MatrixBot import org.fuchss.matrix.bots.emoji import org.fuchss.matrix.bots.matrixTo @@ -36,7 +25,7 @@ import java.nio.file.StandardCopyOption import java.time.LocalTime import java.util.Timer import java.util.TimerTask -import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds class TimerManager(private val matrixBot: MatrixBot, javaTimer: Timer, config: Config) { companion object { @@ -76,36 +65,20 @@ class TimerManager(private val matrixBot: MatrixBot, javaTimer: Timer, config: C } }, millisecondsToNextMinute, - 1.minutes.inWholeMilliseconds + 30.seconds.inWholeMilliseconds ) } - fun addTimer( - roomId: RoomId, - requestMessage: EventId, - timeToRemind: LocalTime, - content: String - ) { - val timer = TimerData(roomId.full, requestMessage.full, timeToRemind, content, null) + fun addTimer(timer: TimerData) { timers.add(timer) saveTimers() } - fun addBotMessageToTimer( - requestMessage: EventId, - botMessageId: EventId - ): Boolean { - val timer = timers.find { it.requestMessage() == requestMessage } ?: return false - timer.botMessageId = botMessageId.full - saveTimers() - return true - } - - fun removeByRequestMessage(eventId: EventId): EventId? { - val timerData = timers.find { it.requestMessage() == eventId } ?: return null - timers.remove(timerData) + fun removeByOriginalRequestMessage(eventId: EventId): TimerData? { + val timer = timers.find { it.originalRequestMessage() == eventId } ?: return null + timers.remove(timer) saveTimers() - return timerData.botMessageId() + return timer } private fun removeTimer(timer: TimerData) { @@ -123,18 +96,13 @@ class TimerManager(private val matrixBot: MatrixBot, javaTimer: Timer, config: C private suspend fun remind(timer: TimerData) { try { - val roomId = timer.roomId() - val messageId = timer.botMessageId() ?: return - val timelineEvent = matrixBot.getTimelineEvent(roomId, messageId) ?: return - - val remainingReactions = removeReactionOfBot(roomId, messageId) - + val remainingReactions = removeReactionOfBot(timer) if (remainingReactions.isEmpty()) { return } - matrixBot.room().sendMessage(roomId) { - reply(timelineEvent) + matrixBot.room().sendMessage(timer.roomId()) { + reply(timer.botMessageId(), null) mentions(remainingReactions.toSet()) text("'${timer.content}' ${remainingReactions.joinToString(", ") { it.matrixTo() }}") } @@ -147,59 +115,58 @@ class TimerManager(private val matrixBot: MatrixBot, javaTimer: Timer, config: C * Remove the reaction of the bot from the message * @return the list of users reacted to the message */ - private suspend fun removeReactionOfBot( - roomId: RoomId, - messageId: EventId - ): List { - val allReactions = matrixBot.room().getTimelineEventReactionAggregationWithIds(roomId, messageId).first() + private suspend fun removeReactionOfBot(timer: TimerData): List { + timer.redactBotReaction(matrixBot) + val allReactions = matrixBot.room().getTimelineEventReactionAggregation(timer.roomId(), timer.botMessageId()).first().reactions val reactions = allReactions[EMOJI] ?: return emptyList() - - val botReaction = reactions.find { it.second == matrixBot.self() } - if (botReaction != null) { - matrixBot.roomApi().redactEvent(roomId, botReaction.first) - } else { - logger.warn("Could not find bot reaction to remove for message $messageId") - } - return reactions.filter { it.second != matrixBot.self() }.map { it.second } + return reactions.filter { it != matrixBot.self() } } - // Adapted from net/folivo/trixnity/client/room/TimelineEventAggregation.kt - private fun RoomService.getTimelineEventReactionAggregationWithIds( - roomId: RoomId, - eventId: EventId - ): Flow>>> = - getTimelineEventRelations(roomId, eventId, RelationType.Annotation) - .map { it?.keys.orEmpty() } - .map { relations -> - coroutineScope { - relations.map { relatedEvent -> - async { - withTimeoutOrNull(1.minutes) { getTimelineEvent(roomId, relatedEvent).first() } - } - }.awaitAll() - }.filterNotNull() - .mapNotNull { - val relatesTo = it.relatesTo as? RelatesTo.Annotation ?: return@mapNotNull null - val key = relatesTo.key ?: return@mapNotNull null - key to (it.eventId to it.sender) - } - .distinct() - .groupBy { it.first } - .mapValues { entry -> entry.value.map { it.second }.toSet() } - } - - private data class TimerData( + data class TimerData( @JsonProperty val roomId: String, - @JsonProperty val requestMessage: String, + @JsonProperty val originalRequestMessage: String, + @JsonProperty val currentRequestMessage: String, @JsonProperty val timeToRemind: LocalTime, @JsonProperty val content: String, - @JsonProperty var botMessageId: String? + @JsonProperty val botMessageId: String, + @JsonProperty val botReactionMessageId: String ) { + constructor( + roomId: RoomId, + originalRequestMessage: EventId, + currentRequestMessage: EventId, + timeToRemind: LocalTime, + content: String, + botMessageId: EventId, + botReactionMessageId: EventId + ) : this( + roomId.full, + originalRequestMessage.full, + currentRequestMessage.full, + timeToRemind, + content, + botMessageId.full, + botReactionMessageId.full + ) + fun roomId() = RoomId(roomId) - fun requestMessage() = EventId(requestMessage) + fun originalRequestMessage() = EventId(originalRequestMessage) + + fun currentRequestMessage() = EventId(currentRequestMessage) - fun botMessageId() = botMessageId?.let { EventId(it) } + fun botMessageId() = EventId(botMessageId) + + fun botReactionMessageId() = EventId(botReactionMessageId) + + suspend fun redactAll(matrixBot: MatrixBot) { + redactBotReaction(matrixBot) + matrixBot.roomApi().redactEvent(roomId(), botMessageId()) + } + + suspend fun redactBotReaction(matrixBot: MatrixBot) { + matrixBot.roomApi().redactEvent(roomId(), botReactionMessageId()) + } } } diff --git a/src/main/kotlin/org/fuchss/matrix/yarb/commands/ReminderCommand.kt b/src/main/kotlin/org/fuchss/matrix/yarb/commands/ReminderCommand.kt index e229bf4..50bcc34 100644 --- a/src/main/kotlin/org/fuchss/matrix/yarb/commands/ReminderCommand.kt +++ b/src/main/kotlin/org/fuchss/matrix/yarb/commands/ReminderCommand.kt @@ -11,12 +11,12 @@ import net.folivo.trixnity.core.model.events.m.RelatesTo import net.folivo.trixnity.core.model.events.m.RelationType import net.folivo.trixnity.core.model.events.m.room.RedactionEventContent import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent -import net.folivo.trixnity.core.model.events.roomIdOrNull import net.folivo.trixnity.core.model.events.senderOrNull import org.fuchss.matrix.bots.MatrixBot import org.fuchss.matrix.bots.command.Command import org.fuchss.matrix.yarb.Config import org.fuchss.matrix.yarb.TimerManager +import org.fuchss.matrix.yarb.getMessageId import java.time.LocalTime class ReminderCommand(private val config: Config, private val timerManager: TimerManager) : Command() { @@ -38,6 +38,17 @@ class ReminderCommand(private val config: Config, private val timerManager: Time parameters: String, textEventId: EventId, textEvent: RoomMessageEventContent.TextBased.Text + ) { + // Handle New Messages + execute(matrixBot, roomId, parameters, textEventId, textEventId) + } + + private suspend fun execute( + matrixBot: MatrixBot, + roomId: RoomId, + parameters: String, + currentMessageEventId: EventId, + initialMessageEventId: EventId ) { val timeXmessage = parameters.split(" ", limit = 2) if (timeXmessage.size != 2) { @@ -61,40 +72,35 @@ class ReminderCommand(private val config: Config, private val timerManager: Time return } - val timelineEvent = matrixBot.getTimelineEvent(roomId, textEventId) ?: return - timerManager.addTimer(roomId, textEventId, time, timeXmessage[1]) - - matrixBot.room().sendMessage(roomId) { - reply(timelineEvent) - text("I'll remind all people at $time with '${timeXmessage[1]}'. If you want to receive a message please click on $EMOJI") - } - } - - suspend fun handleBotMessageForReminder( - matrixBot: MatrixBot, - eventId: EventId, - sender: UserId, - roomId: RoomId, - textEvent: RoomMessageEventContent.TextBased.Text - ) { - if (sender != matrixBot.self()) { - return - } - - val relatesTo = textEvent.relatesTo ?: return - - if (relatesTo.relationType != RelationType.Reply) { + logger.debug("Reminder for {} with '{}'", time, timeXmessage[1]) + + val botMessageTransactionId = + matrixBot.room().sendMessage(roomId) { + reply(initialMessageEventId, null) + text("I'll remind all people at $time with '${timeXmessage[1]}'. If you want to receive a message please click on $EMOJI") + } + logger.debug("Bot Message TransactionId: {}", botMessageTransactionId) + val botMessageId = matrixBot.room().getMessageId(botMessageTransactionId) + if (botMessageId == null) { + logger.error("Could not send bot message :( -- TransactionId: {}", botMessageTransactionId) return } - - val isRelated = timerManager.addBotMessageToTimer(relatesTo.eventId, eventId) - if (!isRelated) { + logger.debug("Bot Message Id: {}", botMessageId) + + val botReactionMessageTransactionId = + matrixBot.room().sendMessage(roomId) { + react(botMessageId, EMOJI) + } + logger.debug("Bot Reaction Message TransactionId: {}", botReactionMessageTransactionId) + val botReactionMessageId = matrixBot.room().getMessageId(botReactionMessageTransactionId) + if (botReactionMessageId == null) { + logger.error("Could not send bot reaction message :( -- TransactionId: {}", botReactionMessageTransactionId) return } + logger.debug("Bot Reaction Message Id: {}", botReactionMessageId) - matrixBot.room().sendMessage(roomId) { - react(eventId, EMOJI) - } + val timer = TimerManager.TimerData(roomId, initialMessageEventId, currentMessageEventId, time, timeXmessage[1], botMessageId, botReactionMessageId) + timerManager.addTimer(timer) } suspend fun handleUserDeleteMessage( @@ -104,10 +110,8 @@ class ReminderCommand(private val config: Config, private val timerManager: Time if (event.senderOrNull == matrixBot.self()) { return } - - val botMessage = timerManager.removeByRequestMessage(event.content.redacts) ?: return - val roomId = event.roomIdOrNull ?: return - matrixBot.roomApi().redactEvent(roomId, botMessage).getOrThrow() + val timer = timerManager.removeByOriginalRequestMessage(event.content.redacts) ?: return + timer.redactAll(matrixBot) } suspend fun handleUserEditMessage( @@ -117,15 +121,15 @@ class ReminderCommand(private val config: Config, private val timerManager: Time roomId: RoomId, textEvent: RoomMessageEventContent.TextBased.Text ) { + logger.debug("Edit Message: {}", textEvent) val relatesTo = textEvent.relatesTo ?: return if (relatesTo.relationType != RelationType.Replace) { return } - val relatedBotMessage = this.timerManager.removeByRequestMessage(relatesTo.eventId) ?: return - - matrixBot.roomApi().redactEvent(roomId, relatedBotMessage).getOrThrow() + val timer = this.timerManager.removeByOriginalRequestMessage(relatesTo.eventId) ?: return + timer.redactAll(matrixBot) val replace = (textEvent.relatesTo as? RelatesTo.Replace) ?: return val newBody = (replace.newContent as? RoomMessageEventContent.TextBased.Text)?.body ?: return @@ -134,6 +138,6 @@ class ReminderCommand(private val config: Config, private val timerManager: Time if (parameters.startsWith(COMMAND_NAME)) { parameters = parameters.substring(COMMAND_NAME.length).trim() } - execute(matrixBot, senderId, roomId, parameters, replace.eventId, textEvent) + execute(matrixBot, roomId, parameters, eventId, relatesTo.eventId) } }