I've just launched my next Telegram Bot: Tyzenhaus. It is a shared expenses tracking bot. Whether you are a group of travelers βοΈ, roommates π , or just friends π«, Tyzenhaus will help you settle up your debts. Just like Splitwise, but simpler and directly in Telegram. This half-minute video describes it:
So, with a dozen bots written in Kotlin for the past two years, I want to sum up my experience and name the two most important design/architecture patterns for Telegram Bots. You'll definitely find them useful when making your own bot.
Chain of Responsibility
When writing web apps, we, the backend developers, are used to have different handlers for different URLs. Those are configured in a framework-specific manner, like annotations in Java, URLconf in Django or routing in Node.js. Let's use this name β routing β to describe this way of configuring the logic.
But this is not the case with Telegram Bot API. If you're using webhooks, all the updates will be sent to a single destination in your application. It also works the same way with long polling: you'll have a single place in your codebase, calling getUpdates
in a loop. There is no routing and you have to implement it yourself.
The best way to do that is by employing a chain of responsibility pattern.
To do that, you should separate your bot logic and enclose it into small bits. Let's name them handlers. Handlers could be functions or classes, it doesn't really matter. Every handler is completely independent in a way that it solely decides whether it processes a particular incoming update or not. This is crucial and this is what makes chain of responsibility different from routing. So let me repeat: a handler decides to process or skip the update on its own, not an external router.
Finally, you combine your handlers in a list and dispatch every incoming update through it.
This is a general concept, actual implementations may vary. This chain shouldn't even be sequential: handlers could run in parallel. Although make sure that at most one handler will process an update, otherwise you may have flaky replies or replies out of order.
Finite State Machine
Another thing we're used to are smarts client for our APIs. Even the so-called thin clients are smarter than Telegram! Although we don't trust the input and always validate it on the backend (don't we?), clients may have forms grouping multiple fields and even wizard-like workflows, combining multiple forms. I mean, usually, you get a whole JSON object with multiple fields as an input. You never have to process the input one field at a time, which is exactly the case with Telegram.
Thus, you have to store all the state on a server. You may have heard about state pattern, but what I'm using is more like FSM.
FSM is just a fancy way of saying "a graph". FSM is a directed graph of states with well-known transitions between them. "Well-known" means that for every possible state you know the exact rules for transitions to other states. This predictability makes FSMs very easy to understand, maintain and extend.
A lot of processes could be described with FSMs. Look at this one, describing the expense workflow in my bot:
Green edges are "success paths", when the input is valid and the logic state could be transitioned. Red edges are unsuccessful attempts of user to provide input. E.g. passing arbitrary non-numeric strings as monetary amounts.
You've got the idea in a few seconds, don't you?
Combining chain of responsibility with FSM
And here is the beauty of those two patterns combined in three simple steps:
- Process updates with a chain of responsibility. Handlers should have two inputs: incoming Telegram Bot API update and current state. It could be a state for a single user in a private chat, a group state, or even a particular user state in a particular group β it doesn't matter;
- Save the resulting state;
- Repeat;
You could implement literally any bot for any messenger with this simple recipe. Don't you believe me? Go and try yourself, I bet you have some great ideas!
π€ Try Tyzenhaus out | β Star it on GitHub | π€¬ Create an issue on GitHub.
Top comments (0)