How to call async code from sync code in another thread? - python-3.x

I'm making a Discord bot which send PM when it receive a Github hook.
It use Discord.py and BottlePy, the last one run in a dedicated thread.
Because both frameworks have a blocking main loop.
In BottlePy callback, I call some Discord.py async code.
I wasn't knowing what is Python async, this appear to be complicated when mixed with synchronous code...
Here's the full source code :
import discord
import bottle
import threading
import asyncio
client = discord.Client()
server = bottle.Bottle()
async def dm_on_github_async(userid,request):
print("Fire Discord dm to "+str(userid))
global client
user = client.get_user(userid)
if (user==None):
abort(500, "User lookup failed");
dm_channel = user.dm_channel
if (dm_channel==None):
dm_channel = await user.create_dm()
if (dm_channel==None):
abort(500, "Fail to create DM channel");
print("DM channel is "+str(asyncio.wait(dm_channel.id)))
await dm_channel.send("There's a Github shot !")
await dm_channel.send(str(request.body))
return
#server.post("/dm_on_github/<userid:int>")
def dm_on_github(userid):
return asyncio.run(dm_on_github_async(userid,bottle.request))
#client.event
async def on_ready():
print('We have logged in as {0.user} '.format(client))
##client.event
#async def on_message(message):
# if message.author == client.user:
# return
#
# if message.content.startswith('$hello'):
# await message.channel.send('Hello!')
# # This sample was working very well
class HTTPThread(threading.Thread):
def run(self):
global server
server.run(port=8080)
server_thread = HTTPThread()
print("Starting HTTP server")
server_thread.start()
print("Starting Discord client")
client.run('super secret key')
print("Client terminated")
server.close()
print("Asked server to terminate")
server_thread.join()
print("Server thread successful join")
I want that my Python bot send the body of the HTTP request as PM.
I get a RuntimeError: Timeout context manager should be used inside a task at return asyncio.run(dm_on_github_async(userid,bottle.request)).
I think I'm not doing this mix in the right way...

After a night, I found the way.
To call async code from sync code in another thread, we ask the loop (here this one from Discord.py) to run the callback with asyncio.run_coroutine_threadsafe(), this return a Task() and we wait for his result with result().
The callback will be run in the loop thread, in my case I need to copy() the Bottle request.
Here's a working program (as long you don't mind to stop it...) :
import discord
import bottle
import threading
import asyncio
client = discord.Client()
server = bottle.Bottle()
class HTTPThread(threading.Thread):
def run(self):
global server
server.run(port=8080)
async def dm_on_github_async(userid,request):
user = client.get_user(userid)
if (user==None):
abort(500, "User lookup failed");
dm_channel = user.dm_channel
if (dm_channel==None):
dm_channel = await user.create_dm()
if (dm_channel==None):
abort(500, "Fail to create DM channel");
# Handle the request
event = request.get_header("X-GitHub-Event")
await dm_channel.send("Got event "+str(event))
#await dm_channel.send(str(request.body)) # Doesn't work well...
return
#server.post("/dm_on_github/<userid:int>")
def dm_on_github(userid):
request = bottle.request
asyncio.run_coroutine_threadsafe(dm_on_github_async(userid,request.copy()),client.loop).result()
#client.event
async def on_ready():
print('We have logged in as {0.user} '.format(client))
# Wait for the old HTTP server
if hasattr(client,"server_thread"):
server.close()
client.server_thread.join()
client.server_thread = HTTPThread()
client.server_thread.start()
##client.event
#async def on_message(message):
# if message.author == client.user:
# return
#
# if message.content.startswith('$hello'):
# await message.channel.send('Hello!')
print("Starting Discord client")
client.run('super secret key')
print("Client terminated")
server.close()
print("Asked server to terminate")
server_thread.join()
print("Server thread successful join")

Related

Discord.py not responding to #client.event commands

I am not getting a "ping" response with this code. It was working before, but I am not sure what changed. There are no errors on my end, just no response.
Any feedback is appreciated.
import os
import random
import discord
from dotenv import load_dotenv
from discord.ext import commands
load_dotenv()
PREFIX = os.getenv("PREFIX")
TOKEN = os.getenv("TOKEN")
intents = discord.Intents().all()
bot = commands.Bot(command_prefix=PREFIX, intents=intents)
#bot.event
async def on_message(message):
if message.author == bot.user: # tells the bot not to respond to itself
return
#bot.event # ping-with-latency
async def on_message(message):
if message.content.startswith(PREFIX + 'ping'):
await message.channel.send(f'pong! {bot.latency}ms')
#bot.event
async def on_ready(): # display if online/ready
print("Bot is ready and logged in as {0.user}!".format(bot))
# run bot on server
bot.run(TOKEN)
I have checked all permissions and privileged gateway intents. I know I could be using client.command, but that also doesnt work.
You're defining two different callbacks for these events - this is probably the problem. Just put the author check in the main on_message.
#bot.event
async def on_message(message):
if message.author == bot.user: # tells the bot not to respond to itself
return
if message.content.startswith(PREFIX + 'ping'):
await message.channel.send(f'pong! {bot.latency}ms')

