カテゴリー: Discordボット開発

Discordボット開発者向け 2026年9月のAPI変更まとめ - ファイル20MiB・スラッシュコマンドの複数行入力・年齢確認(Age Assurance)・招待の対象ユーザー・shard必須、discord.py 2.7.1での対応方法(Pythonサンプル) 🗞️

この記事は韓国語の原文から翻訳したものです。

ひと目でわかる要点

2026年8月末から9月までにDiscord開発者チェンジログに掲載された7つの変更をボット開発者の視点で整理し、コードを直す必要があるものと直さなくていいものに分けます。discord.py 2.7.1にまだない機能は、エンドポイントを直接呼び出すサンプルで対応します。

  • discord.py 2.7.1 · Python 3.13 · 2026-09-30 公式チェンジログに基づいて確認(サンプルは構文・importチェックまで、実サーバーでの実行はご自身での確認が必要)
  • コードを必ず直す必要があるのは、大規模ボットシャーディングアプリのshardクエリ(9/15)と、PRUNE_REQUIRES_ADMINサーバーでのメンバー整理権限(8/27)の2つだけです
  • ファイルの基本上限は20 MiBに上がりましたが、discord.py 2.7.1のguild.filesize_limitはまだ10 MiBを返します → interaction.filesize_limitを信じてください
  • 文字列オプションに改行が入ってくることがあるのでsplitlines()で受け取り、招待の対象ユーザーエンドポイントはbot.http.request(Route(...))で直接呼び出します
目次15項目

こんにちは!Mojangcamです 😎

前回の#24では、11月16日に始まるチャンネル難読化を先取りして試してみましたね。その後、Discord開発者チェンジログには、ひと月のあいだに大きな告知がさらに7つも掲載されました。一つひとつは短いのですが、まとめて見ると「うちのボットで何を直せばいいの?」がわかりにくいんですよね。

そこで今日は新しい機能を作る代わりに、2026年8月末から9月までの変更をボット開発者の目線で一気に整理します。項目ごとに「自分のボットに影響はある?」と「コードを直す必要はある?」に先に答え、discord.py 2.7.1基準の対応コードを付けました。ライブラリにまだない機能は、エンドポイントを直接呼び出す方法で補っています 🛠️

この記事で学べること

  • 9月の変更7つを1つの表で把握する方法(直すもの / 直さなくていいもの)
  • ファイル上限が20 MiBに上がったのに、ライブラリはまだ10 MiBと言っている落とし穴
  • スラッシュコマンドの文字列オプションに改行が入るようになったあとの処理方法
  • discord.pyにない招待の対象ユーザー(target user)エンドポイントをRouteで直接呼び出す方法
  • 大規模ボットシャーディングアプリのshardクエリ、PRUNE_REQUIRES_ADMINの権限チェック
  • 年齢確認(Age Assurance)・プロフィール非公開でカスタムステータスが見えないときに安全に読む方法

📌 ひと目でわかる - 9月の変更7つ

日付 変更 自分のボットに影響? コード修正?
8/27 サーバー機能 PRUNE_REQUIRES_ADMIN (Breaking) メンバー整理(prune)機能を使うボットのみ 権限チェックを追加
9/3 ファイルアップロードの基本上限 10 MiB → 20 MiB ファイルを送るボットすべて(良い方向に) 上限を計算するコードがあれば
9/11 スラッシュコマンド: 文字列オプションの複数行入力、コマンドのあいまい検索、チャンネル移動時のコマンド記憶 文字列オプションを受け取るボットすべて 改行の処理だけ
9/15 大規模ボットシャーディングアプリはGET /users/@me/guildsにshardが必須 (Breaking) 大規模ボットシャーディングアプリのみ 必須(該当する場合)
9/17 プロフィール非公開設定だとカスタムステータスがPresenceから消える ステータスを読むボット Noneの処理を確認
9/18 招待の対象ユーザーエンドポイントを4つ追加 招待リンクを作るボット(任意) 使いたければ
9/22 年齢確認(Age Assurance)を全世界に適用 なし(動作の変化を観察するだけ) なし

結論から言うと、必ず直す必要があるのは2つだけです。大規模ボットシャーディングを使うアプリのshardクエリ、そしてメンバー整理機能があるボットの権限チェック。それ以外は知っておけば十分な変化か、むしろボットが楽になる変化です 😌

