Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create InputChainHandler #290

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

JcMinarro
Copy link
Collaborator

@JcMinarro JcMinarro commented Jun 18, 2023

This new InputChainHandler helps us to collect inputs after a previous handler has been processed.

Let show some examples:

-. A button is pressed to change the name of the device:

addHandler(
        CallbackQueryHandler(
            callbackData = "change-device-name",
            handleCallbackQuery = {
                bot.sendMessage(
                    chatId = ChatId.fromId(callbackQuery.from.id),
                    text = "Which name do you want to use?"
                )
                update.consume()
            }
        ).inputChain<String>(
            inputChainIdGen = { previousInputChainId, update -> "someId" },
            handleInputChain = {
                val name = this.text
                bot.sendMessage(
                    chatId = ChatId.fromId(message.from?.id!!),
                    text = "Ok, I will use '$name' for your device"
                )
                update.consume()
                // Return `null` if we don't want to receive new updates on the chain
                null
            }
        )
    )

-. The user send a command to create a new form

addHandler(
        CommandHandler("newForm") {
            bot.sendMessage(
                chatId = ChatId.fromId(message.from?.id!!),
                text = "The form will have 3 questions.\n1º What is your name?"
            )
            update.consume()
        }.inputChain<Int>(
            inputChainIdGen = { previousInputChainId, update ->
                         when {
                             previousInputChainId == null -> 0
                             previousInputChainId < 2 -> previousInputChainId + 1
                             else -> null
                         }
                              },
            handleInputChain = {
                when(inputChainId) {
                    0 -> {
                        val userName = text
                        bot.sendMessage(
                            chatId = ChatId.fromId(message.from?.id!!),
                            text = "Hello $userName, what month were you born?"
                        )
                    }
                    1 -> {
                        val month = text
                        bot.sendMessage(
                            chatId = ChatId.fromId(message.from?.id!!),
                            text = "$month, what year?"
                        )
                    }
                    2 -> {
                        val year = text
                        bot.sendMessage(
                            chatId = ChatId.fromId(message.from?.id!!),
                            text = "Cool, you have completed the form"
                        )
                    }
                }
                update.consume()
                inputChainIdGen(inputChainId, update)
            }
        )
    )

@JcMinarro JcMinarro force-pushed the feature/input-chain-handler branch 3 times, most recently from 9be06cb to d01c257 Compare June 18, 2023 15:32
Copy link
Member

@vjgarciag96 vjgarciag96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea that you are proposing here, just leaving some questions and comments about the implementation.

private val inputChainIdGen: (previousInputChainId: T?, Update) -> T?,
private val handleInputChain: HandleInputChain<T>,
) : Handler {
private val chainMap = mutableMapOf<Long, T?>()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User id sounds right as discriminator for deciding when to chain updates, but I think it might be possible for the same bot to be in 2 different chats with the same user and thus we could run into an incorrect chain in that situation. Should we be using chat id too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!!!
Should we have:
Map<Long<Map<Long, T?>> (ChatId, UserId, Identifier)
or:
Map<String, T?> ("$chatId$userId, Identifier)

First one should be easier to access.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First sounds good to me. And if we are always going to access a single identifier for a given (ChatId,UserId) pair another option would be to have our own chain discrimator data class with both that we use as the key (like the second option but without having to concatenate the ids, etc...).

class InputChainHandler<T>(
private val filter: Filter,
private val initialHandler: Handler,
private val inputChainIdGen: (previousInputChainId: T?, Update) -> T?,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this supposed to be used? It's not obvious to me by reading the internal code of library, so I think it will be even more difficult for consumers of the library to understand how to (correctly) use this API. Is it possible to encapsulate this detail in the library?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add some showcases to help other devs to use it and understand how to implement their own chain

Comment on lines +171 to +201
fun <T> Handler.inputChain(
filter: Filter = Filter.Text,
inputChainIdGen: (previousInputChainId: T?, Update) -> T?,
handleInputChain: HandleInputChain<T>,
): Handler {
return InputChainHandler(filter, this, inputChainIdGen, handleInputChain)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to also have an example usage of this in the doc and/or one of the sample modules.

private val inputChainIdGen: (previousInputChainId: T?, Update) -> T?,
private val handleInputChain: HandleInputChain<T>,
) : Handler {
private val chainMap = mutableMapOf<Long, T?>()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it looks like the value of the map is not really relevant, should this be a hash set of the visited "userId,chatId" instead? That way we can also get rid of inputChainIdGen. Or is there another reason why we'd like to have that in the environment object?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The T value is the identifier of the "Input Question".
When the "chain action" is executed, it needs to know "which id it comes from".
Imagine you are creating a form with 3 questions like the one I posted in the PR description, but whenever the question is responded we want to store them on a DB. We need to know which question the user responded, and the value of the T is the id used. I used a generic type to allow devs to include their own data type (Maybe more than an int/String value is needed to continue the chain).

}
}
} ?: initialHandler.handleUpdate(bot, update).also {
update.takeIf { it.consumed }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only chaining if the update is consumed? I feel that having to mark the first update as consumed for the chain to work is going to make the API hard to use.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about it...
I assumed whenever "a question" is processed, they (App devs) should consume it to avoid wrong behavior in other commands that match the "initialHandler" checks (And maybe override the ID stored on this chain), but maybe is a good idea removing this check and rely on the "good implementation" of or users

val message: Message,
)

class InputChainHandler<T>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be great to have some tests for this. It would be good as documentation for other maintainers of the library, it would give us more confindence to update this class in the future, it could help us find some edge cases that we may not have considered, etc...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will add some tests for this new feature

@JcMinarro JcMinarro force-pushed the feature/input-chain-handler branch 4 times, most recently from 9685456 to 25a21f5 Compare October 20, 2023 16:18
@red-avtovo
Copy link
Collaborator

@JcMinarro do you find this PR still valid? Do you have any last changes to push? Otherwise I would be happy to accept it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants