Перейти к основному содержимому

Python SDK

pip install reasonspace-bot

Minimal bot

Единственный inbound-канал «пользователь → бот» — это slash-команды. Бот декларирует набор команд через register_commands, ловит вызов хендлером @bot.command(...) (имя без слэша) и отвечает эфемерно через bot.respond.

import asyncio
from reasonspace_bot import Bot, CommandInvokedEvent

bot = Bot() # читает BOT_TOKEN + WEBHOOK_SECRET из env

@bot.command("ping") # реагирует на событие command.invoked для команды `ping`
async def on_ping(event: CommandInvokedEvent) -> None:
await bot.respond(event, "🏓 pong") # ответ виден только инвокеру

async def setup() -> None:
# idempotent bulk-overwrite всего набора команд бота
await bot.client.register_commands([
{"name": "ping", "description": "Проверка связи", "options": []},
])

if __name__ == "__main__":
asyncio.run(setup()) # один раз декларируем команды на платформе
bot.run(port=8765)

Для приёма slash-команд бот должен быть приглашён со scope commands и подписать webhook на событие command.invoked. Текст обычных сообщений E2E-зашифрован и боту недоступен — см. Команды.

Возможности

ASGI receiver

/webhook + / aliases с автоматическим HMAC-verify (Stripe-style).

Decorator API

@bot.on("message.created") для type-safe handler регистрации.

BotClient

Asyncio HTTP-client с keep-alive, auto-respect Retry-After.

Typed events

CommandInvokedEvent, MessageCreatedEvent, MemberJoinedEvent etc.

Graceful errors

Упавший handler не валит webhook (логируется, 200 OK всё равно).

Deprecation aware

SDK логирует warning при наличии Deprecation header от платформы.

Environment variables

VarDefaultНазначение
BOT_TOKENrequiredbot_<48hex>
WEBHOOK_SECRET""dev-mode без verify если пусто; обязательно в production
REASONSPACE_APIhttps://api.reasonspace.ruproduction API

Programmatic init

Если env не подходит — explicit:

bot = Bot(
token="bot_xxx",
webhook_secret="whsec_xxx",
api_url="https://api.reasonspace.ru",
)

Events

Все события — потомки BotEvent с type-safe полями. SDK экспортирует 9 классов:

from reasonspace_bot import (
BotEvent, # базовый — для unknown event-types
CommandInvokedEvent, # command.invoked
MessageCreatedEvent, # message.created
MessageDeletedEvent, # message.deleted
ReactionAddedEvent, # message.reaction.added
MemberJoinedEvent, # member.joined
MemberLeftEvent, # member.left
VoiceJoinedEvent, # voice.joined
VoiceLeftEvent, # voice.left
)

@bot.on("member.left")
async def on_left(event: MemberLeftEvent) -> None:
# member.left приходит боту ТОЛЬКО когда участника кикнул сам бот
# (moderation.kick). member.joined в текущей реализации не эмитится.
print(f"removed: {event.user_id}")

Платформа доставляет ровно 12 кодов событий (message.created/updated/deleted, message.reaction.added, member.joined/left, voice.joined/left, command.invoked, bot.invited/removed/scopes_changed). message.updated и bot-lifecycle (bot.*) попадают в generic BotEvent — для них нет отдельного типизированного класса. Полный список — в каталоге событий.

Если event-type неизвестен SDK (платформа добавила новый, pip не обновлён) — возвращается generic BotEvent с raw_data: dict[str, Any]. Старый код не сломается.

Команды (slash)

Slash-команды — единственный приватный inbound-канал «пользователь → бот». Цикл такой:

Регистрация набора команд

register_commands делает idempotent bulk-overwrite — передавайте весь набор команд бота целиком (а не по одной). Под капотом — PUT /api/bot/commands.

await bot.client.register_commands([
{"name": "play", "description": "Включить трек", "options": [
{"name": "query", "type": "string", "description": "Что играть", "required": True},
]},
{"name": "stop", "description": "Остановить", "options": []},
])

Имя команды — без ведущего /. Список и удаление: await bot.client.list_commands() (GET /api/bot/commands), await bot.client.delete_command("play") (DELETE /api/bot/commands/{name}).

Обработка вызова

Юзер вызывает команду → платформа доставляет адресное событие command.invoked только целевому боту. Хендлер вешается по имени команды (без /):

@bot.command("play")
async def on_play(event: CommandInvokedEvent) -> None:
query = event.options.get("query") # типизированные значения опций
...
Эфемерный ответ

bot.respond(event, content) отвечает инвокеру через interaction_token (HMAC-SHA256, TTL 900 c). Под капотом — POST /api/bot/interactions/respond. Ответ виден только вызвавшему юзеру и не попадает в ленту канала.

await bot.respond(event, "▶️ Играю")

Для slash-команд бот должен быть приглашён со scope commands и подписать webhook на событие command.invoked. Без scope commands каталог команд для юзера будет пустым, а инвокация вернёт 403. Подробнее — Команды.

BotClient напрямую

Для cron-ботов без webhook-receiver'а — допустимо использовать только client:

from reasonspace_bot import BotClient

async with BotClient(token="bot_xxx") as client:
await client.send_message(
channel_id="...",
content="Daily standup time! 🕘",
space_id="...",
)

Voice helper

Команда /play приходит как command.invoked. В событии уже есть channel_id канала, где её вызвали, поэтому угадывать «занятый» voice-канал не нужно — берём канал инвокации напрямую. Эвристика find_occupied_voice_channel остаётся лишь как fallback (например, когда команда вызвана из текстового канала, а не из voice).

@bot.command("play")
async def play(event: CommandInvokedEvent) -> None:
# channel_id канала, где вызвали команду — есть прямо в событии.
ch_id = event.channel_id

# Fallback: если это текстовый канал — взять первый занятый voice-канал.
# Identity участников боту не раскрывается (privacy), выбрать voice-канал
# КОНКРЕТНОГО юзера нельзя.
if not ch_id:
ch_id = await bot.client.find_occupied_voice_channel(space_id=event.space_id)
if not ch_id:
await bot.respond(event, "Зайдите в voice сначала")
return

# Получить voice access token (publish-only по соглашению)
join = await bot.client.voice_join(ch_id, space_id=event.space_id)
# Ответ ровно из 4 полей: join["token"], join["url"], join["room"],
# join["ttl_seconds"]. Под капотом —
# POST /api/bot/spaces/{space_id}/voice/{channel_id}/join.

await bot.respond(event, "▶️ Играю")

# Дальше — собственный audio pipeline: подключиться к выданной голосовой
# комнате (join["url"] + join["token"]) голосовым клиентом, совместимым
# с выданным токеном, и публиковать звук.
# См. /bots/voice/audio-source

Production deploy

bot.app — стандартное ASGI-приложение. Любой ASGI-server:

gunicorn bot:bot.app --workers 4 --worker-class uvicorn.workers.UvicornWorker

Или Docker:

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "bot:bot.app", "--host", "0.0.0.0", "--port", "8765"]

Examples

См. Examples — echo, moderation, music.

Source

GitLab reasonspace-bot-sdk. Issues / MRs welcome.