Python Discord Bot - Keep real time message parser from blocking async

I am writing a Discord Bot to take messages from a live chat and relay them to Discord channel, but want it to have other responsive features. Currently the script relays messages by entering a while loop which runs until the right message is recieved.
def chat_parser():
resp = sock.recv(4096).decode('utf-8')
#print(' - ', end='')
filtered_response = filter_fighter_announce(resp)
if resp.startswith('PING'):
# sock.send("PONG :tmi.twitch.tv\n".encode('utf-8'))
print("Ponging iirc server")
sock.send("PONG\n".encode('utf-8'))
return ''
elif (len(filtered_response) > 0):
if (filtered_response.count('ets are OPEN for') > 0):
filtered_response = get_fighters(filtered_response)
return filtered_response
return ''
fight = fight_tracker('') #initialize first fight object
def message_loop():
global first_loop_global
while True:
chat_reception = chat_parser()
if (chat_reception == ''):
continue
fight.set_variables(chat_reception)
return fight.announcement
return ''
The issue with this is that responsive functions for Discord are stuck waiting for this loop to finish. Here is the other code for reference.
#client.event
async def on_ready():
print('Finding channel...')
for guild in client.guilds:
if guild.name == GUILD:
break
channel = guild.get_channel(salty_bet_chat_id)
print('Connected to Channel.')
try:
print('Beginning main loop.')
while True:
message_out = await message_loop()
if (message_out != None and message_out != None):
print('Sending to Discord: ', message_out)
msg = await channel.send(message_out)
await msg.add_reaction(fight.fighter_1[1])
await msg.add_reaction(fight.fighter_2[1])
print('message sent...')
except KeyboardInterrupt:
print('KeyboardInterrupt')
sock.close()
exit()
#client.event
async def on_raw_reaction_add(reaction):
print(reaction)
#client.event
async def on_message(message):
print(message.author)
print(client.user)
client.run(TOKEN)
I have tried making async functions out of chat_parser() and message_loo() and awaiting their return where they are called, but the code is still blocking for the loop. I am new to both async and coding with Discord's library, so I am not sure how to make an async loop function when the only way to start the Discord client is by client.run(TOKEN), which I could not figure out how to incorporate into another event loop.

How to loop a task in discord.py

I am experimenting with making my own little discord bot that can get information from Twitch, but I'm stumped on how to make the bot loop and check for a condition.
I want the bot to loop a section of code every few seconds that checks if the specified twitch channel is live.
Code
import discord
from discord.ext import commands, tasks
from twitch import TwitchClient
from pprint import pformat
client = TwitchClient(client_id='<twitch token>')
bot = commands.Bot(command_prefix='$')
#bot.event
async def on_ready():
print('We have logged in as {0.user}'.format(bot))
#bot.command()
async def info(ctx, username):
response = await ctx.send("Querying twitch database...")
try:
users = client.users.translate_usernames_to_ids(username)
for user in users:
print(user.id)
userid = user.id
twitchinfo = client.users.get_by_id(userid)
status = client.streams.get_stream_by_user(userid)
if status == None:
print("Not live")
livestat = twitchinfo.display_name + "is not live"
else:
livestat = twitchinfo.display_name + " is " + status.stream_type
responsemsg = pformat(twitchinfo) + "\n" + livestat
await response.edit(content=responsemsg)
except:
await response.edit(content="Invalid username")
bot.run("<discord token>")
I want the bot to run the following code every 10 seconds, for example:
status = client.streams.get_stream_by_user(<channel id>)
if status == None:
print("Not live")
livestat = twitchinfo.display_name + "is not live"
else:
livestat = twitchinfo.display_name + " is " + status.stream_type
I've tried using #tasks.loop(seconds=10) to try and make a custom async def repeat every 10 seconds but it didn't seem to work.
Any ideas?
The newer version of discord.py doesn't support client.command()
To achieve the same I used the following snippet
import discord
from discord.ext import tasks
client = discord.Client()
#tasks.loop(seconds = 10) # repeat after every 10 seconds
async def myLoop():
# work
myLoop.start()
client.run('<your token>')
This can be done like so:
async def my_task(ctx, username):
while True:
# do something
await asyncio.sleep(10)
#client.command()
async def info(ctx, username):
client.loop.create_task(my_task(ctx, username))
References:
asyncio.create_task()
asyncio.sleep()
This is the most proper way to implement background tasks.
from discord.ext import commands, tasks
bot = commands.Bot(...)
#bot.listen()
async def on_ready():
task_loop.start() # important to start the loop
#tasks.loop(seconds=10)
async def task_loop():
... # this code will be executed every 10 seconds after the bot is ready
Check this for more info
I struggled with this as well. The problem I ran into is that none of the examples online were complete. Here is one I came up with that uses #tasks.loop(seconds=10).
import discord
import os
from discord.ext import tasks
from dotenv import load_dotenv
intents = discord.Intents.all()
client = discord.Client(command_prefix="!", intents=intents)
load_dotenv()
token = os.getenv("DISCORD_TOKEN")
CHANNEL_ID = 1234
#client.event
async def on_ready():
print(f"We have logged in as {client.user}")
myloop.start()
#client.event
async def on_message(message):
if message.author == client.user:
return
if message.content.startswith("hi"):
await message.channel.send("Hello!")
#tasks.loop(seconds=10)
async def myloop():
channel = client.get_channel(CHANNEL_ID)
await channel.send("Message")
client.run(token)

