キャッシュと権限¶

bot.cache は、ゲートウェイが Bot に伝えた内容を保持し、ハンドラーがリクエストなしで情報を参照できるようにします。キャッシュはイベントから埋められ、あなたが行う 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)
保持するもの¶
| 保持するもの | 既定 | オプション |
|---|---|---|
| サーバーと、そのロール、絵文字、スタンプ | はい | |
| チャンネルとアクティブなスレッド | はい | |
| ボイス状態 | はい | |
| イベント、ステージ、サウンドボードのサウンド | はい | |
| Bot のユーザーと、各サーバーでの Bot 自身のメンバー | はい | |
| すべてのメンバー | いいえ | members=True。Intents.GUILD_MEMBERS が必要 |
| オフラインでないメンバーのプレゼンス | いいえ | presences=True。Intents.GUILD_PRESENCES が必要 |
| 最新のメッセージ | いいえ | max_messages=N |
| 最新の DM チャンネル | いいえ | max_dm_channels=N |
Bot(cache=False) は何も保持せず、その場合ほかのオプションは ValueError を送出します。インテントが足りないオプションは、Bot の作成時に MissingIntent を送出します。チャンネルは Discord が送ったとおりに保存されますが、CHANNEL_UPDATE が最新に保たないフィールド(last_message_id、message_count、member_count、last_pin_timestamp)は None になります。
members=True の場合、Bot の接続時に Discord は大きなサーバーのメンバーを一部しか送りません。そこで bostie は残りをシャードごとに 1 サーバーずつ要求し、シャードはそのサーバーがすべてそろってはじめて準備完了とみなされます。chunk_members=False を指定するとこの処理を省略し、メンバーが参加したり、変更されたり、発言したりするにつれてキャッシュが埋まっていきます。レート制限されたリクエストは、Discord が指定した待ち時間の後にリトライされます。
参照¶
| メソッド | 戻り値 |
|---|---|
guild(id)、guilds() |
1 つの 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) |
サーバーでの Bot 自身の Member |
member(guild_id, user_id)、guild_members(guild_id) |
メンバー。members=True でなければ Bot 自身のみ |
presence(guild_id, user_id)、guild_presences(guild_id) |
プレゼンス(presences=True の場合)。オフラインなら None |
user(id) |
Bot のユーザー、またはキャッシュされたメンバーや DM チャンネルのユーザー |
dm_channel(user_id) |
Bot とあるユーザーとの DM チャンネル(保持されていれば) |
message(id) |
最新の max_messages 件のメッセージのうちの 1 件 |
permissions(channel_id, member=None) |
チャンネルでのメンバーの権限。member を省略すると Bot の権限 |
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 が返したものは保存されません。キャッシュはあくまでゲートウェイだけに従うので、同じ欠けているオブジェクトをもう一度参照すると、再び問い合わせが行われます。存在しない ID に対する NotFound のような REST のエラーは、そのまま送出されます。
イベント、ステージ、サウンド¶
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 は、Bot が追加されたばかりのサーバーです。それ以外は、利用可能になったサーバーです。
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(サーバーが要求している場合に、モデレーターのアカウントで 2段階認証を必要とする権限)、TIMED_OUT(タイムアウト中のメンバーに残る権限)です。
インタラクションの中ではキャッシュは必要ありません。ctx.interaction.app_permissions にはチャンネルでの Bot の権限が、ctx.member.permissions にはユーザーの権限が入っています。
ロールの階層¶
メンバーがキック、BAN、タイムアウト、編集できるのは、最上位のロールが自分の最上位のロールより低いメンバーだけです。また、Discord はメンバーの名前を、色の付いたロールのうち最上位のものの色で表示します。bostie.permissions には、キャッシュ、インタラクション、REST のいずれかから得た Guild とメンバーを受け取る、3 つのヘルパーがあります。
| ヘルパー | 戻り値 |
|---|---|
top_role(guild, member) |
メンバーの最上位の Role。@everyone しか持たない場合は None |
display_colors(guild, member) |
色を持つロールのうち最上位のものの RoleColors、または None |
can_act_on(guild, actor, target) |
actor が target(メンバーまたはロール)より上位かどうか。オーナーは全員より上位で、オーナーより上位の人はいません。それ以外の場合は、Discord がロール管理で行うのと同じように、actor の最上位のロールを、target の最上位のロール、または target のロールそのものと位置で比較します |
同じ位置にあるロールは、Discord の並べ方と同じく、ID 順(古いロールが先)で順位が決まります。
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)