📌 準備するもの

  • Python 3.8以上(サンプルはPython 3.13で構文チェックしました)
  • discord.py 2.7.1 — 2026-09-30時点の最新バージョンです。3月以降に新しいバージョンが出ていないので、今日扱う9月の変更はまだライブラリに反映されていません
  • python-dotenv — トークンを.envに分けるために必要です
  • ボットを招待済みのテストサーバー(管理者権限だと楽です)
ターミナル
pip install discord.py==2.7.1 python-dotenv

プロジェクトフォルダに.envファイルを作って、トークンを入れてください。

ターミナル
DISCORD_TOKEN=여기에_봇_토큰

💡 .envは必ず.gitignoreに追加してください。GitHubにトークンがアップされると、Discordが自動で検知してトークンを無効化してしまいます 😱

今日のサンプルにはカスタムステータスを読む部分があるので、Developer Portal → Botタブで Server Members Intent と Presence Intent をオンにする必要があります。ステータスを読む必要がなければ、intents.presences = Trueの行を消して、ポータルの設定も飛ばしてかまいません。

📌 1. ファイルアップロードの基本上限20 MiB (9/3)

変更内容: 基本のファイルアップロード上限が10 MiBから20 MiBに上がりました。ユーザーだけでなく、ボット・Webhook・インタラクション応答にも同じように適用されます。

自分のボットに影響?: ファイルを送るボットなら、単純にうれしいニュースです。ブーストなしのサーバーでも、15 MBのログファイルや画像がアップできるようになります。

ただし、落とし穴が1つあります。discord.py 2.7.1のソースを開いてみると、基本上限がまだ古い値のままなんです。

Python
# discord/utils.py (2.7.1)
DEFAULT_FILE_SIZE_LIMIT_BYTES = 10485760   # 10 MiB

そのため、guild.filesize_limitやctx.filesize_limitは、ブーストなしのサーバーでいまだに10 MiBを返します。「上限より大きければ送らない」といった防御コードをこの値で書いていたら、実際には送れるファイルをボット自身が拒否しているわけです 😅

正解は、Discordがインタラクションごとに載せて送ってくる値を使うことです。

Python
@bot.tree.command(name="파일한도", description="このサーバーでボットがアップできるファイルサイズを表示します")
async def file_limit(interaction: discord.Interaction):
    live = interaction.filesize_limit          # Discordが今回のインタラクションに載せて送ってきた値
    cached = interaction.guild.filesize_limit if interaction.guild else None  # ライブラリの表
    msg = f"Discordが教えてくれた上限: **{live / MIB:.0f} MiB**"
    if cached is not None:
        msg += f"\ndiscord.pyの表の基準(guild.filesize_limit): {cached / MIB:.0f} MiB"
    await interaction.response.send_message(msg, ephemeral=True)

一つずつ見ていきましょう 💡

  • interaction.filesize_limit: discord.py 2.6で追加された属性です。インタラクションのペイロードにあるattachment_size_limitの値をそのまま持っているので、ライブラリの表ではなく、Discordのサーバーが今教えてくれている上限です。20 MiBへの変更はここにすぐ反映されます。
  • guild.filesize_limit: ライブラリ内にハードコードされた、ブーストレベルごとの表を見ています。2.7.1ではレベル0・1が10 MiB、レベル2が50 MiB、レベル3が100 MiBです。次のバージョンで直されるまでは、参考程度に見てください。
  • MIB = 1024 * 1024: DiscordのドキュメントはMBではなくMiB(1,048,576バイト)単位です。20 MB(20,000,000バイト)で計算すると、約1 MB損をします。

📌 2. スラッシュコマンドの改善 - 複数行入力・あいまい検索 (9/11)

変更内容は3つです。

  1. 文字列オプションで複数行入力ができるようになりました。ユーザーはShift+Enterで改行して送れます。
  2. コマンド選択画面があいまい検索(ファジー検索)に対応しました。/r aと打つだけで/role addが出てきます。
  3. チャンネルを移動しても、選んでいたコマンドを覚えています。

自分のボットに影響?: 1番がポイントです。これまでは文字列オプションは1行だと信じてよかったのですが、これからは\nが混ざった値が入ってきます。オプションの値をそのまま埋め込みのタイトルに入れたり、ファイル名・DBのキーに使ったり、1行のログとして残したりしていたコードがあれば、表示が崩れるかもしれません。

処理は簡単です。行ごとに分けて受け取る習慣をつけるだけです。

