How to run multiple Discord clients simultaneously - python

I've created a few bots using the discord.py library and I now want to build a 'dispatcher' that reads a configuration file and launches them all. Each of my bots is created as a class extending from an abstract bot class. However, I'm stuck at running them simultaneously. These are some things I've tried:
Using threads. Eg: threading.Thread(target=discord.Client('token').run)).start(). Doesn't work because Client.run() tries to start the asyncio event loop again, causing an error (RuntimeError: Cannot close a running event loop).
Using os.system/multiprocessing/subprocess. To run .py files containing bots. Doesn't work because os.system etc blocks until the subprocess has ended (ie the bot is killed). I'd also prefer not to use this method because it's a bi
Creating tasks and putting them on a single asyncio loop (shown below).
MRE of the last method I tried:
import discord
import asyncio
class Bot:
client = discord.Client()
def __init__(self, token):
self.token = token
print('Bot initiated')
#self.client.event
async def on_ready():
print(f'Logged in as {self.client.user}')
#self.client.event
async def on_message(message):
print(message.content)
async def run(self):
print('Bot running')
self.client.run(self.token)
if __name__ == '__main__':
bot1 = Bot('bot token here')
bot2 = Bot('bot token here')
loop = asyncio.get_event_loop()
loop.create_task(bot1.run())
loop.create_task(bot2.run())
loop.run_forever()
This doesn't work at all - the first bot freezes in the run method and never even logs in. For testing, both bots were logging into the same bot account but that's irrelevant to the problem.
I presume that the ideal solution would be a way to asynchronously run a discord.Client, but I haven't come across any way to do this.

Easiest approach would be using subprocess.Popen
import sys
import subprocess
files = ["bot1.py", "bot2.py", ...]
for f in files:
subprocess.Popen(
[sys.executable, f], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
It will start all the files in the background.

Related

Using two PIP packages together that use event loops

I'm trying to combine a Twitch API package (twitchio) with a webserver (sanic) with the intent of serving chat commands to a game running locally to the python script. I don't have to use Sanic or twitchio but those are the best results I've found for my project.
I think I understand why what I have so far isn't working but I'm at a total loss of how to resolve the problem. Most of the answers I've found so far deal with scripts you've written to use asyncio and not packages that make use of event loops.
I can only get the webserver OR the chat bot to function. I can't get them to both run concurrently.
This is my first attempt at using Python so any guidance is greatly appreciated.
#!/usr/bin/python
import os
from twitchio.ext import commands
from sanic import Sanic
from sanic.response import json
app = Sanic(name='localserver')
bot = commands.Bot(
token=os.environ['TMI_TOKEN'],
client_id=os.environ['CLIENT_ID'],
nick=os.environ['BOT_NICK'],
prefix=os.environ['BOT_PREFIX'],
initial_channels=[os.environ['CHANNEL']]
)
#app.route("/")
async def query(request):
return json(comcache)
#bot.event
async def event_ready():
'Called once when the bot goes online.'
print(f"{os.environ['BOT_NICK']} is online!")
ws = bot._ws # this is only needed to send messages within event_ready
await ws.send_privmsg(os.environ['CHANNEL'], f"/me has landed!")
#bot.event
async def event_message(ctx):
'Runs every time a message is sent in chat.'
# make sure the bot ignores itself and the streamer
if ctx.author.name.lower() == os.environ['BOT_NICK'].lower():
return
await bot.handle_commands(ctx)
# await ctx.channel.send(ctx.content)
if 'hello' in ctx.content.lower():
await ctx.channel.send(f"Hi, #{ctx.author.name}!")
#bot.command(name='test')
async def test(ctx):
await ctx.send('test passed!')
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8080)
bot.run()

What is the difference between having a main bot class versus no class on discord py?

With discord python bots, I can't seem to find a great answer on this and the documentation actually uses both examples randomly. What is the main difference between using a bot class in your bot.py versus starting your bot without a class? See below for an example
Example bot with a class:
import discord
from discord.ext import commands
class MyBot(commands.Bot):
async def on_ready():
# ready
bot = MyBot(command_prefix='!')
bot.run(token)
Example of a regular bot without the class:
import discord
from discord.ext import commands
if __name__ == "__main__":
bot = commands.Bot(command_prefix='!')
async def on_ready():
# ready
bot.run(token)
As far as I know, both of these examples work and do the same thing, but like I said I can't seem to find a great answer to the differences between the two.
Creating your own class has a couple of advantages.
First, you will be able to import your MyBot class in another file. So if you want to split up your project into multiple files (maybe two people are working on the same project) one person can work on the MyBot class while another can import it.
my_bot.py
import ...
class MyBot(commands.Bot):
...
test.py
from my_bot import MyBot
bot = MyBot()
Second, you will be able to more easily change functions inside of MyBot. Right now your async def on_ready(): function doesn't do anything in the second example, even if there was code in it. If you put it inside of a MyBot class it will be called when your bot is ready. See on_ready()
Compare
class MyBot(commands.Bot):
async def on_ready():
print('connected to server!')
bot = MyBot(command_prefix='!')
bot.run(token)
to
if '__name__' == '__main__':
bot = commands.Bot(command_prefix='!')
async def on_ready():
print('connected to server!')
bot.run(token)
In the second example, it will never say "connected to server!" because that code won't run. And if you call it with on_ready() that doesn't really mean you were connected to the server. Turn off your internet connection and you'll see.
Of course, you might do something more useful than just printing a message of course. Maybe you'll write connection logs to a file or something.

How to run scheduler so that other function works in discord.py