Keeeping the loop going until input (discord.py)

I'm running a discord.py bot and I want to be able to send messages through the IDLE console. How can I do this without stopping the bot's other actions? I've checked out asyncio and found no way through.
I'm looking for something like this:
async def some_command():
#actions
if input is given to the console:
#another action
I've already tried pygame with no results but I can also try any other suggestions with pygame.
You can use aioconsole. You can then create a background task that asynchronously waits for input from console.
Example for async version:
from discord.ext import commands
import aioconsole
client = commands.Bot(command_prefix='!')
#client.command()
async def ping():
await client.say('Pong')
async def background_task():
await client.wait_until_ready()
channel = client.get_channel('123456') # channel ID to send goes here
while not client.is_closed:
console_input = await aioconsole.ainput("Input to send to channel: ")
await client.send_message(channel, console_input)
client.loop.create_task(background_task())
client.run('token')
Example for rewrite version:
from discord.ext import commands
import aioconsole
client = commands.Bot(command_prefix='!')
#client.command()
async def ping(ctx):
await ctx.send('Pong')
async def background_task():
await client.wait_until_ready()
channel = client.get_channel(123456) # channel ID to send goes here
while not client.is_closed():
console_input = await aioconsole.ainput("Input to send to channel: ")
await channel.send(console_input)
client.loop.create_task(background_task())
client.run('token')

Timeout WebSocket connections in aiohttp

My WebSocket server implementation is open to the world, but the client is required to send an authenticate message after the connection was established or the server should close the connection.
How can I implement this in aiohttp? It seems, I need to do the following things:
Create an on_open method for every socket connection: I can't find a way (similarly to on_open in Tornado) to create such event.
Create a timer: asyncio's sleep or call_back methods of the main event loop may be used. But I can't find a way to send the WebSocketResponse to the callback function:
await asyncio.sleep(10, timer, loop=request.app.loop)
Closing the connection if not authenticated
This is what I had before with Tornado:
def open(self, *args, **kwargs):
self.timeout = ioloop.IOLoop.instance().add_timeout(
datetime.timedelta(seconds=60),
self._close_on_timeout
)
def remove_timeout_timer(self):
ioloop.IOLoop.instance().remove_timeout(self.timeout)
self.timeout = None
def on_message(self, message):
if message = 'AUTHENTICATE':
self.authenticated = True
self.remove_timeout_timer
def _close_on_timeout(self):
if not self.authenticated:
if self.ws_connection:
self.close()
Here is what I have using aiohttp for setting up a timer:
async def ensure_client_logged(ws):
await asyncio.sleep(3) # wait 3 seconds
await ws.send_str('hello')
async def ws_handler(request):
ws = web.WebSocketResponse()
asyncio.ensure_future(ensure_client_logged(ws), loop=request.app.loop)
But the code is running in a blocking way, meaning the server becomes unresponsive while it is sleeping.
Can someone please point me in the right direction?
You need to establish a deadline for the authentication procedure. asyncio.wait_for is a convenient way to do that:
async def ws_handler(request):
loop = asyncio.get_event_loop()
ws = web.WebSocketResponse()
loop.create_task(handle_client(ws))
async def handle_client(ws):
try:
authenticated = await asyncio.wait_for(_authenticate(ws), 10)
except asyncio.TimeoutError:
authenticated = False
if not authenticated:
ws.close()
return
# continue talking to the client
async def _authenticate(ws):
# implement authentication here, without worrying about
# timeout - the coroutine will be automatically canceled
# once the timeout elapses
...
return True # if successfully authenticated
This is a full working example for the benefits of the future users:
from aiohttp import web
import asyncio
async def wait_for_authentication(ws, app):
async for msg in ws:
if msg.type == web.WSMsgType.TEXT and msg.data == 'AUTHENTICATE': # Implement your own authentication
await ws.send_str('WELCOME')
return True
else:
await ws.send_str('NOT AUTHENTICATED')
async def authenticate(ws, app) -> bool:
try:
authenticated = await asyncio.wait_for(wait_for_authentication(ws, app), 5)
except asyncio.TimeoutError:
authenticated = False
if not authenticated:
await ws.send_str('The AUTHENTICATE command was not received. Closing the connection...')
await ws.close()
return False
async def ws_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
await request.app.loop.create_task(authenticate(ws, request.app))
async for msg in ws:
if msg.type != web.WSMsgType.TEXT:
continue
await ws.send_str(msg.data)
def init():
app = web.Application()
app.router.add_get('/', ws_handler)
return app
web.run_app(init())

Resources