Python
@bot.tree.command(name="여러줄", description="複数行で入力した文章を行ごとに分けて表示します")
@app_commands.describe(text="Shift+Enter で改行して入力してみてください")
async def multiline(interaction: discord.Interaction, text: app_commands.Range[str, 1, 1000]):
    lines = [line.strip() for line in text.splitlines() if line.strip()]
    if not lines:
        await interaction.response.send_message("内容が空です!", ephemeral=True)
        return
    body = "\n".join(f"{i}. {line}" for i, line in enumerate(lines, start=1))
    await interaction.response.send_message(f"合計 **{len(lines)}行** 受け取りました。\n{body}")

一つずつ見ていきましょう 💡

  • text.splitlines(): \n、\r\nのどちらが来ても行ごとに分けてくれます。split("\n")より安全です。
  • if line.strip(): 空行を捨てます。複数行入力ができるようになると、ユーザーがEnterを2回押すケースが必ず出てくるからです。
  • app_commands.Range[str, 1, 1000]: 文字列の長さの上限です。複数行になって長い文章が入りやすくなったので、上限を明示しておくのがおすすめです。Discordのメッセージ1件は2,000文字までなので、1,000文字にしておけば番号を付けて返しても安全です。
  • 1行でなければいけない場所: タイトル・ファイル名・キーに使う値は、text.replace("\n", " ")で1行にまとめてから使ってください。

2番のあいまい検索は、コマンド名を選ぶクライアント側の機能です。#22で作ったオプションのオートコンプリートのコールバックが、自動的にあいまい検索に変わるわけではありません。ただ、ユーザーはこれから「適当に打っても見つけてくれる」体験に慣れていくはずなので、オートコンプリートのフィルターはstartswithのように厳しくせず、#22のようにin(部分一致)でゆるくしておくのがおすすめです 🔍

📌 3. 大規模ボットシャーディングアプリはshardクエリが必須 (9/15, Breaking)

変更内容: 大規模ボットシャーディング(large bot sharding)を使うアプリがGET /users/@me/guilds(自分のボットが参加しているサーバー一覧)を呼ぶときは、shardクエリを必ず付けなければなりません。付けないと400 Bad Requestが返ってきます。値は0からmax_concurrency - 1までで、max_concurrencyはGet Gateway Botのレスポンスのsession_start_limitに入っています。

自分のボットに影響?: 大規模ボットシャーディングは、サーバー数がとても多いボットにDiscordが個別に承認する方式です。このシリーズで作る規模のボットは該当しません。公式ドキュメントでも「大規模ボットシャーディングを使っていないアプリには影響なし」と明言しています。ご安心ください 😌

それでも、該当する方のために書いておきます。discord.py 2.7.1のclient.fetch_guilds()にはshardパラメーターがないので(内部的にはlimit・before・after・with_countsしか送りません)、エンドポイントを直接呼び出す必要があります。

Python
async def fetch_all_guilds_by_shard(client: discord.Client) -> list:
    """大規模ボットシャーディングを使うアプリ用。通常のボットは client.fetch_guilds() をそのまま使えばOKです。"""
    _, _, start_limit = await client.http.get_bot_gateway()
    max_concurrency = start_limit["max_concurrency"]
    guilds = []
    for shard in range(max_concurrency):
        route = Route("GET", "/users/@me/guilds")
        data = await client.http.request(route, params={"shard": shard, "limit": 200})
        guilds.extend(data)
    return guilds

一つずつ見ていきましょう 💡

  • client.http.get_bot_gateway(): GET /gateway/botを呼び出して、(shards, url, session_start_limit)の3つを返します。3つ目の辞書にあるmax_concurrencyが必要な値です。
  • Route("GET", "/users/@me/guilds"): discord.http.Routeは、ライブラリがすべてのリクエストに使っているクラスです。from discord.http import Routeでインポートします。
  • client.http.request(route, params=...): クエリ文字列はparamsで渡します。レート制限の処理はライブラリがやってくれます。戻り値はパース済みのJSON(サーバーの辞書のリスト)です。
  • ドキュメントの案内: 全体の一覧が必要なら、すべてのshard値を回って結果を合わせるように書かれています。そのためrange(max_concurrency)でループします。

⚠️ bot.httpは公開APIではなく、ライブラリ内部のオブジェクトです。次のバージョンでシグネチャが変わる可能性があるので、discord.pyが正式なメソッドを追加したらそちらに乗り換えてください。

📌 4. カスタムステータスの非表示(9/17) + 年齢確認 Age Assurance(9/22)

2つをまとめた理由は、結果が同じだからです。どちらも「ボットが見ていたユーザー情報が、ある日から見えなくなる」という話です。

