Examples
Echo bot
Простейшая slash-команда /ping → эфемерный 🏓 pong (виден только инвокеру).
Сообщения пользователей боту как webhook не доставляются. Единственный
inbound-канал юзер→бот — slash-команды (событие command.invoked). См.
Команды.
import asyncio
from reasonspace_bot import Bot, CommandInvokedEvent
bot = Bot()
@bot.command("ping") # имя БЕЗ слэша
async def ping(event: CommandInvokedEvent) -> None:
await bot.respond(event, "🏓 pong")
async def setup() -> None:
# Один раз декларируем весь набор команд (bulk-overwrite).
async with bot.client as client:
await client.register_commands([
{"name": "ping", "description": "Проверка живости", "options": []},
])
if __name__ == "__main__":
asyncio.run(setup())
bot.run(port=8765)
Scope при invite: commands. Webhook подписать на событие command.invoked.
Регистрация — bulk-overwrite через PUT /api/bot/commands. Подробнее:
Регистрация команд,
Событие и ответ.
Moderation bot
Slash-команда /report — участник адресно жалуется боту на сообщение, бот
удаляет его и эфемерно подтверждает.
import asyncio
from reasonspace_bot import Bot, CommandInvokedEvent
bot = Bot()
@bot.command("report") # имя БЕЗ слэша
async def report(event: CommandInvokedEvent) -> None:
message_id = event.options.get("message_id", "") # типизированное значение опции
if not message_id:
await bot.respond(event, "Укажите message_id")
return
await bot.delete_message(message_id, space_id=event.space_id)
await bot.respond(event, "🚫 Сообщение удалено")
async def setup() -> None:
async with bot.client as client:
await client.register_commands([
{
"name": "report",
"description": "Пожаловаться на сообщение",
"options": [
{
"name": "message_id",
"type": "string",
"description": "ID сообщения для удаления",
"required": True,
},
],
},
])
if __name__ == "__main__":
asyncio.run(setup())
bot.run(port=8766)
Scope'ы при invite: commands, moderation.messages (удаление чужого
сообщения). Webhook подписать на событие command.invoked.
Бот не видит обычный пользовательский текст: сообщения юзеров боту webhook'ом
не доставляются. Событие member.joined боту не приходит, а message.created
приходит только как эхо собственных отправок бота (author_is_bot: true), а
не сообщений других пользователей. Модерация выполняется адресно — через
slash-команды (command.invoked) или по действиям
самого бота. Поля plain_command как приватного inbound-канала на платформе
нет.
Cron bot (без webhook)
Daily-standup напоминание. Без bot.run() — только client.
import asyncio, datetime as dt, os
from reasonspace_bot import BotClient
SPACE_ID = "..."
CHANNEL_ID = "..."
async def daily_standup():
async with BotClient(token=os.getenv("BOT_TOKEN")) as client:
while True:
now = dt.datetime.now()
target = now.replace(hour=10, minute=0, second=0)
if target < now:
target += dt.timedelta(days=1)
sleep_s = (target - now).total_seconds()
await asyncio.sleep(sleep_s)
await client.send_message(
channel_id=CHANNEL_ID,
content=":alarm_clock: Daily standup time!\n\nЧто делали вчера? Чем сегодня?",
space_id=SPACE_ID,
)
if __name__ == "__main__":
asyncio.run(daily_standup())
Scope: messages.send.
Music bot (общая схема)
Reference-имплементация работающего music-бота — приватный репозиторий (не публикуется как public template из соображений безопасности).
Команды play / stop бот декларирует через register_commands и принимает
как событие command.invoked (typed options, адресная доставка только этому
боту). См. Команды. Общая схема обработчика:
import asyncio
from reasonspace_bot import Bot, CommandInvokedEvent
bot = Bot()
@bot.command("play")
async def play(event: CommandInvokedEvent) -> None:
query = event.options.get("query", "") # типизированное значение опции
await handle_play(event, query)
@bot.command("stop")
async def stop(event: CommandInvokedEvent) -> None:
await handle_stop(event)
async def handle_play(event: CommandInvokedEvent, query: str):
# 1. voice-канал. Через команду бот знает канал инвокации (event.channel_id),
# но это текстовый канал. Музыку публикуем в занятый voice-канал из allowed
# (identity участников боту недоступна — privacy).
ch_id = await bot.client.find_occupied_voice_channel(space_id=event.space_id)
if not ch_id:
await bot.respond(event, "Зайдите в voice")
return
# 2. подготовить аудио-источник (см. /bots/voice/audio-source)
audio_source = await prepare_audio(query)
await bot.respond(event, f"🎵 Играю: {query}")
# 3. получить токен голосовой комнаты и подключиться к ней. Бот входит как
# видимый участник «(Bot)» в ту же комнату, что и участники канала, и
# публикует звук (publish-only — по соглашению).
join = await bot.client.voice_join(ch_id, space_id=event.space_id)
session = await connect_voice_publisher(audio_source, join)
# 4. close после окончания трека (отдельного REST-вызова для выхода нет —
# бот выходит, закрывая своё голосовое соединение)
asyncio.create_task(close_after(session, duration))
join содержит ровно 4 поля: token (токен для подключения бота к голосовому
каналу), url (URL голосового шлюза, вида wss://…), room (идентификатор
голосовой комнаты) и ttl_seconds (срок жизни токена в секундах). Подключение
выполняет любой голосовой клиент, совместимый с выданным токеном (как
connect_voice_publisher устроен внутри — выбор разработчика). См.
Audio source.
Дальнейшие возможности
@bot.command("play") + register_commands + bot.respond — уже работает.
Полный гайд по командам.
Storage adapters для bot-side state — подключите своё хранилище
LRU cache для X-Reasonspace-Delivery — встроенный
Клиент SDK сам повторяет запрос при rate-limit (429)