From 62bff0ceacb67d098bea638704b91290f69c92d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Fuch=C3=9F?= Date: Sun, 28 Jul 2024 00:10:36 +0200 Subject: [PATCH] Save reminders in JSON --- pom.xml | 7 +- .../org/fuchss/matrix/yarb/Extensions.kt | 11 -- .../kotlin/org/fuchss/matrix/yarb/Main.kt | 12 +- .../org/fuchss/matrix/yarb/TimerManager.kt | 144 ++++++++++++++++++ .../matrix/yarb/commands/ReminderCommand.kt | 87 ++--------- 5 files changed, 164 insertions(+), 97 deletions(-) delete mode 100644 src/main/kotlin/org/fuchss/matrix/yarb/Extensions.kt create mode 100644 src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt diff --git a/pom.xml b/pom.xml index 65a1375..9013067 100644 --- a/pom.xml +++ b/pom.xml @@ -34,14 +34,13 @@ - com.vdurmont - emoji-java - 5.1.1 + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 org.fuchss matrix-bot-base - 0.11.0 + 0.11.1-SNAPSHOT org.jetbrains.kotlin diff --git a/src/main/kotlin/org/fuchss/matrix/yarb/Extensions.kt b/src/main/kotlin/org/fuchss/matrix/yarb/Extensions.kt deleted file mode 100644 index cfd156a..0000000 --- a/src/main/kotlin/org/fuchss/matrix/yarb/Extensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.fuchss.matrix.yarb - -import com.vdurmont.emoji.Emoji -import com.vdurmont.emoji.EmojiManager - -private const val MATRIX_TO_PREFIX = "https://matrix.to/#/" - -/** - * Convert a string emoji to an [Emoji]. - */ -fun String.emoji(): String = EmojiManager.getForAlias(this).unicode diff --git a/src/main/kotlin/org/fuchss/matrix/yarb/Main.kt b/src/main/kotlin/org/fuchss/matrix/yarb/Main.kt index d3fdceb..54bb60f 100644 --- a/src/main/kotlin/org/fuchss/matrix/yarb/Main.kt +++ b/src/main/kotlin/org/fuchss/matrix/yarb/Main.kt @@ -28,8 +28,14 @@ private lateinit var commands: List fun main() { runBlocking { val config = Config.load() + + val matrixClient = getMatrixClient(config) + val matrixBot = MatrixBot(matrixClient, config) + val timer = Timer(true) - val reminderCommand = ReminderCommand(config, timer) + val timerManager = TimerManager(matrixBot, timer, config) + + val reminderCommand = ReminderCommand(config, timerManager) commands = listOf( HelpCommand(config, "YARB") { @@ -41,10 +47,6 @@ fun main() { reminderCommand ) - val matrixClient = getMatrixClient(config) - - val matrixBot = MatrixBot(matrixClient, config) - // Command Handling matrixBot.subscribeContent { event -> handleCommand(commands, event, matrixBot, config, ReminderCommand.COMMAND_NAME) } matrixBot.subscribeContent { encEvent -> handleEncryptedCommand(commands, encEvent, matrixBot, config, ReminderCommand.COMMAND_NAME) } diff --git a/src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt b/src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt new file mode 100644 index 0000000..83e9a8a --- /dev/null +++ b/src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt @@ -0,0 +1,144 @@ +package org.fuchss.matrix.yarb + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.readValue +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import net.folivo.trixnity.client.room.getTimelineEventReactionAggregation +import net.folivo.trixnity.client.room.message.reply +import net.folivo.trixnity.client.room.message.text +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.RoomId +import org.fuchss.matrix.bots.MatrixBot +import org.fuchss.matrix.bots.emoji +import org.fuchss.matrix.bots.matrixTo +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.time.LocalTime +import java.util.Timer +import java.util.TimerTask +import kotlin.time.Duration.Companion.minutes + +class TimerManager(private val matrixBot: MatrixBot, javaTimer: Timer, config: Config) { + companion object { + val EMOJI = ":+1:".emoji() + private val logger = LoggerFactory.getLogger(TimerManager::class.java) + } + + private val objectMapper = ObjectMapper().registerKotlinModule().registerModule(JavaTimeModule()) + private val timerFileLocation = config.dataDirectory + "/timers.json" + private val timers = mutableListOf() + + init { + val timerFile = File(timerFileLocation) + if (timerFile.exists()) { + val timersFromFile: List = objectMapper.readValue(timerFile) + timers.addAll(timersFromFile) + } + } + + init { + val millisecondsToNextMinute = (60 - LocalTime.now().second) * 1000L + javaTimer.schedule( + object : TimerTask() { + override fun run() { + runBlocking { + logger.debug("Reminders: {}", timers) + val timerCopy = timers.toList() + val now = LocalTime.now() + for (timer in timerCopy) { + if (timer.timeToRemind.isAfter(now)) { + continue + } + removeTimer(timer) + remind(timer) + } + } + } + }, + millisecondsToNextMinute, + 1.minutes.inWholeMilliseconds + ) + } + + fun addTimer( + roomId: RoomId, + requestMessage: EventId, + timeToRemind: LocalTime, + content: String + ) { + val timer = TimerData(roomId.full, requestMessage.full, timeToRemind, content, null) + 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) + saveTimers() + return timerData.botMessageId() + } + + private fun removeTimer(timer: TimerData) { + timers.remove(timer) + saveTimers() + } + + @Synchronized + private fun saveTimers() { + val tempFile = File(timerFileLocation + ".tmp") + objectMapper.writeValue(tempFile, timers) + val timerFile = File(timerFileLocation) + Files.move(tempFile.toPath(), timerFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + + private suspend fun remind(timer: TimerData) { + try { + val roomId = timer.roomId() + val messageId = timer.botMessageId() ?: return + + val reactions = matrixBot.room().getTimelineEventReactionAggregation(roomId, messageId).first().reactions + val peopleToRemind = reactions[EMOJI]?.filter { it != matrixBot.self() }?.map { it.matrixTo() } + if (peopleToRemind.isNullOrEmpty()) { + return + } + + val timelineEvent = matrixBot.getTimelineEvent(roomId, messageId) ?: return + matrixBot.room().sendMessage(roomId) { + reply(timelineEvent) + text("'${timer.content}' ${peopleToRemind.joinToString(", ")}") + } + } catch (e: Exception) { + logger.error("Error during remind: ${e.message}", e) + } + } + + private data class TimerData( + @JsonProperty val roomId: String, + @JsonProperty val requestMessage: String, + @JsonProperty val timeToRemind: LocalTime, + @JsonProperty val content: String, + @JsonProperty var botMessageId: String? + ) { + fun roomId() = RoomId(roomId) + + fun requestMessage() = EventId(requestMessage) + + fun botMessageId() = botMessageId?.let { EventId(it) } + } +} 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 770520c..431a45e 100644 --- a/src/main/kotlin/org/fuchss/matrix/yarb/commands/ReminderCommand.kt +++ b/src/main/kotlin/org/fuchss/matrix/yarb/commands/ReminderCommand.kt @@ -1,8 +1,5 @@ package org.fuchss.matrix.yarb.commands -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import net.folivo.trixnity.client.room.getTimelineEventReactionAggregation import net.folivo.trixnity.client.room.message.react import net.folivo.trixnity.client.room.message.reply import net.folivo.trixnity.client.room.message.text @@ -14,51 +11,21 @@ import net.folivo.trixnity.core.model.events.m.RelationType import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent import org.fuchss.matrix.bots.MatrixBot import org.fuchss.matrix.bots.command.Command -import org.fuchss.matrix.bots.matrixTo import org.fuchss.matrix.yarb.Config -import org.fuchss.matrix.yarb.emoji +import org.fuchss.matrix.yarb.TimerManager import java.time.LocalTime -import java.util.Timer -import java.util.TimerTask -import kotlin.time.Duration.Companion.minutes -class ReminderCommand(private val config: Config, private val timer: Timer) : Command() { +class ReminderCommand(private val config: Config, private val timerManager: TimerManager) : Command() { companion object { const val COMMAND_NAME = "new" private val TIME_REGEX = Regex("^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$") - private val EMOJI = ":+1:".emoji() + private val EMOJI = TimerManager.EMOJI } override val help: String = "Set a reminder for a specific time." override val params: String = " " override val name: String = COMMAND_NAME - private val timers = mutableListOf() - - init { - val millisecondsToNextMinute = (60 - LocalTime.now().second) * 1000L - timer.schedule( - object : TimerTask() { - override fun run() { - runBlocking { - logger.debug("Reminders: {}", timers) - val timerCopy = timers.toList() - val now = LocalTime.now() - for (timer in timerCopy) { - if (timer.timeToRemind.isAfter(now)) { - continue - } - timers.remove(timer) - remind(timer) - } - } - } - }, - millisecondsToNextMinute, - 1.minutes.inWholeMilliseconds - ) - } - override suspend fun execute( matrixBot: MatrixBot, sender: UserId, @@ -90,9 +57,7 @@ class ReminderCommand(private val config: Config, private val timer: Timer) : Co } val timelineEvent = matrixBot.getTimelineEvent(roomId, textEventId) ?: return - - val timerData = TimerData(matrixBot, roomId, textEventId, time, timeXmessage[1], null) - timers.add(timerData) + timerManager.addTimer(roomId, textEventId, time, timeXmessage[1]) matrixBot.room().sendMessage(roomId) { reply(timelineEvent) @@ -117,9 +82,11 @@ class ReminderCommand(private val config: Config, private val timer: Timer) : Co return } - val timerData = this.timers.find { it.roomId == roomId && it.requestMessage == relatesTo.eventId } ?: return + val isRelated = timerManager.addBotMessageToTimer(relatesTo.eventId, eventId) + if (!isRelated) { + return + } - timerData.botMessageId = eventId matrixBot.room().sendMessage(roomId) { react(eventId, EMOJI) } @@ -138,12 +105,9 @@ class ReminderCommand(private val config: Config, private val timer: Timer) : Co return } - val timerData = this.timers.find { it.roomId == roomId && it.requestMessage == relatesTo.eventId } ?: return - timers.remove(timerData) + val relatedBotMessage = this.timerManager.removeByRequestMessage(relatesTo.eventId) ?: return - if (timerData.botMessageId != null) { - matrixBot.roomApi().redactEvent(roomId, timerData.botMessageId!!).getOrThrow() - } + matrixBot.roomApi().redactEvent(roomId, relatedBotMessage).getOrThrow() val replace = (textEvent.relatesTo as? RelatesTo.Replace) ?: return val newBody = (replace.newContent as? RoomMessageEventContent.TextBased.Text)?.body ?: return @@ -154,35 +118,4 @@ class ReminderCommand(private val config: Config, private val timer: Timer) : Co } execute(matrixBot, senderId, roomId, parameters, replace.eventId, textEvent) } - - private suspend fun remind(timer: TimerData) { - try { - val matrixBot = timer.matrixBot - val roomId = timer.roomId - val messageId = timer.botMessageId ?: return - - val reactions = matrixBot.room().getTimelineEventReactionAggregation(roomId, messageId).first().reactions - val peopleToRemind = reactions[EMOJI]?.filter { it != matrixBot.self() }?.map { it.matrixTo() } - if (peopleToRemind.isNullOrEmpty()) { - return - } - - val timelineEvent = matrixBot.getTimelineEvent(roomId, messageId) ?: return - matrixBot.room().sendMessage(roomId) { - reply(timelineEvent) - text("'${timer.content}' ${peopleToRemind.joinToString(", ")}") - } - } catch (e: Exception) { - logger.error("Error during remind: ${e.message}", e) - } - } - - private data class TimerData( - val matrixBot: MatrixBot, - val roomId: RoomId, - val requestMessage: EventId, - val timeToRemind: LocalTime, - val content: String, - var botMessageId: EventId? - ) }