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-зашифрован и боту недоступен — см. Команды.
Возможности
/webhook + / aliases с автоматическим HMAC-verify (Stripe-style).
@bot.on("message.created") для type-safe handler регистрации.
Asyncio HTTP-client с keep-alive, auto-respect Retry-After.
CommandInvokedEvent, MessageCreatedEvent, MemberJoinedEvent etc.
Упавший handler не валит webhook (логируется, 200 OK всё равно).
SDK логирует warning при наличии Deprecation header от платформы.
Environment variables
| Var | Default | Назначение |
|---|---|---|
BOT_TOKEN | required | bot_<48hex> |
WEBHOOK_SECRET | "" | dev-mode без verify если пусто; обязательно в production |
REASONSPACE_API | https://api.reasonspace.ru | production 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.