Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
216ce3a
Update: rootProject.name to remind-app;
olanokhin Sep 17, 2025
780ad18
Delete: unused method orEmpty().
olanokhin Sep 17, 2025
776a92b
Update: wire api url to use staging;
olanokhin Sep 17, 2025
df9af77
Update: plugins to the latest versions;
olanokhin Sep 17, 2025
2e37683
Remove: unnecessary let usage;
olanokhin Sep 17, 2025
fe18a02
Update: test to use own Cron Interpretrer class;
olanokhin Sep 17, 2025
dde82d2
Remove: Wire sdk properties with env variables:
olanokhin Sep 17, 2025
6a8b1eb
Add: sendCompositeMessage implementation;
olanokhin Sep 18, 2025
50c329e
Add: sendCompositeMessage declaration;
olanokhin Sep 18, 2025
833dab6
Fix: cron to text interpreter to show working days properly;
olanokhin Sep 18, 2025
2e60dbc
Update: getReminderListMessages to represent list of reminders as sep…
olanokhin Sep 18, 2025
bb27a86
Add: quick check for Button confirmation
olanokhin Sep 18, 2025
d5e2784
Fix: cron interpreter to fit quarkus crone notation;
olanokhin Sep 18, 2025
454f3b2
Add: sendButtonActionConfirmation interface and implementation;
olanokhin Sep 18, 2025
505f59a
Add: new logic Button Action Event;
olanokhin Sep 18, 2025
3182bd0
Fix: Time parsing test to fit quarkus crone notation;
olanokhin Sep 18, 2025
f4d5d91
Update: Constant MAX_REMINDER_JOBS to public bc of external usage in …
olanokhin Sep 18, 2025
6f40a72
Replace eventDTO with proper message od ButtonAction DTO;
olanokhin Sep 18, 2025
69a5bb0
Format: ktlint issue;
olanokhin Sep 18, 2025
4f049fa
Add: fn to get Schedule for reminder;
olanokhin Sep 18, 2025
926d58b
Add: example of env variables required to start Remind-app;
olanokhin Sep 18, 2025
11d0703
Fix: ktlint format issue;
olanokhin Sep 18, 2025
6d646ab
Add: eu-US locale to have consistent test results for gitHub VM;
olanokhin Sep 18, 2025
b720a15
Update: Time Parsing test to use independent from locale notation;
olanokhin Sep 18, 2025
e58bc70
Fix: cron expression for every hour usecase in quarkus notation;
olanokhin Sep 18, 2025
c9734d6
Update: target JDK to 21 as reference;
olanokhin Sep 18, 2025
4961375
Add: editCompositeMessage method and implementation;
olanokhin Sep 19, 2025
73467fa
Update: deleteReminder logic to edit parent msg for delete button wit…
olanokhin Sep 19, 2025
f5eca14
Update: comment to keep in mind quarkus protobuf version match Wire SDK;
olanokhin Sep 19, 2025
6276ebc
Refactor: methods that create a text message to BiuldMsg object, send…
olanokhin Sep 19, 2025
98a5f42
Replaced: custom regex fn with UUID.fromString;
olanokhin Sep 19, 2025
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
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
!build/*-runner
!build/*-runner.jar
!build/lib/*
!build/quarkus-app/*
!build/quarkus-app/*
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
WIRE_SDK_API_BOT_KEY=API_TOKEN
WIRE_SDK_API_URL=ENVIRONMENT_LINK
WIRE_SDK_EMAIL=EMAIL
WIRE_SDK_ENVIRONMENT=ENVIRONMENT_NAME
WIRE_SDK_PASSWORD=PASSWORD
WIRE_SDK_USER_ID=UUID
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
java-version: '21'
- name: Lint
run: |
./gradlew ktlintCheck
Expand Down
36 changes: 21 additions & 15 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
kotlin("jvm") version "2.2.0"
kotlin("plugin.allopen") version "2.2.0"
kotlin("plugin.noarg") version "2.2.0"
kotlin("plugin.serialization") version "2.2.0"
id("org.jlleitschuh.gradle.ktlint") version "12.2.0"
kotlin("jvm") version "2.2.20"
kotlin("plugin.allopen") version "2.2.20"
kotlin("plugin.noarg") version "2.2.20"
kotlin("plugin.serialization") version "2.2.20"
id("org.jlleitschuh.gradle.ktlint") version "13.1.0"
id("io.gitlab.arturbosch.detekt") version "1.23.8"
id("io.quarkus")
id("com.gradleup.shadow") version "8.3.6"
id("com.gradleup.shadow") version "9.1.0"
}

repositories {
Expand All @@ -24,11 +24,12 @@ val quarkusPlatformVersion: String by project

/*
* Forcing protobuf versions to avoid conflicts with Quarkus dependencies.
* Make it same as in Wire SDK!
*/
configurations.all {
resolutionStrategy {
force("com.google.protobuf:protobuf-java:4.31.0")
force("com.google.protobuf:protobuf-kotlin:4.31.1")
force("com.google.protobuf:protobuf-java:4.32.0")
force("com.google.protobuf:protobuf-kotlin:4.32.0")
}
}