9/17の変更内容: ユーザーのプロフィール公開設定がフレンドのみ、またはフレンドと小規模サーバーのみで、サーバーのメンバーが200人を超える場合、そのサーバーに届くPresence Updateのactivitiesからカスタムステータスが外れます。 Playing・Streaming・Listening・Watching・Competingなど、ほかのアクティビティはそのままです。

9/22の変更内容: 年齢確認が全世界に展開されます。公式ドキュメントの表現をそのまま借りると、「アプリはそのまま動作しますが」、次の4つが違って見える可能性があります。

  1. プロフィール非公開のユーザーは、Presenceでカスタムステータスが見えないことがある(上の9/17と同じ話)
  2. 年齢制限のあるActivityは、ユーザーに年齢グループの確認を求めることがある
  3. 保護者が設定した支出上限のせいで、一部の購入が失敗することがある
  4. Social SDKで送るDMがメッセージリクエストに振り分けられることが増える

自分のボットに影響?: 直すべきコードはありません。ただし、member.activitiesからカスタムステータスを取り出して使うコードがあるなら、ない場合を正常なケースとして扱う必要があります。以前は「ステータスを設定した人なら必ずある」と仮定してもよかったのですが、今はもう違います。

Python
@bot.tree.command(name="상태보기", description="メンバーのカスタムステータスを読みます(見えないこともあります)")
async def show_status(interaction: discord.Interaction, member: discord.Member):
    custom = next((a for a in member.activities if isinstance(a, discord.CustomActivity)), None)
    if custom is None:
        text = "カスタムステータスがないか、プロフィール非公開設定のためボットには見えません。"
    else:
        text = f"{custom.emoji or ''} {custom.name or ''}".strip() or "(絵文字だけのステータス)"
    await interaction.response.send_message(f"{member.display_name}: {text}", ephemeral=True)

一つずつ見ていきましょう 💡

  • member.activities: タプルです。カスタムステータス・ゲーム・Spotifyが混ざっているので、isinstance(a, discord.CustomActivity)で選び出します。member.activity(単数)は最初の1つしか返さないので、カスタムステータスを探すときには使わないでください。
  • next(..., None): なければNoneです。このNoneは今や「設定していない」と「隠されている」の2つのケースの両方を意味しますが、ボット側からは区別する方法がありません。なので、案内メッセージに両方を含めて書きました。
  • custom.name / custom.emoji: どちらもOptionalです。絵文字だけのステータスもあるので、or ''で囲みました。
  • Presence Intent: member.activitiesに値が入るには、intents.presences = TrueとDeveloper PortalのPresence Intentの両方がオンになっている必要があります。オンにしないと常に空のタプルなので、「隠されている」と区別できません。

📌 5. 招待の対象ユーザー(target user)エンドポイント (9/18)

変更内容: 招待リンクに「この人たちだけが使える」リストを付けるエンドポイントが4つできました。

動作 メソッド・パス ボディ
対象を追加 PUT /invites/{invite.code}/target-users/{user.id} なし
対象を削除 DELETE /invites/{invite.code}/target-users/{user.id} なし
複数人を追加 POST /invites/{invite.code}/target-users/bulk-add user_ids配列(最大1,000人)
複数人を削除 POST /invites/{invite.code}/target-users/bulk-delete user_ids配列(最大1,000人)

4つとも招待を作成した本人か、MANAGE_GUILD(サーバー管理)権限が必要で、成功すると204(本文なし)が返ってきます。チェンジログには、招待を作成する時点でtarget_user_ids配列を一緒に渡せるとも書かれています。

自分のボットに影響?: 使わなくても何の問題もありません。ただ、#33のパーティールームのように「招待した人だけが入れる」機能を作るとき、これまではチャンネル権限の上書きで防いでいたとしたら、これからは招待リンクそのものを特定の人専用にできるようになったわけです。

discord.py 2.7.1のcreate_invite()には、このオプションがありません。(target_userというパラメーターはありますが、それは配信視聴招待の配信者を指す別の機能です。まぎらわしいですよね 😅)なので、招待はいつもどおり作り、対象の指定だけを直接呼び出します。

