# Asynchronous library for Telegram bot API

[![Latest PyPI package version](https://badge.fury.io/py/aiotgbot.svg)](https://pypi.org/project/aiotgbot)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/gleb-chipiga/aiotgbot/blob/master/LICENSE)
[![Downloads](https://img.shields.io/pypi/dm/aiotgbot)](https://pypistats.org/packages/aiotgbot)

## Key Features

- Asyncio and [aiohttp](https://github.com/aio-libs/aiohttp) based
- All [Telegram Bot API](https://core.telegram.org/bots/api) types and methods supported
- Tracks Telegram Bot API 7.2 (released March 31, 2024)
- Bot API rate limit support
- Both long polling and webhooks supported
- Fully type annotated ([PEP 484](https://www.python.org/dev/peps/pep-0484/))

## Installation

aiotgbot is available on PyPI. Use pip to install it:

```bash
pip install aiotgbot
```

## Requirements

- Python >= 3.11
- [aiohttp](https://github.com/aio-libs/aiohttp)
- [aiojobs](https://github.com/aio-libs/aiojobs)
- [msgspec](https://github.com/jcrist/msgspec)
- [tenacity](https://github.com/jd/tenacity)
- [frozenlist](https://github.com/aio-libs/frozenlist)
- [aiofreqlimit](https://github.com/gleb-chipiga/aiofreqlimit)
- [yarl](https://github.com/aio-libs/yarl)

## Using aiotgbot

```python
from typing import AsyncIterator

from aiotgbot import (
    Bot,
    BotUpdate,
    BotUpdateKey,
    HandlerTable,
    PollBot,
    PrivateChatFilter,
    Runner,
)
from aiotgbot.storage_memory import MemoryStorage

handlers = HandlerTable()


@handlers.message(filters=[PrivateChatFilter()])
async def reply_private_message(bot: Bot, update: BotUpdate) -> None:
    assert update.message is not None
    name = (
        f"{update.message.chat.first_name} "
        f"{update.message.chat.last_name}"
    )
    update["greeting_count"] = update.get("greeting_count", 0) + 1
    await bot.send_message(update.message.chat.id, f"Hello, {name}!")


async def run_context(runner: Runner) -> AsyncIterator[None]:
    storage = MemoryStorage()
    await storage.connect()
    handlers.freeze()
    bot = PollBot(runner["token"], handlers, storage)
    await bot.start()

    yield

    await bot.stop()
    await storage.close()


def main() -> None:
    runner = Runner(run_context)
    runner["token"] = "some:token"
    runner.run()


if __name__ == "__main__":
    main()
```

## Upgrading to 0.18.0

**New features:**

- **BotUpdateKey** – a dedicated typed key object for items stored on `BotUpdate`.
- `BotUpdate` exposes `get_typed(key)`, `set_typed(key, value)`, and `del_typed(key)` helpers for working with `BotUpdateKey` instances.
- Each `BotUpdateKey` enforces runtime type checking via `isinstance()` so handlers only see the expected payload.

`BotUpdate` remains a regular mutable mapping so filters and handlers can stash arbitrary helper objects between each other. To keep data structured, use `BotUpdateKey` which enforces types per slot:

```python
from dataclasses import dataclass

from aiotgbot import BotUpdateKey


@dataclass
class Session:
    trace_id: str
    retries: int


session_key = BotUpdateKey("session", Session)


async def my_handler(bot: Bot, update: BotUpdate) -> None:
    if session_key.name not in update:
        update.set_typed(session_key, Session(trace_id="abc", retries=0))
    session = update.get_typed(session_key)
    ...
```

## Development

We use [Prek](https://github.com/DetachHead/prek) as a drop-in `pre-commit` replacement backed by `uv` so hook environments resolve quickly and reproducibly. Install it once and run the configured Ruff, `mypy --strict`, and Basedpyright checks via:

```bash
uv tool install prek
prek install
prek run --all-files
```

`prek run` reads `.pre-commit-config.yaml`, so you can still target a subset of hooks or files during local development.

`mise.toml` at the repo root mirrors the common workflows, so you can rely on [mise](https://mise.jdx.dev/) instead of remembering the raw commands. Trust the config once via `mise trust mise.toml` and then run, for example:

```bash
mise run lint
mise run mypy
mise run basedpyright
mise run test
```
