Hi there.
I'm wondering if someone could give me some information about how to create multiple bots in the same project.
I have three discord apps, but related to each other, so I want to use them in the same script. I read the bot's and client's script, also I have read a bit about loops, but I'm newbie in Python so it's hard to understand.
This is what I was trying to do:
Creating the bots
botA = commands.Bot(command_prefix='!', description = 'Example')
botB = commands.Bot(command_prefix='?', description = 'Example')
Creating the loop
loop = asyncio.get_event_loop()
Then I defined the bots structure, using events, and run it through asyncio library.
@botA.event
async def on_ready():
print('[BOT] Created.')
loop.run_until_complete(botA.run('TOKEN_A'))
@botB.event
async def on_ready():
print('[BOT] Created.')
loop.run_until_complete(botB.run('TOKEN_B'))
And at the end of the script
try:
loop.run_forever()
finally:
loop.close()
What am I doing wrong? Could you give me some explanation?
Thank you very much!!
The key to accomplishing this is understanding the underlying mechanics behind both discord bots and asyncio
bot.run is blocking and a function not a coroutine, therefore running until complete won't work with it. Luckily, we have access to the underlying coroutine which we can use ourselves using the same arguments bot.start()
We also must understand how the loop works, to get these running (however you'd like, most basically)
You just have to await the start coroutines however to start the bots concurrently, seeing as bot.run will block
loop = asyncio.get_event_loop()
loop.create_task(bot1.start(your_args))
loop.create_task(bot2.start(more_args))
loop.run_forever()
This will await our two start coroutines and run the bots concurrently. I think this should be the best way to accomplish this, correct me if I'm wrong
run_forever will keep the loop, and therefore the script, still running after the bots logout. You should either use run_until_complete or create a .start task for one bot and use .run on the other.
Just as soon stick an await bot2.start() at the bottom of your on_ready in bot1
This keeps on_ready as part of your call stack for bot2. Use create_task, as is shown in the background task example. Additionally, on_ready triggers every time your bot connects to discord, so if your bot disconnects without crashing and reconnects, you risk running multiple copies of your second bot.
I tried @henry232323 solution, but with run_until_complete. That method ask for future argument, so I went to @Gorialis solution, but I don't know how to properly make the "future" task.
async def create_bots():
await botA.wait_until_ready()
await botB.wait_until_ready()
await botC.wait_until_ready()
while not botA.is_closed and botB.is_closed and botC.is_closed:
loop.run_until_complete(create_bots())
I know the code is incomplete, but I don't know what to do.
Ok there's a few problems with what you have there, wait_until_ready will not happen until the bot is running, what you want is a bot event on_ready predefined for whichever bots.
As for your code,
async def create_bots():
await botA.start(tokenA)
await botB.start(tokenB)
await botC.start(tokenC)
loop.run_until_complete(create_bots())
@henry232323 thanks. I did that but only starts one of the three bots in Discord. Also it doesn't show any confirmation message (on_ready() event) on the console log.
https://github.com/Rapptz/discord.py/issues/516#issuecomment-287506512 is the most correct I believe. you can call loop.stop() whenever you want to shut down the bot.
Edit: this also might work
loop = asyncio.get_event_loop()
task1 = loop.create_task(bot1.start(your_args))
task2 = loop.create_task(bot2.start(more_args))
gathered = asyncio.gather(task1, task2, loop=loop)
loop.run_until_complete(gathered)
Oh duh I'm an idiot trying to await those three, of course they'll consecutively block, yeah, my earlier answer was best, you need to make tasks out of them or the next won't run until after the prior finishes, thanks khazhyk
It worked!!
Solution:
Create the loop
loop = asyncio.get_event_loop()
Create each bot task
loop.create_task(bot.start('TOKEN'))
Run loop and catch exception to stop it
try:
loop.run_forever()
finally:
loop.stop()
Thank you all!!!!!!!
Writing this here for future references.
For starters, you have to realise that Client.run is an abstraction of a couple of more lower level concepts.
There is Client.login which logs in the client and then Client.connect which actually runs the processing. These are coroutines.
asyncio provides the capability of putting things in the event loop for it to work whenever it has time to.
Something like this e.g.
loop = asyncio.get_event_loop()
async def foo():
await asyncio.sleep(10)
loop.close()
loop.create_task(foo())
loop.run_forever()
If we want to wait for something to happen from another coroutine, asyncio provides us with this functionality as well (docs) through the means of synchronisation via asyncio.Event. You can consider this as a boolean that you are waiting for:
e = asyncio.Event()
loop = asyncio.get_event_loop()
async def foo():
await e.wait()
print('we are done waiting...')
loop.stop()
async def bar():
await asyncio.sleep(20)
e.set()
loop.create_task(bar())
loop.create_task(foo())
loop.run_forever() # foo will stop this event loop when 'e' is set to true
loop.close()
It also provides us with the means to wait for multiple 'futures' or coroutines to complete (docs):
a = asyncio.Event()
b = asyncio.Event()
async def foo():
await asyncio.sleep(10)
a.set()
async def bar():
await asyncio.sleep(15)
b.set()
loop = asyncio.get_event_loop()
loop.create_task(bar())
loop.create_task(foo())
loop.run_until_complete(asyncio.wait([a.wait(), b.wait()]))
loop.close()
Using this concept we can apply it to the discord bots themselves.
import asyncio
import discord
from collections import namedtuple
# First, we must attach an event signalling when the bot has been
# closed to the client itself so we know when to fully close the event loop.
Entry = namedtuple('Entry', 'client event')
entries = [
Entry(client=discord.Client(), event=asyncio.Event()),
Entry(client=discord.Client(), event=asyncio.Event())
]
# Then, we should login to all our clients and wrap the connect call
# so it knows when to do the actual full closure
loop = asyncio.get_event_loop()
async def login():
for e in entries:
await e.client.login()
async def wrapped_connect(entry):
try:
await entry.client.connect()
except Exception as e:
await entry.client.close()
print('We got an exception: ', e.__class__.__name__, e)
entry.event.set()
# actually check if we should close the event loop:
async def check_close():
futures = [e.event.wait() for e in entries]
await asyncio.wait(futures)
# here is when we actually login
loop.run_until_complete(login())
# now we connect to every client
for entry in entries:
loop.create_task(wrapped_connect(entry))
# now we're waiting for all the clients to close
loop.run_until_complete(check_close())
# finally, we close the event loop
loop.close()
Hope this helps.
What would the decorator be for this? @client.event doesn't seem to work. How you would you get async def on_ready(): or async def on_message(): to work?
Your issue is irrelevant to what this issue is about. There's no need to comment on a closed issue as well. If you have an issue, open a new one, with relevant code...because it is @client.event that you would want to use so there's a different issue there.
It bothered me that the client did not close properly on Keyboard interruptions
I'm not sure if this is really correct but here it is:
from discord.client import _cancel_tasks
then wrap the run until complete in a try except:
try:
loop.run_until_complete(check_close())
except KeyboardInterrupt:
_cancel_tasks(loop)
Edit:Â tested again, and this code is very wrong, I'll do some more research and update later.
here is the code to be able to wait for all the bot to do a clean logout on program termination
import asyncio
from collections import namedtuple
import logging
import signal
from discord.ext import commands
bot1 = commands.Bot()
bot2 = commands.Bot()
# do all the things you want with your bot…
# run all the bot and be ready to interrupt the program
Entry = namedtuple('Entry', 'client, token')
entries = [
Entry(client=bot1, token='discord_bot1_secret_token'),
Entry(client=bot2, token='discord_bot2_secret_token')
]
loop = asyncio.get_event_loop()
signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)
async def shutdown(signal, running_bots):
logging.info(f"Received exit signal {signal.name}...")
[running_bot.cancel() for running_bot in running_bots]
await asyncio.gather(*running_bots, return_exceptions=True)
loop.stop()
async def wrapped_connect(entry):
try:
await entry.client.start(entry.token)
finally:
logging.info("Clean close of client")
await entry.client.close()
logging.info('Client cleanly closed')
try:
running_bots = []
for entry in entries:
running_bots.append(loop.create_task(wrapped_connect(entry)))
for s in signals:
loop.add_signal_handler(s, lambda s=s: asyncio.create_task(shutdown(s, running_bots)))
loop.run_forever()
finally:
logging.info('Program interruption')
loop.close()
this blog was very helpfull in understanding asyncio: https://www.roguelynn.com/words/asyncio-graceful-shutdowns/
This issue was 2 years ago
Most helpful comment
Writing this here for future references.
For starters, you have to realise that
Client.runis an abstraction of a couple of more lower level concepts.There is
Client.loginwhich logs in the client and thenClient.connectwhich actually runs the processing. These are coroutines.asyncioprovides the capability of putting things in the event loop for it to work whenever it has time to.Something like this e.g.
If we want to wait for something to happen from another coroutine, asyncio provides us with this functionality as well (docs) through the means of synchronisation via
asyncio.Event. You can consider this as a boolean that you are waiting for:It also provides us with the means to wait for multiple 'futures' or coroutines to complete (docs):
Using this concept we can apply it to the discord bots themselves.
Hope this helps.