Python
@bot.tree.command(name="초대대상", description="特定の人だけが使える招待リンクを作ります")
@app_commands.describe(member="招待リンクを使えるようにするメンバー")
async def invite_target(interaction: discord.Interaction, member: discord.Member):
    channel = interaction.channel
    if not isinstance(channel, discord.TextChannel):
        await interaction.response.send_message("テキストチャンネルで使ってください!", ephemeral=True)
        return

    await interaction.response.defer(ephemeral=True)
    invite = await channel.create_invite(max_age=3600, max_uses=1, unique=True,
                                         reason=f"{member} 専用の招待")

    # discord.py 2.7.1 にはまだメソッドがないので、エンドポイントを直接呼び出します
    route = Route("PUT", "/invites/{code}/target-users/{user_id}",
                  code=invite.code, user_id=member.id)
    await bot.http.request(route)   # 成功すると 204 (本文なし)

    await interaction.followup.send(
        f"{member.mention} 専用の招待リンクを作りました(1時間、1回): {invite.url}", ephemeral=True
    )

一つずつ見ていきましょう 💡

  • channel.create_invite(max_age=3600, max_uses=1, unique=True): 1時間・1回限りの招待です。unique=Trueなので、既存の招待を使い回さず新しく作ります。この招待の作成者はボット自身なので、ボットがそのまま「招待を作成した本人」になり、対象を指定する権限を持つことになります。
  • Route("PUT", "/invites/{code}/target-users/{user_id}", code=..., user_id=...): パスの{code}・{user_id}はキーワード引数で埋められます。上のコードで組み立てられるURLはhttps://discord.com/api/v10/invites/<코드>/target-users/<유저ID>です。(実際に出力して確認しました。)
  • await bot.http.request(route): ボディのないPUTです。複数人なら、Route("POST", "/invites/{code}/target-users/bulk-add", code=...)にjson={"user_ids": [id1, id2]}を渡せばOKです。
  • 先にdefer: 招待の作成 + 対象の指定でHTTPリクエストが2回になるので、3秒を超えることがあります。defer()のあとにfollowup.send()で返信します。

📌 6. メンバー整理(prune)に管理者権限 (8/27, Breaking)

9月の記事ですが、8月最終週の変更なので一緒に押さえておきます。見落としやすいBreaking Changeです。

変更内容: サーバーのオーナーがPRUNE_REQUIRES_ADMINというサーバー機能をオンにできるようになりました。これがオンのサーバーでは、整理対象人数の取得(Get Guild Prune Count)と整理の実行(Begin Guild Prune)にADMINISTRATOR権限が必要です。オンになっていないサーバーは、これまでどおりMANAGE_GUILD + KICK_MEMBERSです。

自分のボットに影響?: guild.prune_members()やguild.estimate_pruned_members()を使うボットだけです。ある日突然403 Forbiddenが出始めたら、サーバーのオーナーがこの機能をオンにしたということです。

Python
@bot.tree.command(name="정리예상", description="30日間活動のないメンバーが何人いるか計算します(実際には追い出しません)")
@app_commands.describe(days="何日間活動がなければ対象とみなすか (1~30)")
async def prune_estimate(interaction: discord.Interaction, days: app_commands.Range[int, 1, 30] = 30):
    guild = interaction.guild
    if guild is None:
        await interaction.response.send_message("サーバー内で使ってください!", ephemeral=True)
        return

    perms = guild.me.guild_permissions
    if "PRUNE_REQUIRES_ADMIN" in guild.features:
        if not perms.administrator:
            await interaction.response.send_message(
                "このサーバーは **PRUNE_REQUIRES_ADMIN** がオンになっているため、ボットに管理者権限が必要です。",
                ephemeral=True,
            )
            return
    elif not (perms.kick_members and perms.manage_guild):
        await interaction.response.send_message(
            "ボットに **メンバーをキック + サーバー管理** 権限がないと計算できません。", ephemeral=True
        )
        return

    await interaction.response.defer(ephemeral=True)
    count = await guild.estimate_pruned_members(days=days)
    await interaction.followup.send(f"{days}日基準の整理対象: **{count}人** (計算しただけです)", ephemeral=True)

一つずつ見ていきましょう 💡

  • guild.features: サーバーがオンにしている機能名の文字列リストです。"COMMUNITY"、"NEWS"のような値が入っていて、今回"PRUNE_REQUIRES_ADMIN"が追加されました。ライブラリが新しい値を知らなくても文字列のまま届くので、2.7.1でもすぐに確認できます。
  • guild.me.guild_permissions: サーバー内での自分のボットの最終的な権限です。administrator・kick_members・manage_guildをここで見ます。
  • estimate_pruned_members(days=...): 実際には追い出さず、人数だけを返します。サンプルではあえてこちらだけを使いました。本当に追い出すprune_members()は元に戻せないので、確認ボタンをもう1段階置くのがおすすめです。
  • なぜ事前にチェックするのか: そのまま呼んでも403で失敗はします。でも、ユーザーに「ボットに管理者権限を与えてください」と正確な理由を伝えるのと、「エラーが発生しました」とだけ見せるのとでは、まったく違いますよね。

