Bot v discord.py je proces v Pythonu, který drží otevřený jeden WebSocket k Discordu. Hostovat ho není složité, ale padá na malém počtu konkrétních věcí: nainstalovaná špatná knihovna pod stejným názvem importu, privilegovaný intent, který nikdo nezapnul ve Developer Portal, rozšíření, které se nenačetlo a strhlo s sebou celého bota, a - s velkým náskokem nejčastěji - synchronní volání, které blokuje event loop, dokud to Discord nevzdá na heartbeatu.
Tento průvodce popisuje, kterou verzi knihovny nainstalovat, co patří do requirements.txt, jak do sebe zapadají setup_hook a cogy, jak synchronizovat strom aplikačních příkazů, aniž narazíš na rate limit, a jak má na serveru vypadat spouštěcí příkaz. Obecné zásady provozu bota - jen odchozí spojení, žádný příchozí port, restart při ukončení - jsou stejné jako u Node botů a najdeš je v článku jak hostovat Discord bota 24/7. Paměť a CPU jsou v článku kolik RAM a CPU Discord bot potřebuje.
Výběr knihovny a verze Pythonu#
Tři knihovny sdílejí jeden název importu. import discord může být discord.py, py-cord nebo starý zástupný balíček discord na PyPI a pip klidně nainstaluje více z nich do stejného prostředí, kde si pak soubory navzájem přepíšou a chyby přestanou dávat smysl.
| Balíček | Import | Poznámky |
|---|---|---|
discord.py | discord | Originál. Verze 2.x má aplikační příkazy a cogy |
py-cord | discord | Fork s vlastními dekorátory pro slash příkazy |
nextcord | nextcord | Fork, který si přejmenoval import, takže může existovat vedle ostatních |
discord | discord | Zástupný balíček, který nainstaluje discord.py. Nezávisej na něm |
Pokud se ti importy zbláznily, oprava je odinstalovat všechny a nainstalovat přesně jednu:
$ pip uninstall -y discord.py py-cord discord$ pip install -U discord.pyObě API nejsou zaměnitelná. py-cord používá @bot.slash_command a discord.Option; discord.py používá @bot.tree.command, @app_commands.command a discord.app_commands.describe. Kód zkopírovaný z návodu psaného pro jednu knihovnu na druhé nepoběží a výsledná AttributeError vypadá jako rozbitá instalace, ne jako špatná knihovna. Rozhodni se, kterou používáš, připni ji a ověř, že každý vložený úryvek sedí.
K verzi Pythonu: discord.py 2.x vyžaduje Python 3.8 nebo novější. Používej 3.11 nebo 3.12, pokud nemáš důvod jinak. Python 3.13 odstranil modul audioop ze standardní knihovny, na kterém stála podpora hlasu, takže starší vydání discord.py se na 3.13 vůbec neimportují a novější místo toho stahují balíček s backportem. Pokud tvůj bot přehrává zvuk, verzi interpretu připni záměrně a před přechodem výš si přečti changelog své knihovny.
requirements.txt a instalace na serveru#
Instalace na serveru nemá interaktivní shell ani možnost odpovědět na dotaz. Vše potřebné je v jednom souboru.
# example pins - use the versions you actually tested againstdiscord.py==2.5.2python-dotenv==1.1.1aiosqlite==0.21.0Verze připínej přesně přes ==. Alternativa - bez připnutí nebo s >= - znamená, že server nainstaluje, co vyšlo dnes ráno, a bot, který se šest týdnů nezměnil, se při restartu rozbije. Připnutí také dělá chybu reprodukovatelnou: stejnou sadu si nainstaluješ lokálně a uvidíš stejnou chybu. Nástroje pro připínání důkladně rozebírá requirements a virtualenvy v Pythonu; pip freeze je tupý začátek, protože zapíše celý tranzitivní strom včetně věcí, o které jsi nežádal.
Spouštěcí příkaz na aplikačním serveru obvykle nainstaluje a pak spustí:
pip install --no-cache-dir -r requirements.txt && python -u bot.pyDva přepínače si své místo zaslouží. --no-cache-dir zabrání pipu držet cache wheelů, která nemá smysl v kontejneru, jenž se stejně pokaždé instaluje od začátku, a u tarifu s 5 GB je ta cache pořádný kus disku. python -u vypíná buffering stdout, což je rozdíl mezi tím, že řádky logu vidíš v konzoli hned, a tím, že několik minut nevidíš nic, dokud se buffer nenaplní. Totéž udělá proměnná prostředí PYTHONUNBUFFERED=1.
Pokud potřebuješ hlas, nainstaluj discord.py[voice], které přidá PyNaCl, a ujisti se, že v kontejneru existuje binárka ffmpeg - knihovna ji volá jako externí program a selže až při přehrávání, ne při importu, takže tuhle chybu objevíš před publikem.
Bot, jeho intents a setup_hook#
Intents jsou odběr událostí, který deklaruješ při připojení. discord.Intents.default() dá všechno kromě tří privilegovaných, které se musí zapnout i na záložce Bot ve Developer Portal, nejen v kódu.
import osimport discordfrom discord.ext import commandsintents = discord.Intents.default()intents.message_content = True # privileged: needed for prefix commandsintents.members = False # privileged: the expensive oneclass Bot(commands.Bot): def __init__(self): super().__init__(command_prefix="!", intents=intents) async def setup_hook(self): for extension in ("cogs.moderation", "cogs.levels"): await self.load_extension(extension)bot = Bot()bot.run(os.environ["DISCORD_TOKEN"], log_handler=None)setup_hook se spustí jednou, po přihlášení, ale před tím, než je bot připravený, a patří do něj asynchronní startovací práce: načítání rozšíření, otevření poolu k databázi, registrace trvalých views. Nahrazuje vzor, kdy se práce dělá v on_ready, což je špatně způsobem, který snadno přehlédneš - on_ready se může spustit víckrát, protože se spustí znovu po reconnectu, který nešlo obnovit. Cokoli tam uděláš, proběhne dvakrát.
Dva detaily intents způsobují většinu zmatků. Když je message_content vypnutý, prefixové příkazy tiše nedělají nic, protože bot dostane událost zprávy s prázdným polem content; discord.py při startu zaloguje varování o chybějícím privilegovaném intentu a to varování je celá odpověď. Když je zapnutý members, knihovna při připojení stáhne seznam členů každého serveru, což je pomalé a náročné na paměť u bota ve velkých serverech - předej chunk_guilds_at_startup=False, pokud tu cache opravdu nepotřebuješ.
Cogy: jak bota rozdělit, aniž ho rozbiješ#
Cog je třída, která seskupuje příkazy, posluchače a stav. Rozšíření (extension) je modul, který ji načítá. V discord.py 2.x jsou funkce setup v modulu i load_extension asynchronní, což je největší rozdíl oproti návodům pro verzi 1.x.
import discordfrom discord.ext import commandsfrom discord import app_commandsclass Moderation(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot @app_commands.command(description="Remove recent messages") @app_commands.describe(count="How many messages, 1-100") async def purge(self, interaction: discord.Interaction, count: int): await interaction.response.defer(ephemeral=True) deleted = await interaction.channel.purge(limit=count) await interaction.followup.send(f"Deleted {len(deleted)}.")async def setup(bot: commands.Bot): await bot.add_cog(Moderation(bot))Praktická pravidla pro cogy na serveru:
- Rozbité rozšíření zastaví bota. Výjimka vyhozená v
setupse propaguje zload_extensionven, a pokud je to volání vsetup_hookbez jakéhokoli obalu, proces skončí. Pokud má bot přežít jeden rozbitý cog, obal každé načtení dotrya selhání hlasitě zaloguj, místo aby jeden modul strhl ostatní. - Reload je pro vývoj.
await bot.reload_extension("cogs.levels")modul znovu importuje, což se lokálně hodí a v produkci je to past: objekty vytvořené starou verzí zůstanou naživu v posluchačích a úlohách, které jsi neuklidil. Na serveru raději restartuj. - Uklízej v `cog_unload`. Úloha
tasks.loopspuštěná cogem běží dál, i když cog zmizí, pokud ji tam nezrušíš. - Smyčky na pozadí potřebují `before_loop`. Smyčka, která mluví s Discordem, musí nejdřív počkat, až bude bot připravený, jinak její první iterace běží proti klientovi bez cache.
from discord.ext import tasks@tasks.loop(minutes=15)async def sweep(self): ...@sweep.before_loopasync def before_sweep(self): await self.bot.wait_until_ready()Slash příkazy a synchronizace stromu#
Aplikační příkazy žijí na straně Discordu. bot.tree drží tvoje lokální definice a sync je nahraje. Dokud nesynchronizuješ, nic se neděje, a synchronizace je akce při nasazení, ne při startu.
# Development: instant, one guildGUILD = discord.Object(id=123456789012345678)bot.tree.copy_global_to(guild=GUILD)await bot.tree.sync(guild=GUILD)# Production: global, propagates within about an hourawait bot.tree.sync()Příkazy pro jeden server se objeví okamžitě, proto je používá každý návod. U globálních příkazů dokumentace uvádí, že jim propagace trvá až hodinu, takže chybějící příkaz hned po globální synchronizaci je obvykle prostě brzy. Synchronizace nahradí celou sadu pro daný rozsah, takže příkaz, který jsi smazal z kódu, zmizí při další synchronizaci, a neúplný seznam smaže zbytek.
Nevolej sync() v on_ready ani v setup_hook. Discord omezuje, kolik příkazů smíš za den vytvořit, a bot, který synchronizuje při každém startu a zároveň uvízl ve smyčce restartů, se může na den zablokovat. Běžný vzor je prefixový příkaz jen pro vlastníka, který synchronizuje na požádání, takže nasazení, které příkazy nezměnilo, nestojí nic:
@bot.command()@commands.is_owner()async def sync(ctx): synced = await bot.tree.sync() await ctx.send(f"Synced {len(synced)} commands.")Pokud synchronizace selže s chybou validace, přečti si cestu k poli: názvy musí být malými písmeny, 1 až 32 znaků a bez mezer, popisy mají 1 až 100 znaků. Hybridní příkazy - @commands.hybrid_command() - se z jedné funkce registrují jako prefixový i slash příkaz, což je nejméně bolestivý způsob, jak podporovat obojí, aniž bys všechno psal dvakrát.
Nikdy neblokuj event loop#
Tohle je nejdůležitější část pro produkci a ve většině návodů chybí.
discord.py běží na asyncio. Jedno vlákno, jedna smyčka, střídající se korutiny. Každé await je místo, kde smyčka může dělat něco jiného, včetně odeslání heartbeatu, který udržuje spojení s gateway. Synchronní volání takové místo nemá, takže dokud běží, neběží nic jiného - ani tvé další příkazy, ani heartbeat.
Příznakem je řádek v logu od knihovny:
WARNING discord.gateway Heartbeat blocked for more than 10 seconds.Pokud to trvá dost dlouho, následuje uzavření socketu ze strany Discordu a reconnect bota. Uživatelé vidí bota, který „náhodně vypadává“ na serveru, jehož graf CPU vypadá v pořádku.
Obvyklí viníci a čím je nahradit:
| Blokující | Použij místo toho |
|---|---|
requests.get(...) | aiohttp, už je to závislost |
time.sleep(5) | await asyncio.sleep(5) |
open(...).read() na velkém souboru | await asyncio.to_thread(...) |
| Synchronní databázový ovladač | asyncpg, aiosqlite, motor |
| Zpracování obrázků, zipování, parsování | await asyncio.to_thread(...) |
asyncio.to_thread (Python 3.9 a novější) spustí funkci v pracovním vlákně a počká na výsledek, což je jednořádková oprava pro práci, která je málo náročná na CPU, ale pomalá. Skutečně náročná práce na CPU na botovi vůbec být nemá; pošli ji do fronty a samostatnému workeru, jako v článku úlohy na pozadí na malém serveru.
Stejná disciplína platí pro rate limity. discord.py odpovědi 429 zvládá za tebe a počká na retry_after, ale smyčka, která upravuje zprávu každou sekundu, nebo příkaz, který odešle padesát zpráv najednou, stráví většinu času čekáním. Dávkuj, co jde, a nikdy nedávej volání API dovnitř těsné smyčky přes seznam členů.
Provoz na hostingu#
Podoba nasazení Pythonového bota je stejná jako u jakéhokoli dlouho běžícího procesu: stáhnout kód, nainstalovat závislosti, spustit jeden příkaz a restartovat ho, když skončí.
Proměnné prostředí patří na záložku Startup, ne do repozitáře - token, URL databáze, ID serveru, na který synchronizuješ. Čti je tak, aby chybějící hodnota způsobila tvrdé selhání, protože discord.LoginFailure tři sekundy po startu je mnohem horší chybová zpráva než „DISCORD_TOKEN is not set“. Lokálně čte python-dotenv stejné názvy ze souboru .env, který je v .gitignore. Delší výklad je v článku kam na aplikačním serveru patří secrets.
Na RE:NODE je integrace s Gitem jen pro GitHub, přes GitHub App, která vydává krátkodobé tokeny, takže soukromé repozitáře fungují, aniž by na serveru ležely přihlašovací údaje. Řídí ji dva přepínače: stáhnout větev při každém startu a deploy on push, který restartuje server, když GitHub nahlásí push do té větve - a jen když už běžel. Každé nasazení je jeden záznam, takže poznáš nasazení, které proběhlo, od toho, které selhalo. Stejný postup je popsaný pro Node v článku nasazení Node.js aplikace z GitHubu a pro Python funguje stejně.
K logování zavolej discord.utils.setup_logging() nebo si nakonfiguruj logging sám a pokud děláš vlastní nastavení, předej log_handler=None do bot.run, jinak knihovna nainstaluje druhý handler a každý řádek se objeví dvakrát. Loguj na stdout, aby ho konzole ukazovala živě; logovací soubor uvnitř kontejneru je soubor, pro který si musíš chodit přes SFTP, a když ho nikdo nerotuje, za pár měsíců zaplní disk. O tom, co do logu patří, je logy, které stojí za to uchovávat.
Dvě chování hostingu stojí za to znát dřív, než tě překvapí. Pokud proces dosáhne limitu paměti kontejneru, zastaví se a čistě restartuje místo swapování, takže neomezená cache se projeví jako restart každých pár hodin, ne jako zpomalení. A smyčka pádů se pozná: tři neočekávané restarty za hodinu vyvolají varování na stránce serveru a automaticky otevřou ticket, šest vede k pozastavení. Obojí je popsané z diagnostické strany v článku proč se tvůj server pořád restartuje.
Sharding a kdy ho potřebuješ#
Discord vyžaduje od bota sharding nad 2 500 serverů. Pod touto hranicí je to řešení problému, který nemáš.
Až tam dojdeš, discord.py to usnadní: commands.AutoShardedBot nahradí commands.Bot a spouští všechny shardy v jedné event loop v jednom procesu. Gateway se zeptá, kolik shardů chce, cache jsou sdílené a paměť roste s množstvím dat, ne s počtem shardů. To je zásadně jiná situace než u discord.js, který spouští proces na každý shard a násobí tím základní paměť.
bot = commands.AutoShardedBot(command_prefix="!", intents=intents)Mění se to, že jedna zablokovaná korutina teď zasáhne všechny shardy, takže disciplína kolem event loop z předchozí části přestává být radou a stává se požadavkem. Velcí boti také dostávají od gateway vyšší max_concurrency, což umožňuje identifikaci několika shardů najednou a výrazně zrychluje úplný restart. Na co máš nárok, zjisti ověřeným GET /gateway/bot, nehádej.
Řešení potíží#
`ModuleNotFoundError` po přidání knihovny. Je nainstalovaná lokálně a chybí v requirements.txt. Server instaluje jen to, co říká soubor.
`PrivilegedIntentsRequired` při startu. Privilegovaný intent v kódu, ale ne ve Developer Portal.
`LoginFailure: Improper token has been passed`. Prázdný nebo špatný DISCORD_TOKEN, resetovaný token, který nikdo znovu nenasadil, nebo vložený client secret místo tokenu bota.
Prefixové příkazy nic nedělají, slash příkazy fungují. Intent message_content. Bot dostává událost s prázdným obsahem.
`CommandNotFound` u slash příkazu, který existuje. Nikdy se nesynchronizoval, synchronizoval se na jiný server, nebo se globální synchronizace ještě nepropagovala.
`AttributeError: 'Bot' object has no attribute 'slash_command'`. Kód pro py-cord běžící na discord.py. Vyber jednu knihovnu.
Bot na minutu přestává reagovat. Zablokovaná event loop. Prohledej log kvůli varování k heartbeatu a podívej se, co běželo těsně před ním.
`RuntimeError: Event loop is closed` při vypnutí ve Windows. Lokální vývojová otrava z event loop typu proactor; na linuxovém kontejneru, kde bot hostuješ, se to neděje.
FAQ#
Mám použít discord.py, nebo py-cord?
discord.py je zase aktivně udržovaný a odkazuje na něj většina aktuální dokumentace a odpovědí, takže je bezpečnější výchozí volba. py-cord je rozumná volba, pokud ti vyhovuje jeho syntaxe slash příkazů. Co volba není, je míchání: instalují se pod stejným názvem importu a navzájem se rozbijí.
Potřebuju na straně hostingu virtualenv?
Na spravovaném aplikačním serveru už každý kontejner izoluje tvé závislosti, takže virtualenv uvnitř přidá jen adresář a skoro nic jiného. Používej ho lokálně, kde sdílí jeden interpret víc projektů, a requirements.txt připni, aby obě prostředí nainstalovala totéž.
Kam dám token bota?
Do proměnné prostředí na záložce Startup, čtené přes os.environ["DISCORD_TOKEN"]. Ne do kódu, ne do commitnutého .env, ne do konfiguračního souboru v repozitáři. Pokud už byl pushnutý, resetuj ho ve Developer Portal - rotace je jediná oprava, protože historie Gitu starou hodnotu uchovává.
Proč je můj bot pomalejší, čím víc lidí ho používá?
Obvykle jedno blokující volání v oblíbeném příkazu. Každý uživatel čekající na to volání čeká na stejnou event loop. Najdi synchronní volání knihovny, přesuň ho do asyncio.to_thread nebo na asynchronního klienta a problém zmizí bez dalšího CPU.
Můžu na stejném serveru provozovat bota i malý webový dashboard?
Ano, pokud je držíš v jednom procesu s asynchronním webovým frameworkem, nebo přijmeš, že dva procesy sdílejí jeden limit paměti a jeden podíl CPU. Samostatný malý tarif udrží grafy čitelné a zabrání tomu, aby padající dashboard strhl bota. K webové straně jsou podrobnosti v článku nasazení FastAPI nebo Flasku.
Jak poznám, že bot opravdu běží?
Gateway bot nemá žádný endpoint, na který by se dalo dotazovat, takže čti konzoli, která ukazuje živý nefiltrovaný výstup a grafy paměti a CPU proti limitům. Zaloguj jeden řádek při ready s počtem serverů a jeden řádek, když příkaz skončí chybou. Jak odlišit hluk při startu od skutečné závady, popisuje jak číst konzoli serveru bez hádání.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.