Skip to content

Latest commit

 

History

History
128 lines (104 loc) · 12.1 KB

DEVELOPMENT.md

File metadata and controls

128 lines (104 loc) · 12.1 KB

Development Information

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.

Table of contents

  • 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.

Project structure

Here's a short overview of the files and directories in this repository:

Asynchronous execution

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).

Shared data models

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 of EventVotes.
    • 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.
  • 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 the telegram_app on which the bot runs and the passed command line arguments in args.

Implementation details

Telegram bot

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.

Web server

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 parameter tgWebAppStartParam, which Telegram appends automatically if the t.me link is passed the startapp 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 parameter poll_id or via tgWebAppStartParam, which Telegram appends automatically if the t.me link is passed the startapp 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 the initData 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.

Web pages

HTML

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.

CSS

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.

JavaScript

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.

Debugging

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

  1. Specifically, we use the Quart web framework, and the python-telegram-bot library, both of which are built on top of asyncio.

  2. The user's name is only stored if the poll is not anonymous. Otherwise, it is set to an empty string.