Давно пишу ботов для телеграмм, использую 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 мне не удалось заставить писать рабочий код для телеграмм ботов. Намного больше ошибок.

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

Комментарии (5)


  1. rabotash
    16.11.2024 09:27

    А есть такое же, но с перламутровыми пуговицами?

    Шутка.

    Есть что нибудь чтобы висело и через определенный промежуток времени выпинывало все открытые сессии не внесённые в белый список?

    Родственникам и коллегам сделать сервис прививку от кражи аккаунта мошенниками.


    1. pivolan Автор
      16.11.2024 09:27

      У меня при тестировании возникла проблема, когда я открыл слишком много сессий, то все остальные вессии на всех устройствах резко умерли. И я не мог в течении часа зайти больше ни с какого устройства. Сначала подумал что утекли app_id+app_hash в репу открытую. Но нет. Восстановление через телефон, почту и другие способы - не работало, только когда я погасил все инстансы ботов, смог зайти в свой аккаунт.
      А в целом добавить такой функционал не сложно с помощью нейронки. Спросил ее - есть такое. Но тестировать не хочется.


      1. rcooper
        16.11.2024 09:27

        а кукую нейронку вы используете?


        1. pivolan Автор
          16.11.2024 09:27

          claude.ai


  1. x5dfg
    16.11.2024 09:27

    Для такого давно существует Pyrogram для Python, позволяющий писать и ботов и юзер-ботов