Running Discord.py client & Uvicorn app simultaneously - python

I've been trying to implement a Discord.py bot with a basic FastAPI application.
The API runs with a uvicorn.Server class from uvicorn#742, with a run_in_thread() function, so both processes can't really hold each other.
Unfortunately, when I run both apps together.. it seems that the Discord client is preventing the API from receiving web requests, I believe that's because the bot client uses a bunch of network frameworks. I've tried multiple approaches in solving this issue, but just can't seem to find a right solution.
The root directory contains 2 files.. api.py just for the FastAPI app, and main.py for everything else (client, classes, etc). But I will try to just provide the important parts.
# main.py
from discord.ext.commands import Bot
import uvicorn, contextlib, time
client = Bot() # just a basic discord.ext.commands.Bot class, nothing fancy here.
#client.event
async def on_ready(self):
print(f"Bot online, logged in as {client.user}")
# https://github.com/encode/uvicorn/issues/742#issuecomment-674411676
class Webhook(uvicorn.Server):
def install_signal_handlers(self): pass
#contextlib .contextmanager
def run_in_thread(self):
thread = Thread(target = self.run)
thread.start()
try:
while not self.started:
time.sleep(1e-3)
yield
finally:
self.should_exit = True
thread.join()
if __name__ == "__main__":
api_server = Webhook(
config = uvicorn.Config("api:app", # you can guess what api.py contains already.
host = "127.0.0.1",
port = 8000,
log_level = "info",
loop = "asyncio",
workers = 3)
)
with api_server.run_in_thread():
client.run(TOKEN)
I've played with this a lot, either the bot or the API works in each method. My best guess is that it has to do with event loops, uvicorn.Server loop seems to be hard to handle, but I'm only intermediate with that.

Related

Python flask socketio with discord.py

I am trying to have a server that will be able to give us the status on discord.
I did achieve this by having an extra socketio client that run discord.py aswell.
I would like discord to run on the server instead of on a client. I am unable to make this work at the same time, any insight or help would be appreciated.
I tried launching discord before socketio, also tried launching discord in a different thread.
--Update--
The orginal post had a emit onConenct. this is apparently a known issue and will cause a namespace error. . please find trhe edited code below
as a minimal reproductable example here is the code:
server:
import asyncio
from time import sleep
from flask import Flask
from aiohttp import web
import socketio
from discord.ext import commands
sio = socketio.AsyncServer(async_mode='aiohttp',logger=True)
app = web.Application()
sio.attach(app)
bot = commands.Bot(command_prefix='+')
#bot.command(help="testing")
async def test(ctx):
await ctx.send("Hello world!")
await sio.emit('marco')
#sio.on('polo')
async def message(sid):
print("a wild client responded")
async def start_discord():
print('starting discord')
await bot.start('token')
if __name__ == '__main__':
asyncio.run(asyncio.gather(web.run_app(app),start_discord()))
client :
import asyncio
from click import prompt
import socketio
from discord.ext import commands
sio = socketio.Client(logger=True)
#sio.on("marco")
def polo():
sio.emit('polo')
print("wow i found a server")
if __name__ == "__main__":
sio.connect("http://localhost:8080")
sio.wait()
Ther server and client are connecting but not discord.
Your issue is that the socket server only starts when the discord bot exists. This will never happen.
If you want to run 2 functions concurrently use asyncio.gather in combination with client.start
Like this:
if __name__ == '__main__':
asyncio.run(
asyncio.gather(
bot.start('SuperSecret Token')
sio.start(app, host="localhost", port=8080)
)
)
This will ensure they start the the same time.
Note your code to create a custom event loop is probably unnecessary.
Having multiple calls to asyncio.run in your code is a mistake 99% of the time. You normally need only one async context.

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()

How to run multiple Discord clients simultaneously

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.

Python Flask with Telethon

