缓存与权限¶

bot.cache 保存网关告诉机器人的内容,让处理函数无需发请求就能查找数据。它只由事件填充,永远不会由你发起的 REST 调用填充。
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)
缓存哪些内容¶
| 缓存内容 | 默认 | 选项 |
|---|---|---|
| 服务器,以及其身份组、表情符号和贴纸 | 是 | |
| 频道和活跃的子区 | 是 | |
| 语音状态 | 是 | |
| 服务器活动、舞台实例和音效板音效 | 是 | |
| 机器人的用户,以及它在每个服务器中的成员信息 | 是 | |
| 全部成员 | 否 | members=True,需要 Intents.GUILD_MEMBERS |
| 非离线成员的在线状态 | 否 | presences=True,需要 Intents.GUILD_PRESENCES |
| 最新的消息 | 否 | max_messages=N |
| 最新的私信频道 | 否 | max_dm_channels=N |
Bot(cache=False) 什么都不缓存,此时其他选项会引发 ValueError。缺少相应 Intent 的选项会在创建 Bot 时引发 MissingIntent。频道按 Discord 发送的原样保存,但 CHANNEL_UPDATE 不会保持更新的字段(last_message_id、message_count、member_count、last_pin_timestamp)除外,它们为 None。
设置 members=True 时,机器人连接后 Discord 只会发送大型服务器的部分成员。随后 bostie 会请求其余成员,每个分片一次处理一个服务器,并且只有当分片上的服务器都完整时,该分片才算就绪。chunk_members=False 会跳过这一步,缓存会随着成员加入、变更或发言而逐渐填充。被速率限制的请求会在 Discord 给出的延迟之后重试。
查找¶
| 方法 | 返回 |
|---|---|
guild(id)、guilds() |
一个 Guild,或全部服务器 |
channel(id)、guild_channels(guild_id) |
一个 Channel,或某个服务器的频道及其活跃子区 |
role(id)、guild_roles(guild_id) |
一个 Role,或某个服务器的身份组 |
voice_state(guild_id, user_id)、guild_voice_states(guild_id) |
用户在语音中的位置;用户不在任何语音频道时为 None |
scheduled_event(id)、guild_scheduled_events(guild_id) |
一个 ScheduledEvent,或某个服务器的全部服务器活动 |
stage_instance(id)、guild_stage_instances(guild_id) |
一个正在进行的 StageInstance,或某个服务器的全部舞台实例 |
soundboard_sound(sound_id)、guild_soundboard_sounds(guild_id) |
一个 SoundboardSound,或某个服务器的全部音效 |
me(guild_id) |
机器人在某个服务器中的 Member |
member(guild_id, user_id)、guild_members(guild_id) |
成员;除非设置了 members=True,否则只有机器人自己 |
presence(guild_id, user_id)、guild_presences(guild_id) |
在线状态,需要 presences=True;离线时为 None |
user(id) |
机器人的用户,或某个已缓存成员或私信频道的用户 |
dm_channel(user_id) |
机器人与某个用户的私信频道(若有缓存) |
message(id) |
最新的 max_messages 条消息之一 |
permissions(channel_id, member=None) |
某个成员在某个频道中的权限,省略 member 时为机器人的权限 |
guild_permissions(guild_id, member=None) |
同上,范围为整个服务器 |
对于缓存中没有的内容,查找会返回 None 或空列表。bot.cache.clear() 会清空缓存。更新和删除事件会带上变更之前缓存中的对象,事件中有说明。
优先缓存,否则 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 在缓存中有对象时直接返回,否则通过 REST 获取:user(user_id)、member(guild_id, user_id)、channel(channel_id)、guild(guild_id)、role(guild_id, role_id) 和 message(channel_id, message_id)。REST 返回的内容不会被保存:缓存始终只跟随网关,因此再次查找同一个缺失的对象时会再次发请求。REST 的错误会原样抛出,例如对不存在的 ID 抛出 NotFound。
服务器活动、舞台和音效¶
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)
它们随每个服务器一起到达,并跟随各自的创建、更新和删除事件更新:服务器活动需要 Intents.GUILD_SCHEDULED_EVENTS,音效板音效需要 Intents.GUILD_EXPRESSIONS;两者都包含在 Intents.DEFAULT 中。对 bot.request_soundboard_sounds 的应答会替换某个服务器的音效。它们的更新事件带有 old,SoundboardSoundDelete 带有被移除的 sound。
启动¶
当分片的 READY 中列出的每个服务器都以 GuildCreate 的形式到达后,或者连续 guild_ready_timeout 秒(默认为 2)没有新服务器到达时,该分片就算就绪。Started 会等待所有分片。joined=True 的 GuildCreate 表示机器人刚被添加进的服务器;其他的则是变为可用的服务器。
large_threshold 取值 50 到 250,默认为 50,表示成员数超过多少时,Discord 发送服务器信息时不附带其离线成员。
权限¶
cache.permissions(channel_id, member) 遵循 Discord 的规则:服务器所有者和管理员拥有全部权限;否则依次应用 @everyone 身份组、成员的身份组,然后是频道中针对 @everyone、各身份组和该成员的权限覆盖;被禁言的成员只保留“查看频道”和“阅读消息历史”;没有“查看频道”就什么权限都没有;没有“发送消息”时,依赖它的权限也会失去;在没有“连接”权限的语音频道中,语音相关权限会失去。子区使用其父频道的权限覆盖。当缓存中缺少该频道、该服务器或子区的父频道时,它返回 None。
同样的计算也可以不依赖缓存完成:bostie.permissions.compute_permissions(guild, member, channel=None, *, parent=None),其中 parent 是子区的父频道。该模块还提供了取自 Discord 文档的若干 Permissions 集合:ALL、NONE、TEXT、VOICE 和 STAGE(各类频道适用的权限)、ELEVATED(当服务器要求时,需要版主账号开启双重验证的权限)以及 TIMED_OUT(被禁言的成员保留的权限)。
在交互中无需使用缓存:ctx.interaction.app_permissions 保存机器人在该频道中的权限,ctx.member.permissions 保存用户的权限。
身份组层级¶
成员只能踢出、封禁、禁言或编辑最高身份组低于自己的成员,而 Discord 会用成员最高的有颜色身份组的颜色显示其名字。bostie.permissions 提供了三个辅助函数,它们接受来自缓存、交互或 REST 的 Guild 和成员:
| 辅助函数 | 返回 |
|---|---|
top_role(guild, member) |
成员最高的 Role,只有 @everyone 时为 None |
display_colors(guild, member) |
其最高的有颜色身份组的 RoleColors,或 None |
can_act_on(guild, actor, target) |
actor 的等级是否高于 target(一个成员或一个身份组):所有者高于所有人,没有人高于所有者,其他情况下,按位置比较操作者的最高身份组与目标的最高身份组,或与目标身份组本身,与 Discord 管理身份组时的做法一致 |
位置相同的身份组按 ID 排序,较早创建的身份组在前,与 Discord 的排序方式一致。
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)