Кэш и права¶

bot.cache хранит то, что шлюз сообщил боту, чтобы обработчики могли находить данные без запроса. Он заполняется из событий и никогда — из ваших REST-вызовов.
import bostie
from bostie import Context, Intents, PartialChannel, Permissions, fmt
bot = bostie.Bot(
intents=Intents.DEFAULT | Intents.GUILD_MEMBERS,
members=True,
max_messages=5000,
)
@bot.command
async def server(ctx: Context) -> None:
"""Describe this server from the cache."""
guild = bot.cache.guild(ctx.guild_id or 0)
if guild is None:
await ctx.respond("I only know servers I am in.", ephemeral=True)
return
channels = bot.cache.guild_channels(guild.id)
in_voice = bot.cache.guild_voice_states(guild.id)
members = bot.cache.guild_members(guild.id)
created = fmt.timestamp(guild.created_at, "R")
await ctx.respond(
f"{guild.name}: {len(channels)} channels, {len(members)} members, "
f"{len(in_voice)} in voice, created {created}"
)
@bot.command
async def can_post(ctx: Context, channel: PartialChannel) -> None:
"""Check whether the bot can post in a channel."""
allowed = bot.cache.permissions(channel.id)
if allowed is None:
await ctx.respond("I do not know that channel.", ephemeral=True)
elif Permissions.SEND_MESSAGES in allowed:
await ctx.respond(f"I can post in {channel.mention}.", ephemeral=True)
else:
await ctx.respond(f"I cannot post in {channel.mention}.", ephemeral=True)
Что он хранит¶
| Что хранится | По умолчанию | Параметр |
|---|---|---|
| серверы с их ролями, эмодзи и стикерами | да | |
| каналы и активные ветки | да | |
| голосовые состояния | да | |
| запланированные события, трибуны и звуки звуковой панели | да | |
| пользователь бота и его собственный участник на каждом сервере | да | |
| все участники | нет | members=True, нужен Intents.GUILD_MEMBERS |
| присутствие участников, которые не в статусе «Не в сети» | нет | presences=True, нужен Intents.GUILD_PRESENCES |
| последние сообщения | нет | max_messages=N |
| последние личные каналы | нет | max_dm_channels=N |
Bot(cache=False) ничего не хранит, и тогда остальные параметры вызывают ValueError. Параметр, для которого нет интента, вызывает MissingIntent при создании Bot. Каналы хранятся в том виде, в каком их прислал Discord, кроме полей, которые CHANNEL_UPDATE не поддерживает в актуальном состоянии (last_message_id, message_count, member_count, last_pin_timestamp): они равны None.
С members=True при подключении бота Discord присылает только часть участников больших серверов. Тогда bostie запрашивает остальных — по одному серверу за раз на шард, — и шард считается готовым только после того, как его серверы заполнены. chunk_members=False пропускает это, и кэш заполняется по мере того, как участники присоединяются, меняются или говорят. Запросы, упёршиеся в лимит, повторяются после задержки, указанной Discord.
Поиск¶
| Метод | Возвращает |
|---|---|
guild(id), guilds() |
один Guild или все |
channel(id), guild_channels(guild_id) |
Channel или каналы сервера вместе с его активными ветками |
role(id), guild_roles(guild_id) |
Role или роли сервера |
voice_state(guild_id, user_id), guild_voice_states(guild_id) |
где пользователи находятся в голосе; None, если пользователь не в голосовом канале |
scheduled_event(id), guild_scheduled_events(guild_id) |
ScheduledEvent или события сервера |
stage_instance(id), guild_stage_instances(guild_id) |
идущую StageInstance или трибуны сервера |
soundboard_sound(sound_id), guild_soundboard_sounds(guild_id) |
SoundboardSound или звуки сервера |
me(guild_id) |
собственный Member бота на сервере |
member(guild_id, user_id), guild_members(guild_id) |
участников; только самого бота, если не задано members=True |
presence(guild_id, user_id), guild_presences(guild_id) |
присутствие, с presences=True; None, если пользователь не в сети |
user(id) |
пользователя бота или пользователя участника либо личного канала из кэша |
dm_channel(user_id) |
личный канал бота с пользователем, если он хранится |
message(id) |
одно из последних max_messages сообщений |
permissions(channel_id, member=None) |
права участника в канале или права бота, если member не указан |
guild_permissions(guild_id, member=None) |
то же самое на уровне всего сервера |
Для того, чего нет в кэше, методы поиска возвращают None или пустой список. bot.cache.clear() очищает кэш. События обновления и удаления несут объект из кэша в состоянии до изменения, как объясняет раздел «События».
Кэш, а иначе REST¶
import bostie
from bostie.events import ReactionAdd
bot = bostie.Bot(max_messages=1000)
STAR = "\N{WHITE MEDIUM STAR}"
@bot.listen
async def starred(event: ReactionAdd) -> None:
if event.emoji.name != STAR:
return
message = await bot.lookup.message(event.channel_id, event.message_id)
user = await bot.lookup.user(event.user_id)
await bot.rest.messages.create(
event.channel_id,
f"{user.username} starred a message by {message.author.username}.",
reply_to=message.id,
)
bot.lookup возвращает объект из кэша, если он там есть, а иначе запрашивает его через REST: user(user_id), member(guild_id, user_id), channel(channel_id), guild(guild_id), role(guild_id, role_id) и message(channel_id, message_id). То, что вернул REST, не сохраняется: кэш по-прежнему следует только за шлюзом, поэтому повторный поиск того же отсутствующего объекта снова делает запрос. Ошибки REST пробрасываются дальше, например NotFound для несуществующего ID.
Запланированные события, трибуны и звуки¶
import logging
import bostie
from bostie import Context, fmt
from bostie.events import ScheduledEventUpdate
log = logging.getLogger("my-bot")
bot = bostie.Bot()
@bot.command
async def planned(ctx: Context) -> None:
"""List this server's scheduled events."""
lines = [
f"{event.name} {fmt.timestamp(event.scheduled_start_time, 'R')}"
for event in bot.cache.guild_scheduled_events(ctx.guild_id or 0)
]
await ctx.respond("\n".join(lines) or "Nothing is planned.", ephemeral=True)
@bot.listen
async def moved(update: ScheduledEventUpdate) -> None:
old, new = update.old, update.scheduled_event
if old is not None and old.scheduled_start_time != new.scheduled_start_time:
log.info("%s moved to %s", new.name, new.scheduled_start_time)
Они приходят вместе с каждым сервером и следуют за своими событиями создания, обновления и удаления, для которых нужны Intents.GUILD_SCHEDULED_EVENTS (запланированные события) и Intents.GUILD_EXPRESSIONS (звуки звуковой панели); оба входят в Intents.DEFAULT. Ответ на bot.request_soundboard_sounds заменяет звуки сервера. Их события обновления несут old, а SoundboardSoundDelete — удалённый sound.
Запуск¶
Шард готов, когда каждый сервер, перечисленный в его READY, пришёл как GuildCreate, или когда ни один сервер не приходил guild_ready_timeout секунд (по умолчанию 2). Started ждёт все шарды. GuildCreate с joined=True — это сервер, на который бота только что добавили; остальные — серверы, которые становятся доступными.
large_threshold — от 50 до 250, по умолчанию 50 — число участников, выше которого Discord присылает сервер без участников не в сети.
Права¶
cache.permissions(channel_id, member) применяет правила Discord: у владельца сервера и администраторов есть всё; иначе учитываются роль @everyone, затем роли участника, затем переопределения канала для @everyone, ролей и участника; участник в тайм-ауте сохраняет только «Просматривать канал» и «Читать историю сообщений»; без «Просматривать канал» нет ничего; без «Отправлять сообщения» пропадают права, которые от него зависят; в голосовом канале без «Подключаться» пропадают голосовые права. Ветка использует переопределения своего родительского канала. Метод возвращает None, если в кэше нет канала, сервера или родителя ветки.
То же вычисление доступно и без кэша: bostie.permissions.compute_permissions(guild, member, channel=None, *, parent=None), где parent — родительский канал ветки. В этом же модуле есть наборы Permissions, взятые из документации Discord: ALL, NONE, TEXT, VOICE и STAGE (права, которые применяются к каждому виду канала), ELEVATED (те, что требуют двухфакторной аутентификации в аккаунте модератора, если её требует сервер) и TIMED_OUT (те, что сохраняет участник в тайм-ауте).
Внутри взаимодействия кэш не нужен: ctx.interaction.app_permissions хранит права бота в канале, а ctx.member.permissions — права пользователя.
Иерархия ролей¶
Участник может выгнать, забанить, отправить в тайм-аут или изменить только тех участников, чья высшая роль ниже его собственной, а Discord показывает имя участника цветом его высшей цветной роли. В bostie.permissions есть три вспомогательные функции, которые принимают Guild и участников из кэша, взаимодействия или REST:
| Функция | Возвращает |
|---|---|
top_role(guild, member) |
высшую Role участника или None, если у него есть только @everyone |
display_colors(guild, member) |
RoleColors его высшей роли с цветом или None |
can_act_on(guild, actor, target) |
стоит ли actor выше target — участника или роли: владелец выше всех, никто не выше владельца, а в остальных случаях высшая роль actor сравнивается по позиции с высшей ролью цели или с самой ролью, как это делает Discord при управлении ролями |
Роли на одной позиции упорядочиваются по ID — сначала более старая, — так же, как их упорядочивает Discord.
import bostie
from bostie import Context, Embed, Permissions, ResolvedMember
from bostie.permissions import can_act_on, display_colors, top_role
bot = bostie.Bot()
@bot.command(default_permissions=Permissions.KICK_MEMBERS)
async def kick(ctx: Context, member: ResolvedMember) -> None:
"""Kick a member whom both you and the bot outrank."""
guild = bot.cache.guild(ctx.guild_id or 0)
me = bot.cache.me(ctx.guild_id or 0)
if guild is None or me is None or ctx.member is None:
await ctx.respond("Use this in a server I am in.", ephemeral=True)
return
if not (can_act_on(guild, ctx.member, member) and can_act_on(guild, me, member)):
await ctx.respond(f"{member.mention} ranks too high.", ephemeral=True)
return
await bot.rest.members.remove(
guild.id, member.id, reason=f"/kick by {ctx.user.username}"
)
await ctx.respond(f"Kicked {member.display_name}.", ephemeral=True)
@bot.command
async def rank(ctx: Context, member: ResolvedMember) -> None:
"""Show a member's highest role in its color."""
guild = bot.cache.guild(ctx.guild_id or 0)
if guild is None:
await ctx.respond("Use this in a server I am in.", ephemeral=True)
return
role = top_role(guild, member)
colors = display_colors(guild, member)
embed = Embed(
description=f"{member.mention}: {role.mention if role else '@everyone'}",
color=colors.primary_color if colors else None,
)
await ctx.respond(embeds=[embed], ephemeral=True)