💡 8月に見落としたかもしれないもの

  • 8/12 チャンネル難読化 — 11/16から、ボットが見られないチャンネルが隠されます。#24で詳しく扱いました。
  • 8/5 添付ファイルオプションのfile_types — ATTACHMENTオプションとファイルアップロードコンポーネントに「この拡張子だけ受け付ける」フィルターができました。discord.py 2.7.1にはまだないので(ソースにfile_typesがありません)、ライブラリのアップデートを待つ必要があります。
  • 8/5 チャンネルのapplication_idがnullになりうる — チャンネルオブジェクトのapplication_idが常にIDだと仮定しているコードがあれば、Noneの処理を入れてください。

📌 全体のコード (コピペOK)

sept_check.pyファイル1つで、上のコマンド5つがすべて動きます。そのままコピーして使ってください!

Python
# discord.py 2.7.1 / Python 3.8+
# 2026年9月 Discord API 変更チェックボット (sept_check.py)
import os

import discord
from discord import app_commands
from discord.ext import commands
from discord.http import Route
from dotenv import load_dotenv

load_dotenv()
TOKEN = os.getenv("DISCORD_TOKEN")
if not TOKEN:
    raise SystemExit("❌ .env ファイルに DISCORD_TOKEN がありません!")

intents = discord.Intents.default()
intents.members = True      # メンバー情報 (Developer Portal でオンにする必要があります)
intents.presences = True    # カスタムステータス確認用 (Developer Portal でオンにする必要があります)
bot = commands.Bot(command_prefix="!", intents=intents)

MIB = 1024 * 1024


@bot.event
async def on_ready():
    await bot.tree.sync()
    print(f"✅ ログイン: {bot.user} / discord.py {discord.__version__}")


# ① 9/11 - 文字列オプションの複数行入力
@bot.tree.command(name="여러줄", description="複数行で入力した文章を行ごとに分けて表示します")
@app_commands.describe(text="Shift+Enter で改行して入力してみてください")
async def multiline(interaction: discord.Interaction, text: app_commands.Range[str, 1, 1000]):
    lines = [line.strip() for line in text.splitlines() if line.strip()]
    if not lines:
        await interaction.response.send_message("内容が空です!", ephemeral=True)
        return
    body = "\n".join(f"{i}. {line}" for i, line in enumerate(lines, start=1))
    await interaction.response.send_message(f"合計 **{len(lines)}行** 受け取りました。\n{body}")


# ② 9/3 - ファイルアップロードの基本上限 20 MiB
@bot.tree.command(name="파일한도", description="このサーバーでボットがアップできるファイルサイズを表示します")
async def file_limit(interaction: discord.Interaction):
    live = interaction.filesize_limit          # Discordが今回のインタラクションに載せて送ってきた値
    cached = interaction.guild.filesize_limit if interaction.guild else None  # ライブラリの表
    msg = f"Discordが教えてくれた上限: **{live / MIB:.0f} MiB**"
    if cached is not None:
        msg += f"\ndiscord.pyの表の基準(guild.filesize_limit): {cached / MIB:.0f} MiB"
    await interaction.response.send_message(msg, ephemeral=True)


# ③ 8/27 - PRUNE_REQUIRES_ADMIN サーバー機能
@bot.tree.command(name="정리예상", description="30日間活動のないメンバーが何人いるか計算します(実際には追い出しません)")
@app_commands.describe(days="何日間活動がなければ対象とみなすか (1~30)")
async def prune_estimate(interaction: discord.Interaction, days: app_commands.Range[int, 1, 30] = 30):
    guild = interaction.guild
    if guild is None:
        await interaction.response.send_message("サーバー内で使ってください!", ephemeral=True)
        return

    perms = guild.me.guild_permissions
    if "PRUNE_REQUIRES_ADMIN" in guild.features:
        if not perms.administrator:
            await interaction.response.send_message(
                "このサーバーは **PRUNE_REQUIRES_ADMIN** がオンになっているため、ボットに管理者権限が必要です。",
                ephemeral=True,
            )
            return
    elif not (perms.kick_members and perms.manage_guild):
        await interaction.response.send_message(
            "ボットに **メンバーをキック + サーバー管理** 権限がないと計算できません。", ephemeral=True
        )
        return

    await interaction.response.defer(ephemeral=True)
    count = await guild.estimate_pruned_members(days=days)
    await interaction.followup.send(f"{days}日基準の整理対象: **{count}人** (計算しただけです)", ephemeral=True)


