run a function concurrently with client.run() in discord - python

so i have a discord bot and also another function that i want to run, but i want them to run concurrently but i couldn't make them run without one of them blocking the other, here's an example code
ps: disnake is a fork of dicord.py so it works the same mostly
import disnake
import time
bot = commands.Bot()
async on_message(message):
if message.content == "hello bot":
await message.channel.send("hey user")
x=0
def counting():
while True:
x += 1
print(x)
time.sleep(1)
#here is the issue mostly
bot.run(token)
counting()
i want the bot to run normally, while the other function is running and counting "1" "2" on my terminal, but i can't get to make them both work

Disnake is an async-await library, which means that you should use non-blocking functions. Moreover, the fonctions you want to make concurrent should be async and contain await suspension points in order to let other concurrent routines an opportunity to execute.
The issue is that Bot.run() is a blocking call as the documentation mentions. Moreover your counting() function is not async and does not contain any await. Thus, the two functions cannot run concurrently. Also, you should not use time.sleep() with AsyncIO as it is a blocking operation. You should use asyncio.sleep().
the program could look like this:
import asyncio
import disnake
async on_message(message):
if message.content == "hello bot":
await message.channel.send("hey user")
async def run_bot(bot: Bot, token: str):
try:
await bot.start(token)
except KeyboardInterrupt:
await bot.close()
async def count():
count = 0
while True:
await asyncio.sleep(1.0)
count += 1
print(count)
async def main():
bot = commands.Bot()
token = "<user_token>"
await asyncio.gather(
count(),
run_bot(bot, token)
)
await bot.close()
if __name__ == "__main__":
asyncio.run(main())
Note: you did not seem to use on_message() in your code, which is probably an error.

Related

Discord cogs doesn't load

I am trying to run two different Discord Bots using a single python script using cogs. But when I try to run the 2nd bot it throws an ImportError even-though I didn't use that specific Library. The reaction roles bot works fine without the anti spam bot. Here's my code. FYI I am working inside a Virtual Env.
main.py
if __name__ == "__main__":
try:
reaction_role_bot = commands.Bot(command_prefix=config["reaction_role_bot"]["bot_prefix"], intents=discord.Intents.all())
reaction_slash = SlashCommand(reaction_role_bot, sync_commands=True)
reaction_role_bot.load_extension(f"cogs.{str(os.path.basename('cogs/reaction_roles.py')[:-3])}")
anti_spam_bot = commands.Bot(command_prefix=config["anti_spam_bot"]["bot_prefix"], intents=discord.Intents.default())
spam_slash = SlashCommand(anti_spam_bot, sync_commands=True)
anti_spam_bot.load_extension(f"cogs.{str(os.path.basename('cogs/anti_spam.py')[:-3])}")
event_loop = asyncio.get_event_loop()
event_loop.create_task(reaction_role_bot.run(config["reaction_role_bot"]["token"]))
event_loop.create_task(anti_spam_bot.run(config["anti_spam_bot"]["token"]))
event_loop.run_forever()
except Exception as e:
print(e)
anti_spam.py
import platform
import os
import discord
from discord.ext import commands
from antispam import AntiSpamHandler
from antispam.plugins import AntiSpamTracker, Options
class AntiSpamBot(commands.Cog):
def __init__(self, client):
self.client = client
# Initialize the AntiSpamHandler
self.client.handler = AntiSpamHandler(self.client, options=Options(no_punish=True))
# 3 Being how many 'punishment requests' before is_spamming returns True
self.client.tracker = AntiSpamTracker(self.client.handler, 3)
self.client.handler.register_extension(self.client.tracker)
#commands.Cog.listener()
async def on_ready(self):
print("---------------------------------")
print(f"Logged in as {str(self.client.user)}")
print(f"Discord.py API version: {discord.__version__}")
print(f"Python version: {platform.python_version()}")
print(f"Running on: {platform.system()} {platform.release()} ({os.name})")
await self.client.change_presence(status=discord.Status.idle, activity=discord.Game(name="Head of Security"))
print("---------------------------------\n")
# The code in this event is executed every time a valid commands catches an error
#commands.Cog.listener()
async def on_command_error(context, error):
raise error
#commands.Cog.listener()
async def on_message(self, message):
await self.client.handler.propagate(message)
if self.client.tracker.is_spamming(message):
await message.delete()
await message.channel.send(f"{message.author.mention} has been automatically kicked for spamming.")
await message.author.kick()
await self.client.process_commands(message)
def setup(client):
client.add_cog(AntiSpamBot(client))
Error
Extension 'cogs.anti_spam' raised an error: ImportError: cannot import name 'AsyncMock' from 'unittest.mock' (/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py)
I've no experiences using cogs and its bit confusing me. Any kind of help would help me to sort this out! Thanks in advance!
I do not believe this is a cog registration issue. I believe this is an import error from some of the dependencies in your cog file. I googled your error and found something similar, I recommend checking it out here for some more information.
As a blanket statement, I would double check that you have mock installed, and that you're installing it on the version of Python that you think you're installing it on. It can get wonky if you have multiple python versions insealled.
Also, on an unrelated note:: It is best to avoid running multiple bot instances in one python file, but I can help you do it the best way possible.
For starters, you have to realize 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 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()
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()

