Skip to content

Commit

Permalink
SlashCommand: implement parser
Browse files Browse the repository at this point in the history
  • Loading branch information
bmarty committed Apr 9, 2019
1 parent fab1d24 commit eae8f99
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ package im.vector.riotredesign.features.command
import androidx.annotation.StringRes
import im.vector.riotredesign.R

/**
* Defines the command line operations
* the user can write theses messages to perform some actions
* the list will be displayed in this order
*/
enum class Command(val command: String, val parameters: String, @StringRes val description: Int) {
EMOTE("/me", "<message>", R.string.command_description_emote),
BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,176 @@

package im.vector.riotredesign.features.command

import timber.log.Timber

object CommandParser {

/**
* Convert the text message into a Slash command.
*
* @param textMessage the text message
* @return a parsed slash command (ok or error)
*/
fun parseSplashCommand(textMessage: String): ParsedCommand {
// check if it has the Slash marker
if (!textMessage.startsWith("/")) {
return ParsedCommand.ErrorNotACommand
} else {
Timber.d("parseSplashCommand")

// "/" only
if (textMessage.length == 1) {
return ParsedCommand.ErrorEmptySlashCommand
}

// Exclude "//"
if ("/" == textMessage.substring(1, 2)) {
return ParsedCommand.ErrorNotACommand
}

var messageParts: List<String>? = null

try {
messageParts = textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
} catch (e: Exception) {
Timber.e(e, "## manageSplashCommand() : split failed " + e.message)
}

// test if the string cut fails
if (messageParts.isNullOrEmpty()) {
return ParsedCommand.ErrorEmptySlashCommand
}

val slashCommand = messageParts[0]

when (slashCommand) {
Command.CHANGE_DISPLAY_NAME.command -> {
val newDisplayname = textMessage.substring(Command.CHANGE_DISPLAY_NAME.command.length).trim()

return if (newDisplayname.isNotEmpty()) {
ParsedCommand.ChangeDisplayName(newDisplayname)
} else {
ParsedCommand.ErrorSyntax(Command.CHANGE_DISPLAY_NAME)
}
}
Command.TOPIC.command -> {
val newTopic = textMessage.substring(Command.TOPIC.command.length).trim()

return if (newTopic.isNotEmpty()) {
ParsedCommand.ChangeTopic(newTopic)
} else {
ParsedCommand.ErrorSyntax(Command.TOPIC)
}
}
Command.EMOTE.command -> {
val message = textMessage.substring(Command.EMOTE.command.length).trim()

return ParsedCommand.SendEmote(message)
}
Command.JOIN_ROOM.command -> {
val roomAlias = textMessage.substring(Command.JOIN_ROOM.command.length).trim()

return if (roomAlias.isNotEmpty()) {
ParsedCommand.JoinRoom(roomAlias)
} else {
ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
}
}
Command.PART.command -> {
val roomAlias = textMessage.substring(Command.PART.command.length).trim()

return if (roomAlias.isNotEmpty()) {
ParsedCommand.PartRoom(roomAlias)
} else {
ParsedCommand.ErrorSyntax(Command.PART)
}
}
Command.INVITE.command -> {
return if (messageParts.size == 2) {
ParsedCommand.Invite(messageParts[1])
} else {
ParsedCommand.ErrorSyntax(Command.INVITE)
}
}
Command.KICK_USER.command -> {
return if (messageParts.size >= 2) {
val user = messageParts[1]
val reason = textMessage.substring(Command.KICK_USER.command.length
+ 1
+ user.length).trim()

ParsedCommand.KickUser(user, reason)
} else {
ParsedCommand.ErrorSyntax(Command.KICK_USER)
}
}
Command.BAN_USER.command -> {
return if (messageParts.size >= 2) {
val user = messageParts[1]
val reason = textMessage.substring(Command.BAN_USER.command.length
+ 1
+ user.length).trim()

ParsedCommand.BanUser(user, reason)
} else {
ParsedCommand.ErrorSyntax(Command.BAN_USER)
}
}
Command.UNBAN_USER.command -> {
return if (messageParts.size == 2) {
ParsedCommand.UnbanUser(messageParts[1])
} else {
ParsedCommand.ErrorSyntax(Command.UNBAN_USER)
}
}
Command.SET_USER_POWER_LEVEL.command -> {
return if (messageParts.size == 3) {
val userID = messageParts[1]
val powerLevelsAsString = messageParts[2]

try {
val powerLevelsAsInt = Integer.parseInt(powerLevelsAsString)

ParsedCommand.SetUserPowerLevel(userID, powerLevelsAsInt)
} catch (e: Exception) {
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
}
} else {
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
}
}
Command.RESET_USER_POWER_LEVEL.command -> {
return if (messageParts.size == 2) {
val userId = messageParts[1]

ParsedCommand.SetUserPowerLevel(userId, 0)
} else {
ParsedCommand.ErrorSyntax(Command.SET_USER_POWER_LEVEL)
}
}
Command.MARKDOWN.command -> {
return if (messageParts.size == 2) {
when {
"on".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(true)
"off".equals(messageParts[1], true) -> ParsedCommand.SetMarkdown(false)
else -> ParsedCommand.ErrorSyntax(Command.MARKDOWN)
}
} else {
ParsedCommand.ErrorSyntax(Command.MARKDOWN)
}
}
Command.CLEAR_SCALAR_TOKEN.command -> {
return if (messageParts.size == 1) {
ParsedCommand.ClearSclalarToken
} else {
ParsedCommand.ErrorSyntax(Command.CLEAR_SCALAR_TOKEN)
}
}
else -> {
// Unknown command
return ParsedCommand.ErrorUnknownSlashCommand(slashCommand)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2019 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.riotredesign.features.command

/**
* Represent a parsed command
*/
sealed class ParsedCommand {
// This is not a Slash command
object ErrorNotACommand : ParsedCommand()

object ErrorEmptySlashCommand : ParsedCommand()

// Unknown/Unsupported slash command
class ErrorUnknownSlashCommand(val slashCommand: String) : ParsedCommand()

// A slash command is detected, but there is an error
class ErrorSyntax(val command: Command) : ParsedCommand()

// Valid commands:

class SendEmote(val message: String) : ParsedCommand()
class BanUser(val userId: String, val reason: String) : ParsedCommand()
class UnbanUser(val userId: String) : ParsedCommand()
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
class Invite(val userId: String) : ParsedCommand()
class JoinRoom(val roomAlias: String) : ParsedCommand()
class PartRoom(val roomAlias: String) : ParsedCommand()
class ChangeTopic(val topic: String) : ParsedCommand()
class KickUser(val userId: String, val reason: String) : ParsedCommand()
class ChangeDisplayName(val displayName: String) : ParsedCommand()
class SetMarkdown(val enable: Boolean) : ParsedCommand()
object ClearSclalarToken : ParsedCommand()
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import android.os.Parcelable
import android.text.Editable
import android.text.Spannable
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
Expand All @@ -35,9 +36,11 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
Expand Down Expand Up @@ -80,7 +83,6 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
}

private val session by inject<Session>()
// TODO Inject?
private val glideRequests by lazy {
GlideApp.with(this)
}
Expand All @@ -104,6 +106,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
setupComposer()
roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
}

override fun onResume() {
Expand Down Expand Up @@ -192,8 +195,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac

// Add the span
val user = session.getUser(item.userId)
// FIXME avatar is not displayed
val span = PillImageSpan(glideRequests, context!!, item.userId, user)
span.bind(composerEditText)

editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

Expand All @@ -209,7 +212,6 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
composerEditText.text = null
}
}
}
Expand All @@ -236,6 +238,32 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
autocompleteUserPresenter.render(state.asyncUsers)
}

private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) {
is SendMessageResult.MessageSent, is SendMessageResult.SlashCommandHandled -> {
// Clear composer
composerEditText.text = null
}
is SendMessageResult.SlashCommandError -> {
displayError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
}
is SendMessageResult.SlashCommandUnknown -> {
displayError(getString(R.string.unrecognized_command, sendMessageResult.command))
}
is SendMessageResult.SlashCommandNotImplemented -> {
activity!!.toast(R.string.not_implemented)
}
}
}

private fun displayError(message: String) {
AlertDialog.Builder(activity!!)
.setTitle(R.string.command_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}

// TimelineEventController.Callback ************************************************************

override fun onUrlClicked(url: String) {
Expand Down
Loading

0 comments on commit eae8f99

Please sign in to comment.