I want to use Telethon Telegram API from my Flask Web App. But when I am running it, I am getting following error:
RuntimeError: There is no current event loop in thread 'Thread-1'.
I think there is some issues with asyncio. But I am not sure about that.
Here is my code
#!/usr/bin/python3
from flask import Flask
from telethon import TelegramClient
from telethon import sync
app = Flask(__name__)
#app.route('/')
def index():
api_id = XXXXXX
api_hash = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
client = TelegramClient('XXXXXX', api_id, api_hash)
client.start()
return 'Index Page'
if __name__ == '__main__':
app.run()
Here's what I learned after trying this out. First, Make sure you know what asyncio is, it's really super easy. Then You can work on it with more productivity.
Telethon uses asyncio which means that when you call blocking methods you have to wait until the coroutine finishes.
client.loop ###Doesn't work inside flask, it might have to do with threads.
You can easily import asyncio and use the main loop. like this.
import asyncio
loop = asyncio.get_event_loop()
Now you're ready to wait for coroutines to finish.
Create a new async function and add await to the blocking methods.
Execute the code using the main event loop.
Here's a code sample.
async def getYou():
return await client.get_me()
#app.route("/getMe", methods=['GET'])
def getMe():
return {"MyTelegramAccount": loop.run_until_complete(getYou())}
And one more thing. don't use telethon.sync, it's not fully translated to sync, it uses the above pattern it awaits all of the methods.
Basically, it's due to Python's GIL. If you don't want to dig into asyncio internals, just pip3 install telethon-sync and you're good to go.
In your place I would consider using Quart it's suggested on Telethon own's documentation and it's much easier.

Running Flask & a Discord bot in the same application

