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

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.

Дальнейшие возможности

Slash-команды

@bot.command("play") + register_commands + bot.respond — уже работает. Полный гайд по командам.

Persistent state

Storage adapters для bot-side state — подключите своё хранилище

Webhook deduplication

LRU cache для X-Reasonspace-Delivery — встроенный

Авто-ретрай на 429

Клиент SDK сам повторяет запрос при rate-limit (429)