Давно пишу ботов для телеграмм, использую golang. Понадобился функционал - сканировать каналы по ссылке. Бот такое не может, это уже более сложное апи, порылся - нашел библиотеку на golang, попробовал - сложно. Нашел на питоне - проще. Но на питоне не хочется. Так родилась идея сделать простую обертку REST API для основного функционала: вступить в группу, прочитать сообщения, узнать информацию о группе, написать сообщение, и чтобы курлом все работало...


Кратко: https://github.com/pivolan/telegram_app_wrapper. Это стейтлесс на питоне простой, как три рубля. С такими методами:

POST /auth/send_code
POST /auth/verify_code
POST /auth/verify_password
DELETE /auth/logout
GET /chats
GET /messages/
GET /messages/media/{message_id}
POST /groups/join
POST /messages/send
POST /messages/send_with_file
DELETE /messages/delete
POST /messages/forward
POST /messages/edit

Сессия немного шифрованная и передается в заголовке на каждом запросе. От вас потребуется app_id, app_hash, номер телефона.

  1. Отправляем номер телефона и API-креды (получаем строку сессии):

POST /auth/send_code
{  
  "phone": "+7XXXXXXXXXX",  
  "api_id": YOUR_API_ID,  
  "api_hash": "YOUR_API_HASH"
}
  1. Отправляем код из СМС/Telegram (используя полученную строку сессии в заголовке):

POST /auth/verify_code
X-Session-String: {session_string}
{ "code": "123456"}
  1. Если включена двухфакторка, отправляем пароль:

POST /auth/verify_password
X-Session-String: {session_string}
{"password": "your_2fa_password"}

После успешной авторизации используем полученную строку сессии (X-Session-String) во всех последующих запросах. Сессия остаётся валидной до вызова logout или перезапус��а сервера.

Немного опыта

изначально идея была такая: попросить у нейронки написать простой враппер для всех этих методов. А она прям много кода выдала, да еще и не рабочего. начал с простых шагов, как авторизоваться по этому апи, авторизация работала через консоль, т.е. запускаю скрипт на питоне, он в консоли меня спрашивает номер телефона, коды, потом ожидает ввода подтверждения, далее свой пароль. Потом это удалось вынести в рест апи, чтобы не через консоль. Первая мысли была сохранять сессии в базе, с привязкой к номеру. Это для личного использования просто. Возникла проблема, если сделать общедоступным, то нужна какая то авторизация, ключи, oauth - сложно, такое делать не хочется. В целом разобрался как работают сессии в телеграм, и сделал передачу сессии в заголовке на каждый апи запрос. Однако помимо сессии требуются api_id api_hash, каждый раз в открытую их передавать не хочется. По итогу сделал передачу этих данных только на этапе авторизации, далее уже отдаю шифрованный(простейшим способом) ключик, в нем содержится сессия и нужные ключи. В общем получилось stateless. Ну а дальше уже дело простое, каждый новый хендлер просто берет заголовок, восстанавливает сессию делает запрос отдает ответ.

Весь код написан с помощью claude.ai. Сам лишь разбил на файлы и ключи вставлял. Нейронки научились писать довольно объемный код и сразу рабочий, но вот связи между файлами по прежнему большая проблема. Утилита на коленке - супер, чуть сложнее, и приходится думать самому.

Про код

Использовал pydantic, fastapi, telethon. Весь код выложен на гитхаб. Так же запущен сервер где можно пощупа��ь. Только свои аккаунты не пробуйте, создайте новый который не жалко.

Самое сложное было - авторизация, ее и покажу:


@app.post("/auth/send_code", response_model=AuthResponse)
async def send_code(credentials: ApiCredentials):
    try:
        # Create new client with provided credentials
        client = TelegramClient(StringSession(), credentials.api_id, credentials.api_hash)
        await client.connect()

        # Send authentication code
        await client.send_code_request(credentials.phone)

        # Get session and combine with encrypted credentials
        temp_session = client.session.save()
        combined_session = encode_session_with_credentials(
            temp_session,
            credentials.api_id,
            credentials.api_hash
        )

        # Store client
        clients[combined_session] = client

        return AuthResponse(
            message="Verification code sent",
            next_step="verify_code",
            session_string=combined_session
        )
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))


@app.post("/auth/verify_code", response_model=AuthResponse)
async def verify_code(
        verification_data: VerificationCode,
        session_string: str = Header(..., alias="X-Session-String")
):
    try:
        if session_string not in clients:
            raise HTTPException(status_code=401, detail="Invalid session")

        client = clients[session_string]
        session, api_id, api_hash = decode_session_with_credentials(session_string)

        try:
            # Try to sign in with the code
            await client.sign_in(code=verification_data.code)

            # Get new session and combine with credentials
            new_session = client.session.save()
            new_combined_session = encode_session_with_credentials(
                new_session,
                api_id,
                api_hash
            )

            # Update clients dictionary
            clients[new_combined_session] = client
            del clients[session_string]

            return AuthResponse(
                message="Successfully authenticated",
                next_step="completed",
                session_string=new_combined_session
            )
        except Exception as e:
            if "password" in str(e).lower():
                return AuthResponse(
                    message="2FA password required",
                    next_step="verify_password",
                    session_string=session_string
                )
            raise e

    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.post("/auth/verify_password", response_model=AuthResponse)
