Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 26bdb26
Author: Dominik Fuchß <[email protected]>
Date:   Sat Aug 3 00:53:19 2024 +0200

    Fix IDs in edited messages

commit 9cf9422
Author: Dominik Fuchß <[email protected]>
Date:   Fri Aug 2 23:09:46 2024 +0200

    Simplify more code.

commit 815cd35
Author: Dominik Fuchß <[email protected]>
Date:   Fri Aug 2 22:53:36 2024 +0200

    Simplify using outbox

commit 2779a9b
Author: Dominik Fuchß <[email protected]>
Date:   Fri Aug 2 22:03:07 2024 +0200

    Start work on outbox
  • Loading branch information
dfuchss committed Aug 2, 2024
1 parent d63e21b commit 90d5f7c
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 135 deletions.
11 changes: 11 additions & 0 deletions src/main/kotlin/org/fuchss/matrix/yarb/Helper.kt
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 2 additions & 11 deletions src/main/kotlin/org/fuchss/matrix/yarb/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<RoomMessageEventContent.TextBased.Text>(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<RoomMessageEventContent.TextBased.Text> { eventId, sender, roomId, content ->
reminderCommand.handleUserEditMessage(matrixBot, eventId, sender, roomId, content)
Expand All @@ -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"))
Expand Down
139 changes: 53 additions & 86 deletions src/main/kotlin/org/fuchss/matrix/yarb/TimerManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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() }}")
}
Expand All @@ -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<UserId> {
val allReactions = matrixBot.room().getTimelineEventReactionAggregationWithIds(roomId, messageId).first()
private suspend fun removeReactionOfBot(timer: TimerData): List<UserId> {
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<Map<String, Set<Pair<EventId, UserId>>>> =
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())
}
}
}
80 changes: 42 additions & 38 deletions src/main/kotlin/org/fuchss/matrix/yarb/commands/ReminderCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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) {
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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)
}
}

0 comments on commit 90d5f7c

Please sign in to comment.