Running Quart and Telethon with multiple clients (sessions) - python-asyncio

I'm trying to run Quart+Telethon with multiple clients. This example shows one global client. I have this working.
Now I need my app to handle multiple users simultaneously logging in and doing stuff.
This post suggests using asyncio.gather.
How do I need to change my code such that I can have multiple people logging in?
Here I placed the bulk of Quart functionality into the work() function (wherever client is referenced). In the def main() I start the app and then invoke asyncio.gather.
When running the app with two work() functions (two clients) in asyncio.gather I get error "AssertionError: Handler is overwriting existing for endpoint phone_form".
And if I run only with one work() function I get a different error: "ConnectionError('Cannot send requests while disconnected')"
What am I missing?
Thx
import os
import asyncio
from dotenv import load_dotenv
from quart import Quart, render_template_string, request, render_template
from telethon import TelegramClient
load_dotenv('.env')
API_ID = int(os.getenv('API_ID'))
API_HASH = str(os.getenv('API_HASH'))
app = Quart(__name__)
async def work(client):
async with client:
#app.route("/")
async def phone_form():
return await render_template('phone_form.html')
#app.route("/validation_form", methods=['POST'])
async def validation_form():
""" Ask the user for the confirmation code they just got. """
global phone
# Check form parameters (phone/code)
form = await request.form
if 'phone' in form:
phone = form['phone']
await client.send_code_request(phone)
return await render_template('validation_form.html', phone_nr=phone)
async def main():
import hypercorn.asyncio
# create task so that starting hypercorn server is no blocking functions that come after it
server = asyncio.create_task(hypercorn.asyncio.serve(app, hypercorn.Config()))
# have many clients here, using the app asynchronously
await asyncio.gather(
work(TelegramClient('user1', API_ID_, API_HASH)),
work(TelegramClient('user2', API_ID, API_HASH)),
)
# this is to start blocking - means after this subsequent functions will need to wait until hypercorn is finished (hypercorn runs forever!)
# this await also lets server run indefinitely
await server
if __name__ == '__main__':
asyncio.run(main())

The code is trying to set N function handlers to the same routes, that's what causing the error here. I believe you need a different approach here - make adding sessions a continuous process with routes dealing with the last (new) session.
I'm not sure how hypercorn server should be started so using it's part from your example.
eg.
API_ID = int(os.getenv('API_ID'))
API_HASH = str(os.getenv('API_HASH'))
app = Quart(__name__)
clients = []
async def work():
new_cli = None
phone = None
#app.route("/")
async def index():
""" Some page containing 'new session' button redirecting to /phone_form """
new_cli = None
phone = None
return render_template('index.html')
#app.route("/phone_form")
async def phone_form():
num = len(clients) + 1
new_cli = TelegramClient(f'user{num}', API_ID_, API_HASH)
await new_cli.connect()
return await render_template('phone_form.html')
#app.route("/validation_form", methods=['POST'])
async def validation_form():
""" Ask the user for the confirmation code they just got. """
# Check form parameters (phone/code)
form = await request.form
if 'phone' in form:
phone = form['phone']
await client.send_code_request(phone)
return await render_template('validation_form.html', phone_nr=phone)
#app.route("/confirm_code", methods=['POST'])
async def confirm_code():
""" Finish auth process. """
form = await request.form
if 'code' in form:
await new_cli.sign_in(phone, form['code'])
clients.append(new_cli)
new_cli = None
phone = None
async def main():
import hypercorn.asyncio
server = asyncio.create_task(hypercorn.asyncio.serve(app, hypercorn.Config()))
await work()
await server
if __name__ == '__main__':
asyncio.run(main())
You may also want to add checkers if new client session is present. Resulted sessions can be used later.

Related

Discord.py 2.0 - Button showing before message finishes editing view

