Python Twitchio unable to use command and event_message at same time - python

I'm trying to record twitch chat messages while still recognizing commands, however, recording the messages using event_message seems to stop the command from working.
Anybody know how you would recognize/respond to commands and record message at the same time?
from twitchio.ext import commands
import time
from datetime import datetime
class Bot(commands.Bot):
def __init__(self):
super().__init__(
token='oauth:',
prefix='!',
initial_channels=['channelname']
)
# This function isn't running; event_message is stealing the input!
#commands.command(name='ping')
async def my_command(self, ctx):
print('Caught ping')
await ctx.send('pong')
# records the messages (or in this case, the time a message is sent)
async def event_message(self, message):
print(time.mktime(datetime.now().timetuple())) # prints the unix second
if __name__ == '__main__':
bot = Bot()
bot.run()
Reducing this down even more, the same issue occurs here; with the bot responding to the command, but not printing the message author or content:
from twitchio.ext import commands
import os
bot = commands.Bot(
token='', prefix="!", initial_channels=[""], client_id='', nick='', client_secret='')
#This isn't working; Should still grab the message author/name and print.
Ive tried twitchio versions between 1.3.0 and 2.3.0, but the issue persists.
#bot.event
async def event_message(ctx):
print(ctx.author.name)
print(ctx.content)
await bot.handle_commands(ctx)
#This is running
#bot.command(name='test')
async def test_command(ctx):
await ctx.send('this is a test response')
if __name__ == '__main__':
bot.run()

Issue was not including an "await bot.handle_commands(ctx)" command below the event_message function.
Attached below is a working code
from twitchio.ext import commands
class Bot(commands.Bot):
def __init__(self):
super().__init__(token='', prefix="!", initial_channels=[""])
#commands.command()
async def ping(self, ctx):
print('Caught Ping')
await ctx.send(f'Pong!')
async def event_message(self, message):
print(message.author.name, message.content)
await bot.handle_commands(message) # <--This is the new line
bot = Bot()
bot.run()
NOTE: An error is raised by the event_message function when the bot sees one of its' own messages in chat. This doesn't seem to inhibit function, but it can be fixed by changing the code to the following:
async def event_message(self, message):
if hasattr(message.author, 'name'): # <-- New Line
print(message.author.name, message.content)
await bot.handle_commands(message)

Related

Runtime error not showing when using asyncio to run discord bot

I'm developing discord bot with discord.py==2.1.0.
I use cog to write the main function that I wanna use, but I found when the whole bot is wrapped in async function and called by asyncio.run(), my terminal won't show any error message when there is any runtime error in my cog script.
Here is the example application. I stored my bot token in environment variable.
bot.py
import os
import discord
from discord.ext import commands
import asyncio
token = os.environ["BOT_TOKEN"]
class Bot(commands.Bot):
def __init__(self):
intents = discord.Intents.default()
intents.members = True
intents.message_content = True
description = "bot example."
super().__init__(
command_prefix=commands.when_mentioned_or('!'),
intents=intents,
description=description
)
async def on_ready(self):
print(f'Logged in as {self.user} (ID: {self.user.id})')
print('------')
bot = Bot()
async def load_extensions():
for f in os.listdir("./cogs"):
if f.endswith(".py"):
await bot.load_extension("cogs." + f[:-3])
async def main():
async with bot:
await load_extensions()
await bot.start(token)
asyncio.run(main())
./cogs/my_cog.py
from discord.ext import commands
class Test(commands.Cog):
def __init__(self, client):
self.client = client
#commands.Cog.listener()
async def on_ready(self):
print("Ready")
#commands.command()
async def command(self, ctx):
test_error_message() # An runtime error example
print("Command")
async def setup(client):
await client.add_cog(Test(client))
Command that I run in terminal to start the bot.
python bot.py
When I type !command in the discord channel, there is no error message showing in the terminal, but there is no "Command" printed out so I'm sure the code stopped at the line I called test_error_message()
I expected that it should show error message normally, but I cannot find useful reference to make it work :(
There is one reason I need to use asyncio, I have a task loop to run in the bot, like the code below.
from discord.ext import tasks
#tasks.loop(seconds=10)
async def avatar_update():
# code here
async def main():
async with bot:
avatar_update.start()
await load_extensions()
await bot.start(token)
I would happy to know if there are some great practice to handle error in this situation.
Thanks!
Client.start() doesn't configure logging, so if you want to use that then you have to do it yourself (there's setup_logging to add a basic config). run() configures logging for you.
For more info, read the docs. https://discordpy.readthedocs.io/en/stable/logging.html?highlight=logging

