Discord.py-Rewrite Cycle through statuses using background tasks - python

My Goal
Hello, I want my bot to cycle through 3 prefixes, The prefix for the bot, The amount of servers its in, and another message of my choosing. I use the #bot.command/#bot thing, so the example in the FaQ in the readthedocs website doesn't help me. What i tried didn't work, and i also do not know how to get the number of servers the bot is in.
What I tried
async def my_background_task(self):
await bot.change_presence(activity=discord.Game(name="message"))
await asyncio.sleep(7)
await bot.change_presence(activity=discord.Game(name="PREFIX: CF>"))
await asyncio.sleep(7)
#bot.event
async def on_ready(self):
print('Bot is online and ready.')
self.bg_task = self.loop.create_task(self.my_background_task())

This solution has been tested:
prefixes = [lambda: 'prefix1', lambda: 'prefix2', lambda: str(len(bot.guilds))]
bot = commands.Bot(command_prefix=prefixes[0]())
async def my_background_task():
prefix_num = 0
while True:
prefix = prefixes[prefix_num]()
await bot.change_presence(activity=discord.Game(name=prefix))
bot.command_prefix = prefix
# increase the current prefix - if it's reached the length of the prefix list, set it back to 0
prefix_num = (prefix_num + 1) % len(prefixes)
await asyncio.sleep(7)
#bot.event
async def on_ready():
bot.loop.create_task(my_background_task())
First of all, your background task and on_ready function shouldn't have any self in them. Also, your background task has to have a while loop inside of it, otherwise, it will only run once.
This code utilizes anonymous lambda functions because it allows the bot to be added to a different server and adjust the prefix when that happens. When it comes time to change the prefix, one of the functions is called, returning a string.
I would also recommend looking at the API reference because it is very helpful.

Related

SyntaxError: 'await' outside function | discord.py [duplicate]

This question already has an answer here:
SyntaxError (Python): 'await' outside async function
(1 answer)
Closed 1 year ago.
I know - there are a lot of questions with this title, but most are for Dark Sky or are different to my problem.
class Counter(discord.ui.View):
#discord.ui.button(label='0', style=discord.ButtonStyle.red)
async def counter(self, button: discord.ui.Button, interaction:
discord.Interaction):
number = int(button.label)
button.label = str(number + 1)
if number + 1 >= 5:
button.style = discord.ButtonStyle.green
await interaction.message.edit(view=self)
view = Counter()
await ctx.send('Press to increment', view=view)
Error message:
File "main.py", line 27
await ctx.send('Press to increment', view=view)
^
SyntaxError: 'await' outside function
I'm very new to python and am not sure why this is at all. Any help would be greatly appreciated!
you need to put the
view = Counter()
await ctx.send('Press to increment', view=view)
code inside a asynchron function, because the await keyword can only be used inside of asynchron functions.
example functions
async def my_function(): # this is an async function
...
def my_other_function(): # this is a normal function
...
Async functions have to be called with the await keyword, or else you'll get an exception
await my_function() # calling an async function
my_other_function() # calling a normal function
as you can see in this example, in the async function, you can use await
async def something():
print("hello async world")
async def my_function():
await something()
but if you try to await an async function inside a normal function
asnyc def something():
print("hello async world")
def my_other_function():
await something()
You will get the SyntaxError: 'await' outside function error
The best way to do this together with a discord bot is by adding the code to a listener. For example with the on_message event which will be fired whenever a message is incoming
Example for on_message event
import discord
client = discord.Client()
#client.event
async def on_message(message): # The event
if message.content == "your_text" # Insert a text here that is needed to use the "command"
# your code will be executed whenever the bot receives a message with the content "your_text"
view = Counter()
await message.channel.send('Press to increment', view=view)
client.run("your unique token here")
I would recommend to take a look at https://discordpy.readthedocs.io/en/stable/quickstart.html before starting
According to the ctx, your code is made for the client's #commands.command function.
(this should fix your code)
from discord.ext import commands
bot = commands.Bot(command_prefix="!")
class Counter(discord.ui.View):
#discord.ui.button(label='0', style=discord.ButtonStyle.red)
async def counter(self, button: discord.ui.Button, interaction:
discord.Interaction):
number = int(button.label)
button.label = str(number + 1)
if number + 1 >= 5:
button.style = discord.ButtonStyle.green
await interaction.message.edit(view=self)
#bot.command("test")
async def command(ctx):
view = Counter()
await ctx.send('Press to increment', view=view)
bot.run("your unique token here")
In order to use your code, you need to send a message in a channel where your bot can read messages with the content !test to invoke the command
The typical structure of your code will be (using sample code from the docs)
#bot.command()
async def foo(ctx, arg):
await ctx.send(arg)
That's also where the ctx you're using comes from--if you removed the await you'd get an error that ctx wasn't defined. Your code will specify functions that correspond to commands or other events on discord, and discord.py will handle calling the functions you specify when users perform those actions.
You can only use the await keyword in async functions. The idea behind these functions is that if your code has to do something that takes a long time, but mostly just involves waiting, python can go do other stuff while it waits. For discord.py, since a lot of your operations involve waiting for slow network operations like sending messages, that's very important.
here's one intro to the ideas behind async/await, I haven't read it too much, so if it's bad or anybody has better suggestions drop a link in the comments.

