Intents, presença e o gateway¶
O gateway é o WebSocket pelo qual o Discord envia eventos e o bot envia a sua presença e as suas requisições. O núcleo em Rust do bostie o mantém conectado: heartbeats, retomadas, reconexões, compressão e o rate limit de envio. Esta página cobre o que o bot pede a ele.
Intents¶
O Discord só envia um evento a um bot que pediu a intent dele. Bot(intents=...) aceita uma flag Intents, Intents.DEFAULT quando omitida: todas as intents exceto as três privilegiadas.
| Intent privilegiada | Necessária para |
|---|---|
GUILD_MEMBERS |
eventos de entrada, atualização e saída de membros, todos os membros no cache, pedir todos os membros |
GUILD_PRESENCES |
atualizações de presença e presenças no cache |
MESSAGE_CONTENT |
o conteúdo, as incorporações, os anexos e os componentes de mensagens em servidores, exceto mensagens que mencionam o bot; mensagens diretas não precisam de intent para isso |
Uma intent privilegiada também precisa ser ativada na página Bot do Developer Portal. Sem isso, o Discord fecha a conexão com o código 4014 e start() levanta PrivilegedIntentsRequired, cuja mensagem diz onde ativá-la. Adicione intents com |: Intents.DEFAULT | Intents.MESSAGE_CONTENT. O bostie avisa quando um listener precisa de uma intent que o bot não tem, e levanta MissingIntent quando uma opção ou uma requisição precisa de uma.
A voz precisa de GUILD_VOICE_STATES, que DEFAULT inclui.
import bostie
from bostie import Intents
bot = bostie.Bot(intents=Intents.ALL & ~Intents.GUILD_PRESENCES)
Três predefinições ficam ao lado das intents individuais: Intents.DEFAULT, também chamada Intents.UNPRIVILEGED, tem todas as intents exceto as privilegiadas, Intents.PRIVILEGED as três privilegiadas e Intents.ALL as duas coisas. & ~ tira intents de uma predefinição, como acima.
Presença¶
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 o que o bot mostra quando conecta. await bot.update_presence(...) muda isso em todos os shards, e nos shards que conectarem depois; uma opção omitida mantém o seu valor.
status é um Status: ONLINE, IDLE, DND ou INVISIBLE. bostie.activity monta as atividades:
| Builder | Mostra |
|---|---|
activity.playing(name) |
Jogando name |
activity.listening(name) |
Ouvindo name |
activity.watching(name) |
Assistindo name |
activity.competing(name) |
Competindo em name |
activity.streaming(name, url) |
Transmitindo name, para os serviços de transmissão que o Discord suporta |
activity.custom(text) |
um status personalizado com text |
Todos, exceto custom, aceitam state=, uma segunda linha de texto, e retornam um activity.ActivityDict.
Requisições¶
Alguns dados só vêm pelo gateway, como resposta a uma requisição:
await bot.request_members(guild_id, query="ab", limit=10)retorna os membros cujo nome de usuário ou apelido começa comquery, no máximolimit(de 0 a 100).user_ids=[...]pede até 100 membros por ID no lugar disso. Sem nenhum dos dois, ele pede todos os membros, o que precisa deGUILD_MEMBERS.presences=Trueadiciona as presenças deles e precisa deGUILD_PRESENCES. A resposta é umRequestedMemberscommembers,presencesenot_found, os IDs que o Discord não encontrou. O Discord aplica rate limit a essa requisição: uma recusada levantaMemberRequestRateLimitedcomretry_after.await bot.request_soundboard_sounds(guild_id)retorna os sons do painel de sons do servidor.await bot.request_channel_info(guild_id)retorna o status de voz e o horário de início da voz dos canais do servidor;fields=["status"]pede menos.
Cada uma espera até 30 segundos por cada parte da resposta, timeout= para membros, e levanta TimeoutError depois disso. Uma requisição a um shard que está iniciando uma nova sessão levanta StateError; tente de novo quando ele estiver pronto.
Comandos brutos do gateway¶
bot.gateway.send(op, d) envia qualquer comando do gateway. O opcode 3 (presença) vai para todos os shards; um payload com guild_id vai para o shard desse servidor, um com guild_ids para cada shard com a sua parte dos IDs, e qualquer outra coisa para o shard 0. As respostas do Discord chegam como eventos, e só para nomes de evento que algo escuta, então combine um comando bruto com um listener listen_raw ou um wait_for.
bot.gateway também oferece shard_ids, os shards que este processo roda; shard_for(guild_id), o shard para o qual o Discord envia os eventos de um servidor; shard(shard_id), com o seu próprio send, reconnect e latency; reconnect(), que derruba todas as conexões e retoma as sessões em conexões novas; e latency, a média do tempo de ida e volta do heartbeat. Um comando para um shard que este processo não roda levanta ShardNotRunning, e um que fica na fila atrás de muitos outros levanta GatewayQueueFull.
Contrapressão¶
O núcleo em Rust lê os eventos tão rápido quanto o Discord os envia e os entrega ao Python. Quando o event loop fica para trás, um shard para de ler até o Python alcançá-lo, escreve um aviso no log e envia ShardBackpressure com os bytes em espera. Trabalho bloqueante longo em um handler ou listener causa isso; mova esse trabalho para uma thread com asyncio.to_thread.