Intents, presencia y el gateway¶
El gateway es el WebSocket por el que Discord envía eventos y el bot envía su presencia y sus solicitudes. El núcleo en Rust de bostie lo mantiene conectado: heartbeats, reanudaciones, reconexiones, compresión y el rate limit de envío. Esta página cubre lo que el bot le pide.
Intents¶
Discord envía un evento solo a un bot que pidió su intent. Bot(intents=...) acepta un flag Intents, Intents.DEFAULT si no se indica: todos los intents salvo los tres privilegiados.
| Intent privilegiado | Se necesita para |
|---|---|
GUILD_MEMBERS |
los eventos de unión, actualización y salida de miembros, todos los miembros en la caché, pedir todos los miembros |
GUILD_PRESENCES |
las actualizaciones de presencia y las presencias en la caché |
MESSAGE_CONTENT |
el contenido, los embeds, los archivos adjuntos y los componentes de los mensajes en servidores, salvo los mensajes que mencionan al bot; en los MD no hace falta ningún intent para eso |
Un intent privilegiado también hay que activarlo en la página Bot del Developer Portal. Si no, Discord cierra la conexión con el código 4014 y start() lanza PrivilegedIntentsRequired, cuyo mensaje dice dónde activarlo. Agrega intents con |: Intents.DEFAULT | Intents.MESSAGE_CONTENT. bostie advierte cuando un listener necesita un intent que el bot no tiene, y lanza MissingIntent cuando lo necesita una opción o una solicitud.
La voz necesita GUILD_VOICE_STATES, que está incluido en DEFAULT.
import bostie
from bostie import Intents
bot = bostie.Bot(intents=Intents.ALL & ~Intents.GUILD_PRESENCES)
Junto a los intents individuales hay tres combinaciones predefinidas: Intents.DEFAULT, que también se llama Intents.UNPRIVILEGED, contiene todos los intents salvo los privilegiados; Intents.PRIVILEGED, los tres privilegiados; e Intents.ALL, todos. & ~ le quita intents a una combinación, como arriba.
Presencia¶
import bostie
from bostie import Context, Intents, Status, activity
bot = bostie.Bot(
intents=Intents.DEFAULT | Intents.GUILD_MEMBERS,
status=Status.IDLE,
activities=[activity.custom("Starting up")],
)
@bot.command
async def online(ctx: Context) -> None:
"""Switch to online and show what the bot is doing."""
await bot.update_presence(
status=Status.ONLINE, activities=[activity.listening("/help")]
)
await ctx.respond("Online.", ephemeral=True)
@bot.command
async def find(ctx: Context, name: str) -> None:
"""Find members whose name starts with the text."""
if ctx.guild_id is None:
await ctx.respond("Use this in a server.", ephemeral=True)
return
found = await bot.request_members(ctx.guild_id, query=name, limit=10)
names = ", ".join(member.display_name for member in found.members) or "nobody"
await ctx.respond(f"Found {names}.", ephemeral=True)
@bot.command
async def sounds(ctx: Context) -> None:
"""List this server's soundboard sounds."""
if ctx.guild_id is not None:
found = await bot.request_soundboard_sounds(ctx.guild_id)
await ctx.respond(", ".join(sound.name for sound in found) or "No sounds.")
Bot(status=..., activities=[...], afk=False, idle_since=None) define lo que muestra el bot cuando se conecta. await bot.update_presence(...) lo cambia en todos los shards, y en los que se conecten después; una opción que no se pasa conserva su valor.
status es un Status: ONLINE, IDLE, DND o INVISIBLE. bostie.activity construye las actividades:
| Builder | Muestra |
|---|---|
activity.playing(name) |
Jugando a name |
activity.listening(name) |
Escuchando name |
activity.watching(name) |
Viendo name |
activity.competing(name) |
Compitiendo en name |
activity.streaming(name, url) |
Transmitiendo name, para los servicios de streaming que admite Discord |
activity.custom(text) |
un estado personalizado con text |
Todos salvo custom aceptan state=, una segunda línea de texto, y devuelven un activity.ActivityDict.
Solicitudes¶
Algunos datos solo llegan por el gateway, como respuesta a una solicitud:
await bot.request_members(guild_id, query="ab", limit=10)devuelve los miembros cuyo nombre de usuario o apodo empieza conquery, como máximolimit(de 0 a 100).user_ids=[...]pide en su lugar hasta 100 miembros por id. Sin ninguno de los dos, pide todos los miembros, lo que necesitaGUILD_MEMBERS.presences=Trueagrega sus presencias y necesitaGUILD_PRESENCES. La respuesta es unRequestedMembersconmembers,presencesynot_found, los ids que Discord no encontró. Discord le aplica un rate limit a esta solicitud: una rechazada lanzaMemberRequestRateLimitedconretry_after.await bot.request_soundboard_sounds(guild_id)devuelve los sonidos del panel de sonidos del servidor.await bot.request_channel_info(guild_id)devuelve el estado de voz y la hora de inicio de la voz de los canales del servidor;fields=["status"]pide menos.
Cada una espera hasta 30 segundos por cada parte de la respuesta, timeout= en el caso de los miembros, y después lanza TimeoutError. Una solicitud a un shard que está iniciando una sesión nueva lanza StateError; vuelve a intentarlo cuando esté listo.
Comandos del gateway sin procesar¶
bot.gateway.send(op, d) envía cualquier comando del gateway. El opcode 3 (presencia) va a todos los shards; un payload con guild_id va al shard de ese servidor, uno con guild_ids a cada shard con su parte de los ids, y cualquier otro al shard 0. Las respuestas de Discord llegan como eventos, y solo para los nombres de evento que algo escucha, así que acompaña un comando sin procesar con un listener de listen_raw o un wait_for.
bot.gateway también ofrece shard_ids, los shards que ejecuta este proceso; shard_for(guild_id), el shard al que Discord envía los eventos de un servidor; shard(shard_id), con sus propios send, reconnect y latency; reconnect(), que corta todas las conexiones y reanuda las sesiones en conexiones nuevas; y latency, el promedio del tiempo de ida y vuelta del heartbeat. Un comando para un shard que este proceso no ejecuta lanza ShardNotRunning, y uno que queda en cola detrás de demasiados otros lanza GatewayQueueFull.
Contrapresión¶
El núcleo en Rust lee los eventos tan rápido como los envía Discord y se los pasa a Python. Cuando el event loop se queda atrás, un shard deja de leer hasta que Python se pone al día, deja una advertencia en el log y envía ShardBackpressure con los bytes en espera. La causa es un trabajo bloqueante largo en un handler o un listener; mueve ese trabajo a un hilo con asyncio.to_thread.