From eb4475b4d9741571f8a79d11a73ab89de76f5461 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jan 2022 18:43:48 +0100 Subject: [PATCH 1/6] Command parser is not a static object anymore --- .../main/java/im/vector/app/features/command/CommandParser.kt | 3 ++- .../home/room/detail/composer/MessageComposerViewModel.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index f5861c7c53..a79050cf78 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -23,8 +23,9 @@ import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber +import javax.inject.Inject -object CommandParser { +class CommandParser @Inject constructor() { /** * Convert the text message into a Slash command. diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 0f3fb973f6..2ab43f549a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -66,6 +66,7 @@ class MessageComposerViewModel @AssistedInject constructor( private val session: Session, private val stringProvider: StringProvider, private val vectorPreferences: VectorPreferences, + private val commandParser: CommandParser, private val rainbowGenerator: RainbowGenerator, private val voiceMessageHelper: VoiceMessageHelper, private val voicePlayerHelper: VoicePlayerHelper @@ -183,7 +184,7 @@ class MessageComposerViewModel @AssistedInject constructor( withState { state -> when (state.sendMode) { is SendMode.Regular -> { - when (val slashCommandResult = CommandParser.parseSlashCommand(action.text)) { + when (val slashCommandResult = commandParser.parseSlashCommand(action.text)) { is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) From 4f1de34d4c273145bc9f16e5a7e1c99a0ee37561 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jan 2022 19:03:25 +0100 Subject: [PATCH 2/6] Small cleanup --- .../im/vector/app/features/command/CommandParser.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index a79050cf78..9d854fdbee 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -35,11 +35,9 @@ class CommandParser @Inject constructor() { */ fun parseSlashCommand(textMessage: CharSequence): ParsedCommand { // check if it has the Slash marker - if (!textMessage.startsWith("/")) { - return ParsedCommand.ErrorNotACommand + return if (!textMessage.startsWith("/")) { + ParsedCommand.ErrorNotACommand } else { - Timber.v("parseSlashCommand") - // "/" only if (textMessage.length == 1) { return ParsedCommand.ErrorEmptySlashCommand @@ -53,7 +51,7 @@ class CommandParser @Inject constructor() { val messageParts = try { textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } } catch (e: Exception) { - Timber.e(e, "## manageSlashCommand() : split failed") + Timber.e(e, "## parseSlashCommand() : split failed") null } @@ -65,7 +63,7 @@ class CommandParser @Inject constructor() { val slashCommand = messageParts.first() val message = textMessage.substring(slashCommand.length).trim() - return when { + when { Command.PLAIN.matches(slashCommand) -> { if (message.isNotEmpty()) { ParsedCommand.SendPlainText(message = message) From 87adaee54917409e2bb69e3825232f02c9d0b9c7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jan 2022 19:05:05 +0100 Subject: [PATCH 3/6] use sealed interface --- .../app/features/command/ParsedCommand.kt | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 584272f3f4..93c1e835e1 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -22,51 +22,51 @@ import org.matrix.android.sdk.api.session.identity.ThreePid /** * Represent a parsed command */ -sealed class ParsedCommand { +sealed interface ParsedCommand { // This is not a Slash command - object ErrorNotACommand : ParsedCommand() + object ErrorNotACommand : ParsedCommand - object ErrorEmptySlashCommand : ParsedCommand() + object ErrorEmptySlashCommand : ParsedCommand // Unknown/Unsupported slash command - class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand() + class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand // A slash command is detected, but there is an error - class ErrorSyntax(val command: Command) : ParsedCommand() + class ErrorSyntax(val command: Command) : ParsedCommand // Valid commands: - class SendPlainText(val message: CharSequence) : ParsedCommand() - class SendEmote(val message: CharSequence) : ParsedCommand() - class SendRainbow(val message: CharSequence) : ParsedCommand() - class SendRainbowEmote(val message: CharSequence) : ParsedCommand() - class BanUser(val userId: String, val reason: String?) : ParsedCommand() - class UnbanUser(val userId: String, val reason: String?) : ParsedCommand() - class IgnoreUser(val userId: String) : ParsedCommand() - class UnignoreUser(val userId: String) : ParsedCommand() - class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand() - class ChangeRoomName(val name: String) : ParsedCommand() - class Invite(val userId: String, val reason: String?) : ParsedCommand() - class Invite3Pid(val threePid: ThreePid) : ParsedCommand() - class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand() - class PartRoom(val roomAlias: String?) : ParsedCommand() - class ChangeTopic(val topic: String) : ParsedCommand() - class RemoveUser(val userId: String, val reason: String?) : ParsedCommand() - class ChangeDisplayName(val displayName: String) : ParsedCommand() - class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand() - class ChangeRoomAvatar(val url: String) : ParsedCommand() - class ChangeAvatarForRoom(val url: String) : ParsedCommand() - class SetMarkdown(val enable: Boolean) : ParsedCommand() - object ClearScalarToken : ParsedCommand() - class SendSpoiler(val message: String) : ParsedCommand() - class SendShrug(val message: CharSequence) : ParsedCommand() - class SendLenny(val message: CharSequence) : ParsedCommand() - object DiscardSession : ParsedCommand() - class ShowUser(val userId: String) : ParsedCommand() - class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() - class CreateSpace(val name: String, val invitees: List) : ParsedCommand() - class AddToSpace(val spaceId: String) : ParsedCommand() - class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand() - class LeaveRoom(val roomId: String) : ParsedCommand() - class UpgradeRoom(val newVersion: String) : ParsedCommand() + class SendPlainText(val message: CharSequence) : ParsedCommand + class SendEmote(val message: CharSequence) : ParsedCommand + class SendRainbow(val message: CharSequence) : ParsedCommand + class SendRainbowEmote(val message: CharSequence) : ParsedCommand + class BanUser(val userId: String, val reason: String?) : ParsedCommand + class UnbanUser(val userId: String, val reason: String?) : ParsedCommand + class IgnoreUser(val userId: String) : ParsedCommand + class UnignoreUser(val userId: String) : ParsedCommand + class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand + class ChangeRoomName(val name: String) : ParsedCommand + class Invite(val userId: String, val reason: String?) : ParsedCommand + class Invite3Pid(val threePid: ThreePid) : ParsedCommand + class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand + class PartRoom(val roomAlias: String?) : ParsedCommand + class ChangeTopic(val topic: String) : ParsedCommand + class RemoveUser(val userId: String, val reason: String?) : ParsedCommand + class ChangeDisplayName(val displayName: String) : ParsedCommand + class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand + class ChangeRoomAvatar(val url: String) : ParsedCommand + class ChangeAvatarForRoom(val url: String) : ParsedCommand + class SetMarkdown(val enable: Boolean) : ParsedCommand + object ClearScalarToken : ParsedCommand + class SendSpoiler(val message: String) : ParsedCommand + class SendShrug(val message: CharSequence) : ParsedCommand + class SendLenny(val message: CharSequence) : ParsedCommand + object DiscardSession : ParsedCommand + class ShowUser(val userId: String) : ParsedCommand + class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand + class CreateSpace(val name: String, val invitees: List) : ParsedCommand + class AddToSpace(val spaceId: String) : ParsedCommand + class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand + class LeaveRoom(val roomId: String) : ParsedCommand + class UpgradeRoom(val newVersion: String) : ParsedCommand } From c7dc08ef5da4975818e1ec64cd08c0e988ff814c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jan 2022 20:40:40 +0100 Subject: [PATCH 4/6] data class. --- .../app/features/command/ParsedCommand.kt | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 93c1e835e1..5f2e7f56a5 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -29,44 +29,44 @@ sealed interface ParsedCommand { object ErrorEmptySlashCommand : ParsedCommand // Unknown/Unsupported slash command - class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand + data class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand // A slash command is detected, but there is an error - class ErrorSyntax(val command: Command) : ParsedCommand + data class ErrorSyntax(val command: Command) : ParsedCommand // Valid commands: - class SendPlainText(val message: CharSequence) : ParsedCommand - class SendEmote(val message: CharSequence) : ParsedCommand - class SendRainbow(val message: CharSequence) : ParsedCommand - class SendRainbowEmote(val message: CharSequence) : ParsedCommand - class BanUser(val userId: String, val reason: String?) : ParsedCommand - class UnbanUser(val userId: String, val reason: String?) : ParsedCommand - class IgnoreUser(val userId: String) : ParsedCommand - class UnignoreUser(val userId: String) : ParsedCommand - class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand - class ChangeRoomName(val name: String) : ParsedCommand - class Invite(val userId: String, val reason: String?) : ParsedCommand - class Invite3Pid(val threePid: ThreePid) : ParsedCommand - class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand - class PartRoom(val roomAlias: String?) : ParsedCommand - class ChangeTopic(val topic: String) : ParsedCommand - class RemoveUser(val userId: String, val reason: String?) : ParsedCommand - class ChangeDisplayName(val displayName: String) : ParsedCommand - class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand - class ChangeRoomAvatar(val url: String) : ParsedCommand - class ChangeAvatarForRoom(val url: String) : ParsedCommand - class SetMarkdown(val enable: Boolean) : ParsedCommand + data class SendPlainText(val message: CharSequence) : ParsedCommand + data class SendEmote(val message: CharSequence) : ParsedCommand + data class SendRainbow(val message: CharSequence) : ParsedCommand + data class SendRainbowEmote(val message: CharSequence) : ParsedCommand + data class BanUser(val userId: String, val reason: String?) : ParsedCommand + data class UnbanUser(val userId: String, val reason: String?) : ParsedCommand + data class IgnoreUser(val userId: String) : ParsedCommand + data class UnignoreUser(val userId: String) : ParsedCommand + data class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand + data class ChangeRoomName(val name: String) : ParsedCommand + data class Invite(val userId: String, val reason: String?) : ParsedCommand + data class Invite3Pid(val threePid: ThreePid) : ParsedCommand + data class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand + data class PartRoom(val roomAlias: String?) : ParsedCommand + data class ChangeTopic(val topic: String) : ParsedCommand + data class RemoveUser(val userId: String, val reason: String?) : ParsedCommand + data class ChangeDisplayName(val displayName: String) : ParsedCommand + data class ChangeDisplayNameForRoom(val displayName: String) : ParsedCommand + data class ChangeRoomAvatar(val url: String) : ParsedCommand + data class ChangeAvatarForRoom(val url: String) : ParsedCommand + data class SetMarkdown(val enable: Boolean) : ParsedCommand object ClearScalarToken : ParsedCommand - class SendSpoiler(val message: String) : ParsedCommand - class SendShrug(val message: CharSequence) : ParsedCommand - class SendLenny(val message: CharSequence) : ParsedCommand + data class SendSpoiler(val message: String) : ParsedCommand + data class SendShrug(val message: CharSequence) : ParsedCommand + data class SendLenny(val message: CharSequence) : ParsedCommand object DiscardSession : ParsedCommand - class ShowUser(val userId: String) : ParsedCommand - class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand - class CreateSpace(val name: String, val invitees: List) : ParsedCommand - class AddToSpace(val spaceId: String) : ParsedCommand - class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand - class LeaveRoom(val roomId: String) : ParsedCommand - class UpgradeRoom(val newVersion: String) : ParsedCommand + data class ShowUser(val userId: String) : ParsedCommand + data class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand + data class CreateSpace(val name: String, val invitees: List) : ParsedCommand + data class AddToSpace(val spaceId: String) : ParsedCommand + data class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand + data class LeaveRoom(val roomId: String) : ParsedCommand + data class UpgradeRoom(val newVersion: String) : ParsedCommand } From 6710f9320b42e54112da31a36d090a7bf71f8218 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jan 2022 20:52:31 +0100 Subject: [PATCH 5/6] Add some unit test for the command parser. Not all commands are covered, could add more tests later. --- .../app/features/command/CommandParserTest.kt | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt diff --git a/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt b/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt new file mode 100644 index 0000000000..4af03a36f5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.command + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test + +class CommandParserTest { + @Test + fun parseSlashCommandEmpty() { + test("/", ParsedCommand.ErrorEmptySlashCommand) + } + + @Test + fun parseSlashCommandUnknown() { + test("/unknown", ParsedCommand.ErrorUnknownSlashCommand("/unknown")) + test("/unknown with param", ParsedCommand.ErrorUnknownSlashCommand("/unknown")) + } + + @Test + fun parseSlashCommandNotACommand() { + test("", ParsedCommand.ErrorNotACommand) + test("test", ParsedCommand.ErrorNotACommand) + test("// test", ParsedCommand.ErrorNotACommand) + } + + @Test + fun parseSlashCommandEmote() { + test("/me test", ParsedCommand.SendEmote("test")) + test("/me", ParsedCommand.ErrorSyntax(Command.EMOTE)) + } + + @Test + fun parseSlashCommandRemove() { + // Nominal + test("/remove @foo:bar", ParsedCommand.RemoveUser("@foo:bar", null)) + // With a reason + test("/remove @foo:bar a reason", ParsedCommand.RemoveUser("@foo:bar", "a reason")) + // Trim the reason + test("/remove @foo:bar a reason ", ParsedCommand.RemoveUser("@foo:bar", "a reason")) + // Alias + test("/kick @foo:bar", ParsedCommand.RemoveUser("@foo:bar", null)) + // Error + test("/remove", ParsedCommand.ErrorSyntax(Command.REMOVE_USER)) + } + + private fun test(message: String, expectedResult: ParsedCommand) { + val commandParser = CommandParser() + val result = commandParser.parseSlashCommand(message) + result shouldBeEqualTo expectedResult + } +} From e9f9c7e739b907e899c7a79f60dbc0e70dbdb6f4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 19 Jan 2022 21:13:00 +0100 Subject: [PATCH 6/6] Changelog --- changelog.d/4998.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4998.misc diff --git a/changelog.d/4998.misc b/changelog.d/4998.misc new file mode 100644 index 0000000000..1283b33b1c --- /dev/null +++ b/changelog.d/4998.misc @@ -0,0 +1 @@ + Small iteration on command parser and unit test it. \ No newline at end of file