# ④ 9/18 - 招待の対象ユーザー(target user)エンドポイント
@bot.tree.command(name="초대대상", description="特定の人だけが使える招待リンクを作ります")
@app_commands.describe(member="招待リンクを使えるようにするメンバー")
async def invite_target(interaction: discord.Interaction, member: discord.Member):
    channel = interaction.channel
    if not isinstance(channel, discord.TextChannel):
        await interaction.response.send_message("テキストチャンネルで使ってください!", ephemeral=True)
        return

    await interaction.response.defer(ephemeral=True)
    invite = await channel.create_invite(max_age=3600, max_uses=1, unique=True,
                                         reason=f"{member} 専用の招待")

    # discord.py 2.7.1 にはまだメソッドがないので、エンドポイントを直接呼び出します
    route = Route("PUT", "/invites/{code}/target-users/{user_id}",
                  code=invite.code, user_id=member.id)
    await bot.http.request(route)   # 成功すると 204 (本文なし)

    await interaction.followup.send(
        f"{member.mention} 専用の招待リンクを作りました(1時間、1回): {invite.url}", ephemeral=True
    )


# ⑤ 9/17・9/22 - カスタムステータスが見えないことがあります
@bot.tree.command(name="상태보기", description="メンバーのカスタムステータスを読みます(見えないこともあります)")
async def show_status(interaction: discord.Interaction, member: discord.Member):
    custom = next((a for a in member.activities if isinstance(a, discord.CustomActivity)), None)
    if custom is None:
        text = "カスタムステータスがないか、プロフィール非公開設定のためボットには見えません。"
    else:
        text = f"{custom.emoji or ''} {custom.name or ''}".strip() or "(絵文字だけのステータス)"
    await interaction.response.send_message(f"{member.display_name}: {text}", ephemeral=True)


# ⑥ 9/15 - 大規模ボットシャーディングアプリは shard クエリが必須
async def fetch_all_guilds_by_shard(client: discord.Client) -> list:
    """大規模ボットシャーディングを使うアプリ用。通常のボットは client.fetch_guilds() をそのまま使えばOKです。"""
    _, _, start_limit = await client.http.get_bot_gateway()
    max_concurrency = start_limit["max_concurrency"]
    guilds = []
    for shard in range(max_concurrency):
        route = Route("GET", "/users/@me/guilds")
        data = await client.http.request(route, params={"shard": shard, "limit": 200})
        guilds.extend(data)
    return guilds


bot.run(TOKEN)

💡 #19のcogs/構成を使っているなら、コマンド部分だけを切り出してcogs/sept_check.pyに移しても同じように動きます。

📌 実行結果

ターミナル
python sept_check.py

ボットが起動したら、サーバーでコマンドを1つずつ打ってみてください。すべて自分だけに見える(ephemeral)返信なので、テストサーバーが散らかりません。

サンプル画面: /파일한도 の実行結果 - ブーストなしのサーバーで「Discordが教えてくれた上限: 20 MiB / discord.pyの表の基準: 10 MiB」の2行が異なって表示された画面

/파일한도は、ブーストなしのサーバーなら上のように2つの数字が違って表示されるのが正常です。上の行(20 MiB)が本当の上限で、下の行(10 MiB)はライブラリの表が古いという証拠です。

サンプル画面: /여러줄 コマンドにShift+Enterで3行を入力している画面と、「合計3行受け取りました」のあとに1. 2. 3. の番号が付いて返ってきた返信

/여러줄は、入力欄でShift+Enterを押して改行してみてください。ボットが行数を数えて、番号を付けて返してくれます。

/초대대상は、リンクを対象外のアカウントで開いてみて初めて本当に確認できます。対象外なら参加がブロックされるのが正常です。

サンプル画面: /정리예상 の結果 - 「30日基準の整理対象: N人 (計算しただけです)」の返信

⚠️ 正直に言うと、この記事のコードはPython 3.13での構文・importチェックと、RouteのURL組み立てまでしか確認しておらず、実際のサーバーでコマンドを動かしてはいません。特に/초대대상は9/18に新しくできたエンドポイントなので、実際のレスポンスを必ず確認してみてください。うまくいかなければ、コメントではなくお問い合わせから教えていただければ記事を直します。

📌 よく出るエラー

