Перейти к содержанию

Кэш и права

Бости опускает косточку в коробку

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)