Skip to content

Commit e2dca68

Browse files
authored
Feat: Use composite messages to improve UX WPB-17856 (#27)
* Update: rootProject.name to remind-app; Update: SDK password to fit latest 0.0.16 version (32 chars) * Delete: unused method, orEmpty(). * Update: wire API URL to use staging; * Update: plugins to the latest versions; * Remove: unnecessary let usage; * Update: test to use own Cron Interpreter class; * Replace: Wire SDK properties with env variables: WIRE_SDK_API_URL, WIRE_SDK_API_BOT_KEY; * Add: sendCompositeMessage implementation; * Add: sendCompositeMessage declaration; * Fix: cron to text interpreter to show working days properly; * Update: getReminderListMessages to represent list of reminders as separate composite messages, where delete button has same UUID as a reminder; * Add: quick check for Button confirmation * Fix: cron interpreter to fit quarkus crone notation; * Add: sendButtonActionConfirmation interface and implementation; * Add: new logic Button Action Event; Create: separate DTO for new Message and new ButtonAction; * Fix: Time parsing test to fit quarkus crone notation; * Update: Constant MAX_REMINDER_JOBS to public bc of external usage in CommandHandler; * Replace eventDTO with proper message on ButtonAction DTO; Split: process logic; Update: delete reminder, to listen button actions and send button action confirmation; * Add: fn to get Schedule for reminder; * Add: example of env variables required to start Remind-app; * Fix: ktlint format issue; * Add: eu-US locale to have consistent test results for GitHub VM; * Update: Time Parsing test to use independent of locale notation; * Fix: cron expression for every hour use case in quarkus notation; * Update: target JDK to 21 as reference; * Add: editCompositeMessage method and implementation; * Update: deleteReminder logic to edit parent msg for delete button with confirmation text; * Update: comment to keep in mind quarkus protobuf version match Wire SDK; * Refactor: methods that create a text message to BuildMsg object, send messages in separate helper functions to make core methods of Command handler easier to read and understand; * Replaced: custom regex function with UUID.fromString;
1 parent aeb9f03 commit e2dca68

File tree

25 files changed

+504
-222
lines changed

25 files changed

+504
-222
lines changed

.dockerignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
!build/*-runner
33
!build/*-runner.jar
44
!build/lib/*
5-
!build/quarkus-app/*
5+
!build/quarkus-app/*

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
WIRE_SDK_API_BOT_KEY=API_TOKEN
2+
WIRE_SDK_API_URL=ENVIRONMENT_LINK
3+
WIRE_SDK_EMAIL=EMAIL
4+
WIRE_SDK_ENVIRONMENT=ENVIRONMENT_NAME
5+
WIRE_SDK_PASSWORD=PASSWORD
6+
WIRE_SDK_USER_ID=UUID

.github/workflows/pull-request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- uses: actions/setup-java@v4
1313
with:
1414
distribution: 'zulu'
15-
java-version: '17'
15+
java-version: '21'
1616
- name: Lint
1717
run: |
1818
./gradlew ktlintCheck

build.gradle.kts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
44
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
55

66
plugins {
7-
kotlin("jvm") version "2.2.0"
8-
kotlin("plugin.allopen") version "2.2.0"
9-
kotlin("plugin.noarg") version "2.2.0"
10-
kotlin("plugin.serialization") version "2.2.0"
11-
id("org.jlleitschuh.gradle.ktlint") version "12.2.0"
7+
kotlin("jvm") version "2.2.20"
8+
kotlin("plugin.allopen") version "2.2.20"
9+
kotlin("plugin.noarg") version "2.2.20"
10+
kotlin("plugin.serialization") version "2.2.20"
11+
id("org.jlleitschuh.gradle.ktlint") version "13.1.0"
1212
id("io.gitlab.arturbosch.detekt") version "1.23.8"
1313
id("io.quarkus")
14-
id("com.gradleup.shadow") version "8.3.6"
14+
id("com.gradleup.shadow") version "9.1.0"
1515
}
1616

1717
repositories {
@@ -24,11 +24,12 @@ val quarkusPlatformVersion: String by project
2424

2525
/*
2626
* Forcing protobuf versions to avoid conflicts with Quarkus dependencies.
27+
* Make it same as in Wire SDK!
2728
*/
2829
configurations.all {
2930
resolutionStrategy {
30-
force("com.google.protobuf:protobuf-java:4.31.0")
31-
force("com.google.protobuf:protobuf-kotlin:4.31.1")
31+
force("com.google.protobuf:protobuf-java:4.32.0")
32+
force("com.google.protobuf:protobuf-kotlin:4.32.0")
3233
}
3334
}
3435

@@ -51,11 +52,10 @@ dependencies {
5152

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

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

6868
java {
69-
sourceCompatibility = JavaVersion.VERSION_17
70-
targetCompatibility = JavaVersion.VERSION_17
69+
sourceCompatibility = JavaVersion.VERSION_21
70+
targetCompatibility = JavaVersion.VERSION_21
7171
}
7272

7373
ktlint {
@@ -82,7 +82,7 @@ ktlint {
8282
}
8383

8484
detekt {
85-
toolVersion = "1.23.7"
85+
toolVersion = "1.23.8"
8686
config.setFrom(file("$rootDir/config/detekt/detekt.yml"))
8787
baseline = file("$rootDir/config/detekt/baseline.xml")
8888
parallel = true
@@ -106,11 +106,17 @@ noArg {
106106

107107
tasks.withType<KotlinJvmCompile>().configureEach {
108108
compilerOptions {
109-
jvmTarget.set(JvmTarget.JVM_17)
109+
jvmTarget.set(JvmTarget.JVM_21)
110110
javaParameters.set(true)
111111
}
112112
}
113113

114114
tasks.named<ShadowJar>("shadowJar") {
115115
mergeServiceFiles()
116116
}
117+
118+
tasks.withType<Test> {
119+
systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
120+
systemProperty("user.language", "en")
121+
systemProperty("user.country", "US")
122+
}

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ services:
2323
- reminders_bot-db:/var/lib/postgresql/data/
2424

2525
volumes:
26-
reminders_bot-db:
26+
reminders_bot-db:

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#Gradle properties
22
quarkusPluginId=io.quarkus
3-
quarkusPluginVersion=3.22.3
3+
quarkusPluginVersion=3.26.3
44
quarkusPlatformGroupId=io.quarkus.platform
55
quarkusPlatformArtifactId=quarkus-bom
6-
quarkusPlatformVersion=3.22.3
6+
quarkusPlatformVersion=3.26.3
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists

settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ pluginManagement {
1010
id(quarkusPluginId) version quarkusPluginVersion
1111
}
1212
}
13-
rootProject.name = "reminders-bot"
13+
rootProject.name = "remind-app"

src/main/kotlin/com/wire/bots/application/EventDTO.kt

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,42 @@ import kotlinx.serialization.Serializable
44
import kotlinx.serialization.Transient
55
import com.wire.integrations.jvm.model.QualifiedId
66

7+
interface EventDTO {
8+
val type: EventTypeDTO
9+
val userId: String?
10+
val conversationId: QualifiedId
11+
}
12+
713
@Serializable
8-
data class EventDTO(
9-
val type: EventTypeDTO,
10-
val userId: String? = null,
11-
val conversationId: QualifiedId,
14+
data class MessageEventDTO(
15+
override val type: EventTypeDTO,
16+
override val userId: String? = null,
17+
override val conversationId: QualifiedId,
1218
val text: TextContent? = null,
1319
val handle: String? = null,
1420
val locale: String? = null,
1521
val conversation: String? = null,
1622
val messageId: String? = null,
1723
val refMessageId: String? = null,
1824
val emoji: String? = null
19-
)
25+
) : EventDTO
26+
27+
@Serializable
28+
data class ButtonActionEventDTO(
29+
override val type: EventTypeDTO,
30+
override val userId: String? = null,
31+
override val conversationId: QualifiedId,
32+
val text: TextContent? = null,
33+
val handle: String? = null,
34+
val locale: String? = null,
35+
val conversation: String? = null,
36+
val messageId: String? = null,
37+
val refMessageId: String? = null,
38+
val emoji: String? = null,
39+
val buttonId: String? = null,
40+
val referencedMessageId: String? = null,
41+
val sender: String? = null
42+
) : EventDTO
2043

2144
@Serializable
2245
data class TextContent(

src/main/kotlin/com/wire/bots/application/EventMapper.kt

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,49 @@ import arrow.core.right
77
import com.wire.bots.domain.event.BotError
88
import com.wire.bots.domain.event.Command
99
import com.wire.integrations.jvm.model.QualifiedId
10+
import java.util.UUID
1011

1112
object EventMapper {
1213
/**
13-
* Maps the [EventDTO] to a [Command] object so it can be processed by the application.
14+
* Maps the [MessageEventDTO] to a [Command] object so it can be processed by the application.
1415
*/
1516
fun fromEvent(eventDTO: EventDTO): Either<BotError, Command> =
1617
runCatching {
1718
when (eventDTO.type) {
1819
EventTypeDTO.NEW_TEXT -> {
20+
require(eventDTO is MessageEventDTO) { "Wrong DTO for this event type." }
1921
parseCommand(
2022
conversationId = eventDTO.conversationId,
2123
rawCommand = eventDTO.text?.data.orEmpty()
2224
)
2325
}
26+
27+
EventTypeDTO.BUTTON_ACTION -> {
28+
require(eventDTO is ButtonActionEventDTO) { "Wrong DTO for this event type." }
29+
val buttonId = eventDTO.buttonId.orEmpty()
30+
val senderId = eventDTO.userId?.let {
31+
QualifiedId(UUID.fromString(it), "")
32+
}
33+
34+
val parsedUuid = runCatching { UUID.fromString(buttonId) }.getOrNull()
35+
if (parsedUuid != null) {
36+
Command
37+
.DeleteReminder(
38+
conversationId = eventDTO.conversationId,
39+
reminderId = buttonId,
40+
referencedMessageId = eventDTO.referencedMessageId,
41+
senderId = senderId
42+
).right()
43+
} else {
44+
parseCommand(
45+
conversationId = eventDTO.conversationId,
46+
rawCommand = buttonId,
47+
referencedMessageId = eventDTO.referencedMessageId,
48+
senderId = senderId
49+
)
50+
}
51+
}
52+
2453
else -> BotError.Skip.left()
2554
}
2655
}.getOrElse {
@@ -36,7 +65,9 @@ object EventMapper {
3665
*/
3766
private fun parseCommand(
3867
conversationId: QualifiedId,
39-
rawCommand: String
68+
rawCommand: String,
69+
referencedMessageId: String? = null,
70+
senderId: QualifiedId? = null
4071
): Either<BotError, Command> =
4172
either {
4273
val words = rawCommand.split(COMMAND_EXPRESSION)
@@ -45,21 +76,27 @@ object EventMapper {
4576
"/remind" ->
4677
parseCommandArgs(
4778
conversationId = conversationId,
48-
args = rawCommand.substringAfter("/remind").trimStart()
79+
args = rawCommand.substringAfter("/remind").trimStart(),
80+
referencedMessageId = referencedMessageId,
81+
senderId = senderId
4982
)
5083
else -> BotError.Skip.left()
5184
}
5285
}
5386

5487
private fun parseCommandArgs(
5588
conversationId: QualifiedId,
56-
args: String
89+
args: String,
90+
referencedMessageId: String? = null,
91+
senderId: QualifiedId? = null
5792
): Either<BotError, Command> =
5893
when {
5994
args.trim() == "help" -> Command.Help(conversationId).right()
6095
args.trim() == "list" -> Command.ListReminders(conversationId).right()
6196
args.startsWith("to") -> parseToCommand(conversationId, args)
62-
args.startsWith("delete") -> parseDeleteCommand(conversationId, args)
97+
args.startsWith(
98+
"delete"
99+
) -> parseDeleteCommand(conversationId, args, referencedMessageId, senderId)
63100
else ->
64101
BotError
65102
.Unknown(
@@ -73,22 +110,29 @@ object EventMapper {
73110
args: String
74111
): Either<BotError, Command> {
75112
val regex = Regex("[\"“”]([^\"“”]*)[\"“”]")
76-
val matches = regex.findAll(args.substringAfter("to"))
113+
val matches = regex
114+
.findAll(args.substringAfter("to"))
77115
.map { it.groupValues[1] }
78116
.toList()
79117
return when {
80-
matches.size < 2 -> BotError.ReminderError(
81-
conversationId = conversationId,
82-
errorType = BotError.ErrorType.INVALID_REMINDER_USAGE
83-
).left()
84-
matches[0].isBlank() -> BotError.ReminderError(
85-
conversationId = conversationId,
86-
errorType = BotError.ErrorType.EMPTY_REMINDER_TASK
87-
).left()
88-
matches[1].isBlank() -> BotError.ReminderError(
89-
conversationId = conversationId,
90-
errorType = BotError.ErrorType.INVALID_REMINDER_USAGE
91-
).left()
118+
matches.size < 2 ->
119+
BotError
120+
.ReminderError(
121+
conversationId = conversationId,
122+
errorType = BotError.ErrorType.INVALID_REMINDER_USAGE
123+
).left()
124+
matches[0].isBlank() ->
125+
BotError
126+
.ReminderError(
127+
conversationId = conversationId,
128+
errorType = BotError.ErrorType.EMPTY_REMINDER_TASK
129+
).left()
130+
matches[1].isBlank() ->
131+
BotError
132+
.ReminderError(
133+
conversationId = conversationId,
134+
errorType = BotError.ErrorType.INVALID_REMINDER_USAGE
135+
).left()
92136
else -> {
93137
val task = matches[0]
94138
val schedule = matches[1]
@@ -106,19 +150,24 @@ object EventMapper {
106150

107151
private fun parseDeleteCommand(
108152
conversationId: QualifiedId,
109-
args: String
153+
args: String,
154+
referencedMessageId: String? = null,
155+
senderId: QualifiedId? = null
110156
): Either<BotError, Command> {
111157
val reminderId = args.substringAfter("delete").trim()
112158
return if (reminderId.isBlank()) {
113-
BotError.ReminderError(
114-
conversationId = conversationId,
115-
errorType = BotError.ErrorType.INVALID_REMINDER_ID
116-
).left()
159+
BotError
160+
.ReminderError(
161+
conversationId = conversationId,
162+
errorType = BotError.ErrorType.INVALID_REMINDER_ID
163+
).left()
117164
} else {
118165
Command
119166
.DeleteReminder(
120167
conversationId = conversationId,
121-
reminderId = reminderId
168+
reminderId = reminderId,
169+
referencedMessageId = referencedMessageId,
170+
senderId = senderId
122171
).right()
123172
}
124173
}

0 commit comments

Comments
 (0)