Please help me deal with async and threads in the telethon module. I'm trying to make a bot that, with a certain command placed in the queue, will either send a file or send a message, but I ran into difficulty
I'm trying to run telethon async inside the stream, but the clock function does not start, although the function works with the event
import asyncio
import threading
from queue import Queue
from telethon import TelegramClient, events, sync
async def send_message():
loop = asyncio.new_event_loop()
api_id = 11
api_hash = '11'
try:
client = TelegramClient('session', api_id, api_hash, loop=loop)
await client.connect()
if not await client.is_user_authorized():
client.disconnected()
await client.start()
except:
print('qqqqq')
#client.on(events.NewMessage(chats='me'))
async def handler(event):
await client.forward_messages('me', event.message)
async def clock():
global cl
while True:
while (queue_send_al.qsize() == 0):
print("33")
event_send_all.wait()
async with client:
cl = queue_send_al.get()
print(cl)
if cl[0] == 1:
await client.send_file('me', str(cl[1]),use_cache=False, part_size_kb=512)
event_send_all.clear()
elif cl[0] == 2:
print('1111')
event_send_all.clear()
loop.create_task(clock())
print('22')
await client.run_until_disconnected()
def go():
asyncio.run(send_message())
queue_send_al = Queue()
event_send_all = threading.Event()
queue_send_al.put([2, fr"11111111"])
event_send_all.set()
th3 = threading.Thread(target=go).start()
Related
I want my discord users to be able to queue up some tasks. In this example it is just pinging websites, but I eventually want it to do other long running tasks. When I run my bot, I get the following error:
ping-bot.py:22: RuntimeWarning: coroutine 'respond_to_channel' was never awaited
respond_to_channel()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
Here's the code:
import os, discord, subprocess, asyncio
from discord import app_commands
from dotenv import load_dotenv
from time import sleep
from threading import Thread
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
CHANNEL=os.getenv('CHANNEL_ID')
GUILD=os.getenv('GUILD_ID')
queue = []
def ping_test():
global queue
old_queue = queue.copy()
while True:
if queue != old_queue and len(queue) > 0:
old_queue = queue.copy()
retcode = subprocess.call("ping " + "-c 5 " + old_queue[0][0], shell=True)
queue.pop(0)
if len(queue) > 0:
respond_to_channel()
print(f"Now working on {queue[0][0]}")
sleep(1)
async def respond_to_channel():
ping_channel = client.get_channel(CHANNEL)
await ping_channel.send(f"Testing... Now working on {queue[0][0]}")
class MyClient(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
self.synced = False
daemon = Thread(target=ping_test, daemon=True, name='Monitor')
daemon.start()
async def on_ready(self):
print(f'Logged on as {self.user}!')
if not self.synced:
await tree.sync(guild = discord.Object(id = GUILD))
self.synced = True
client = MyClient()
tree = app_commands.CommandTree(client)
#tree.command(name = "greet", description = "Greetings!", guild = discord.Object(id = GUILD))
async def self(interaction: discord.Interaction, name: str):
await interaction.response.send_message(f"Hello {name}! I was made with Discord.py!")
print(f'Sent from {interaction.user}')
#tree.command(name = "ping-test", description = "Use my computer to ping websites", guild = discord.Object(id = GUILD))
async def self(interaction: discord.Interaction, website: str):
queue.append([website, interaction.user.id])
await interaction.response.send_message(f"Hello <#{interaction.user.id}>! You want me to ping {website}. There are currently {len(queue) - 1} jobs in front of you.", file=discord.File('test.png'))
client.run(TOKEN)
I've found some 'solutions' that do work for the ping test, but I am going to allow my discord users to use my computer for some computationally heavy tasks that can take 5+ minutes. These other solution I found break the bot if the task takes more than a few minutes.
For this reason, I decided to implement a thread that process items in a list that will tag the user in a channel when it is done.
I actually wanted to use discord.ext.tasks as it handles everything for you and runs in the async loop so nothing breaks.
I changed the thread call to
#tasks.loop(seconds = 1)
async def ping_test():
global queue
global being_worked
global client
ping_channel = client.get_channel(CHANNEL)
if len(queue) > 0:
if queue[0] != being_worked[0]:
being_worked = queue.copy()
retcode = subprocess.call("ping " + "-c 5 " + being_worked[0][0], shell=True)
queue.pop(0)
await ping_channel.send(f"Done working on {being_worked[0][0]}")
if len(queue) > 0:
await ping_channel.send(f"Testing... Now working on {queue[0][0]}")
print(f"Now working on {queue[0][0]}")
return
So now the full code looks like this:
import os, discord, subprocess
from discord import app_commands
from discord.ext import tasks
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
CHANNEL=int(os.getenv('CHANNEL_ID'))
GUILD=os.getenv('GUILD_ID')
queue = []
being_worked = []
#tasks.loop(seconds = 1)
async def ping_test():
global queue
global being_worked
global client
ping_channel = client.get_channel(CHANNEL)
if len(queue) > 0:
if queue != being_worked:
being_worked = queue.copy()
retcode = subprocess.call("ping " + "-c 15 " + being_worked[0][0], shell=True)
queue.pop(0)
await ping_channel.send(f"Done working on {being_worked[0][0]}")
if len(queue) > 0:
await ping_channel.send(f"Testing... Now working on {queue[0][0]}")
print(f"Now working on {queue[0][0]}")
return
class MyClient(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
self.synced = False
async def on_ready(self):
print(f'Logged on as {self.user}!')
if not self.synced:
await tree.sync(guild = discord.Object(id = GUILD))
self.synced = True
await ping_test.start()
client = MyClient()
tree = app_commands.CommandTree(client)
#tree.command(name = "greet", description = "Greetings!", guild = discord.Object(id = GUILD))
async def self(interaction: discord.Interaction, name: str):
await interaction.response.send_message(f"Hello {name}! I was made with Discord.py!")
print(f'Sent from {interaction.user}')
#tree.command(name = "ping-test", description = "Use my computer to ping websites", guild = discord.Object(id = GUILD))
async def self(interaction: discord.Interaction, website: str):
queue.append([website, interaction.user.id])
await interaction.response.send_message(f"Hello <#{interaction.user.id}>! You want me to ping {website}. There are currently {len(queue) - 1} jobs in front of you.", file=discord.File('test.png'))
client.run(TOKEN)
Adding await in front of the function respond_to_channel() should fix the issue.
await respond_to_channel()
I am writing a personal telegram bot, one of its functions is to show the balance of my accounts market.csgo.com. My code:
import asyncio
import aiogram
import aiohttp
...
async def get_balance(session, profiles_dict, message):
async with session.get(f'https://market.csgo.com/api/v2/get-money?key={profiles_dict[1][1]}') as resp:
html = await resp.json()
each_wallet = int(html['money'])
await bot.send_message(message.from_user.id,
f'🟢 {profiles_dict[0]} : <i>{each_wallet}</i>',
disable_web_page_preview=True, parse_mode=types.ParseMode.HTML)
...
#dp.message_handler(content_types=['text'])
async def main(message):
profiles = users()
async with aiohttp.ClientSession(trust_env=True) as session:
tasks = []
if message.text == 'Balance 💸':
await bot.send_message(message.from_user.id, 'Information request. Wait..')
for i in profiles.items():
task = asyncio.ensure_future(get_balance(session, i, message, stats))
tasks.append(task)
await asyncio.gather(*tasks)
if message.text == 'On Sale 💰':
...
if message.text == 'Timeout Items ⌛':
...
executor.start_polling(dp, skip_updates=False)
get_balance() works in async mode, sends aiohttp requests to the API and outputs information await bot.send_message(). Result:
Now the launch of the function is implemented through the keyboard button, but how to make the function run every hour? I am aware of the existence of asynchronous task scheduler aioschedule and have seen this example. But they run a function without arguments, but I have as many as 3 of them async def get_balance(session, profiles_dict, message). I tried to do this:
import asyncio
import aioschedule
async def scheduler(session, profiles_dict, message):
aioschedule.every().hour.do(get_balance(session, profiles_dict, message))
while True:
await aioschedule.run_pending()
await asyncio.sleep(1)
async def on_startup(session, profiles_dict, message):
asyncio.create_task(scheduler(session, profiles_dict, message))
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=False, on_startup=on_startup(session, profiles_dict, message))
Obviously it doesn't work that way.
My question is:
How to run an async function with arguments that sends aiohttp requests through task scheduling aioschedule and display the result through telegram aiogram?
Solution:
import aiogram
import asyncio
import aiohttp
import aioschedule
...
async def get_balance(session, profiles_dict):
async with session.get(f'https://market.csgo.com/api/v2/get-money?key={profiles_dict[1][1]}') as resp:
html = await resp.json()
each_wallet = int(html['money'])
await bot.send_message(MY_TELEGRAM_ID,
f'🟢 {profiles_dict[0]} : <i>{each_wallet}</i>',
disable_web_page_preview=True, parse_mode=types.ParseMode.HTML)
...
#dp.message_handler(content_types=['text'])
async def main(message):
profiles = users()
async with aiohttp.ClientSession(trust_env=True) as session:
tasks = []
if message.text == 'Balance 💸':
await bot.send_message(message.from_user.id, 'Information request. Wait..')
for i in profiles.items():
task = asyncio.ensure_future(get_balance(session, i))
tasks.append(task)
await asyncio.gather(*tasks)
if message.text == 'On Sale 💰':
...
if message.text == 'Timeout Items ⌛':
...
# Client session get_balance function
async def session_get_balance():
profiles = users()
async with aiohttp.ClientSession(trust_env=True) as session:
tasks = []
for i in profiles.items():
task = asyncio.ensure_future(get_balance(session, i))
tasks.append(task)
await asyncio.gather(*tasks)
# Schedule functions by time
async def scheduler():
aioschedule.every().hour.do(session_get_balance)
while True:
await aioschedule.run_pending()
await asyncio.sleep(1)
# Function at start
async def on_startup(_):
asyncio.create_task(scheduler())
# Launch telegram bot
if __name__ == "__main__":
executor.start_polling(dp, skip_updates=True, on_startup=on_startup)
Since this is my personal bot, instead of message.from_user.id I specified my MY_TELEGRAM_ID.
await bot.send_message(MY_TELEGRAM_ID,
f'🟢 {profiles_dict[0]} : <i>{each_wallet}</i>',
disable_web_page_preview=True, parse_mode=types.ParseMode.HTML)
I write a telegram bot on aiogram that gives me information about my accounts market.csgo.com. The meaning of the script is simple - I click on the button, it displays the text and and the function is run.
My functions send async requests and work fine, but I don't know how to get aiohttp and aiogram to work together.
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor
from auth import *
import asyncio
import aiohttp
bot = Bot(token=token)
dp = Dispatcher(bot)
def users():
***Data of my accounts from txt to dict***
async def get_info(session, dictt, message):
total_wallet = 0
async with session.get(f'https://market.csgo.com/api/v2/get-money?key={dictt[1][1]}') as resp:
html = await resp.json()
total_wallet += int(html['money'])
#await bot.send_message(message.from_user.id, f'{total_wallet}')
async def get_on_sale(session, dictt, message):
sale_total_sum = 0
async with session.get(f'https://market.csgo.com/api/v2/items?key={dictt[1][1]}') as resp:
html = await resp.json()
for i in html['items']:
sale_total_sum += i['price']
#await bot.send_message(message.from_user.id, f'{sale_total_sum}')
#dp.message_handler(content_types=['text'])
async def Main():
try:
profiles = users()
async with aiohttp.ClientSession(trust_env=True) as session:
tasks = []
if message.text == 'info 📊':
await bot.send_message(message.from_user.id, 'Wait for information..')
for i in profiles.items():
task = asyncio.ensure_future(get_info(session, i))
tasks.append(task)
await asyncio.gather(*tasks)
if message.text == 'on sale 💰':
await bot.send_message(message.from_user.id, 'Wait for information..')
for i in profiles.items():
task = asyncio.ensure_future(get_on_sale(session, i))
tasks.append(task)
await asyncio.gather(*tasks)
except Exception as ex:
print(f'Error {ex}')
loop = asyncio.get_event_loop()
loop.run_until_complete(Main())
executor.start_polling(dp, skip_updates=True)
My problem is that I don't know how to properly pass the message argument to the Main function
#dp.message_handler(content_types=['text'])
async def Main(): #async def Main(message)
And run aiogram along with aiohttp.
loop.run_until_complete(Main()) #loop.run_until_complete(Main(message))
If I do like this: async def Main(message) and loop.run_until_complete(Main(message)) Then I get an error:
loop.run_until_complete(Main(message))
NameError: name 'message' is not defined
or if I use only async def Main(message) get this:
loop.run_until_complete(Main())
TypeError: Main() missing 1 required positional argument: 'message'
Solution:
async def loop_on(message):
loop = asyncio.get_event_loop()
loop.run_until_complete(Main(message))
Below is (working) code for a generic websocket streamer.
It creates a daemon thread from which performs asyncio.run(...).
The asyncio code spawns 2 tasks, which never complete.
How to correctly destroy this object?
One of the tasks is executing a keepalive 'ping', so I can easily exit that loop using a flag. But the other is blocking on a message from the websocket.
import json
import aiohttp
import asyncio
import gzip
import asyncio
from threading import Thread
class WebSocket:
KEEPALIVE_INTERVAL_S = 10
def __init__(self, url, on_connect, on_msg):
self.url = url
self.on_connect = on_connect
self.on_msg = on_msg
self.streams = {}
self.worker_thread = Thread(name='WebSocket', target=self.thread_func, daemon=True).start()
def thread_func(self):
asyncio.run(self.aio_run())
async def aio_run(self):
async with aiohttp.ClientSession() as session:
self.ws = await session.ws_connect(self.url)
await self.on_connect(self)
async def ping():
while True:
print('KEEPALIVE')
await self.ws.ping()
await asyncio.sleep(WebSocket.KEEPALIVE_INTERVAL_S)
async def main_loop():
async for msg in self.ws:
def extract_data(msg):
if msg.type == aiohttp.WSMsgType.BINARY:
as_bytes = gzip.decompress(msg.data)
as_string = as_bytes.decode('utf8')
as_json = json.loads(as_string)
return as_json
elif msg.type == aiohttp.WSMsgType.TEXT:
return json.loads(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
print('⛔️ aiohttp.WSMsgType.ERROR')
return msg.data
data = extract_data(msg)
self.on_msg(data)
# May want this approach if we want to handle graceful shutdown
# W.task_ping = asyncio.create_task(ping())
# W.task_main_loop = asyncio.create_task(main_loop())
await asyncio.gather(
ping(),
main_loop()
)
async def send_json(self, J):
await self.ws.send_json(J)
I'd suggest the use of asyncio.run_coroutine_threadsafe instead of asyncio.run. It returns a concurrent.futures.Future object which you can cancel:
def thread_func(self):
self.future = asyncio.run_coroutine_threadsafe(
self.aio_run(),
asyncio.get_event_loop()
)
# somewhere else
self.future.cancel()
Another approach would be to make ping and main_loop a task, and cancel them when necessary:
# inside `aio_run`
self.task_ping = asyncio.create_task(ping())
self.main_loop_task = asyncio.create_task(main_loop())
await asyncio.gather(
self.task_ping,
self.main_loop_task
return_exceptions=True
)
# somewhere else
self.task_ping.cancel()
self.main_loop_task.cancel()
This doesn't change the fact that aio_run should also be called with asyncio.run_coroutine_threadsafe. asyncio.run should be used as a main entry point for asyncio programs and should be only called once.
I would like to suggest one more variation of the solution. When finishing coroutines (tasks), I prefer minimizing the use of cancel() (but not excluding), since sometimes it can make it difficult to debug business logic (keep in mind that asyncio.CancelledError does not inherit from an Exception).
In your case, the code might look like this(only changes):
class WebSocket:
KEEPALIVE_INTERVAL_S = 10
def __init__(self, url, on_connect, on_msg):
# ...
self.worker_thread = Thread(name='WebSocket', target=self.thread_func)
self.worker_thread.start()
async def aio_run(self):
self._loop = asyncio.get_event_loop()
# ...
self._ping_task = asyncio.create_task(ping())
self._main_task = asyncio.create_task(main_loop())
await asyncio.gather(
self._ping_task,
self._main_task,
return_exceptions=True
)
# ...
async def stop_ping(self):
self._ping_task.cancel()
try:
await self._ping_task
except asyncio.CancelledError:
pass
async def _stop(self):
# wait ping end before socket closing
await self.stop_ping()
# lead to correct exit from `async for msg in self.ws`
await self.ws.close()
def stop(self):
# wait stopping ping and closing socket
asyncio.run_coroutine_threadsafe(
self._stop(), self._loop
).result()
self.worker_thread.join() # wait thread finish
I'm new to this library. I want to make my bot to have a queue option, I already tried declaring an empty list queue=[] and use
#commands.command()
async def play(self, ctx, *, query: str):
while True:
tracks = await self.bot.wavelink.get_tracks(f'ytsearch:{query}')
if not tracks:
continue
else:
break
player = self.bot.wavelink.get_player(ctx.guild.id)
if not player.is_connected:
await ctx.invoke(self.connect_)
channel = ctx.author.voice.channel
if player.is_playing:
if any(f"{channel.id}" in s for s in queue):
await ctx.channel.send(embed=discord.Embed(description='Your queue exceed maximum capacity of 1 element',color=0xfd2121))
else:
queue.append(f'{channel.id}:{tracks[0]}')
await ctx.channel.send(embed=discord.Embed(description=f"Your song has been queued\n{tracks[0]}",color=0xfd2121))
else:
await ctx.channel.send(embed=discord.Embed(description=f'Now playing {tracks[0]} .', color=0xfd2121))
await player.play(tracks[0])
while player.is_playing:
await asyncio.sleep(1)
if not any(f"{channel.id}" in s for s in queue):
await player.disconnect()
else:
res = [i for i in queue if f'{channel.id}' in i]
sw=''.join(res)
while True:
sw2 = await self.bot.wavelink.get_tracks(f'ytsearch:{sw[19:]}')
if not sw2:
continue
else:
break
player = self.bot.wavelink.get_player(ctx.guild.id)
await player.play(sw2[0])
queue.remove(f'{channel.id}:{sw[19:]}')
but this can only contain 1 queue per channel id and the scripts are way too complicated. I saw some people are using asyncio.Queue - some kind of background process - can anybody provide me with an example of using asyncio.Queue to queue up songs?
Here is a simple example of setting up a queue system with asyncio.Queue() and asyncio.Event(), utilising wavelink to handle audio.
import wavelink
import asyncio
from discord.ext import commands
client = commands.Bot(command_prefix='!')
songs = asyncio.Queue()
play_next_song = asyncio.Event()
if not hasattr(client, 'wavelink'):
client.wavelink = wavelink.Client(bot=client)
async def on_event_hook(event):
if isinstance(event, (wavelink.TrackEnd, wavelink.TrackException)):
play_next_song.set()
async def audio_player_task():
await client.wait_until_ready()
node = await client.wavelink.initiate_node(host='127.0.0.1',
port=2333,
rest_uri='http://127.0.0.1:2333',
password='PASSWORD',
identifier='TEST',
region='us_central')
node.set_hook(on_event_hook)
while True:
play_next_song.clear()
song, guild_id = await songs.get()
player = client.wavelink.get_player(guild_id)
await player.play(song)
await play_next_song.wait()
#client.command()
async def play(ctx, *, query: str):
tracks = await client.wavelink.get_tracks(f'ytsearch:{query}')
if not tracks:
return await ctx.send('Could not find any songs with that query.')
player = client.wavelink.get_player(ctx.guild.id)
if not player.is_connected:
await player.connect(ctx.author.voice.channel.id)
await ctx.send(f'Added {str(tracks[0])} to the queue.')
queue_item = (tracks[0], ctx.guild.id)
await songs.put(queue_item)
client.loop.create_task(audio_player_task())
client.run('token')