1. discord.errors.Forbidden: 403 Forbidden (error code: 50013): Missing Permissions — /정리예상

  • 原因: サーバーがPRUNE_REQUIRES_ADMINをオンにしたのにボットに管理者権限がないか、オンにしていないサーバーでメンバーのキック・サーバー管理権限が抜けています。
  • 解決: 上のコードのように、先にguild.featuresを見て権限を案内してください。ボットのロールに必要な権限を与えてから、もう一度試します。

2. discord.errors.Forbidden: 403 — /초대대상 のPUTリクエスト

  • 原因: 対象ユーザーの指定には、招待を作成した本人か、サーバー管理権限が必要です。ほかの人が作った招待コードを入れたり、ボットに招待の作成(Create Invite)権限がなかったりすると失敗します。
  • 解決: サンプルのようにボットが自分で作った招待にだけ対象を付けるか、ボットのロールにサーバー管理権限を与えてください。

3. discord.errors.HTTPException: 400 Bad Request — GET /users/@me/guilds

  • 原因: 大規模ボットシャーディングアプリが、shardクエリなしでサーバー一覧を呼び出しました。9/15から400が返ってきます。
  • 解決: fetch_all_guilds_by_shard()のようにparams={"shard": n}を入れて、0からmax_concurrency - 1までループしてください。通常のボットではこのエラーは出ません。

4. /상태보기 が常に「カスタムステータスがないか...」しか出ない

  • 原因: intents.presences = Trueを入れていないか、Developer PortalでPresence Intentをオンにしていません。この場合、member.activitiesは常に空のタプルです。
  • 解決: コードとポータルの両方をオンにして、ボットを再起動してください。両方オンにしても見えないなら、そのユーザーはプロフィール非公開(9/17)なので、ボットからは見えません。それが正常です。

5. ファイルを送ったら413 Payload Too Large / Request entity too large

  • 原因: 20 MiBはブーストなしのサーバーの基本値です。それより大きいファイルは相変わらず送れませんし、MB(1,000,000)とMiB(1,048,576)を混ぜて計算すると、境界ではじかれます。
  • 解決: 送る前にos.path.getsize()とinteraction.filesize_limitを比較してください。guild.filesize_limit(2.7.1基準で10 MiB)で比較すると送れるファイルまで拒否してしまうので、使わないでください。

6. /초대대상 で discord.errors.NotFound: 404 (error code: 10062): Unknown interaction

  • 原因: 3秒以内に応答できませんでした。HTTPリクエストが2回あるので遅くなることがあります。
  • 解決: 重い処理の前にawait interaction.response.defer()を先に呼び、そのあとはinteraction.followup.send()で送ってください。

💬 今日必ず覚えておきたい3つのこと

  1. 直す必要があるのは2つ。 大規模ボットシャーディングアプリのshardクエリと、メンバー整理ボットのPRUNE_REQUIRES_ADMIN権限チェック。それ以外は知っておくだけで大丈夫です。
  2. ファイル上限はinteraction.filesize_limitで。 ライブラリの表(guild.filesize_limit)は、まだ10 MiBのまま古くなっています。
  3. 文字列オプションに改行が入ってきます。 splitlines()で受け取り、1行でなければいけない場所ではまとめてから使ってください。カスタムステータスはないのが正常なケースになりました。

📌 おわりに

今日は新機能の代わりに、9月ひと月分のDiscord API変更を一気に振り返りました 🗞️

まとめると、ファイル上限が20 MiBに広がり(ライブラリの値は信じないこと)、スラッシュコマンドの文字列に複数行が入るようになり、招待リンクを特定の人専用にするエンドポイントができました。年齢確認とプロフィール非公開のせいでカスタムステータスが見えないケースが増え、大規模ボットシャーディングアプリはshardクエリを、メンバー整理ボットはPRUNE_REQUIRES_ADMINを押さえておく必要があります。discord.py 2.7.1は3月以降新しいバージョンが出ていないので、新しいエンドポイントはRouteで直接呼ぶ方法で補いました ✅

そして忘れないでください。11月16日のチャンネル難読化がいちばん大きな変更です。まだ#24の/채널점검を試していないなら、今週末に一度動かしておいてください 💣

Discordボット開発シリーズは、#33のパーティールームに続いて、次回はボットをDockerでデプロイする方法(予定)を取り上げるつもりです。お楽しみに! 🙌

📚 あわせて読みたい記事

著者

Mojangcam

自分でコードを書き、サーバーを動かし、詰まったエラーを解決しながら学んだことを記録する開発・IT実践ブログ