How to make a Discord Bot that can accept arguments from Python of what message to send to who and be able to close itself?

I'm trying to do something pretty simple. I just want Python to be able to call a quick function that sends a direct message over Discord. Basically a modified version of the FAQ example. I want another Python class to start the Discord client and pass the message string and user id. And I want the Discord client to simply send the string and then close and delete the entire class. After looking through docs I made these modifications:
import discord
class MyClient(discord.Client):
async def on_ready(self):
print('Logged on as {0}!'.format(self.user))
user = await self.fetch_user(self.destination_user)
await user.send(self.message)
await self.close()
def __init__(self, user_id, user_message):
self.destination_user = user_id
self.message = user_message
client = MyClient(desired_id, desired_message)
client.run('My Client ID')
#wait for client to end and then continue
However, I'm running into 2 problems. It looks like discord.Client doesn't allow an __init__() function, and when I try to do async def __init__() there's also an error. Are there other ways to pass arguments to it through Python, rather than react to messages in Discord? Also, self.close() results in "RuntimeError: Cannot close a running event loop". What's the proper way to wait for on_ready() to finish and then close it?
I can get it working by hardcoding the user ID and message, eliminating __init__() and simply not closing the class. However, for some reason, even then self.get_user() doesn't work, only self.fetch_user(). The docs say to use get_user(), so I'd prefer that, but I don't know what I'm doing wrong. My bot is in the same server as the target user, and I've given it both privileged intents, but get_user() continues to return "None", whereas fetch_user() properly messages the user, with the same exact arguments.
I found out what seems the proper way to run discord.Client in this fashion:
import discord
import nest_asyncio
import asyncio
nest_asyncio.apply()
class DiscordClient(discord.Client):
async def on_ready(self):
user = await self.fetch_user(self.send_user)
await user.send(self.message)
async def on_message(self, message):
#Pause so there's no warning about operations not finishing when class closes
await asyncio.sleep(0.1)
await DiscordClient.close(self)
async def parameters(self, user, message):
self.send_user = user
self.message = message
async def send_discord(user, message):
print("Sending Discord message")
client = DiscordClient()
await client.parameters(user, message)
await client.start('Private Key')
print("Discord message sent")
asyncio.run(send_discord(user_id, "Test message contents"))
on_message runs even it's the bot that sent the message, so it will quickly close itself. I had to add the wait or there were some race conditions or something causing a warning every time it closed.

Use listener event to call function within the same cog using discord.py

I'm trying to use a listener event to activate a function I have in a cog using discord.py. This same code functions properly in the main bot.py file (removing self and using #client.event instead of a listener) but when used in the cog, it tells me that the repeater() function is an undefined variable when being used in the listener event
import discord
from discord.ext import commands, tasks
class Repeater(commands.Cog):
def __init__(self, client):
self.client = client
#tasks.loop(seconds = 10)
async def repeater(self):
channel = self.client.get_channel(834554679265329172)
await channel.send('test')
#commands.Cog.listener()
async def on_ready(self):
repeater.start()
def setup(client):
client.add_cog(Repeater(client))
EDIT:
So I changed the code match this after a comment recommended it, and the console throws this error
#commands.Cog.listener()
async def on_ready(self):
self.repeater()
> discord.ext.commands.errors.ExtensionFailed: Extension 'cogs.repeater'
> raised an error: TypeError: Cog.listener expected str but received
> 'function' instead.
EDIT 2:
Changed the code to match this, and it will run the loop exactly one time, but doesn't actually loop
#commands.Cog.listener()
async def on_ready(self):
await self.repeater()
In class you have to use self. to access its method self.repeater and self.repeater.start()
I tested it on this code and it works for me.
import discord
from discord.ext import commands, tasks
import os
import datetime
TOKEN = os.getenv('DISCORD_TOKEN')
CHANNEL = 834554679265329172
class Repeater(commands.Cog):
def __init__(self, client):
self.client = client
#tasks.loop(seconds=10)
async def repeater(self):
#channel = self.client.get_channel(CHANNEL)
await self.channel.send(datetime.datetime.now().strftime("It's %H:%M.%S"))
#commands.Cog.listener()
async def on_ready(self):
self.channel = self.client.get_channel(CHANNEL)
print('starting repeater')
self.repeater.start()
def setup(client):
client.add_cog(Repeater(client))
# --- main ---
client = commands.Bot(command_prefix='!')
setup(client)
print('starting bot')
client.run(TOKEN)
repeater is a function of the class. So you can call it with self.repeater. And to start a discord task use start attribute. self.repeater.start() is the answer.

