Cache et permissions¶

bot.cache conserve ce que la gateway a dit au bot, pour que les gestionnaires puissent consulter des données sans requête. Il est rempli à partir des événements, jamais à partir des appels REST que vous faites.
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)
Ce qu'il conserve¶
| Conservé | Par défaut | Option |
|---|---|---|
| les serveurs, avec leurs rôles, émojis et autocollants | oui | |
| les salons et les fils actifs | oui | |
| les états vocaux | oui | |
| les événements programmés, les instances de conférence et les sons du soundboard | oui | |
| l'utilisateur du bot et son propre membre dans chaque serveur | oui | |
| tous les membres | non | members=True, nécessite Intents.GUILD_MEMBERS |
| les présences des membres qui ne sont pas hors ligne | non | presences=True, nécessite Intents.GUILD_PRESENCES |
| les messages les plus récents | non | max_messages=N |
| les salons de messages privés les plus récents | non | max_dm_channels=N |
Bot(cache=False) ne conserve rien, et les autres options lèvent alors ValueError. Une option dont l'intent manque lève MissingIntent à la création du Bot. Les salons sont stockés tels que Discord les a envoyés, sauf les champs que CHANNEL_UPDATE ne tient pas à jour (last_message_id, message_count, member_count, last_pin_timestamp), qui valent None.
Avec members=True, Discord n'envoie qu'une partie des membres des grands serveurs à la connexion du bot. bostie demande alors le reste, un serveur à la fois par shard, et un shard n'est considéré comme prêt qu'une fois ses serveurs complets. chunk_members=False saute cette étape, et le cache se remplit à mesure que les membres rejoignent le serveur, changent ou parlent. Les requêtes soumises à une limite de débit sont réessayées après le délai indiqué par Discord.
Consultations¶
| Méthode | Renvoie |
|---|---|
guild(id), guilds() |
un Guild ou tous |
channel(id), guild_channels(guild_id) |
un Channel, ou les salons d'un serveur avec ses fils actifs |
role(id), guild_roles(guild_id) |
un Role, ou les rôles d'un serveur |
voice_state(guild_id, user_id), guild_voice_states(guild_id) |
où se trouvent les utilisateurs en vocal ; None quand l'utilisateur n'est dans aucun salon vocal |
scheduled_event(id), guild_scheduled_events(guild_id) |
un ScheduledEvent, ou ceux d'un serveur |
stage_instance(id), guild_stage_instances(guild_id) |
une StageInstance en cours, ou celles d'un serveur |
soundboard_sound(sound_id), guild_soundboard_sounds(guild_id) |
un SoundboardSound, ou ceux d'un serveur |
me(guild_id) |
le propre Member du bot dans un serveur |
member(guild_id, user_id), guild_members(guild_id) |
les membres ; uniquement celui du bot sauf avec members=True |
presence(guild_id, user_id), guild_presences(guild_id) |
les présences, avec presences=True ; None si hors ligne |
user(id) |
l'utilisateur du bot, ou l'utilisateur d'un membre ou d'un salon de messages privés en cache |
dm_channel(user_id) |
le salon de messages privés du bot avec un utilisateur, s'il est conservé |
message(id) |
l'un des max_messages messages les plus récents |
permissions(channel_id, member=None) |
les permissions d'un membre dans un salon, celles du bot quand member est omis |
guild_permissions(guild_id, member=None) |
la même chose, à l'échelle du serveur |
Les consultations renvoient None, ou une liste vide, pour ce que le cache ne contient pas. bot.cache.clear() le vide. Les événements de mise à jour et de suppression portent l'objet mis en cache d'avant le changement, comme l'explique Événements.
Le cache, sinon 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 renvoie un objet depuis le cache quand il le contient, et le demande à REST sinon : user(user_id), member(guild_id, user_id), channel(channel_id), guild(guild_id), role(guild_id, role_id) et message(channel_id, message_id). Ce que REST renvoie n'est pas stocké : le cache continue de ne suivre que la gateway, si bien qu'une deuxième consultation du même objet absent refait la demande. Les erreurs REST sont transmises telles quelles, comme NotFound pour un id qui n'existe pas.
Événements programmés, conférences et 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)
Ils arrivent avec chaque serveur et suivent leurs événements de création, de mise à jour et de suppression, qui nécessitent Intents.GUILD_SCHEDULED_EVENTS pour les événements programmés et Intents.GUILD_EXPRESSIONS pour les sons du soundboard ; les deux font partie de Intents.DEFAULT. La réponse à bot.request_soundboard_sounds remplace les sons d'un serveur. Leurs événements de mise à jour portent old, et SoundboardSoundDelete porte le sound supprimé.
Démarrage¶
Un shard est prêt une fois que chaque serveur listé dans son READY est arrivé sous forme de GuildCreate, ou une fois qu'aucun serveur n'est arrivé depuis guild_ready_timeout secondes, 2 par défaut. Started attend chaque shard. Un GuildCreate avec joined=True est un serveur auquel le bot vient d'être ajouté ; les autres sont des serveurs qui deviennent disponibles.
large_threshold, de 50 à 250 et 50 par défaut, est le nombre de membres au-delà duquel Discord envoie un serveur sans ses membres hors ligne.
Permissions¶
cache.permissions(channel_id, member) applique les règles de Discord : le propriétaire du serveur et les administrateurs ont tout ; sinon le rôle @everyone, puis les rôles du membre, puis les surcharges de permissions du salon pour @everyone, les rôles et le membre ; un membre exclu temporairement ne garde que Voir les salons et Voir les anciens messages ; sans Voir les salons, il n'y a rien ; sans Envoyer des messages, les permissions qui en dépendent disparaissent ; dans un salon vocal sans Se connecter, les permissions vocales disparaissent. Un fil utilise les surcharges de permissions de son salon parent. La méthode renvoie None quand le cache n'a pas le salon, le serveur ou le parent d'un fil.
Le même calcul est disponible sans le cache : bostie.permissions.compute_permissions(guild, member, channel=None, *, parent=None), où parent est le salon parent d'un fil. Ce module contient aussi des ensembles Permissions tirés de la documentation de Discord : ALL, NONE, TEXT, VOICE et STAGE (les permissions qui s'appliquent à chaque type de salon), ELEVATED (celles qui exigent l'authentification à deux facteurs sur le compte du modérateur quand le serveur l'impose) et TIMED_OUT (celles qu'un membre exclu temporairement conserve).
Dans une interaction, le cache est inutile : ctx.interaction.app_permissions contient les permissions du bot dans le salon, et ctx.member.permissions celles de l'utilisateur.
Hiérarchie des rôles¶
Un membre ne peut expulser, bannir, exclure temporairement ou modifier que les membres dont le rôle le plus élevé est inférieur au sien, et Discord affiche le nom d'un membre dans la couleur de son rôle coloré le plus élevé. bostie.permissions propose trois fonctions utilitaires, qui prennent le Guild et les membres depuis le cache, une interaction ou REST :
| Fonction | Renvoie |
|---|---|
top_role(guild, member) |
le Role le plus élevé du membre, ou None quand il n'a que @everyone |
display_colors(guild, member) |
les RoleColors de son rôle le plus élevé qui a une couleur, ou None |
can_act_on(guild, actor, target) |
si actor est au-dessus de target, un membre ou un rôle : le propriétaire est au-dessus de tout le monde, personne n'est au-dessus du propriétaire, et sinon le rôle le plus élevé de l'acteur est comparé par position au rôle le plus élevé de la cible, ou au rôle lui-même, comme le fait Discord pour la gestion des rôles |
Les rôles à la même position sont classés par id, le plus ancien en premier, comme Discord les ordonne.
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)