Cache und Berechtigungen¶

bot.cache behält, was das Gateway dem Bot mitgeteilt hat, sodass Handler Dinge ohne Anfrage nachschlagen können. Er wird aus Events gefüllt, nie aus REST-Aufrufen, die du machst.
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)
Was er behält¶
| Behalten | Standardmäßig | Option |
|---|---|---|
| Server, mit ihren Rollen, Emojis und Stickern | ja | |
| Kanäle und aktive Threads | ja | |
| Sprachzustände | ja | |
| geplante Events, Stage-Instanzen und Soundboard-Sounds | ja | |
| der Benutzer des Bots und sein eigenes Mitglied auf jedem Server | ja | |
| alle Mitglieder | nein | members=True, braucht Intents.GUILD_MEMBERS |
| Präsenzen von Mitgliedern, die nicht offline sind | nein | presences=True, braucht Intents.GUILD_PRESENCES |
| die neuesten Nachrichten | nein | max_messages=N |
| die neuesten DM-Kanäle | nein | max_dm_channels=N |
Bot(cache=False) behält nichts, und die übrigen Optionen lösen dann ValueError aus. Eine Option, deren Intent fehlt, löst beim Erstellen des Bot MissingIntent aus. Kanäle werden so gespeichert, wie Discord sie gesendet hat, außer den Feldern, die CHANNEL_UPDATE nicht aktuell hält (last_message_id, message_count, member_count, last_pin_timestamp); diese sind None.
Mit members=True sendet Discord beim Verbinden des Bots nur einen Teil der Mitglieder großer Server. bostie fragt dann den Rest an, pro Shard einen Server nach dem anderen, und ein Shard gilt erst als bereit, wenn seine Server vollständig sind. chunk_members=False überspringt das, und der Cache füllt sich, während Mitglieder beitreten, sich ändern oder sprechen. Anfragen, die ein Rate-Limit treffen, werden nach Discords Verzögerung wiederholt.
Nachschlagen¶
| Methode | Gibt zurück |
|---|---|
guild(id), guilds() |
einen Guild oder alle |
channel(id), guild_channels(guild_id) |
einen Channel, oder die Kanäle eines Servers mit seinen aktiven Threads |
role(id), guild_roles(guild_id) |
eine Role, oder die Rollen eines Servers |
voice_state(guild_id, user_id), guild_voice_states(guild_id) |
wo Benutzer in Sprachkanälen sind; None, wenn der Benutzer in keinem Sprachkanal ist |
scheduled_event(id), guild_scheduled_events(guild_id) |
ein ScheduledEvent, oder die eines Servers |
stage_instance(id), guild_stage_instances(guild_id) |
eine laufende StageInstance, oder die eines Servers |
soundboard_sound(sound_id), guild_soundboard_sounds(guild_id) |
einen SoundboardSound, oder die eines Servers |
me(guild_id) |
das eigene Member des Bots auf einem Server |
member(guild_id, user_id), guild_members(guild_id) |
Mitglieder; nur das eigene des Bots, außer bei members=True |
presence(guild_id, user_id), guild_presences(guild_id) |
Präsenzen, mit presences=True; None, wenn offline |
user(id) |
den Benutzer des Bots oder den Benutzer eines gecachten Mitglieds oder DM-Kanals |
dm_channel(user_id) |
den DM-Kanal des Bots mit einem Benutzer, falls behalten |
message(id) |
eine der neuesten max_messages Nachrichten |
permissions(channel_id, member=None) |
die Berechtigungen eines Mitglieds in einem Kanal, die des Bots, wenn member fehlt |
guild_permissions(guild_id, member=None) |
dasselbe, serverweit |
Für alles, was der Cache nicht enthält, geben Abfragen None oder eine leere Liste zurück. bot.cache.clear() leert ihn. Update- und Delete-Events enthalten das gecachte Objekt von vor der Änderung, wie Events erklärt.
Der Cache, sonst 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 gibt ein Objekt aus dem Cache zurück, wenn er es enthält, und fragt sonst per REST danach: user(user_id), member(guild_id, user_id), channel(channel_id), guild(guild_id), role(guild_id, role_id) und message(channel_id, message_id). Was REST zurückgibt, wird nicht gespeichert: Der Cache folgt weiterhin nur dem Gateway, sodass eine zweite Abfrage desselben fehlenden Objekts erneut anfragt. Die REST-Fehler werden durchgereicht, etwa NotFound für eine ID, die nicht existiert.
Geplante Events, Stages und Sounds¶
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)
Sie kommen mit jedem Server an und folgen ihren Create-, Update- und Delete-Events, die für geplante Events Intents.GUILD_SCHEDULED_EVENTS und für Soundboard-Sounds Intents.GUILD_EXPRESSIONS brauchen; beide sind in Intents.DEFAULT enthalten. Die Antwort auf bot.request_soundboard_sounds ersetzt die Sounds eines Servers. Ihre Update-Events enthalten old, und SoundboardSoundDelete den entfernten sound.
Start¶
Ein Shard ist bereit, sobald jeder Server, den sein READY aufgeführt hat, als GuildCreate angekommen ist, oder sobald guild_ready_timeout Sekunden lang, standardmäßig 2, kein Server mehr angekommen ist. Started wartet auf jeden Shard. Ein GuildCreate mit joined=True ist ein Server, dem der Bot gerade hinzugefügt wurde; die anderen sind Server, die verfügbar werden.
large_threshold, 50 bis 250 und standardmäßig 50, ist die Mitgliederzahl, ab der Discord einen Server ohne seine Offline-Mitglieder sendet.
Berechtigungen¶
cache.permissions(channel_id, member) wendet Discords Regeln an: Der Eigentümer des Servers und Administratoren haben alles; ansonsten zählen die Rolle @everyone, dann die Rollen des Mitglieds, dann die Überschreibungen des Kanals für @everyone, für die Rollen und für das Mitglied; ein Mitglied im Timeout behält nur „Kanal ansehen“ und „Nachrichtenverlauf anzeigen“; ohne „Kanal ansehen“ gibt es gar nichts; ohne „Nachrichten senden“ entfallen die Berechtigungen, die sie voraussetzen; in einem Sprachkanal ohne „Verbinden“ entfallen die Sprachberechtigungen. Ein Thread verwendet die Überschreibungen seines übergeordneten Kanals. Es gibt None zurück, wenn dem Cache der Kanal, der Server oder der übergeordnete Kanal eines Threads fehlt.
Dieselbe Berechnung gibt es auch ohne Cache: bostie.permissions.compute_permissions(guild, member, channel=None, *, parent=None), wobei parent der übergeordnete Kanal eines Threads ist. Dieses Modul enthält außerdem Permissions-Mengen aus Discords Dokumentation: ALL, NONE, TEXT, VOICE und STAGE (die Berechtigungen, die für die jeweilige Kanalart gelten), ELEVATED (die, die Zwei-Faktor-Authentifizierung auf dem Konto des Moderators erfordern, wenn der Server sie verlangt) und TIMED_OUT (die, die ein Mitglied im Timeout behält).
Innerhalb einer Interaktion brauchst du den Cache nicht: ctx.interaction.app_permissions enthält die Berechtigungen des Bots im Kanal, und ctx.member.permissions die des Benutzers.
Rollenhierarchie¶
Ein Mitglied kann nur Mitglieder kicken, bannen, in den Timeout schicken oder bearbeiten, deren höchste Rolle niedriger ist als die eigene, und Discord zeigt den Namen eines Mitglieds in der Farbe seiner höchsten farbigen Rolle. bostie.permissions hat dafür drei Helfer, die den Guild und die Mitglieder aus dem Cache, einer Interaktion oder REST nehmen:
| Helfer | Gibt zurück |
|---|---|
top_role(guild, member) |
die höchste Role des Mitglieds, oder None, wenn es nur @everyone hat |
display_colors(guild, member) |
die RoleColors seiner höchsten Rolle mit Farbe, oder None |
can_act_on(guild, actor, target) |
ob actor über target steht, einem Mitglied oder einer Rolle: Der Eigentümer steht über allen, niemand steht über dem Eigentümer, und ansonsten wird die höchste Rolle des Akteurs nach Position mit der höchsten Rolle des Ziels verglichen, oder mit der Rolle selbst, so wie Discord es bei der Rollenverwaltung tut |
Rollen auf derselben Position werden nach ID geordnet, die ältere zuerst, so wie Discord sie sortiert.
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)