Discord.py Bot Takes too Long to Respond

Goal:
I'm developing a discord bot which scans a url every 5 seconds or so, checks for a specified change on that webpage, and will send a message in the discord channel if that change occurs. I've done this by sending the url to the bot using an if statement in on_message. The url is then passed to a tasks.loop() function, where it is scanned and processed in another function for the change.
Problem:
I'd like to be able to send a message in the discord channel which quickly ends the process taking place in the tasks.loop(), so that I can pass it a different url to scan using the on_message function. In its current form, it works-- just very slowly. From the time the cancel trigger is sent, it takes around 3 minutes to send the verification message that the process has been cancelled. I need to make this 5 seconds or less. For what its worth, the bot is kept running using replit and uptime robot, but I am sure that the long response time is not related to the frequency the repl is awoken by uptime robot.
Code:
My code is much more complex and riddled with obscurely named variables, so here is a much simpler snippet of code with the same general structure.
client = discord.Client()
channel = client.get_channel(CHANNEL_ID)
#tasks.loop()
async def myloop(website, dataframe):
channel = client.get_channel(CHANNEL_ID)
try:
# iteratively scrape data from a website for
# a predefined change in the dataframe
if change = True:
await channel.send(notification)
except:
pass
#client.event
async def on_message(message):
channel = client.get_channel(CHANNEL_ID)
msg = message.content
if msg.startswith('track'):
website = msg[6:]
await channel.send('Now tracking '+str(website))
myloop(website,df)
if msg.starswith('stop'):
myloop.cancel()
await channel.send('Done tracking, awaiting orders.')
Attempted Solutions:
I have tried using some forms of threading, which I am very new to, and I haven't found a way to make it work any faster. Any suggestions or solutions would be greatly appreciated! I've been combing the web for help for quite some time now.
Looks like you could use client.loop.create_task to create asyncio task objects, and their cancel method to immediately cancel those asyncio tasks at the right time, e.g.
import asyncio
from replit import db
_task = None
async def myloop():
website = db['website']
dataframe = db['dataframe']
channel = client.get_channel(CHANNEL_ID)
while not client.is_closed():
await asyncio.sleep(5)
try:
# iteratively scrape data from a website for
# a predefined change in the dataframe
if change:
await channel.send(notification)
except:
pass
#client.event
async def on_message(message):
global _task # This gives the function access to the variable that was already created above.
msg = message.content
if msg.startswith('track'):
website = msg[6:]
await message.channel.send('Now tracking '+str(website))
db['website'] = website
db['dataframe'] = df
if _task is not None:
_task.cancel()
_task = client.loop.create_task(myloop())
if msg.startswith('stop'):
if _task is not None:
_task.cancel()
_task = None
await message.channel.send('Done tracking, awaiting orders.')
The argument create_task takes is a coroutine that takes no arguments, so the website URL and dataframe need to be accessible to the function a different way (I'm not sure which way you would prefer or would be best; using replit's db is just an example).
With this approach, you should be able to use track again to change which website is being monitored without using stop in between.
More details in the docs:
discord.Client.loop
loop.create_task
Task.cancel
asyncio.sleep
discord.Client.is_closed
Replit db

how to run a command in tasks.loop discord.py bot

so im trying to get a command to run every 5 minutes on my dsicrod.py bot and need ctx to get guild members and certain details like that so I need it in a bot.command, but I cant have that properly do that without tasks.loop(minutes=5) so I tried getting it to send the command with tasks.loop but it wouldn't work so I went to the pythin discord and got help they got me to this point
#bot.command(pass_context=True)
async def update_member_count(ctx):
await ctx.send(ctx.guild.member_count)
channel = discord.utils.get(ctx.guild.channels, id=829355122122424330)
await channel.edit(name = f'Member Count: {ctx.guild.member_count}')
#tasks.loop(minutes=5)
async def update_member_count2(ctx):
await update_member_count(ctx)
and It still gives errors saying that ctx arg is missing in update_member_count2. pls help
You can create your loop-function in another way. Your approach is a bit unclear to me.
Try out the following:
async def update_member_count(ctx):
while True:
await ctx.send(ctx.guild.member_count)
channel = discord.utils.get(ctx.guild.channels, id=YourID)
await channel.edit(name=f'Member Count: {ctx.guild.member_count}')
await asyncio.sleep(TimeInSeconds)
#bot.command()
async def updatem(ctx):
bot.loop.create_task(update_member_count(ctx)) # Create loop/task
await ctx.send("Loop started, changed member count.") # Optional
(Or you simply create the task in your on_ready event, depends on you)
What did we do?
Created a loop out of a function (async def update_member_count(ctx):)
Exectued updatem one time to activate the loop
Changed your actual command to a "function"
You could go with #Dominik's answer but its near impossible to stop a running While loop. Going ahead with discord.py tasks.
If you want to start the task with a command:
async def start(ctx):
update_member_count2.start(ctx)
If you want to start it when the bot starts, you have to slightly modify the code
#tasks.loop(minutes=5)
async def update_members():
guild = bot.get_guild(guildID)
channel = guild.get_channel(channelID)
#update channel, don't sleep
# inside init or on_ready
if not update_members.is_running():
update_members.start()
Use this in a on_member_join event
#bot.event
async def on_member_join(member):
channel = discord.utils.get(member.guild.channels, id=962732335386746910)
await channel.edit(name = f'⚫┃・Members: {member.guild.member_count}')

