This file contains detailed information about the development of the bot. It is only relevant if you want to contribute to the bot or host your own instance of it. Note that additional documentation is available in the source code itself.
- Project structure: Contains a short overview of the files and directories in this repository.
- Asynchronous execution: Contains information about how the bot and web server are run asynchronously in the same process.
- Shared data models: Contains information about the shared data models used by both the bot and the web server.
- Implementation details: Contains information about the implementation details of the bot and web server.
- Telegram bot: Contains information about the implementation of the Telegram bot.
- Web server: Contains information about the implementation of the web server.
- Web pages: Contains information about the implementation of the web pages.
- HTML: Contains information about the HTML templates.
- CSS: Contains information about the CSS style.
- JavaScript: Contains information about the JavaScript files.
- Debugging: Contains some tips on how to debug the bot.
Here's a short overview of the files and directories in this repository:
bot.py
: The main entry point of the bot. This is where the bot is initialized, and the web app is started.requirements.txt
: A list of all dependencies of the bot.src/
: Contains the Python source files for the bot and web server.src/arguments.py
: Contains the code for parsing command line arguments.src/shared.py
: Contains shared data models (and the shared context) used by both the bot and the web server.src/webapp_server.py
: Contains the code for the web server.
static/
: Contains static files (excluding templates) for the web server.static/css/
: Contains thestyle.css
file, which contains the CSS styles for the web app.static/js/
: Contains JavaScript files for the web app.static/js/common.js
: Contains common code used by all pages.static/js/create.js
: Contains the code for the creation page.static/js/results.js
: Contains the code for the results page.static/js/vote.js
: Contains the code for the voting page.
templates/
: Contains the Jinja2 templates for the web server.templates/base.html
: Contains the base template, which is extended by all other templates.templates/create.html
: Contains the template for the creation page.templates/error.html
: Contains the template for error pages.templates/results.html
: Contains the template for the results page.templates/vote.html
: Contains the template for the voting page.
Note that we use Python's asyncio
framework1 to run the bot and web server asynchronously in the same process, which means we only have to execute bot.py
to run both.
This allows us to share data between the bot and web server (see Shared data models below).
We achieve this by registering the Telegram bot's initialization methods in the startup()
method in bot.py
, which is decorated with @webapp.before_serving
and thus called before the web server is started.
The web server itself is run using webapp.run_task()
.
Similarly, when the web server is shutdown (i.e., when the process is terminated), the Telegram bot is stopped in the shutdown()
method, which is decorated with @webapp.after_serving
.
The Updater in python-telegram-bot
may time out, which is why it takes up to 5 seconds (the default timeout) for the process to terminate (we also have to ignore the TimedOut
exception in the shutdown handler).
The shared.py
file contains the shared data models used by both the bot and the web server.
Specifically, the following data models are defined:
Event
: Represents a single event (i.e., a poll). Apart from the event ID and creation time, it contains the event's title, description, available days, and anonymity/notification settings, along with a list ofEventVote
s.- Methods for counting (
num_votes
) or returning (day_votes
) the number of votes for a given day and given type of vote (yes/no/maybe) are also defined. - The
best_days
method returns a list of the "most suitable" days for the event. Most suitable, in this case, means the days with the largest number of yes votes. If there are multiple such days, we choose from these the ones with the largest number of maybe votes. If there are no days with at least one yes vote, we return an empty list.
- Methods for counting (
EventVote
: Represents a user's vote on a poll. It consists of the user's ID and name2, a dictionary mapping days to the type of vote (yes/no/maybe), and the time at which the vote was cast.SharedContext
: Represents the shared context between the bot and the web server. It contains thetelegram_app
on which the bot runs and the passed command line arguments inargs
.
The Telegram bot has been implemented using the python-telegram-bot library – read their excellent documentation to learn more.
Persistence has also been implemented using the library's built-in pickling functionality:
We store all polls in the bot_data['events']
dictionary, which maps event IDs to Event
objects.
The web server accesses this dictionary through the telegram_app.bot_data
attribute available on the SharedContext
object.
The web server has been implemented using the Quart web framework, which has been chosen due to its similarity to Flask (which I am more familiar with) and its built-in support for asynchronous execution.
In the webapp_server.py
file, we define the following routes:
/
: The index page, which just displays a short message. May be useful for testing whether the web server is running correctly./create
: The page for creating a new poll. It contains a form for entering the poll's title, description, and available days, along with a checkbox for making the poll anonymous and a checkbox for enabling notifications./vote
: The page for voting in a poll. The poll ID is passed via the URL parametertgWebAppStartParam
, which Telegram appends automatically if thet.me
link is passed thestartapp
URL parameter. The page contains a form for selecting "Yes/No/Maybe" for each available day./results
: The page for viewing the results of a poll. The poll ID is passed either via the URL parameterpoll_id
or viatgWebAppStartParam
, which Telegram appends automatically if thet.me
link is passed thestartapp
URL parameter. The page contains a table with the number of yes/no/maybe votes for each day, listing what each user voted (if the poll is not anonymous), along with a button for deleting the poll if the user is the creator of the poll./polls
: The API endpoint which the JavaScript files use. Note that we validate theinitData
before doing anything else, to make sure nothing weird is going on. We differentiate on the HTTP method used:POST
: This creates a new poll based on the JSON included in the request body.PATCH
: This casts a vote in a poll based on the JSON included in the request body. Alternatively, if a vote for this user on this poll already exists, it is overwritten.GET
: This returns the results link and the JSON-encoded vote for the user and poll specified in the URL parameters.DELETE
: This deletes the poll specified in the JSON included in the request body.
An additional note on the validation: The data is also rejected if the sent data is more than 60 minutes old. This is to prevent replay attacks, where an attacker could send the same data multiple times to the server. The relatively high number of 60 minutes was chosen just in case the user spends a long time entering some poll data, we wouldn't want to reject it just because it took them that long.
We use Jinja2 templates for the web pages, which are located in the templates/
directory.
On the results page, we pass the result data from the server to the client via template parameters, which on the one hand construct the page to contain the votes for each day, and on the other hand set some hidden <input>
fields which the JavaScript files can then access.
For example, the poll.id
is passed this way so that the JavaScript files can create a shareable link to the results page.
Since any user that has the link to the results page can see the results, we don't need to validate the client in any way here before showing the data.
This is different on the voting page: Polls can be anonymous, which means that instead of passing the data via template parameters to the client, we do it via the API endpoint.
Otherwise, an attacker may send a forged request containing the user's ID and name, and the server would happily accept it (since it does not have access to the initData
at the point the template is rendered).
This way, the server can validate the client before sending the private votes of the user.
Requests in which any data is modified (poll creation/deletion and voting) are always validated.
We use Bootstrap 5 for the web pages, which is included in the base.html
template.
In the style.css
file, we make liberal use of the CSS variables Telegram passes to the web app (such as --tg-theme-bg-color
), so that the theme of the web app matches the theme of the Telegram app.
Additionally, there are a lot of smaller modifications to the default Bootstrap styles, which are mostly there to make the app look as close as possible to the Telegram app.
The JavaScript files are split into three files, one for each page.
Additionally, in common.js
we define some common functions used by all pages.
For example, we define replaceDateElements
, which is used to render the dates in the user's locale, and runOnVersion
, which only runs the given callback if the user's app supports it, and runs an alternative callback otherwise.
An additional note:
On the results screen (results.js
), we offer the user to share the results with a Telegram chat.
By default, we use switchInlineQuery()
for this, but if it isn't available, we use the always available openTelegramLink()
to open a link of the form https://t.me/share/url?url=>results_url>
, which at least has a similar effect.
To make debugging easier, you can pass the --debug
argument to bot.py
to enable debug logging and enable the debug modes of both asyncio and Quart.
As an additional tip, if you pass one or more Telegram user IDs to the --admin-ids
argument, you will be able to use the /dump
command to dump the bot's data to the current chat (it will also be printed to the console).
Otherwise, the command will not be available.
Footnotes
-
Specifically, we use the
Quart
web framework, and thepython-telegram-bot
library, both of which are built on top ofasyncio
. ↩ -
The user's name is only stored if the poll is not anonymous. Otherwise, it is set to an empty string. ↩