Allow bot commands (only for testing)

I would like to create a simple smoke test for my application using pytest. It would be really simple for me, that's why I tried. The problem is that bots does not reacts for other bot's message as I noticed. I looked at the code (bot.py) and modified the process_commands just for the smoke test. Unfortunately it's still works only for human messages during testing.
Edited code, a complete test (only HASH and channel id are imported)
from discord.ext import commands
import pytest
import threading
import time
from build_config import HASH, channel_id
bot = commands.Bot(command_prefix = '.', description="This is a test bot for stackoverflow!")
class Test_Message(commands.Cog):
#commands.command(pass_context = True)
async def message(self, ctx):
await ctx.send("text_message")
def run_bot():
bot.add_cog(Test_Message())
bot.run(HASH.DEBUG)
#pytest.fixture
def create_bot():
t1 = threading.Thread(target=run_bot, args=[])
t1.start()
class Test_Events:
successful = False
#staticmethod
#bot.event
async def on_ready():
channel = bot.get_channel(channel_id)
await channel.send('.message')
#staticmethod
#bot.event
async def on_message(message):
await bot.process_commands(message)
if (message.channel.name == "general" and message.clean_content == "text_message"):
Test_Events.successful = True
#staticmethod
#bot.event
async def process_commands(message):
ctx = await bot.get_context(message)
await bot.invoke(ctx)
#pytest.mark.asyncio
async def test_Smoke(create_bot):
must_end = time.time() + 60
while time.time() < must_end:
if Test_Events.successful:
break
time.sleep(1)
assert Test_Events.successful
Basically someone marked this one as a solution in Allow Discord Rewrite bot to respond to other bots, but it doesn’t work for me.
Edit: So I debug the discord.py, unfortunately there is at least another check in get_context self._skip_check(message.author.id, self.user.id).
So it turned out that the creators of discord.py created an overwritable process_commands function. That's why you can remove the
if message.author.bot:
return
part. Unfortunately this is not enough for anything, there is such a part deep in the code which reject the bot commands:
if self._skip_check(message.author.id, self.user.id):
return ctx
The solution is to take this check out:
bot._skip_check = lambda x, y: False
I only use this workaround for testing, it is forbidden to use anything like this one on a normal server.

How to toggle trigger Cog listener with command in discord

I am currently trying to make a discord bot where when you type the command !purge in the desired channel, it will constantly delete all the messages sent, and when you type it again, it will stop deleting all the messages.
From learning online I know I have to use a Cog listener, but I don't exactly know how to "trigger" it with a command. I would need the #commands.Cog.listener() paragraph to use the on_message listener, but I also can't figure out how I would get it to delete messages in the channel where the command !purge was executed.
I tried already using a boolean value to toggle on and off when the command was typed and it would use a while loop to constantly delete messages, but then the while loop would stop. It might have been because the session of the command expired, not sure about that though.
Any thoughts on how I can use get this working? More specifically, how I can somehow link the Cog to the command? Thanks!
(Edits of my code I tried to use to be here but I have deleted them because of irrelevancy)
Cog Thread I found: https://stackoverflow.com/a/53528504/11805086
You'd have to create a trigger command, which enables or disables the purge mode and then, in your on_message function, you'd have to check if the purge mode is enabled or not.
Here's how to do it (within a cog):
Edited by poster
In cogs.py
from discord.ext import commands
class Purge_Cog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.channels = {}
#commands.command()
async def purge(self, ctx):
try:
if self.channels[ctx.channel]:
self.channels[ctx.channel] = False
await ctx.channel.send("Purge mode disabled!")
else:
self.channels[ctx.channel] = True
await ctx.channel.send("Purge mode enabled!")
except:
self.channels[ctx.channel] = True
await ctx.channel.send("Purge mode enabled!")
#commands.Cog.listener()
async def on_message(self, message):
try:
if self.channels[message.channel]:
await message.channel.purge(limit=999)
except:
pass
def setup(bot):
bot.add_cog(Purge_Cog(bot))
In your main file add this (bot.py or main.py)
import discord
from discord.ext import commands
initial_extensions = ['cogs.cogs']
if __name__ == '__main__':
for extension in initial_extensions:
bot.load_extension(extension)
bot.run('Your token')

Categories