Expand All @@ -51,11 +52,10 @@ dependencies {

// Other project dependencies
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("com.rubiconproject.oss:jchronic:0.2.8")
implementation("io.github.yamilmedina:natural-kron:2.0.0")
implementation("io.arrow-kt:arrow-core:2.1.2")
implementation("com.wire:wire-apps-jvm-sdk:0.0.8")
implementation("com.wire:wire-apps-jvm-sdk:0.0.16")

// Test dependencies
testImplementation("io.quarkus:quarkus-junit5")
Expand All @@ -66,8 +66,8 @@ group = "com.wire.bots"
version = "1.0.0-SNAPSHOT"

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

ktlint {
Expand All @@ -82,7 +82,7 @@ ktlint {
}

detekt {
toolVersion = "1.23.7"
toolVersion = "1.23.8"
config.setFrom(file("$rootDir/config/detekt/detekt.yml"))
baseline = file("$rootDir/config/detekt/baseline.xml")
parallel = true
Expand All @@ -106,11 +106,17 @@ noArg {

tasks.withType<KotlinJvmCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
jvmTarget.set(JvmTarget.JVM_21)
javaParameters.set(true)
}
}

tasks.named<ShadowJar>("shadowJar") {
mergeServiceFiles()
}

tasks.withType<Test> {
systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
systemProperty("user.language", "en")
systemProperty("user.country", "US")
}
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ services:
- reminders_bot-db:/var/lib/postgresql/data/

volumes:
reminders_bot-db:
reminders_bot-db:
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Gradle properties
quarkusPluginId=io.quarkus
quarkusPluginVersion=3.22.3
quarkusPluginVersion=3.26.3
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=3.22.3
quarkusPlatformVersion=3.26.3
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ pluginManagement {
id(quarkusPluginId) version quarkusPluginVersion
}
}
rootProject.name = "reminders-bot"
rootProject.name = "remind-app"
33 changes: 28 additions & 5 deletions src/main/kotlin/com/wire/bots/application/EventDTO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,42 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import com.wire.integrations.jvm.model.QualifiedId

interface EventDTO {
val type: EventTypeDTO
val userId: String?
val conversationId: QualifiedId
}

@Serializable
data class EventDTO(
val type: EventTypeDTO,
val userId: String? = null,
val conversationId: QualifiedId,
data class MessageEventDTO(
override val type: EventTypeDTO,
override val userId: String? = null,
override val conversationId: QualifiedId,
val text: TextContent? = null,
val handle: String? = null,
val locale: String? = null,
val conversation: String? = null,
val messageId: String? = null,
val refMessageId: String? = null,
val emoji: String? = null
)
) : EventDTO

@Serializable
data class ButtonActionEventDTO(
override val type: EventTypeDTO,
override val userId: String? = null,
override val conversationId: QualifiedId,
val text: TextContent? = null,
val handle: String? = null,
val locale: String? = null,
val conversation: String? = null,
val messageId: String? = null,
val refMessageId: String? = null,
val emoji: String? = null,
val buttonId: String? = null,
val referencedMessageId: String? = null,
val sender: String? = null
) : EventDTO

@Serializable
data class TextContent(
Expand Down
97 changes: 73 additions & 24 deletions src/main/kotlin/com/wire/bots/application/EventMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,49 @@ import arrow.core.right
import com.wire.bots.domain.event.BotError
import com.wire.bots.domain.event.Command
import com.wire.integrations.jvm.model.QualifiedId
import java.util.UUID

object EventMapper {
/**
* Maps the [EventDTO] to a [Command] object so it can be processed by the application.
* Maps the [MessageEventDTO] to a [Command] object so it can be processed by the application.
*/
fun fromEvent(eventDTO: EventDTO): Either<BotError, Command> =
runCatching {
when (eventDTO.type) {
EventTypeDTO.NEW_TEXT -> {
require(eventDTO is MessageEventDTO) { "Wrong DTO for this event type." }
parseCommand(
conversationId = eventDTO.conversationId,
rawCommand = eventDTO.text?.data.orEmpty()
)
}

EventTypeDTO.BUTTON_ACTION -> {
require(eventDTO is ButtonActionEventDTO) { "Wrong DTO for this event type." }
val buttonId = eventDTO.buttonId.orEmpty()
val senderId = eventDTO.userId?.let {
QualifiedId(UUID.fromString(it), "")
}

val parsedUuid = runCatching { UUID.fromString(buttonId) }.getOrNull()
if (parsedUuid != null) {
Command
.DeleteReminder(
conversationId = eventDTO.conversationId,
reminderId = buttonId,
referencedMessageId = eventDTO.referencedMessageId,
senderId = senderId
).right()
} else {
parseCommand(
conversationId = eventDTO.conversationId,
rawCommand = buttonId,
referencedMessageId = eventDTO.referencedMessageId,
senderId = senderId
)
}
}

else -> BotError.Skip.left()
}
}.getOrElse {
Expand All @@ -36,7 +65,9 @@ object EventMapper {
*/
private fun parseCommand(
conversationId: QualifiedId,
rawCommand: String
rawCommand: String,
referencedMessageId: String? = null,
senderId: QualifiedId? = null
): Either<BotError, Command> =
either {
val words = rawCommand.split(COMMAND_EXPRESSION)
Expand All @@ -45,21 +76,27 @@ object EventMapper {
"/remind" ->
parseCommandArgs(
conversationId = conversationId,
args = rawCommand.substringAfter("/remind").trimStart()
args = rawCommand.substringAfter("/remind").trimStart(),
referencedMessageId = referencedMessageId,
senderId = senderId
)
else -> BotError.Skip.left()
}
}

private fun parseCommandArgs(
conversationId: QualifiedId,
args: String
args: String,
referencedMessageId: String? = null,
senderId: QualifiedId? = null
): Either<BotError, Command> =
when {
args.trim() == "help" -> Command.Help(conversationId).right()
args.trim() == "list" -> Command.ListReminders(conversationId).right()
args.startsWith("to") -> parseToCommand(conversationId, args)
args.startsWith("delete") -> parseDeleteCommand(conversationId, args)
args.startsWith(
"delete"
) -> parseDeleteCommand(conversationId, args, referencedMessageId, senderId)
else ->
BotError
.Unknown(
Expand All @@ -73,22 +110,29 @@ object EventMapper {
args: String
): Either<BotError, Command> {
val regex = Regex("[\"“”]([^\"“”]*)[\"“”]")
val matches = regex.findAll(args.substringAfter("to"))
val matches = regex
.findAll(args.substringAfter("to"))
.map { it.groupValues[1] }
.toList()
return when {
matches.size < 2 -> BotError.ReminderError(
conversationId = conversationId,
errorType = BotError.ErrorType.INVALID_REMINDER_USAGE
).left()
matches[0].isBlank() -> BotError.ReminderError(
conversationId = conversationId,
errorType = BotError.ErrorType.EMPTY_REMINDER_TASK
).left()
matches[1].isBlank() -> BotError.ReminderError(
conversationId = conversationId,
errorType = BotError.ErrorType.INVALID_REMINDER_USAGE
).left()
matches.size < 2 ->
BotError
.ReminderError(
conversationId = conversationId,
errorType = BotError.ErrorType.INVALID_REMINDER_USAGE
).left()
matches[0].isBlank() ->
BotError
.ReminderError(
conversationId = conversationId,
errorType = BotError.ErrorType.EMPTY_REMINDER_TASK
).left()
matches[1].isBlank() ->
BotError
.ReminderError(
conversationId = conversationId,
errorType = BotError.ErrorType.INVALID_REMINDER_USAGE
).left()
else -> {
val task = matches[0]
val schedule = matches[1]
Expand All @@ -106,19 +150,24 @@ object EventMapper {

private fun parseDeleteCommand(
conversationId: QualifiedId,
args: String
args: String,
referencedMessageId: String? = null,
senderId: QualifiedId? = null
): Either<BotError, Command> {
val reminderId = args.substringAfter("delete").trim()
return if (reminderId.isBlank()) {
BotError.ReminderError(
conversationId = conversationId,
errorType = BotError.ErrorType.INVALID_REMINDER_ID
).left()
BotError
.ReminderError(
conversationId = conversationId,
errorType = BotError.ErrorType.INVALID_REMINDER_ID
).left()
} else {
Command
.DeleteReminder(
conversationId = conversationId,
reminderId = reminderId
reminderId = reminderId,
referencedMessageId = referencedMessageId,
senderId = senderId
).right()
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/com/wire/bots/application/EventTypeDTO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ enum class EventTypeDTO {
USER_JOINED,

@SerialName("conversation.reaction")
REACTION
REACTION,

@SerialName("conversation.button.action")
BUTTON_ACTION
}
Loading