How to run async function as a thread in discord.py?

I have a counting functions that sends a number every second to a text channel (it's in a while-true loop), while this is happening I want the bot to do other things as well such as answering commands in a different channel but it is stuck in that while-true loop.
I've tried using a call-back function but that didn't let me send any messages..
This is my full code:
#bot.event
async def on_ready():
text_channel = bot.get_channel(xxxxxxxxxxx)
await count_in_server(text_channel)
#bot.command()
async def ping(ctx):
await ctx.send('pong')
#bot.event
async def count_in_server(chat):
count = 0
while True:
await chat.send(count)
await asyncio.sleep(1)
count += 1
My question more specfic is how do you run count_in_server() as a thread? (please provide code example)
EDIT:
Thanks to AKX I changed asyncio.sleep(1) to await asyncio.sleep(1) and now between each second it lets the other script run, but I'm looking for another way to do this.
await count_in_server(text_channel)
replace the line above with:
asyncio.create_task(count_in_server(text_channel))
And you should be ok.
Tasks are scheduled to run in parallel. "awaited" coroutines block the code where the "await" keyword is until the target resolves.

asyncio print something while waiting for user input

I have a simple script, I want the user to be able to input something whenever he wants, but I want to also print something out in the meantine, this is my code:
import asyncio
async def user_input():
while True:
content = input('> ')
async def print_something():
await asyncio.sleep(10)
print('something')
async def main():
tasks = [user_input(), print_something()]
await asyncio.gather(*tasks)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
It lets me input, but it doesn't print something, how can I achieve that?
input is a blocking function and cannot be used with inside coroutines straightforwardly. But you could start it in a separate thread by means of run_in_executor:
import asyncio
async def user_input():
while True:
loop = asyncio.get_event_loop()
content = await loop.run_in_executor(None, input, "> ")
print(content)
async def print_something():
await asyncio.sleep(5)
print('something')
async def main():
tasks = [user_input(), print_something()]
await asyncio.gather(*tasks)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
Update: Also you can use aioconsole lib, which provides asyncio-based console functions:
from aioconsole import ainput
async def user_input():
while True:
content = await ainput(">")
print(content)
The short answer is async cannot do what you are wanting. While you did declare your functions as async the python function input is not async, it is a blocking function. So it will block the event loop and nothing else will run.
I answered a question awhile back that kinda explains how async works in python. I'll link it. But in order to do what you want you need to use threads not async.
https://stackoverflow.com/a/63237798/9270488
If you want to use threads with async then look into ThreadExecutor

How to queue requests until the function returns a response for a previous request?

When the user sends !bot in a special channel, the code runs a function that runs for 20-30 seconds (depends on some_var). My problem is that if several people write !bot , the code will reverse this in multiple threads. How do I queue for these requests?
I tried to understand asyncio, discord.ext.tasks, but can't figure out how it works
#client.command()
async def test(ctx, *args):
data = args
if data:
answer = some_function(data[0]) #works from 5 seconds to 1 minute or more
await ctx.send(answer)
Everything works well, but I just don't want to load the system so much, I need a loop to process requests first in - first out
You can use an asyncio.Queue to queue tasks, then process them sequentially in a background loop:
import asyncio
from discord.ext import commands, tasks
queue = asyncio.Queue()
bot = commands.Bot('!')
#tasks.loop(seconds=1.0)
async def executor():
task = await queue.get()
await task
queue.task_done()
#executor.before_loop
async def before():
await bot.wait_until_ready()
#bot.command()
async def example(ctx, num: int):
await queue.put(helper(ctx, num))
async def helper(ctx, num):
await asyncio.sleep(num)
await ctx.send(num)
executor.start()
bot.run('token')
Make some_function() to async then await it. Then all test command will be processed simultaneously.
async some_function(...):
# something to do
#client.command()
async def test(ctx, *args):
if args:
answer = await some_function(data[0])
await ctx.send(answer)

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