Pular para conteúdo

Cache e permissões

Bostie largando um osso dentro de uma caixa

bot.cache guarda o que o gateway contou ao bot, para que os handlers possam consultar coisas sem fazer uma requisição. Ele é preenchido a partir de eventos, nunca a partir das chamadas REST que você faz.

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)

O que ele guarda

Guardado Por padrão Opção
servidores, com os seus cargos, emojis e figurinhas sim
canais e tópicos ativos sim
estados de voz sim
eventos agendados, instâncias de palco e sons do painel de sons sim
o usuário do bot e o próprio membro dele em cada servidor sim
todos os membros não members=True, precisa de Intents.GUILD_MEMBERS
presenças dos membros que não estão offline não presences=True, precisa de Intents.GUILD_PRESENCES
as mensagens mais recentes não max_messages=N
os canais de mensagem direta mais recentes não max_dm_channels=N

Bot(cache=False) não guarda nada, e as outras opções então levantam ValueError. Uma opção cuja intent está faltando levanta MissingIntent quando o Bot é criado. Os canais são guardados como o Discord os enviou, exceto os campos que CHANNEL_UPDATE não mantém atualizados (last_message_id, message_count, member_count, last_pin_timestamp), que ficam None.

Com members=True, o Discord envia só alguns membros dos servidores grandes quando o bot conecta. O bostie então pede o resto, um servidor de cada vez por shard, e um shard só conta como pronto quando os seus servidores estão completos. chunk_members=False pula isso, e o cache se preenche conforme os membros entram, mudam ou falam. As requisições que sofrem rate limit são repetidas depois do atraso do Discord.

Consultas

Método Retorna
guild(id), guilds() um Guild ou todos eles
channel(id), guild_channels(guild_id) um Channel, ou os canais de um servidor com os seus tópicos ativos
role(id), guild_roles(guild_id) um Role, ou os cargos de um servidor
voice_state(guild_id, user_id), guild_voice_states(guild_id) onde os usuários estão na voz; None quando o usuário não está em nenhum canal de voz
scheduled_event(id), guild_scheduled_events(guild_id) um ScheduledEvent, ou os de um servidor
stage_instance(id), guild_stage_instances(guild_id) uma StageInstance ao vivo, ou as de um servidor
soundboard_sound(sound_id), guild_soundboard_sounds(guild_id) um SoundboardSound, ou os de um servidor
me(guild_id) o próprio Member do bot em um servidor
member(guild_id, user_id), guild_members(guild_id) membros; só o do próprio bot, a menos que members=True
presence(guild_id, user_id), guild_presences(guild_id) presenças, com presences=True; None quando offline
user(id) o usuário do bot, ou o usuário de um membro ou canal de mensagem direta em cache
dm_channel(user_id) o canal de mensagem direta do bot com um usuário, se guardado
message(id) uma das últimas max_messages mensagens
permissions(channel_id, member=None) as permissões de um membro em um canal, as do bot quando member é omitido
guild_permissions(guild_id, member=None) o mesmo, no servidor inteiro

As consultas retornam None, ou uma lista vazia, para o que o cache não tem. bot.cache.clear() o esvazia. Os eventos de atualização e exclusão trazem o objeto em cache de antes da mudança, como Eventos explica.

O cache e, se não houver, o 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 retorna um objeto do cache quando ele o tem, e o pede ao REST caso contrário: user(user_id), member(guild_id, user_id), channel(channel_id), guild(guild_id), role(guild_id, role_id) e message(channel_id, message_id). O que o REST retorna não é guardado: o cache continua seguindo só o gateway, então uma segunda consulta do mesmo objeto ausente pergunta de novo. Os erros do REST passam adiante, como NotFound para um ID que não existe.

Eventos agendados, palcos e sons

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)

Eles chegam com cada servidor e acompanham os seus eventos de criação, atualização e exclusão, que precisam de Intents.GUILD_SCHEDULED_EVENTS para eventos agendados e de Intents.GUILD_EXPRESSIONS para sons do painel de sons; as duas estão em Intents.DEFAULT. A resposta a bot.request_soundboard_sounds substitui os sons de um servidor. Os eventos de atualização deles trazem old, e SoundboardSoundDelete traz o sound removido.

Inicialização

Um shard fica pronto assim que cada servidor listado no seu READY chegou como um GuildCreate, ou assim que nenhum servidor chega por guild_ready_timeout segundos, 2 por padrão. Started espera todos os shards. Um GuildCreate com joined=True é um servidor ao qual o bot acabou de ser adicionado; os outros são servidores que ficam disponíveis.

large_threshold, de 50 a 250 e 50 por padrão, é a quantidade de membros acima da qual o Discord envia um servidor sem os seus membros offline.

Permissões

cache.permissions(channel_id, member) aplica as regras do Discord: o dono do servidor e os administradores têm tudo; caso contrário, o cargo @everyone, depois os cargos do membro, depois as substituições de permissão do canal para @everyone, para os cargos e para o membro; um membro de castigo mantém só Ver canal e Ler histórico de mensagens; sem Ver canal não há nada; sem Enviar mensagens, as permissões que dependem dela somem; em um canal de voz sem Conectar, as permissões de voz somem. Um tópico usa as substituições de permissão do seu canal pai. Ele retorna None quando o cache não tem o canal, o servidor ou o pai de um tópico.

O mesmo cálculo está disponível sem o cache: bostie.permissions.compute_permissions(guild, member, channel=None, *, parent=None), em que parent é o canal pai de um tópico. Esse módulo também guarda conjuntos de Permissions tirados da documentação do Discord: ALL, NONE, TEXT, VOICE e STAGE (as permissões que se aplicam a cada tipo de canal), ELEVATED (as que exigem autenticação em duas etapas na conta do moderador quando o servidor a exige) e TIMED_OUT (as que um membro de castigo mantém).

Dentro de uma interação, o cache não é necessário: ctx.interaction.app_permissions guarda as permissões do bot no canal, e ctx.member.permissions as do usuário.

Hierarquia de cargos

Um membro só pode expulsar, banir, castigar ou editar membros cujo cargo mais alto está abaixo do seu, e o Discord mostra o nome de um membro na cor do seu cargo colorido mais alto. bostie.permissions tem os três helpers, que recebem o Guild e os membros vindos do cache, de uma interação ou do REST:

Helper Retorna
top_role(guild, member) o Role mais alto do membro, ou None quando ele só tem @everyone
display_colors(guild, member) as RoleColors do seu cargo mais alto com uma cor, ou None
can_act_on(guild, actor, target) se actor está acima de target, um membro ou um cargo: o dono está acima de todos, ninguém está acima do dono e, nos outros casos, o cargo mais alto do ator é comparado por posição com o cargo mais alto do alvo, ou com o próprio cargo, como o Discord faz no gerenciamento de cargos

Cargos na mesma posição são ordenados pelo ID, o cargo mais antigo primeiro, como o Discord os ordena.

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)