I thought the schedule wouldn't interfere with the bot commands but it does. After i run a schedule let's say every one minute, it blocks other functions in my bot. I'm looking for a solution to run 1 simple task via scheduler ( i'm using this schedule module ) and keeping all the primary bot functionality - any commands or events.
Example:
#client.event
async def on_message(message):
if message.author.id == xxxxx:
print("im working")
def test():
print("hello")
job = schedule.every().second.do(test)
while 1:
schedule.run_pending()
I would like to run the test function and be able to detect messages via on_message function at the same time.
Thank you for any help
Discord.py has a feature for that, see the full documentation here. Here a short example:
from discord.ext import tasks
#tasks.loop(seconds=5)
async def foo():
print('This function runs every 5 Seconds')

Discord.py bot, can I do a heavy task "off to the side" so I don't lag inputs?

I have a Discord bot in Python / Discord.py where people can enter commands, and normally the bot responds very quickly.
However the bot is also gathering/scraping webdata every iteration of the main loop. Normally the scraping is pretty short and sweet so nobody really notices, but from time to time the code is set up to do a more thorough scraping which takes a lot more time. But during these heavy scrapings, the bot is sort of unresponsive to user commands.
#bot.command()
async def sample_command(ctx):
# may actually take a while for this command to respond if we happen to be
# in the middle of a heavier site scrape
await ctx.channel.send("Random message, something indicating bot has responded")
async def main_loop():
sem = asyncio.Semaphore(60)
connector = aiohttp.TCPConnector(limit=60)
async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
while True:
# main guts of loop here ...
scrapers = [scraper_1(session, sem), scraper_2(session, sem), ...]
data = list(chain(*await asyncio.gather(*scrapers))) # this may take a while
# do stuff with data
Is there a way to sort of have it go "Hey, you want to do a heavy scrape, fine go process it elsewhere - meanwhile let's continue with the main loop and I'll hook back up with you later when you're done and we'll process the data then", if that makes sense?
I mainly want to separate this scraping step so it's not holding up the ability for people to actually interact with the rest of the bot.
You can use the discord.py tasks extension docs.
For example:
from discord.ext import tasks
#bot.event()
async def on_ready():
main_loop.start()
#bot.command()
async def sample_command(ctx):
await ctx.channel.send("Random message, something indicating bot has responded")
#tasks.loop(seconds=60)
async def main_loop():
do_something()
Note: It's not recommended to start the tasks in on_ready because the bot will reconnect to discord and the task will start several times, Put it somewhere else or on_ready check if this the first connect.
Another simple tip: you can use await ctx.send() instead of await ctx.channel.send()
You can use asyncio.create_task() to spawn the scraping in the "background":
async def scrape_and_process(...):
scrapers = [scraper_1(session, sem), scraper_2(session, sem), ...]
data = list(chain(*await asyncio.gather(*scrapers))) # this may take a while
# do stuff with data
async def main_loop():
sem = asyncio.Semaphore(60)
connector = aiohttp.TCPConnector(limit=60)
async with aiohttp.ClientSession(connector=connector, headers=headers) as session:
while True:
# main guts of loop here ...
# initiate scraping and processing in the background and
# proceed with the loop
asyncio.create_task(scrape_and_process(...))
You can try to use python threading.
Learn more here
It basically allows you to run it on different threads
example:
import threading
def 1():
print("Helo! This is the first thread")
def 2():
print("Bonjour! This is the second thread")
thread1 = threading.Thread(target=1)
thread2 = Threading.Thread(target=2)
thread1.start()
thread2.start()

Asyncio process blocking

So i have a basic discord bot which accepts input
import discord
import asyncio
import threading
loop = asyncio.new_event_loop()
bot = discord.Client()
def run_asyncio_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
Hangman.set_bot(bot)
#bot.event
async def on_message(message):
bot.loop.create_task(Hangman.main(message))
asyncioLoop = threading.Thread(target = run_asyncio_loop, args = (loop,))
asyncioLoop.start()
bot.run(BotConstants.TOKEN)
In this example it calls the hangman game which does not block anything as i have tested this using asyncio.sleep(n) but when i go to do a something in hangman it blocks it.
class Hangman():
async def main(message):
await Hangman.make_guess(message)
async def update_score(message):
sheetLoaded = Spreadsheet.load_ws(...)
userExists = Spreadsheet.user_exists(...)
if (not userExists):
Spreadsheet.add_user(...)
Spreadsheet.add_score(...)
await Hangman.bot.send_message(message.channel, msg)
elif (not sheetLoaded):
await Hangman.bot.send_message(message.channel, msg)
async def make_guess(message):
# perform guess
if (matched):
await Hangman.bot.send_message(message.channel, msg)
Hangman.GAMES.pop(message.server.id)
await Hangman.update_score(message)
When Hangman.update_score() is called it blocks it. so it won't process any commands until the score has been updated which means for about 5 or so seconds (not long but with lots of users spamming it it's an issue) the bot does not accept any other messages
What am i missing to be able to make the process run in the background while still accept new inputs?
Asyncio is still single-threaded. The only way for the event loop to run is for no other coroutine to be actively executing. Using yield from/await suspends the coroutine temporarily, giving the event loop a chance to work. So unless you call another coroutine using yield (from) or await or return, the process is blocked. You can add await asyncio.sleep(0) in between steps of Hangman.update_score to divide the process blocking in multiple parts, but that will only ensure less "hanging" time, not actually speed up your thread.
To make the process actually run in the background, you could try something along the lines of:
from concurrent.futures import ProcessPoolExecutor
executor = ProcessPoolExecutor(2)
asyncio.ensure_future(loop.run_in_executor(executor, Hangman.update_score(message)))

Categories

Resources