Server Side Swift with Vapor Post #2 - Bot Requirements, Design & Diagrams
Where we gather requirements📝 for BitBot , and draw a dependency diagram 🌌.
Previous post: Introduction to Vapor
Gathering Requirements
Before we start writing any code, we should try to document and explain what the requirements for BitBot are, so that we can create a plan for building our bot. We can consider the bot "finished" when it meets all of these requirements.
So in order for our Telegram bot to be "a bot" 🤖, what would it actually need to do?
The bot...
- Receives HTTP messages sent to it by a Telegram API server, and
- Responds to the Telegram API server with a logical "bit" of information (yes or no) of some type, with some extra 💥chaos💥 thrown in for fun.
The two requirements above are pretty generic; we could write an application from them, but we should probably narrow down those requirements into smaller, more manageable pieces.
BitBot Requirements
- The bot will run as a Vapor application on a public server
- The bot will receive a Telegram Update message from the API server via the web hook URL.
- The bot will parse the Telegram Update message, and determine which of the
following message/media types are contained in the message:
- A Plain Ol' Text Message (POTM)
- A POTM containing a "slash" command for the bot (Example: /foo, or /foo bar)
- A media file, such as a photo or video, audio file, animation, etc.
- Forwarded messages, which can be either POTMs (but NOT slash commands) or media from other users or channels
- An edited message, which can be any of the above message types (POTM, media file, forwarded message)
- Once the message is parsed, handle the command or message as needed
- If a message was sent to the bot, check that the message ends in a
question mark (?)
- If so, create a text response with a "Bit" of information. The text response will be one of the following options:
- YES! (45% chance)
- NO! (45% chance)
- NO NO NO NO NO! (9% chance)
- COUGH (1% chance)
- If a command was sent to the bot, handle the command, and create a text response to return to the user if needed
- If neither a message with a trailing question mark (?) nor a command was sent to the bot, respond to the user with an ellipsis (...)
- If a message was sent to the bot, check that the message ends in a
question mark (?)
- When responding to the Telegram API message...
- If a text response needs to be returned to the Telegram API server (a bot reply for example), then generate a Telegram sendMessage JSON message, and return the response to the Telegram API server along with an HTTP 200 status code.
- If no text message response needs to be sent to the Telegram API server, then return an HTTP 200 message to the Telegram API server with no HTTP response body
- The bot should ALWAYS return HTTP 200 to the server, no matter what; this is explained more fully in the next post of this series when we go over the Telegram Bot API
BitBot will handle text messages only, no media files, captions, forwarded or edited messages. As a general rule, bots that implement more complex functionality will also have to deal with more complex Telegram API messages and message types.
BitBot Application Design
Now that we have requirements, we will go ahead and describe the major components of our bot, and explain how they work work together to process incoming HTTP requests.
For a given HTTP request, the Vapor framework will use the Router to parse and dispatch valid requests to a handler function in the appropriate Controller.
Inside of our Controller function, we need to do whatever message parsing and handling that is required for the request, then compose a response of some kind to return to Vapor (JSON data, HTML, or HTTP status code), which will then send the response inside of an HTTP message back to the requestor, in this case the Telegram API server.
So for BitBot 🤖, the set of components that will make up our application will look something like:
- Application - provided by Vapor
- Router - provided by Vapor, maps URLs to controller functions
- Request - generated by Vapor from the incoming HTTP request
- Controller - sets up the message parser and handler and calls them as
needed
- <MessageParser> - A protocol responsible for parsing the incoming
HTTP request body, and returning the type of Telegram message as a Swift
enum
; the implementation of this protocol will be called BitBotMsgParser - <MessageHandler> - A protocol that handles the bot's response to the parsed message; the implementation of this protocol will be called BitBotMessageHandler
- <MessageParser> - A protocol responsible for parsing the incoming
HTTP request body, and returning the type of Telegram message as a Swift
The response to the Telegram API server will be either:
- A Bare HTTP 200 status code if we have no data to return, or
- An HTTP 200 status code along with a Telegram sendMessage JSON response
Both the bot <\MessageParser> and <MessageHandler> were specified as Swift Protocols, as opposed to classes that can be inherited. We use Swift Protocols for these components to make our code more modular, it will be easy to compose new implementations of these protocols in the future in order to change the behavior of the bot.
How does this work in practice?
If we wanted to write a new Telegram bot for example, we could write up a new implementation of <MessageHandler> to handle commands, messages, and media, and after changing the tests for <MessageHandler>, the rest of the application would need minimal changes in order to run with the new handler.
If we wanted to use our bot on a completely different network that for example used "bang" (!) characters to denote bot commands, we would most likely only need a different <MessageParser> implementation (and tests) to handle the new command syntax, and minimal changes would be needed for the rest of the application.
For BitBot, if we wanted to visualize all of the components listed above as a diagram, it might look something like the following...
BitBot Dependency Diagram
The above diagram is somewhat based on Unified Modeling Language.
When drawing a dependency diagram, we want to QUICKLY sketch out all of the components and modules in our application, and show how they relate to each other. Just enough diagram to get the point across, and that's it! Wizardly 🧙 levels of UML are not required here 🪄.
We want to create dependency diagrams before writing any code for our application in order to visualize the systems that we are trying to model.
With a dependency diagram like this in hand...
- How easy would it be to explain your proposed implementation to your peers and/or management?
- How hard would it be to spot design problems when modeling systems?
- How much easier would it be to split up the development of application features amongst team members based on the design shown in the dependency diagram?
- What if, during the process of onboarding a new team member, you could point to a component on this diagram like this and say "Look, we are working on this component HERE, how would you like to help us out?"
As an example of what a dependency diagram can show you, you can see in the diagram that there is currently a strong dependency between the components that will implement <MessageParser> and <MessageHandler> protocols (the solid arrow with a filled head that is pointing from <MessageHandler> to <MessageParser>).
In order to keep this demonstration project "simple" for now, we will be using
a Swift enum
to describe the result of the parsed Telegram messages, which
means that the <MessageHandler> implementation will need to know about
the parsed message enum
type that is defined in the <MessageParser>
protocol.
One way we could refactor the app to remove this dependency on the enum
from
<MessageParser> would be to use a Swift Result enum
instead. You
might see this refactoring shown in a later post, so stay tuned!
Conclusion
After gathering and refining requirements, we can use them to plan the implementation of our application, in this case, the BitBot Telegram bot.
When the application meets all of these requirements, our application can be considered "finished". Of course, new requirements can always be added at a later date.
By quickly mapping out component dependencies in a dependency diagram, we make it easier for ourselves and others to visualize how the application is designed, and have a better chance to spot design issues that may lead to problems during development.
In the next post, we will begin exploring the Telegram Bot API, and we will explain why you should always return an HTTP 200 status code to Telegram Bot API web hook requests.
Previous post: Introduction to Vapor
A special thanks to Essential Developer for being a great resource for learning about Swift, iOS and macOS.
Copyright ©2022 by Brian Manning.
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.