Categoría: Bots de Discord
Todos los cambios de septiembre de 2026 en la API de Discord para desarrolladores de bots - archivos de 20 MiB, entrada multilínea en comandos slash, verificación de edad (Age Assurance), usuarios destino de invitaciones y shard obligatorio: cómo adaptarte con discord.py 2.7.1 (ejemplos en Python) 🗞️
Esta entrada es una traducción del original en coreano.
De un vistazo
Repasamos los 7 cambios del changelog de desarrolladores de Discord de finales de agosto a septiembre de 2026 pensando en bots, y separamos lo que obliga a tocar código de lo que no. Lo que discord.py 2.7.1 aún no trae lo cubrimos llamando al endpoint directamente.
- discord.py 2.7.1 · Python 3.13 · según el changelog oficial al 2026-09-30 (ejemplos: solo sintaxis e imports, falta probarlos en un servidor real)
- Solo dos obligan a tocar código: shard en apps con sharding para bots grandes (15 sep.) y permisos de prune con PRUNE_REQUIRES_ADMIN (27 ago.)
- El límite base de archivos subió a 20 MiB, pero guild.filesize_limit de discord.py 2.7.1 aún devuelve 10 MiB → fíate de interaction.filesize_limit
- Las opciones de texto pueden traer saltos de línea (usa splitlines()); el endpoint de usuarios destino se llama con bot.http.request(Route(...))
Índice15 secciones
¡Hola! Soy Mojangcam 😎
En el #24 nos adelantamos a la ofuscación de canales que llega el 16 de noviembre. Desde entonces, el changelog de desarrolladores de Discord ha sumado otros 7 anuncios importantes en solo un mes. Cada uno es corto, pero cuando los juntas cuesta saber "¿qué tengo que cambiar en mi bot?".
Así que hoy, en lugar de crear una función nueva, vamos a repasar de una vez, con ojos de desarrollador de bots, los cambios de finales de agosto a septiembre de 2026. Para cada punto respondo primero "¿afecta a mi bot?" y "¿tengo que cambiar código?", y añado código con discord.py 2.7.1 para adaptarte. Lo que la librería aún no incluye lo cubrimos llamando directamente al endpoint 🛠️
Lo que aprenderás en esta entrada
- Cómo ver los 7 cambios de septiembre en una sola tabla (qué arreglar / qué no hace falta arreglar)
- La trampa de que el límite de archivos subió a 20 MiB pero la librería todavía dice 10 MiB
- Qué hacer ahora que empiezan a llegar saltos de línea en las opciones de texto de los comandos slash
- Llamar directamente con
Routeal endpoint de usuarios destino (target user) de invitaciones, que discord.py no tiene - La consulta
sharden apps con sharding para bots grandes y la comprobación de permisos dePRUNE_REQUIRES_ADMIN - Leer de forma segura cuando el estado personalizado no se ve por la verificación de edad (Age Assurance) o un perfil privado
📌 De un vistazo - los 7 cambios de septiembre
| Fecha | Cambio | ¿Afecta a mi bot? | ¿Cambiar código? |
|---|---|---|---|
| 27 ago. | Función de servidor PRUNE_REQUIRES_ADMIN (Breaking) |
Solo bots que usan la limpieza de miembros (prune) | Añadir comprobación de permisos |
| 3 sep. | Límite base de subida de archivos 10 MiB → 20 MiB | Todos los bots que envían archivos (para bien) | Si tienes código que calcula el límite |
| 11 sep. | Comandos slash: entrada multilínea en opciones de texto, búsqueda difusa de comandos, recuerda el comando al cambiar de canal | Todos los bots que reciben opciones de texto | Solo manejar saltos de línea |
| 15 sep. | Las apps con sharding para bots grandes deben enviar shard en GET /users/@me/guilds (Breaking) |
Solo apps con sharding para bots grandes | Obligatorio (si aplica) |
| 17 sep. | Con el perfil en privado, el estado personalizado desaparece de Presence | Bots que leen estados | Revisar el manejo de None |
| 18 sep. | 4 endpoints nuevos de usuarios destino de invitaciones | Bots que crean enlaces de invitación (opcional) | Si quieres usarlos |
| 22 sep. | Verificación de edad (Age Assurance) en todo el mundo | Ninguno (solo observar cambios de comportamiento) | Ninguno |
En resumen: solo hay dos cosas que debes arreglar sí o sí. La consulta shard en apps que usan sharding para bots grandes, y la comprobación de permisos en bots con función de limpieza de miembros. Lo demás son cambios que basta con conocer, o que incluso le facilitan la vida a tu bot 😌
📌 Lo que necesitas
- Python 3.8 o superior (revisé la sintaxis de los ejemplos con Python 3.13)
- discord.py 2.7.1 — es la última versión al 2026-09-30. No ha salido ninguna nueva desde marzo, así que los cambios de septiembre que vemos hoy todavía no están en la librería
- python-dotenv — para separar el token en un archivo .env
- Un servidor de pruebas con el bot invitado (con permisos de administrador es más cómodo)
pip install discord.py==2.7.1 python-dotenvCrea un archivo .env en la carpeta del proyecto y pon ahí el token del bot.
DISCORD_TOKEN=여기에_봇_토큰💡 Añade sí o sí .env a .gitignore. Si subes el token a GitHub, Discord lo detecta automáticamente y lo revoca 😱
Como el ejemplo de hoy lee estados personalizados, tienes que activar Server Members Intent y Presence Intent en Developer Portal → pestaña Bot. Si no necesitas leer estados, borra la línea intents.presences = True y sáltate también el ajuste del portal.
📌 1. Límite base de subida de archivos de 20 MiB (3 sep.)
Qué cambió: el límite base de subida de archivos pasó de 10 MiB a 20 MiB. Se aplica igual a usuarios, bots, webhooks y respuestas a interacciones.
¿Afecta a mi bot?: si tu bot envía archivos, es simplemente una buena noticia. En servidores sin boost, ahora se pueden subir archivos de log o imágenes de 15 MB.
Pero hay una trampa. Si abres el código fuente de discord.py 2.7.1, el límite base todavía tiene el valor antiguo.
# discord/utils.py (2.7.1)
DEFAULT_FILE_SIZE_LIMIT_BYTES = 10485760 # 10 MiBPor eso guild.filesize_limit y ctx.filesize_limit siguen devolviendo 10 MiB en servidores sin boost. Si escribiste código defensivo del tipo "si supera el límite, no lo envíes" con ese valor, tu bot está rechazando por su cuenta archivos que en realidad sí puede enviar 😅
La solución es usar el valor que Discord envía con cada interacción.
@bot.tree.command(name="파일한도", description="Muestra el tamaño de archivo que el bot puede subir en este servidor")
async def file_limit(interaction: discord.Interaction):
live = interaction.filesize_limit # valor que Discord envió con esta interacción
cached = interaction.guild.filesize_limit if interaction.guild else None # tabla de la librería
msg = f"Límite que indica Discord: **{live / MIB:.0f} MiB**"
if cached is not None:
msg += f"\nSegún la tabla de discord.py (guild.filesize_limit): {cached / MIB:.0f} MiB"
await interaction.response.send_message(msg, ephemeral=True)Vamos por partes 💡
- interaction.filesize_limit: atributo añadido en discord.py 2.6. Guarda tal cual el valor
attachment_size_limitdel payload de la interacción, así que es el límite que te indica ahora el servidor de Discord, no la tabla de la librería. El cambio a 20 MiB se refleja aquí al instante. - guild.filesize_limit: consulta una tabla por nivel de boost escrita a mano (hardcoded) dentro de la librería. En 2.7.1, los niveles 0 y 1 son 10 MiB, el nivel 2 es 50 MiB y el nivel 3 es 100 MiB. Hasta que lo corrijan en la próxima versión, úsalo solo como referencia.
- MIB = 1024 * 1024: la documentación de Discord no usa MB sino MiB (1 048 576 bytes). Si calculas con 20 MB (20 000 000 bytes), pierdes cerca de 1 MB.
📌 2. Mejoras en los comandos slash - entrada multilínea y búsqueda difusa (11 sep.)
Qué cambió: son tres cosas.
- Las opciones de texto admiten varias líneas. El usuario puede cambiar de línea con Shift+Enter y enviarlo así.
- El selector de comandos admite búsqueda difusa (fuzzy search). Con escribir solo
/r aya aparece/role add. - Aunque cambies de canal, recuerda el comando que estabas eligiendo.
¿Afecta a mi bot?: lo clave es el punto 1. Hasta ahora podías dar por hecho que una opción de texto era una sola línea, pero ahora llegarán valores con \n. Si tienes código que mete el valor de la opción tal cual en el título de un embed, lo usa como nombre de archivo o clave de base de datos, o lo guarda como un log de una sola línea, el formato se puede romper.
La solución es sencilla. Basta con acostumbrarte a recibirlo dividido por líneas.
@bot.tree.command(name="여러줄", description="Divide por líneas el texto de varias líneas que escribas y te lo muestra")
@app_commands.describe(text="Prueba a escribir cambiando de línea con 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("¡El contenido está vacío!", ephemeral=True)
return
body = "\n".join(f"{i}. {line}" for i, line in enumerate(lines, start=1))
await interaction.response.send_message(f"Recibí **{len(lines)} líneas** en total.\n{body}")Vamos por partes 💡
- text.splitlines(): corta por líneas tanto si llega
\ncomo\r\n. Es más seguro quesplit("\n"). - if line.strip(): descarta las líneas vacías. Con la entrada multilínea, siempre habrá quien pulse Enter dos veces.
- app_commands.Range[str, 1, 1000]: es el límite de longitud del texto. Con varias líneas es más fácil que llegue texto largo, así que conviene fijar un máximo. Un mensaje de Discord admite hasta 2000 caracteres, así que con 1000 puedes devolverlo con números añadidos sin problema.
- Donde debe ser una sola línea: los valores que vayas a usar como título, nombre de archivo o clave, aplánalos a una línea con
text.replace("\n", " ").
La búsqueda difusa del punto 2 es una función del cliente para elegir el nombre del comando. El callback de autocompletado de opciones que hicimos en el #22 no se convierte en búsqueda difusa por sí solo. Aun así, los usuarios se van a acostumbrar a que "lo encuentre aunque escriba a medias", así que te recomiendo no dejar el filtro de autocompletado tan estricto como startswith, sino flexible con in (coincidencia parcial), como en el #22 🔍
📌 3. Las apps con sharding para bots grandes deben enviar la consulta shard (15 sep., Breaking)
Qué cambió: las apps que usan sharding para bots grandes (large bot sharding) deben incluir obligatoriamente la consulta shard al llamar a GET /users/@me/guilds (la lista de servidores en los que está tu bot). Sin ella, reciben un 400 Bad Request. El valor va de 0 a max_concurrency - 1, y max_concurrency viene en session_start_limit de la respuesta de Get Gateway Bot.
¿Afecta a mi bot?: el sharding para bots grandes es un modo que Discord aprueba aparte para bots con muchísimos servidores. Los bots del tamaño que hacemos en esta serie no están afectados. La documentación oficial también lo deja claro: "las apps que no usan sharding para bots grandes no se ven afectadas". Puedes estar tranquilo 😌
Aun así, lo dejo escrito para quien sí esté en ese caso. client.fetch_guilds() de discord.py 2.7.1 no tiene parámetro shard (internamente solo envía limit, before, after y with_counts), así que hay que llamar al endpoint directamente.
async def fetch_all_guilds_by_shard(client: discord.Client) -> list:
"""Para apps con sharding para bots grandes. Un bot normal puede usar client.fetch_guilds() sin más."""
_, _, 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 guildsVamos por partes 💡
- client.http.get_bot_gateway(): llama a
GET /gateway/boty devuelve tres valores:(shards, url, session_start_limit). El valor que necesitas esmax_concurrency, dentro del tercer diccionario. - Route("GET", "/users/@me/guilds"):
discord.http.Routees la clase que la librería usa para todas sus peticiones. Se importa confrom discord.http import Route. - client.http.request(route, params=...): la query string se pasa en
params. La librería se encarga de los rate limits. Devuelve el JSON ya parseado (una lista de diccionarios de servidores). - Lo que indica la documentación: si necesitas la lista completa, hay que recorrer todos los valores de shard y juntarlos. Por eso recorremos
range(max_concurrency).
⚠️ bot.http no es una API pública, sino un objeto interno de la librería. Su firma puede cambiar en la próxima versión, así que cuando discord.py añada un método oficial, pásate a ese.
📌 4. Estado personalizado oculto (17 sep.) + verificación de edad Age Assurance (22 sep.)
Los junto porque el resultado es el mismo. En ambos casos, "la información de usuario que veía el bot deja de verse de un día para otro".
Qué cambió el 17 sep.: si la configuración de privacidad del perfil de un usuario es solo amigos, o amigos y servidores pequeños y el servidor tiene más de 200 miembros, el estado personalizado desaparece de activities en el Presence Update que llega a ese servidor. Las demás actividades, como Playing, Streaming, Listening, Watching o Competing, siguen igual.
Qué cambió el 22 sep.: la verificación de edad se despliega en todo el mundo. Citando la documentación oficial, "las apps siguen funcionando igual", pero hay cuatro cosas que pueden verse distintas.
- Los usuarios con perfil privado pueden no mostrar su estado personalizado en Presence (lo mismo del 17 sep.)
- Las Activities con restricción de edad pueden pedir al usuario confirmar su grupo de edad
- Algunas compras pueden fallar por límites de gasto fijados por los padres
- Los MD enviados con Social SDK se clasifican más a menudo como solicitudes de mensaje
¿Afecta a mi bot?: no hay que cambiar código. Pero si tienes código que saca el estado personalizado de member.activities, debes tratar que no esté como un caso normal. Antes podías suponer que "si alguien puso un estado, siempre estará ahí", pero ya no.
@bot.tree.command(name="상태보기", description="Lee el estado personalizado de un miembro (puede que no se vea)")
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 = "No tiene estado personalizado, o su perfil privado impide que el bot lo vea."
else:
text = f"{custom.emoji or ''} {custom.name or ''}".strip() or "(estado con solo un emoji)"
await interaction.response.send_message(f"{member.display_name}: {text}", ephemeral=True)Vamos por partes 💡
- member.activities: es una tupla. Mezcla estados personalizados, juegos, Spotify y más, así que filtramos con
isinstance(a, discord.CustomActivity).member.activity(en singular) solo da la primera, así que no lo uses para buscar el estado personalizado. - next(..., None): si no hay, devuelve None. Ahora ese None significa dos casos, "no lo configuró" y "está oculto", y el bot no tiene forma de distinguirlos. Por eso el mensaje menciona los dos.
- custom.name / custom.emoji: los dos son Optional. Hay estados con solo un emoji, así que los envolvemos con
or ''. - Presence Intent: para que
member.activitiesse llene hacen faltaintents.presences = Truey el Presence Intent del Developer Portal, los dos activados. Si no, siempre será una tupla vacía y no podrás distinguirlo de "está oculto".
📌 5. Endpoints de usuarios destino (target user) de invitaciones (18 sep.)
Qué cambió: hay 4 endpoints nuevos para añadir a un enlace de invitación una lista de "solo estas personas pueden usarlo".
| Acción | Método y ruta | Cuerpo |
|---|---|---|
| Añadir destino | PUT /invites/{invite.code}/target-users/{user.id} |
Ninguno |
| Quitar destino | DELETE /invites/{invite.code}/target-users/{user.id} |
Ninguno |
| Añadir varios | POST /invites/{invite.code}/target-users/bulk-add |
Array user_ids (máx. 1000 usuarios) |
| Quitar varios | POST /invites/{invite.code}/target-users/bulk-delete |
Array user_ids (máx. 1000 usuarios) |
Los cuatro requieren ser quien creó la invitación o tener el permiso MANAGE_GUILD (gestionar servidor), y si todo va bien devuelven 204 (sin cuerpo). El changelog también indica que se puede pasar el array target_user_ids desde el momento de crear la invitación.
¿Afecta a mi bot?: si no lo usas, no pasa nada. Pero al crear funciones de "solo entra quien fue invitado", como la sala de fiesta del #33, si hasta ahora lo bloqueabas con sobrescrituras de permisos del canal, ahora puedes hacer que el propio enlace de invitación sea exclusivo para personas concretas.
create_invite() de discord.py 2.7.1 no tiene esta opción. (Tiene un parámetro llamado target_user, pero es otra función: se refiere al streamer en las invitaciones para ver una transmisión. Perfecto para confundirse 😅) Así que creamos la invitación como siempre y solo la asignación de destino la llamamos directamente.
@bot.tree.command(name="초대대상", description="Crea un enlace de invitación que solo puede usar una persona concreta")
@app_commands.describe(member="Miembro que podrá usar el enlace de invitación")
async def invite_target(interaction: discord.Interaction, member: discord.Member):
channel = interaction.channel
if not isinstance(channel, discord.TextChannel):
await interaction.response.send_message("¡Úsalo en un canal de texto!", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
invite = await channel.create_invite(max_age=3600, max_uses=1, unique=True,
reason=f"Invitación exclusiva para {member}")
# discord.py 2.7.1 aún no tiene un método para esto, así que llamamos al endpoint directamente
route = Route("PUT", "/invites/{code}/target-users/{user_id}",
code=invite.code, user_id=member.id)
await bot.http.request(route) # si va bien, 204 (sin cuerpo)
await interaction.followup.send(
f"Creé un enlace de invitación exclusivo para {member.mention} (1 hora, 1 uso): {invite.url}", ephemeral=True
)Vamos por partes 💡
- channel.create_invite(max_age=3600, max_uses=1, unique=True): una invitación de 1 hora y de un solo uso. Con
unique=Trueno reutiliza una invitación existente, sino que crea una nueva. Como quien crea esta invitación es nuestro bot, el bot pasa a ser "quien creó la invitación" y obtiene permiso para asignar destinos. - Route("PUT", "/invites/{code}/target-users/{user_id}", code=..., user_id=...):
{code}y{user_id}de la ruta se rellenan con los argumentos de palabra clave. La dirección que genera el código de arriba eshttps://discord.com/api/v10/invites/<코드>/target-users/<유저ID>, con el código de invitación y el ID de usuario en esos huecos. (Lo comprobé imprimiéndola.) - await bot.http.request(route): es un PUT sin cuerpo. Para varias personas, pasa
json={"user_ids": [id1, id2]}aRoute("POST", "/invites/{code}/target-users/bulk-add", code=...). - Primero defer: crear la invitación y asignar el destino son dos peticiones HTTP, así que puede pasar de 3 segundos. Respondemos con
followup.send()después dedefer().
📌 6. Permiso de administrador para la limpieza de miembros (prune) (27 ago., Breaking)
Es un artículo de septiembre, pero como es un cambio de la última semana de agosto, lo incluimos también. Es un Breaking Change fácil de pasar por alto.
Qué cambió: ahora el dueño del servidor puede activar una función de servidor llamada PRUNE_REQUIRES_ADMIN. En los servidores que la tienen activada, consultar el número estimado de miembros a limpiar (Get Guild Prune Count) y ejecutar la limpieza (Begin Guild Prune) requieren el permiso ADMINISTRATOR. En los servidores que no la activan, sigue siendo MANAGE_GUILD + KICK_MEMBERS como hasta ahora.
¿Afecta a mi bot?: solo a los bots que usan guild.prune_members() o guild.estimate_pruned_members(). Si de repente empiezas a recibir 403 Forbidden, es que el dueño del servidor activó esta función.
@bot.tree.command(name="정리예상", description="Calcula cuántos miembros llevan 30 días sin actividad (no expulsa a nadie)")
@app_commands.describe(days="Días sin actividad para contar a alguien como objetivo (1 a 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("¡Úsalo dentro de un servidor!", ephemeral=True)
return
perms = guild.me.guild_permissions
if "PRUNE_REQUIRES_ADMIN" in guild.features:
if not perms.administrator:
await interaction.response.send_message(
"Este servidor tiene **PRUNE_REQUIRES_ADMIN** activado, así que el bot necesita permiso de administrador.",
ephemeral=True,
)
return
elif not (perms.kick_members and perms.manage_guild):
await interaction.response.send_message(
"El bot necesita los permisos **Expulsar miembros + Gestionar servidor** para calcularlo.", ephemeral=True
)
return
await interaction.response.defer(ephemeral=True)
count = await guild.estimate_pruned_members(days=days)
await interaction.followup.send(f"Objetivo de limpieza con {days} días: **{count} miembros** (solo lo calculé)", ephemeral=True)Vamos por partes 💡
- guild.features: lista de cadenas con los nombres de las funciones que el servidor tiene activadas. Contiene valores como
"COMMUNITY"o"NEWS", y ahora se suma"PRUNE_REQUIRES_ADMIN". Aunque la librería no conozca el valor nuevo, llega tal cual como cadena, así que en 2.7.1 ya se puede comprobar. - guild.me.guild_permissions: los permisos finales de nuestro bot dentro del servidor. Aquí miramos
administrator,kick_membersymanage_guild. - estimate_pruned_members(days=...): no expulsa a nadie, solo devuelve el número. En el ejemplo usé solo este a propósito.
prune_members(), que sí expulsa, no se puede deshacer, así que recomiendo poner además un botón de confirmación. - Por qué comprobarlo antes: si lo llamas sin más, igualmente fallará con un 403. Pero decirle al usuario el motivo exacto, "dale permiso de administrador al bot", es muy distinto de mostrar solo "hubo un error".
💡 Lo que quizá te perdiste en agosto
- 12 ago., ofuscación de canales — desde el 16 nov., los canales que el bot no puede ver quedan ocultos. Lo vimos en detalle en el #24.
- 5 ago.,
file_typesen opciones de archivo adjunto — la opciónATTACHMENTy el componente de subida de archivos tienen ahora un filtro de "solo aceptar estas extensiones". discord.py 2.7.1 aún no lo tiene (no hayfile_typesen su código fuente), así que toca esperar a una actualización de la librería. - 5 ago.,
application_idde los canales puede ser null — si tienes código que da por hecho queapplication_iddel objeto canal siempre es un ID, añade el manejo de None.
📌 Código completo (listo para copiar y pegar)
Con un solo archivo, sept_check.py, funcionan los 5 comandos de arriba. ¡Cópialo tal cual!
# discord.py 2.7.1 / Python 3.8+
# Bot para revisar los cambios de la API de Discord de septiembre de 2026 (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("❌ ¡No hay DISCORD_TOKEN en el archivo .env!")
intents = discord.Intents.default()
intents.members = True # información de miembros (actívalo en el Developer Portal)
intents.presences = True # para ver estados personalizados (actívalo en el Developer Portal)
bot = commands.Bot(command_prefix="!", intents=intents)
MIB = 1024 * 1024
@bot.event
async def on_ready():
await bot.tree.sync()
print(f"✅ Sesión iniciada: {bot.user} / discord.py {discord.__version__}")
# ① 11 sep. - entrada multilínea en opciones de texto
@bot.tree.command(name="여러줄", description="Divide por líneas el texto de varias líneas que escribas y te lo muestra")
@app_commands.describe(text="Prueba a escribir cambiando de línea con 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("¡El contenido está vacío!", ephemeral=True)
return
body = "\n".join(f"{i}. {line}" for i, line in enumerate(lines, start=1))
await interaction.response.send_message(f"Recibí **{len(lines)} líneas** en total.\n{body}")
# ② 3 sep. - límite base de subida de archivos de 20 MiB
@bot.tree.command(name="파일한도", description="Muestra el tamaño de archivo que el bot puede subir en este servidor")
async def file_limit(interaction: discord.Interaction):
live = interaction.filesize_limit # valor que Discord envió con esta interacción
cached = interaction.guild.filesize_limit if interaction.guild else None # tabla de la librería
msg = f"Límite que indica Discord: **{live / MIB:.0f} MiB**"
if cached is not None:
msg += f"\nSegún la tabla de discord.py (guild.filesize_limit): {cached / MIB:.0f} MiB"
await interaction.response.send_message(msg, ephemeral=True)
# ③ 27 ago. - función de servidor PRUNE_REQUIRES_ADMIN
@bot.tree.command(name="정리예상", description="Calcula cuántos miembros llevan 30 días sin actividad (no expulsa a nadie)")
@app_commands.describe(days="Días sin actividad para contar a alguien como objetivo (1 a 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("¡Úsalo dentro de un servidor!", ephemeral=True)
return
perms = guild.me.guild_permissions
if "PRUNE_REQUIRES_ADMIN" in guild.features:
if not perms.administrator:
await interaction.response.send_message(
"Este servidor tiene **PRUNE_REQUIRES_ADMIN** activado, así que el bot necesita permiso de administrador.",
ephemeral=True,
)
return
elif not (perms.kick_members and perms.manage_guild):
await interaction.response.send_message(
"El bot necesita los permisos **Expulsar miembros + Gestionar servidor** para calcularlo.", ephemeral=True
)
return
await interaction.response.defer(ephemeral=True)
count = await guild.estimate_pruned_members(days=days)
await interaction.followup.send(f"Objetivo de limpieza con {days} días: **{count} miembros** (solo lo calculé)", ephemeral=True)
# ④ 18 sep. - endpoints de usuarios destino (target user) de invitaciones
@bot.tree.command(name="초대대상", description="Crea un enlace de invitación que solo puede usar una persona concreta")
@app_commands.describe(member="Miembro que podrá usar el enlace de invitación")
async def invite_target(interaction: discord.Interaction, member: discord.Member):
channel = interaction.channel
if not isinstance(channel, discord.TextChannel):
await interaction.response.send_message("¡Úsalo en un canal de texto!", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
invite = await channel.create_invite(max_age=3600, max_uses=1, unique=True,
reason=f"Invitación exclusiva para {member}")
# discord.py 2.7.1 aún no tiene un método para esto, así que llamamos al endpoint directamente
route = Route("PUT", "/invites/{code}/target-users/{user_id}",
code=invite.code, user_id=member.id)
await bot.http.request(route) # si va bien, 204 (sin cuerpo)
await interaction.followup.send(
f"Creé un enlace de invitación exclusivo para {member.mention} (1 hora, 1 uso): {invite.url}", ephemeral=True
)
# ⑤ 17 sep. y 22 sep. - puede que el estado personalizado no se vea
@bot.tree.command(name="상태보기", description="Lee el estado personalizado de un miembro (puede que no se vea)")
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 = "No tiene estado personalizado, o su perfil privado impide que el bot lo vea."
else:
text = f"{custom.emoji or ''} {custom.name or ''}".strip() or "(estado con solo un emoji)"
await interaction.response.send_message(f"{member.display_name}: {text}", ephemeral=True)
# ⑥ 15 sep. - las apps con sharding para bots grandes deben enviar la consulta shard
async def fetch_all_guilds_by_shard(client: discord.Client) -> list:
"""Para apps con sharding para bots grandes. Un bot normal puede usar client.fetch_guilds() sin más."""
_, _, 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)💡 Si usas la estructura cogs/ del #19, puedes pasar solo la parte de los comandos a cogs/sept_check.py y funcionará igual.
📌 Resultado
python sept_check.pyCuando el bot se encienda, prueba los comandos uno a uno en el servidor. Todas las respuestas son visibles solo para ti (ephemeral), así que el servidor de pruebas no se llena de mensajes.

/파일한도: en un servidor sin boost, lo normal es que los dos números salgan distintos, como arriba. La línea de arriba (20 MiB) es el límite real, y la de abajo (10 MiB) es la prueba de que la tabla de la librería está desactualizada.

/여러줄: prueba a cambiar de línea con Shift+Enter en la caja de texto. El bot cuenta las líneas y te las devuelve numeradas.
/초대대상: solo se comprueba de verdad abriendo el enlace con una cuenta que no sea la destinataria. Si no lo es, lo normal es que no pueda unirse.

⚠️ Para ser sincero, el código de este artículo solo lo revisé en Python 3.13 hasta la sintaxis, los imports y el armado de la dirección de Route; no ejecuté los comandos en un servidor real. Sobre todo /초대대상 usa un endpoint que apareció el 18 sep., así que asegúrate de comprobar la respuesta real. Si no funciona, avísame a través de contacto (en lugar de comentarios) y corregiré el artículo.
📌 Errores frecuentes
1. discord.errors.Forbidden: 403 Forbidden (error code: 50013): Missing Permissions — /정리예상
- Causa: el servidor activó
PRUNE_REQUIRES_ADMINy el bot no tiene permiso de administrador, o en un servidor sin esa función le faltan los permisos de expulsar miembros y gestionar servidor. - Solución: como en el código de arriba, mira primero
guild.featurese indica qué permiso falta. Da al rol del bot los permisos necesarios y vuelve a intentarlo.
2. discord.errors.Forbidden: 403 — la petición PUT de /초대대상
- Causa: asignar usuarios destino requiere ser quien creó la invitación o tener el permiso de gestionar servidor. Falla si pusiste un código de invitación creado por otra persona o si el bot no tiene el permiso Create Invite (crear invitación).
- Solución: como en el ejemplo, asigna destinos solo a invitaciones creadas por el propio bot, o da al rol del bot el permiso de gestionar servidor.
3. discord.errors.HTTPException: 400 Bad Request — GET /users/@me/guilds
- Causa: una app con sharding para bots grandes pidió la lista de servidores sin la consulta
shard. Desde el 15 sep. devuelve 400. - Solución: como en
fetch_all_guilds_by_shard(), añadeparams={"shard": n}y recorre de0amax_concurrency - 1. A un bot normal no le aparece este error.
4. /상태보기 siempre responde "No tiene estado personalizado..."
- Causa: no pusiste
intents.presences = Trueo no activaste Presence Intent en el Developer Portal. En ese casomember.activitiessiempre es una tupla vacía. - Solución: actívalo en el código y en el portal, y reinicia el bot. Si con los dos activados sigue sin verse, ese usuario tiene el perfil privado (17 sep.) y el bot no puede verlo. Eso es lo normal.
5. Envié un archivo y da 413 Payload Too Large / Request entity too large
- Causa: 20 MiB es el valor base para servidores sin boost. Los archivos más grandes siguen sin poder enviarse, y si mezclas MB (1 000 000) y MiB (1 048 576) en el cálculo, fallará justo en el límite.
- Solución: antes de enviar, compara
os.path.getsize()coninteraction.filesize_limit. Si comparas conguild.filesize_limit(10 MiB en 2.7.1), rechazarás archivos que sí se pueden enviar, así que no lo uses.
6. discord.errors.NotFound: 404 (error code: 10062): Unknown interaction en /초대대상
- Causa: no respondiste en 3 segundos. Con dos peticiones HTTP puede ir lento.
- Solución: llama primero a
await interaction.response.defer()antes del trabajo pesado, y después responde coninteraction.followup.send().
💬 3 cosas para recordar hoy
- Solo hay dos cosas que arreglar. La consulta
sharden apps con sharding para bots grandes, y la comprobación de permisos dePRUNE_REQUIRES_ADMINen bots que limpian miembros. Lo demás basta con saberlo. - El límite de archivos es
interaction.filesize_limit. La tabla de la librería (guild.filesize_limit) sigue anticuada, en 10 MiB. - Llegan saltos de línea en las opciones de texto. Recíbelas con
splitlines()y, donde deba ser una sola línea, aplánalas. Y que no haya estado personalizado pasó a ser un caso normal.
📌 Para terminar
Hoy, en lugar de una función nueva, repasamos de una vez un mes entero de cambios en la API de Discord 🗞️
En resumen: el límite de archivos subió a unos generosos 20 MiB (no te fíes del valor de la librería), en las opciones de texto de los comandos slash empiezan a llegar varias líneas, y hay endpoints para crear enlaces de invitación exclusivos para personas concretas. Por la verificación de edad y los perfiles privados hay más casos en que el estado personalizado no se ve; las apps con sharding para bots grandes deben cuidar la consulta shard, y los bots de limpieza de miembros, PRUNE_REQUIRES_ADMIN. discord.py 2.7.1 no ha tenido versiones nuevas desde marzo, así que los endpoints nuevos los cubrimos llamándolos directamente con Route ✅
Y no lo olvides: la ofuscación de canales del 16 de noviembre es el cambio más grande. Si todavía no has ejecutado el /채널점검 del #24, hazlo este fin de semana 💣
En la serie Crear un bot de Discord, después de la sala de fiesta del #33, la próxima entrega tratará sobre cómo desplegar el bot con Docker (previsto). ¡No te la pierdas! 🙌
📚 Artículos relacionados
- Ver toda la serie Crear un bot de Discord (categoría)
- #1 - Domina los Slash Commands
- #2 - Añadir opciones (Arguments) a un Slash Command
- #22 - Crear autocompletado (Autocomplete) para comandos slash
- #24 - Adaptarse a la ofuscación de canales (Channel Obfuscation)
- #33 - Ampliar los canales de voz temporales: crear salas de fiesta
- Los 10 errores más comunes de discord.py