I am building a Discord bot in Python and would like to receive HTTP requests from Twitch.tv's API (See Webhooks Guide & Webhooks Reference) (To subscribe to events like; X streamer has gone live) and based on the content of the HTTP (POST or GET) request received from Twitch, do something on the Discord bot, e.g: Output a message on a text channel.
I am using the discord.py Python Discord API/Library.
I've looked into the matter and found that Flask seemed like a good minimalist choice for a webserver to receive these requests on.
I should preface this by saying I'm very new to Python and I've never used Flask before.
Now. The problem is I can't seem to figure out a way to run the Flask server inside of my discord bot.
I've tried adding this simple code into my discord.py script:
from flask import Flask, request
app = Flask(__name__)
#app.route('/posts', methods=['POST'])
def result():
print(request.form['sched'])
# Send a message to a discord text channel etc...
return 'Received !'
When I run my discord.py script which looks something like this:
(Stripped away some commands and features for the sake of keeping this shorter)
import discord
import asyncio
from flask import Flask, request
app = Flask(__name__)
#app.route('/posts', methods=['POST'])
def result():
print(request.form['sched'])
# Send a message to a discord text channel etc...
return 'Received !'
client = discord.Client()
#client.event
async def on_ready():
print('Logged in as')
print(client.user.name)
print(client.user.id)
print('------')
#client.event
async def on_message(message):
if message.author == client.user:
return
content = message.content
fullUser = message.author.name+'#'+message.author.discriminator
print(str(message.timestamp)+" #"+message.channel.name+" "+fullUser+": "+str(content.encode('ascii', 'ignore').decode('ascii')))
if content.startswith('!'):
content = content[1:]
if content.startswith('test'):
counter = 0
tmp = await client.send_message(message.channel, 'Calculating messages...')
async for log in client.logs_from(message.channel, limit=100):
if log.author == message.author:
counter += 1
await client.edit_message(tmp, 'You have {} messages.'.format(counter))
client.run('MyTokenHere')
It seems like if I point flask to discord.py (the above) and run it, it'll start the code, get to the "client.run('MyTokenHere')" part for discord, and just stop at that and run the discord bot. It's not until I exit out of the bot by doing Ctrl+C that the actual Flask server starts, but now the discord bot is disconnected and no longer does any processing.
The same problem persists if I were to for example add "app.run()" somewhere in my code (before calling "client.run()" which starts the Discord bot part) to launch the Flask server; It'll just run the flask, get stuck on that until I Ctrl+C out of the Flask server, then it'll proceed to start the Discord bot.
Ultimately, I need to use the Discord API and I need to be connected to the Discord API gateway and all that good jazz to actually send messages to a channel with the bot, so I don't really know what to do here.
So. I think I've tried my best to explain what I'm ultimately trying to achieve here, and hopefully someone can help me find a way to either make this work with Flask, or if there's a better and easier way, provide a different solution.
This is cog example in discord.py
I made this thing for dbl (Discord Bot Lists) you can implement, the thing you need.
Note: run your heroku app with webprocess
example :
web: python main.py
Then go on https://uptimerobot.com/
and setup to ping your web app after every 5 minutes
from aiohttp import web
from discord.ext import commands, tasks
import discord
import os
import aiohttp
app = web.Application()
routes = web.RouteTableDef()
def setup(bot):
bot.add_cog(Webserver(bot))
class Webserver(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.web_server.start()
#routes.get('/')
async def welcome(request):
return web.Response(text="Hello, world")
#routes.post('/dbl')
async def dblwebhook(request):
if request.headers.get('authorization') == '3mErTJMYFt':
data = await request.json()
user = self.bot.get_user(data['user']) or await self.bot.fetch_user(data['user'])
if user is None:
return
_type = f'Tested!' if data['type'] == 'test' else f'Voted!'
upvoted_bot = f'<#{data["bot"]}>'
embed = discord.Embed(title=_type, colour=discord.Color.blurple())
embed.description = f'**Upvoter :** {user.mention} Just {_type}' + f'\n**Upvoted Bot :** {upvoted_bot}'
embed.set_thumbnail(url=user.avatar_url)
channel = self.bot.get_channel(5645646545142312312)
await channel.send(embed=embed)
return 200
self.webserver_port = os.environ.get('PORT', 5000)
app.add_routes(routes)
#tasks.loop()
async def web_server(self):
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, host='0.0.0.0', port=self.webserver_port)
await site.start()
#web_server.before_loop
async def web_server_before_loop(self):
await self.bot.wait_until_ready()
As the kind commenters informed me; threading seems like the way to go.
Thanks guys!
Another cool way if you aren't going to use flask extensions, use quart instead of flask, then it will super easy for you.
Note: Run your heroku app with webprocess
example :
web: python main.py
Then go on https://uptimerobot.com/ and setup to ping your web app after every 5 minutes
# Code Example
from discord.ext import commands
from quart import Quart
import os
app = Quart(__name__)
bot = commands.Bot('!')
#bot.command()
async def something(ctx):
...
"""
Note: On Heroku you cant bind your webserver with 5000 port as they aren't static.
To fix above problem you will have to get dynamic port from the environment variable and you are good to go.
"""
PORT = os.environ.get('PORT')
bot.loop.create_task(app.run_task('0.0.0.0', PORT))
bot.run('Token')
Or you can use the Terminal Multiplexer, tmux to run them independently!.
If you are running on a Linux platform, tmux python3 flaskapp.py would run the flask app, while you can independently run the discord bot.
You can use asyncio with flask to perform this
import discord
import asyncio
class PrintDiscordChannelsClient(discord.Client):
def __init__(self, *args, **kwargs):
self.guild_id = kwargs.pop('guild_id')
super().__init__(*args, **kwargs)
async def on_ready(self):
try:
await self.wait_until_ready()
guild = self.get_guild(int(self.guild_id))
print(f'{guild.name} is ready!')
channels = guild.text_channels
print(f'{(channels)} channels found')
await self.close()
except Exception as e:
print(e)
await self.close()
async def print_discord_channels(guild_id):
client = PrintDiscordChannelsClient(guild_id=guild_id)
await client.start('DISCORD_BOT_TOKEN')
#application.route("/discord-api", methods=["GET"])
def discord_api():
guild_id = request.args.get('guild_id')
asyncio.run(print_discord_channels(guild_id=guild_id))
return make_response("Success", 200)

Categories

Resources