Using two PIP packages together that use event loops - python

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

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.

What is the difference between having a main bot class versus no class on discord py?

With discord python bots, I can't seem to find a great answer on this and the documentation actually uses both examples randomly. What is the main difference between using a bot class in your bot.py versus starting your bot without a class? See below for an example
Example bot with a class:
import discord
from discord.ext import commands
class MyBot(commands.Bot):
async def on_ready():
# ready
bot = MyBot(command_prefix='!')
bot.run(token)
Example of a regular bot without the class:
import discord
from discord.ext import commands
if __name__ == "__main__":
bot = commands.Bot(command_prefix='!')
async def on_ready():
# ready
bot.run(token)
As far as I know, both of these examples work and do the same thing, but like I said I can't seem to find a great answer to the differences between the two.
Creating your own class has a couple of advantages.
First, you will be able to import your MyBot class in another file. So if you want to split up your project into multiple files (maybe two people are working on the same project) one person can work on the MyBot class while another can import it.
my_bot.py
import ...
class MyBot(commands.Bot):
...
test.py
from my_bot import MyBot
bot = MyBot()
Second, you will be able to more easily change functions inside of MyBot. Right now your async def on_ready(): function doesn't do anything in the second example, even if there was code in it. If you put it inside of a MyBot class it will be called when your bot is ready. See on_ready()
Compare
class MyBot(commands.Bot):
async def on_ready():
print('connected to server!')
bot = MyBot(command_prefix='!')
bot.run(token)
to
if '__name__' == '__main__':
bot = commands.Bot(command_prefix='!')
async def on_ready():
print('connected to server!')
bot.run(token)
In the second example, it will never say "connected to server!" because that code won't run. And if you call it with on_ready() that doesn't really mean you were connected to the server. Turn off your internet connection and you'll see.
Of course, you might do something more useful than just printing a message of course. Maybe you'll write connection logs to a file or something.

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.

Connection constantly closing with Python websockets

I've tried setting up a WebSocket server, and an API to access it via browser
I'm a week new into Python, and I hope this somewhat makes sense.
I have a Server class
**Server.py**
# WS server that sends messages at random intervals
import asyncio
import datetime
import random
import websockets
async def time(websocket, path):
while True:
now = datetime.datetime.utcnow().isoformat() + "Z"
await websocket.send(now)
await asyncio.sleep(random.random() * 3)
async def echo(websocket, path):
async for message in websocket:
await websocket.send(message)
async def router(websocket, path):
if path == "/get":
await time(websocket)
elif path == "/post":
await echo(websocket)
asyncio.get_event_loop().run_until_complete(
websockets.serve(router, "127.0.0.1", 8081, ping_interval=None))
asyncio.get_event_loop().run_forever()
and an API class
**API.py**
from quart import Quart
import json
import websockets
app = Quart(__name__)
#app.route('/get',methods=['GET'])
async def foo1():
uri = "ws://localhost:8081"
async with websockets.connect(uri) as websocket:
try:
receivedMessage = await websocket.recv()
print(f"< {receivedMessage}")
except:
print('Reconnecting')
websocket = await websockets.connect(uri)
return ' '
#app.route('/post', methods=['POST'])
async def foo2():
uri = "ws://localhost:8081"
async with websockets.connect(uri) as websocket:
await websocket.send("Hello world!")
try:
await websocket.recv()
except:
print('Reconnecting')
websocket = await websockets.connect(uri)
if __name__ == '__main__':
app.run()
My aim was to be able to access different functions from the Server, based on the API's Path which was being accessed (seen it here: https://github.com/aaugustin/websockets/issues/547)
The problem I'm getting is that "Reconnecting" appears constantly, and nothing actually happens when I make use of /get or /post (i.e. it seems to be closing after .recv() occurs - I know this because I had seen ConnectionClosedOk - no reason 1000 earlier.
I really don't understand what I'm doing right now lol, it is very confusing. I did get things to work when Server only had 1 function (constantly spitting out timestamps), and the API only had a GET function (to display the current time of the request).. things now don't really work after I implemented the router function.
Any help would be greatly, greatly appreciated. My Python knowledge is incredibly limited, so as verbose an answer as possible would really help me out.
I'm assuming that the POST method is going to result in "Hello world!" appearing on the console for the Server, but I'm probably wrong about that :( I just seen it in some tutorial documentation.
EDIT: Also, I'm not able to access localhost:(port)/post; it says Specified method is invalid for this resource; why is that?

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