Prerequisites

Registering the bot

  • Go to the BotFather
  • Send the /newbot command
  • Enter your bot display name
  • Enter your bot user name

BotFather will then send you a message with a very important thing: your API token.

Keep your API token secure and store it safely.

Creating the project

Create the project with Poetry:

poetry new weatherbot

Go into the project directory and install python-telegram-bot:

cd weatherbot
poetry add python-telegram-bot

Create a file named main.py with the following code:

from telegram import Update
from telegram.ext import Updater, CommandHandler, CallbackContext


def hello(update: Update, context: CallbackContext) -> None:
    update.message.reply_text(f"Hello {update.effective_user.first_name}")


def main():
    # We create an Updater instance with our Telegram token.
    updater = Updater("YOUR TOKEN HERE")

    # We register our command handlers.
    updater.dispatcher.add_handler(CommandHandler("hello", hello))

    # Let's start the bot!
    # Calling this method is non-blocking.
    updater.start_polling()

    # Run the bot until you press Ctrl-C.
    # Or until the process receives SIGINT, SIGTERM or SIGABRT.
    updater.idle()


if __name__ == "__main__":
    main()

Run the script:

poetry run python main.py

In Telegram, go to your bot and type /hello.

The bot should answer Hello <your first name>.

Everything works. Let’s go further.

Integrating OpenWeather

OpenWeather provides historical, current and forecasted weather data via APIs. We are going to use their service to request current weather data for a given location via our Telegram bot.

They offer a free plan with a limit of 60 calls/minute, which is quite enough for our needs.

Create an account first, than go to “API keys” tab and copy your key.

Keep your API key secure and store it safely.

Install the Python client PyOWM:

poetry add pyowm

We will use it later to access the API.

Creating a dot env file

Create a file named .env at the root of your project. Never commit this file in your DVCS. It will contain environment variables with your secret tokens.

touch .env

Edit this file and add your Telegram and OpenWeather tokens inside:

TG_TOKEN='YOUR TOKEN HERE'
OWM_TOKEN='YOUR TOKEN HERE'

Install python-dotenv package:

poetry add python-dotenv

Edit main.py to load your environment variables from this file:

from dotenv import load_dotenv
from telegram import Update
from telegram.ext import Updater, CommandHandler, CallbackContext


def hello(update: Update, context: CallbackContext) -> None:
    update.message.reply_text(f"Hello {update.effective_user.first_name}")


def main():
    # Load environment variables from .env file.
    load_dotenv()

    # We create an Updater instance with our API token.
    updater = Updater(os.environ.get("TG_TOKEN"))

    # We register our command handlers.
    updater.dispatcher.add_handler(CommandHandler("hello", hello))
    
    # Let's start the bot!
    # Calling this method is non-blocking.
    updater.start_polling()

    # Run the bot until you press Ctrl-C.
    # Or until the process receives SIGINT, SIGTERM or SIGABRT.
    updater.idle()


if __name__ == "__main__":
    main()

Implementing bot commands

Here is the commented code of the modified version of main.py file.

import logging
import os

from dotenv import load_dotenv
from pyowm import OWM
from pyowm.weatherapi25.observation import Observation
from telegram import Update
from telegram.ext import Updater, CommandHandler, CallbackContext


logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.ERROR
)

logger = logging.getLogger(__name__)

# Default values for the webhook server.
DEFAULT_WEBHOOK_ADDR = "0.0.0.0"
DEFAULT_WEBHOOK_PORT = 8080


# Message displayed when we call /start or /help commands.
help_msg = (
    "/start - test this bot\n"
    "/help - show help\n"
    "/weather - device location weather\n"
    "/weather <city> - city weather\n"
)


def start_handler(update: Update, _: CallbackContext) -> None:
    update.message.reply_text(f"Hi {update.effective_user.name}!\n\n{help_msg}")


def help_handler(update: Update, _: CallbackContext) -> None:
    update.message.reply_text(help_msg)


def error_handler(update: Update, context: CallbackContext) -> None:
    logger.warning(f'Update "{update}" caused error "{context.error}"')


def weather_handler(update: Update, context: CallbackContext):
    # We get text after /weather command.
    args_location = " ".join(context.args).strip()

    # If the user has enabled geolocation, we should
    # be able to retrieve their latitude and longitude.
    user_location = update.message.location

    # We declare our observation instance to use it
    # later to format our bot answer.
    observation: Observation = None

    # We instantiate our PyOWM OWM instance with our API token,
    # then get the weather_manager.
    owm = OWM(os.environ.get("OWM_TOKEN"))
    mgr = owm.weather_manager()

    # Get the weather from the user's device current location.
    if user_location is not None:
        try:
            observation = mgr.weather_at_coords(
                lat=user_location["latitude"], lon=user_location["longitude"]
            )
        except:
            pass
    # The user has disabled geolocation.
    # Get the weather from the city/country passed to the command.
    else:
        try:
            observation = mgr.weather_at_place(args_location)
        except:
            pass

    # We didn't find anything.
    # Let's early return and inform the user.
    if observation is None:
        update.message.reply_text("Sorry, I'm unable to find your location.")
        return

    # Success!
    update.message.reply_text(
        "{} ({})\n\n{}\n{}°C (feels like {}°C)\n{}% humidity".format(
            observation.location.name,
            observation.location.country,
            observation.weather.detailed_status.capitalize(),
            int(observation.weather.temperature(unit="celsius")["temp"]),
            int(observation.weather.temperature(unit="celsius")["feels_like"]),
            observation.weather.humidity,
        )
    )