I'm hosting my bot online and sometimes messages take time to edit their own View components which is fine. The problem is when i modify a view by calling
await message.edit(view=...)
, the new button/select components are displayed instantly but their callbacks are not operational because the message editing is taking some time to complete. Thus, unknown interaction error tends to occur when clicking on the button a little too early, the callbacks are not being called, and I need to wait to re-click.
My question is : Is it possible to wait for a message.edit() to fully complete before showing the buttons, or is there another way to solve this issue?
Code sample :
async def throw_dice(self,ctx):
try :
superself = self
async def action(superself):
...
if isinstance(self.current,PlayerDiscord) :
class myButton(ui.Button):
def __init__(self,label,style,row=None):
super().__init__(label=label,style=style,row=row)
async def callback(self,interaction):
await interaction.response.defer()
nonlocal superself
if interaction.user.id==superself.current.member.id:
self.view.stop()
await superself.msg_play.edit(view=None)
await action(superself)
self.view = FRPGame.myView(ctx,self) #Create new view
self.view.add_item(myButton("\U0001F3B2",style=discord.ButtonStyle.primary))
#self.msg_play stores the message
await self.msg_play.edit(content=self.content,view=self.view) #<-- problem is this single line
else :
...
except BaseException :
traceback.print_exc()
Please send your entire code so we can see what could be done
#code here
asyncio.sleep(10) #Will wait 10 seconds before sending
You can use the wait_for method to wait for the message to be edited. You can then use the message.edit method to edit the message and add the buttons.
import discord
from discord.ext import commands
from discord_components import DiscordComponents, Button, ButtonStyle
bot = commands.Bot(command_prefix="!")
DiscordComponents(bot)
#bot.command()
async def test(ctx):
message = await ctx.send("Hello")
await message.edit(content="Hello World")
await message.edit(components=[
Button(style=ButtonStyle.red, label="Red"),
Button(style=ButtonStyle.green, label="Green"),
Button(style=ButtonStyle.blue, label="Blue")
])
bot.run("token")
This uses discord-components, which you can get here:
https://devkiki7000.gitbook.io/discord-components

Can't figure out how to use discord.py ctx

I have tried to convert my discord bot's commands to using ctx instead of interactions but the code I have tried below does not work.
#bot.command(description="Says test")
async def test(ctx):
await ctx.send("Test")
My issue is that the command never loads.
Updated code but still broken
import discord
from discord import ui
from discord.ext import commands
bot = commands.Bot(command_prefix="/", intents = discord.Intents.all())
# Run code
#bot.event
async def on_ready():
print('The bot has sucuessfully logged in: {0.user}'.format(bot))
await bot.change_presence(activity=discord.Activity(type = discord.ActivityType.listening, name = f"Chilling Music - {len(bot.guilds)}"))
# Commands
#bot.command(description="Says Hello")
async def hello(ctx):
await ctx.send(f"Hello {ctx.author.mention}!")
#bot.command(description="Plays music")
async def play(ctx, song_name : str):
await ctx.author.voice.channel.connect()
await ctx.send(f"Now playing: {song_name}")
#bot.command(description="The amount of servers the bot is in")
async def servercount(ctx):
await ctx.send(f"Chill Bot is in {len(bot.guilds)} discord servers!")
#bot.command(description="The bots ping")
async def ping(ctx):
await ctx.send(f"๐Ÿ“Pong\nChill Bot's ping is\n{round(bot.latency * 1000)} ms")
#bot.command(description="Announcement command")
async def announce(ctx, message : str):
await ctx.send(message)
#bot.command(description="Support Invite")
async def support(ctx):
await ctx.send(f"{ctx.author.mention} Please join our server for support! [invite]", ephemeral=True)
#bot.command(description = "Syncs up commands")
async def sync(ctx):
await ctx.send("Synced commands!", ephemeral=True)
bot.run('')
Your commands don't work because you incorrectly created an on_message event handler.
By overwriting it and not calling Bot.process_commands(), the library no longer tries to parse incoming messages into commands.
The docs also show this in the Frequently Asked Questions (https://discordpy.readthedocs.io/en/stable/faq.html#why-does-on-message-make-my-commands-stop-working)
Overriding the default provided on_message forbids any extra commands from running. To fix this, add a bot.process_commands(message) line at the end of your on_message.
Also, don't auto-sync (you're doing it in on_ready). Make a message command and call it manually whenever you want to sync.