async def verify_password(
        password_data: Password,
        session_string: str = Header(..., alias="X-Session-String")
):
    try:
        if session_string not in clients:
            raise HTTPException(status_code=401, detail="Invalid session")

        client = clients[session_string]
        session, api_id, api_hash = decode_session_with_credentials(session_string)

        # Sign in with password
        await client.sign_in(password=password_data.password)

        # Get new session and combine with credentials
        new_session = client.session.save()
        new_combined_session = encode_session_with_credentials(
            new_session,
            api_id,
            api_hash
        )

        # Update clients dictionary
        clients[new_combined_session] = client
        del clients[session_string]

        return AuthResponse(
            message="Successfully authenticated with 2FA",
            next_step="completed",
            session_string=new_combined_session
        )
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

Тут правда не совсем stateless, в переменной хранится, на диск не сохраняется.

И пример одного из методов, получить список чатов �� каналов:


@app.get("/chats", response_model=ChatsResponse)
async def get_chats(
        limit: int = 100,
        session_string: str = Header(..., alias="X-Session-String")
):
    try:
        client = await get_client_from_session(session_string)

        # Get dialogs
        dialogs = await client.get_dialogs(limit=limit)

        chats_list = []
        for dialog in dialogs:
            entity = dialog.entity

            # Determine chat type
            if isinstance(entity, Channel):
                chat_type = "channel" if entity.broadcast else "supergroup"
            elif isinstance(entity, Chat):
                chat_type = "group"
            elif isinstance(entity, User):
                chat_type = "private"
            else:
                continue

            chat_info = ChatInfo(
                name=dialog.name,
                id=dialog.id,
                type=chat_type,
                members_count=getattr(entity, 'participants_count', None),
                is_private=not hasattr(entity, 'username') or entity.username is None,
                username=getattr(entity, 'username', None)
            )

            chats_list.append(chat_info)

        return ChatsResponse(
            chats=chats_list,
            total_count=len(chats_list)
        )
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

Функционал

Работа с чатами и группами:

  • Получить список всех чатов: GET /chats?limit=100

  • Подключиться к группам/каналам:

    POST /groups/join
    {  
       "group_identifier": "@username" // публичная группа по юзернейму  
       "group_identifier": "https://t.me/groupname" // публичная группа по ссылке  
       "group_identifier": "https://t.me/+InviteHash" // приватная группа по инвайт-ссылке
    }
    

Чтение сообщений:

  • Получить сообщения из чата (работает с любым типом идентификатора):

    GET /messages/?chat_id={id}&limit=100
    chat_id может быть:
    - числовой ID: 1234567890
    - юзернейм: @username
    - приватный чат: user_id
  • Дополнительные параметры для фильтрации:

    GET /messages/?chat_id={id}  &limit=50  &offset_id=0      
    // начать с определенного сообщения  &search=keyword   
    // поиск по тексту  &from_date=2024-03-01T00:00:00Z  
    // фильтр по дате  &to_date=2024-03-14T23:59:59Z
  • Скачать медиа из сообщения: GET /messages/media/{message_id}?chat_id={chat_id}

Отправка сообщений:

  • Простое текстовое сообщение:

    POST /messages/send
    {  
       "chat_id": "@username",     // работает с любым типом идентификатора  
       "text": "сообщение",  
       "reply_to_message_id": 123  // опционально для ответа
    }
  • Сообщение с файлом: POST /messages/send_with_file (multipart/form-data)

  • Переслать сообщение:

    POST /messages/forward
    {  
       "from_chat_id": "@chat1",  
       "to_chat_id": "@chat2",  
       "message_id": 123
    }
    
  • Редактировать сообщение:

    POST /messages/edit
    {
      "chat_id": "@chat",
      "message_id": "123",
      "new_text": "новый текст"
    }
  • Удалить сообщения:

    DELETE /messages/delete
    {
      "chat_id": "@chat",
      "message_ids": [123, 124, 125]
    }

Ограничения:

  • Работает как обычный пользовательский аккаунт, поэтому нет доступа к специальным возможностям ботов (кнопки, веб-хуки и т.д.)

  • Нет постоянного соединения/стриминга новых сообщений

  • Все операции выполняются в контексте одного аккаунта

  • Некоторые действия могут быть ограничены правами доступа в конкретном чате

Итог

Хотел сделать рекламного бота, который будет без человека премодерировать канал, прежде чем запускать рекламу, для этого нужна была возможность на этот канал зайти по ссылке и проверить что там есть. Чтобы купить рекламу своего канала в другом канале без участия человеческого менеджера.

Попробовал написать полностью с помощью нейронки весь код приложения, которое чуть сложнее чем пузырьковая сортировка. Кстати chatgpt мне не удалось заставить писать рабочий код для телеграмм ботов. Намного больше ошибок.

Писать код легко, сложно и долго проверять что это работает. Еще сложнее не погружаясь в проект написать что-то более объемное по бизнес логике. То что здесь написано - уже на пределе нейронки, когда можно не используя свою голову писать приложение. Но чуть сложнее, и каждый новый кусок кода генерирует больше проблем чем пользы, связи нарушаются, прошлые правила забываются, общая идея теряется. И бездумная копипаста перестает работать совсем.