def main():
    # We load environment variables from .env file.
    load_dotenv()

    # We retrieve the environment mode: development (default) or production.
    # This variable is used to choose between long polling or webhooks.
    env = os.environ.get("ENV", "development").lower()

    # We retrieve the Telegram API token from the environment.
    token = os.environ.get("TG_TOKEN")

    # We retrieve the webhook server settings from the environment.
    webhook_addr = os.environ.get("WEBHOOK_ADDR", DEFAULT_WEBHOOK_ADDR)
    webhook_port = os.environ.get("WEBHOOK_PORT", DEFAULT_WEBHOOK_PORT)
    webhook_url = os.environ.get("WEBHOOK_URL")

    # We create an Updater instance with our API token.
    updater = Updater(token)

    # We register our command handlers.
    updater.dispatcher.add_handler(CommandHandler("start", start_handler))
    updater.dispatcher.add_handler(CommandHandler("help", help_handler))
    updater.dispatcher.add_handler(CommandHandler("weather", weather_handler))
    updater.dispatcher.add_error_handler(error_handler)

    # We are going to use webhooks on production server
    # but long polling for development on local machine.
    if env == "production":
        # Start a small HTTP server to listen for updates via webhook.
        updater.start_webhook(
            listen=webhook_addr,
            port=webhook_port,
            url_path=token,
            webhook_url=f"{webhook_url}/{token}",
        )
        logger.info(f"Start webhook HTTP server - {webhook_addr}:{webhook_port}")
    else:
        # Start polling updates from Telegram.
        updater.start_polling()
        logger.info(f"Start polling updates")

    # Run the bot until you press Ctrl-C.
    # Or until the process receives SIGINT, SIGTERM or SIGABRT.
    updater.idle()


if __name__ == "__main__":
    main()

Deployment

This Telegram bot works properly but locally on your machine. If you switch your computer off or stop your bot, it won’t work anymore. It’s time to deploy it on a production server.

To do so, we are going to use Fly.io free allowances, because deploying on that platform is very easy and does not require any server maintenance, and Docker approach to simplify the deployment steps. With Fly, you don’t need to install Docker on your machine. It only requires a Dockerfile.

In the project directory, create a file named Dockerfile with the following lines:

FROM python:3.10-alpine AS builder
WORKDIR /app
ADD pyproject.toml poetry.lock /app/
RUN apk add build-base libffi-dev
RUN pip install poetry==1.2.0a2
RUN poetry config virtualenvs.in-project true
RUN poetry install --no-ansi


FROM python:3.10-alpine
WORKDIR /app
COPY --from=builder /app /app
ADD main.py /app
RUN adduser app -h /app -u 1000 -g 1000 -DH
USER 1000
CMD ["/app/.venv/bin/python", "main.py"]

Install the flyctl command-line utility.

For macOS or Linux (for Windows, refer to the documentation):

curl -L https://fly.io/install.sh | sh

Open your $HOME/.zshrc and add these lines:

export FLYCTL_INSTALL="$HOME/.fly"
export PATH="$FLYCTL_INSTALL/bin:$PATH"

Restart your shell session.

Create a Fly account:

flyctl auth signup

Or log in if you already have an account:

flyctl auth login

To enforce security, don’t forget to enable the two factor authentication in your profile settings.

From your project’s root directory, create your Fly app:

fly launch

You’ll have to answer a couple of questions:

Basically:

  • Give a name to your Fly app or leave blank to use an auto-generated one
  • Select a server region of your convenience
  • Don’t setup a PostgreSQL database now
  • Don’t deploy now

This command generates a fly.toml file with a default configuration.

Edit fly.toml and add these lines:

[env]
  ENV = "production"
  WEBHOOK_URL = "https://<app name>.fly.dev"

Replace <app name> with your app name. You can get it with:

fly info

A very important step is to set Telegram and OpenWeather API tokens. Locally, these tokens are stored into TG_TOKEN and OWM_TOKEN environment variables, dynamically loaded from .env file. These tokens can be called “secrets” as they are sensitive data.

Fly provides a command to set secrets on their servers. So let’s create them:

flyctl secrets set TG_TOKEN='YOUR TOKEN HERE'
flyctl secrets set OWM_TOKEN='YOUR TOKEN HERE'

Check if they have been successfully created:

flyctl secrets list

Your app is ready for prime time, let’s deploy it:

fly deploy

Once done, you can easily acccess your app dashboard with:

fly dashboard

You can now ask your bot the weather at any time.