Sending a message through discord.py after message edit check

I have a function that checks if 'your' is changed to 'you're' and I am having trouble having the bot send a message after this check. What is the proper way to do this? (I want this to apply to all channels.)
import discord
from discord.ext import commands
client = discord.Client()
bot = commands.Bot(command_prefix='$')
#client.event
async def on_ready():
print("We have logged in as {0.user}".format(client))
#client.event
async def on_message_edit(before,after):
if(before.content == "your" and after.content == "you're"):
# I want to send message here
client.run('Token') # I have my token here
Add ctx into the parameters (before,after,ctx), and
then put in await ctx.send("message") where you want your message to send.
the parameters before and after are of type discord.Message. discord.Message has an attribute channel, with which you can use the function send.
#client.event
async def on_message_edit(before, after):
if before.content == 'your' and after.content == "you're":
await before.channel.send(send_whatever_you_want)

Using CTX in on_reaction_add event -- Discord.py

async def on_reaction_add(reaction, user):
emoji = reaction.emoji
if emoji == "๐Ÿ’Œ":
await user.channel.send("HI")
I got problem here with user.
I want to use here with ctx like ctx.channel.send.
but also it occured error how to use ctx in here?
Instead of using the on_reaction_add event, it's better in this case to use a wait_for command event. This would mean the event can only be triggered once and only when the command was invoked. However with your current event, this allows anyone to react to a message with that emoji and the bot would respond.
By using client.wait_for("reaction_add"), this would allow you to control when a user can react to the emoji. You can also add checks, this means only the user would be able to use the reactions on the message the bot sends. Other parameters can be passed, but it's up to you how you want to style it.
In the example below shows, the user can invoke the command, then is asked to react. The bot already adds these reactions, so the user would only need to react. The wait_for attribute would wait for the user to either react with the specified emojis and your command would send a message.
#client.command()
async def react(ctx):
message = await ctx.send('React to this message!')
mail = '๐Ÿ’Œ'
post = '๐Ÿ“ฎ'
await message.add_reaction(mail)
await message.add_reaction(post)
def check(reaction, user):
return user == ctx.author and str(
reaction.emoji) in [mail, post]
member = ctx.author
while True:
try:
reaction, user = await client.wait_for("reaction_add", timeout=10.0, check=check)
if str(reaction.emoji) == mail:
await ctx.send('Hi you just received')
if str(reaction.emoji) == post:
await ctx.send('Hi you just posted...')
You need to use reaction.message.channel.send
async def on_reaction_add(reaction, user):
emoji = reaction.emoji
if str(emoji) == "๐Ÿ’Œ": await reaction.message.channel.send("HI")

How do I make my logs server specific | discord.py

Currently, I have a logs on my bot, It works fine but the problem is that it takes logs from all servers that the bot it in. So, how do I make it server specific?
#commands.Cog.listener()
async def on_message_delete(self, message):
embed = Embed(
description=f"Message deleted in {message.channel.mention}", color=0x4040EC
).set_author(name=message.author, url=Embed.Empty, icon_url=message.author.avatar_url)
embed.add_field(name="Message", value=message.content)
embed.timestamp = message.created_at
channel=self.bot.get_channel(channel_id)
await channel.send(embed=embed)
This is my code for message deleting
Compare message's guild id with yours
Below is the revised code:
#commands.Cog.listener()
async def on_message_delete(self, message):
if message.guild.id == YOUR_GUILD_ID:
embed = Embed(
description=f"Message deleted in {message.channel.mention}", color=0x4040EC
).set_author(name=message.author, url=Embed.Empty, icon_url=message.author.avatar_url)
embed.add_field(name="Message", value=message.content)
embed.timestamp = message.created_at
channel = self.bot.get_channel(channel_id)
await channel.send(embed=embed)

Resources