Is there a way to kick members without using client = discord.Client() instead of a bot on Python

So, I'm new to programming and I have a discord bot, I used client = discord.Client() instead of using client = commands.Bot(...), and now i want to be able to kick users. But I can't see any command using my client to kick someone using their username.I konw it's simpler to do it with a bot but i have already such a big code that I don't want to redo it.
Help would be appreciated :)
If you change it to commands.Bot everything is going to work just fine (if you add await client.process_commands(message) at the end of the on_message event. But nevertheless here's how to make a kick 'command' using the on_message event:
#client.event
async def on_message(message):
guild = message.guild
if message.startswith('!kick'):
args = message.content.split()[1:]
member_id = int(args[0])
member = guild.get_member(member_id)
reason = ' '.join(args[1:])
await member.kick(reason=reason)
# If you actually change your mind and want to use `commands.Bot`
await client.process_commands(message)
# To invoke
# !kick 172399871237987 drama seeker
I strongly DO NOT recommend using this method at all, just compare how much better, easier and cleaner is with commands
#client.command()
#commands.has_permissions(kick_members=True)
async def kick(ctx, member: discord.Member, *, reason):
await member.kick(reason=reason)
# While invoking here you can actually mention the user
# !kick #some_user drama seeker
# But you also can use the ID
# !kick 761876328716327186 drama seeker
It's only 4 lines, 3 if we don't count the has_permissions decorator.

How to add a function to discord.py event loop?

I am using Python with discord.py. Documentation here
I've got a bot that is running on a Discord server that links the server with a subreddit. Users have various commands that do things like getting the top submissions, getting the latest submissions, and so on.
I want to add some features to the bot, with one of them being a keyword notifier. The bot should search the subreddit for keywords in the title, and then notify users if they are on the list for that keyword. I know how to do this, I've done it plenty of times, but I don't know how to do it with a Discord bot. I have no experience with asynchio or any kind of asynchronous programming.
The way I've tried to do it works, but it is very janky and definitely not good. At the top of the on message() function, I just add a call to the search_submissions() function, so that whenever someone puts sends a new message on the server, the bot will scan the Reddit submissions. The server is busy enough that this would work relatively okay, but I really want to do it the "proper" way.
I don't know how to call the search_submissions() function without putting it inside of on_message().
Edit for extra code:
import discord
TOKEN = "redacted"
client = discord.Client()
#client.event
async def reddit_search():
print("Searching")
#client.event
async def on_message(message):
if message.content.startswith("reddit!hot"):
# Get hot
# Do other things.
#client.event
async def on_ready():
print("Connected to Discord as {}.".format(client.user.name))
client.run(TOKEN)
You can add a function to the bot event loop with Client.loop.create_task(search_submissions()) like this:
async def search_submissions():
pass
client = discord.Client()
client.loop.create_task(search_submissions())
client.run(TOKEN)
Update:
If you want your function to continue working you can put it in a while loop with some sleeping in between:
async def search_submissions():
while(true):
# do your stuff
await asyncio.sleep(1)
The other answers here don't take into account discord.py's helpful tasks.loop decorator.
To make an event occur every 5 seconds, you would use
from discord.ext import tasks, commands
class MyCog(commands.Cog):
def __init__(self):
self.foo.start()
def cog_unload(self):
self.printer.cancel()
#tasks.loop(seconds=5.0)
async def foo(self):
print('bar')
More can be found here: https://discordpy.readthedocs.io/en/latest/ext/tasks/
You want your search_submissions() function to be async so other functions of your bot can still be invoked and your bot stays responsive. Define it to be def async and use aiohttp to send async HTTP requests to reddit -- what this does is send off the request, relinquish control to the event loop, and then take back control once the results have been transmitted back. If you use a standard HTTP library here instead then your whole bot will be blocked until the result comes back. This of course only makes sense if the task is mainly I/O-bound and less CPU-bound.
Then call search_submissions() in on_message(message) -- but call it asynchronously using result = await search_submissions(). This will resume execution of on_message once the result of search_submissions is ready.
If you truly want to do something else in the same context while waiting on search_submissions (which I think is unlikely), dispatch it as task = asyncio.create_task(search_submissions()). This will start the task immediately and allow you to do something else within the same function. Once you need the result you will have to result = await task.
async def search_submissions():
async with aiohttp.ClientSession() as session:
async with session.get(some_reddit_url) as response:
return await response.read()
#client.event
async def on_message(message):
if message.content.startswith("reddit!hot"):
result = await search_submissions()
await message.channel.send(result)

Categories