How to start a thread in telethon correctly? - python

I am writing a bot and I need to implement the following functionality: the bot once every 10 minutes(for example) parse a certain URL and if there were changes from the previous call, writes to the chat.
Since the bot is also engaged in other things, I decided to loop the parsing in the function with sleep at the end. If there are changes, I try to send a message to the chat, but then a problem happens.
Since a successful combination of circumstances does not arise from an event in the chat, I can't pull the "entity" from the "event" for the "send_message" function. therefore, we have to get through the "get_entity" function and links to the chat as a parameter, but for some reason this does not work from another stream. below is a simplified code:
import threading, queue
from time import sleep
import asyncio
from telethon.sync import TelegramClient, events
import config as cfg
bot = TelegramClient('Bot', cfg.api_id, cfg.api_hash)
#bot.on(events.NewMessage(pattern=r'^(?i)(idchat){1}$'))
async def echoidchat(event):
channelaa = await bot.get_entity('https://t.me/elvistest')
await bot.send_message(channelaa, 'ответ')
def parseurls():
for x in range(10):
q.put(x)
pass
async def pre_sendmsg():
while True:
try:
msg = q.get_nowait()
except Exception as e:
await asyncio.sleep(1.0)
else:
await sendmsg(msg)
q.task_done()
async def sendmsg(msg):
channel = await bot.get_entity('https://t.me/elvistest')
await bot.send_message(channel, f'ответ из другого потока {msg}')
if __name__ == '__main__':
q = queue.Queue()
parseurls()
bot.start(bot_token=cfg.bot_token)
threading.Thread(target=asyncio.run, daemon=True, args=(pre_sendmsg(),)).start()
bot.run_until_disconnected()
The thing is that on the line " boot.get_entity" nothing happens. The script execution is lost somewhere and does not go further, that is, the next line with "bot. send_message" is simply not executed. however, "def echoidchat" is working at this time.

Well done!
This is work like I want.
import random
import threading, queue
from time import sleep
import asyncio
from telethon import TelegramClient, events
import config as cfg
bot = TelegramClient('Bot', cfg.api_id, cfg.api_hash)
#bot.on(events.NewMessage(pattern=r'^(?i)(idchat){1}$'))
async def echoidchat(event):
await bot.send_message(event.chat, 'ответ')
async def parseurls():
while True:
ts = abs(int(random.random()*10))
print(f'parseurls({ts})')
await sendmsg(ts)
await asyncio.sleep(ts)
async def sendmsg(msg):
print(f'sendmsg({msg}) - start')
channel = await bot.get_entity('https://t.me/elvistest')
await bot.send_message(channel, f'ответ из другого потока {msg}')
print(f'sendmsg({msg}) - done')
def main():
bot.start(bot_token=cfg.bot_token)
loop = asyncio.get_event_loop()
tasks = [
loop.create_task(parseurls()),
loop.create_task(bot.run_until_disconnected()),
]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
if __name__ == '__main__':
main()

Related

python decorators in asyncio event listener with Telethon

Every time a message is sent to the channel the handler function will be called because of the python decorator.
I want to to both recieve messages and print them out. This is how i imagined the program should look like.
But its not working because the event is not passed to the handler function in main.
How can i do that asynchronously, when the #client decorator is controlling the messages and stopping me from using using the handler function inside other functions, and pass on the tokenData
from telethon import TelegramClient, events
client = TelegramClient(username, api_id, api_hash)
client.start()
channel = 'https://t.me/DEXTNewPairsBotBSC'
#client.on(events.NewMessage(chats=channel))
async def handler(event):
# tokenData = event
tokenData = event.message.message
msg.append(tokenData)
await asyncio.sleep(2)
return tokenData
async def dumpIt():
await asyncio.sleep(2)
return msg[0]
async def main():
task1 = asyncio.create_task(handler(event))
task2 = asyncio.create_task(dumpIt())
await asyncio.wait([task1,task2])
client.loop.run_until_complete(main())
Use asyncio.Queue:
import asyncio
from telethon import TelegramClient, events
client = ...
queue = asyncio.Queue()
#client.on(events.NewMessage(chats=channel))
async def handler(event):
queue.put_nowait(event.message.message)
async def dumpIt():
msg = await queue.get()
return msg
async def main():
await dumpIt()
client.loop.run_until_complete(main())
As long as the asyncio event loop is running (which it is while it's "blocked" in queue.get()), Telethon will receive updates.

Discord cogs doesn't load

I am trying to run two different Discord Bots using a single python script using cogs. But when I try to run the 2nd bot it throws an ImportError even-though I didn't use that specific Library. The reaction roles bot works fine without the anti spam bot. Here's my code. FYI I am working inside a Virtual Env.
main.py
if __name__ == "__main__":
try:
reaction_role_bot = commands.Bot(command_prefix=config["reaction_role_bot"]["bot_prefix"], intents=discord.Intents.all())
reaction_slash = SlashCommand(reaction_role_bot, sync_commands=True)
reaction_role_bot.load_extension(f"cogs.{str(os.path.basename('cogs/reaction_roles.py')[:-3])}")
anti_spam_bot = commands.Bot(command_prefix=config["anti_spam_bot"]["bot_prefix"], intents=discord.Intents.default())
spam_slash = SlashCommand(anti_spam_bot, sync_commands=True)
anti_spam_bot.load_extension(f"cogs.{str(os.path.basename('cogs/anti_spam.py')[:-3])}")
event_loop = asyncio.get_event_loop()
event_loop.create_task(reaction_role_bot.run(config["reaction_role_bot"]["token"]))
event_loop.create_task(anti_spam_bot.run(config["anti_spam_bot"]["token"]))
event_loop.run_forever()
except Exception as e:
print(e)
anti_spam.py
import platform
import os
import discord
from discord.ext import commands
from antispam import AntiSpamHandler
from antispam.plugins import AntiSpamTracker, Options
class AntiSpamBot(commands.Cog):
def __init__(self, client):
self.client = client
# Initialize the AntiSpamHandler
self.client.handler = AntiSpamHandler(self.client, options=Options(no_punish=True))
# 3 Being how many 'punishment requests' before is_spamming returns True
self.client.tracker = AntiSpamTracker(self.client.handler, 3)
self.client.handler.register_extension(self.client.tracker)
#commands.Cog.listener()
async def on_ready(self):
print("---------------------------------")
print(f"Logged in as {str(self.client.user)}")
print(f"Discord.py API version: {discord.__version__}")
print(f"Python version: {platform.python_version()}")
print(f"Running on: {platform.system()} {platform.release()} ({os.name})")
await self.client.change_presence(status=discord.Status.idle, activity=discord.Game(name="Head of Security"))
print("---------------------------------\n")
# The code in this event is executed every time a valid commands catches an error
#commands.Cog.listener()
async def on_command_error(context, error):
raise error
#commands.Cog.listener()
async def on_message(self, message):
await self.client.handler.propagate(message)
if self.client.tracker.is_spamming(message):
await message.delete()
await message.channel.send(f"{message.author.mention} has been automatically kicked for spamming.")
await message.author.kick()
await self.client.process_commands(message)
def setup(client):
client.add_cog(AntiSpamBot(client))
Error
Extension 'cogs.anti_spam' raised an error: ImportError: cannot import name 'AsyncMock' from 'unittest.mock' (/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py)
I've no experiences using cogs and its bit confusing me. Any kind of help would help me to sort this out! Thanks in advance!
I do not believe this is a cog registration issue. I believe this is an import error from some of the dependencies in your cog file. I googled your error and found something similar, I recommend checking it out here for some more information.
As a blanket statement, I would double check that you have mock installed, and that you're installing it on the version of Python that you think you're installing it on. It can get wonky if you have multiple python versions insealled.
Also, on an unrelated note:: It is best to avoid running multiple bot instances in one python file, but I can help you do it the best way possible.
For starters, you have to realize that Client.run is an abstraction of a couple of more lower level concepts.
There is Client.login which logs in the client and then Client.connect which actually runs the processing. These are coroutines.
asyncio provides the capability of putting things in the event loop for it to work whenever it has time to.
Something like this e.g.
loop = asyncio.get_event_loop()
async def foo():
await asyncio.sleep(10)
loop.close()
loop.create_task(foo())
loop.run_forever()
If we want to wait for something to happen from another coroutine, asyncio provides us with this functionality as well through the means of synchronisation via asyncio.Event. You can consider this as a boolean that you are waiting for:
e = asyncio.Event()
loop = asyncio.get_event_loop()
async def foo():
await e.wait()
print('we are done waiting...')
loop.stop()
async def bar():
await asyncio.sleep(20)
e.set()
loop.create_task(bar())
loop.create_task(foo())
loop.run_forever() # foo will stop this event loop when 'e' is set to true
loop.close()
Using this concept we can apply it to the discord bots themselves.
import asyncio
import discord
from collections import namedtuple
# First, we must attach an event signalling when the bot has been
# closed to the client itself so we know when to fully close the event loop.
Entry = namedtuple('Entry', 'client event')
entries = [
Entry(client=discord.Client(), event=asyncio.Event()),
Entry(client=discord.Client(), event=asyncio.Event())
]
# Then, we should login to all our clients and wrap the connect call
# so it knows when to do the actual full closure
loop = asyncio.get_event_loop()
async def login():
for e in entries:
await e.client.login()
async def wrapped_connect(entry):
try:
await entry.client.connect()
except Exception as e:
await entry.client.close()
print('We got an exception: ', e.__class__.__name__, e)
entry.event.set()
# actually check if we should close the event loop:
async def check_close():
futures = [e.event.wait() for e in entries]
await asyncio.wait(futures)
# here is when we actually login
loop.run_until_complete(login())
# now we connect to every client
for entry in entries:
loop.create_task(wrapped_connect(entry))
# now we're waiting for all the clients to close
loop.run_until_complete(check_close())
# finally, we close the event loop
loop.close()

How to detect BinanceSocketManager websocket disconnect in Python?

Binance API & python-binance offers async functionality for non-blocking execution as per discussed in Async basics for Binance.
I am using BinanceSocketManager listening (async non-blocking) to live data via websocket.
In scenarios like network intermittent connection lost, I wish to add an auto-reconnect feature to my project. But I can't seems to find any info with BinanceSocketManager. I was only able to find a guide which uses ThreadedWebsocketManager, but it was not an async implementation.
Does anyone know how to implement a Binance websocket disconnect detection and auto-reconnect mechanism?
Here is some code of what I have so far:
import asyncio
from binance import AsyncClient, BinanceSocketManager
async def main():
client = await AsyncClient.create()
await kline_listener(client)
async def kline_listener(client):
bm = BinanceSocketManager(client)
async with bm.kline_socket(symbol='BTCUSDT') as stream:
while True:
res = await stream.recv()
print(res)
# a way detect websocket error/disconnect, callback 'disconnect_callback'
async def disconnect_callback():
await client.close_connection()
await main() # restart client and kline socket
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
In case someone else is looking at this, for this, you should be looking at the BinanceAPIException. Code could look something like this then:
from binance import AsyncClient, BinanceSocketManager
from binance.exceptions import BinanceAPIException
async def main():
client = await AsyncClient.create()
bm = BinanceSocketManager(client, user_timeout=60)
# start any sockets here, i.e a trade socket
kline_candles = bm.kline_socket('BNBUSDT', interval=client.KLINE_INTERVAL_1MINUTE)
# start receiving messages
try:
status = await client.get_system_status()
print(status['msg'])
async with kline_candles as stream:
for _ in range(5):
res = await stream.recv() # create/await response
await process_message(msg=res, client=client) # process message
except BinanceAPIException as e:
print(e)
await disconnect_callback(client=client)
async def disconnect_callback(client):
await client.close_connection() # close connection
time.sleep(60) # wait a minute before restarting
await main() # restart client and kline socket
async def process_message(msg, client):
if msg['e'] == 'error':
await disconnect_callback(client=client)
print('ERROR OCCURED')
else:
candle = msg['k'] # get only the candle info within the general dict
start_time = datetime.utcfromtimestamp(candle['t']/1000).strftime('%Y-%m-%d %H:%M:%S')
close_time = datetime.utcfromtimestamp(candle['T']/1000).strftime('%Y-%m-%d %H:%M:%S')
print(f'__ start: {start_time}, close: {close_time}')
print(msg)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
The disconnect has not been tested yet, but I assume this will work. If anyone has any additional notes, just let me know.
I have tested above code and it proves rather stable.
Here are some improvements I have made.
I'm not sure what happens if your internet connection is completely gone when this line is executed:
client = await AsyncClient.create()
This could probably be solved like this (I'm open for better ideas):
while True:
try:
client = await AsyncClient.create()
except Exception as error_msg:
print(f"error: {error_msg}")
# should we add a sleep here?
# time.sleep(3)
else:
print("finally got through the loop")
break
Surrounding this with a try/except is a good idea:
bm = BinanceSocketManager(client, user_timeout=60)
The call to stream.recv() should be extended with asyncio.wait_for() to cover the situation, when there is no data coming in for a longer period of time. It usually means there's something wrong.
async with kline_candles as stream:
for _ in range(5):
try:
res = await asyncio.wait_for(stream.recv(), timeout=60) # create/await response
await process_message(msg=res, client=client) # process message
except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosed, asyncio.exceptions.CancelledError, asyncio.exceptions.TimeoutError) as error_msg_1:
print(f"Error! in main loop 1:\n{error_msg_1}")
await disconnect_callback(client=client)

Discord.py with threads, RuntimeError: Timeout context manager should be used inside a task

So I'm creating a discord bot that is constantly scraping a webpage, checking if the item is in stock. When the item becomes in stock, send a message in the chat. Since the scraping is in a constant loop, I figured I would put that in a thread, one for each page that needs to be scraped. The problem is that each thread is producing the same error,
RuntimeError: Timeout context manager should be used inside a task
Does anyone know why, or a better approach to what I am trying to do?
async def searchPage(input_url):
currentUrl = input_url
driver = webdriver.Chrome(options=options, executable_path=DRIVER_PATH)
driver.get(currentUrl)
while True:
try:
shipIt = driver.find_element_by_css_selector('[data-test="shippingBlock"]')
await alertUsers(currentUrl)
await asyncio.sleep(5)
except NoSuchElementException:
continue
#driver.quit()
async def alertUsers(current_input):
channel = client.get_channel(795513308869296181)
await channel.send(f'Go buy this: {current_input}')
async def create_threads():
thread1 = threading.Thread(target=asyncio.run, args=(searchPage("https://www.target.com/p/set-of-2-kid-century-modern-kids-chairs-b-spaces/-/A-81803789?preselect=80929605#lnk=sametab"),))
thread1.start()
thread2 = threading.Thread(target=asyncio.run, args=(searchPage("https://www.target.com/p/wrangler-men-s-big-tall-relaxed-fit-jeans-with-flex/-/A-79321830?preselect=52428349#lnk=sametab"),))
thread2.start()
#client.event
async def on_ready():
guild = discord.utils.get(client.guilds, name=GUILD)
print(
f'{client.user} is connected to the following guild:\n'
f'{guild.name}(id: {guild.id})'
)
await create_threads()
client.run(TOKEN)
Some frameworks don't like to run in threads - i.e. all GUI frameworks has to run windows and widgets in main thread. They are called not thread-safe.
And you may have similar problem - inside thread you can't use async created in main thread.
You should run searchPage in new thread as normal function and send results to main thread which should send some message to discord. You can use global variable or better queue to send results. And main thread should run some function which periodically check queue and send messages to discord.
Discord has #tasks.loop(second=...) which can run periodically function.
Minimal working code
import discord
from discord.ext import tasks
import os
import time
import queue
import threading
from selenium import webdriver
MY_CHANNEL = 795513308869296181
# queue to communicate with threads
queue = queue.Queue()
client = discord.Client()
# --- normal functions ---
def searchPage(url, queue):
driver = webdriver.Chrome()
#driver = webdriver.Chrome(options=options, executable_path=DRIVER_PATH)
while True:
try:
driver.get(url)
ship_it = driver.find_element_by_css_selector('[data-test="shippingBlock"]')
#print('[DEBUG] queue put:', url)
queue.put(url)
time.sleep(5)
except NoSuchElementException as ex:
#print('[DEBUG] queue put: not found')
#queue.put('not found')
#print('Exception:', ex)
pass
#driver.quit()
def create_threads():
urls = [
"https://www.target.com/p/set-of-2-kid-century-modern-kids-chairs-b-spaces/-/A-81803789?preselect=80929605#lnk=sametab",
"https://www.target.com/p/wrangler-men-s-big-tall-relaxed-fit-jeans-with-flex/-/A-79321830?preselect=52428349#lnk=sametab",
]
for url in urls:
t = threading.Thread(target=searchPage, args=(url, queue))
t.start()
# --- async functions ---
#tasks.loop(seconds=5)
async def alert_users():
#print('[DEBUG] alert_users')
if not queue.empty():
current_input = queue.get()
channel = client.get_channel(MY_CHANNEL)
await channel.send(f'Go buy this: {current_input}')
#client.event
async def on_ready():
create_threads()
alert_users.start() # start `task.loop`
TOKEN = os.getenv('DISCORD_TOKEN')
client.run(TOKEN)

How to exit when task is finished in asyncio

In this shorted and simplified code I want to wait for all tasks to be completed( queue.join) and then leave.
This code open a network connection and download some data.
Unfortunately I have to push 'ctrl-c' because the 'client.run_until_disconnected()'.
The complete code works but I dont'know how to exit without keyboard interruption.
I have to schedule it with crontab so I can't use 'client.run_until_disconnected()'
EDIT : updated code
#!/usr/bin/env python3.7
import asyncio
import telethon
from telethon import TelegramClient
from telethon import functions, types
from datetime import datetime
api_id = 00000 # your api_id
api_hash = "your api hash"
async def worker(queue):
while True:
queue_book = await queue.get()
book_name = queue_book.file.name
print(book_name)
loop = asyncio.get_event_loop()
await client.download_media(queue_book,book_name)
queue.task_done()
async def main():
#Free ebook medical articles
channel = await client(functions.messages.CheckChatInviteRequest('your channel hash'))
#values message's id depend on the chosen channel
ids = [63529,63528,63527,63526,63525,63524,63523,63522]
queue = asyncio.Queue(1)
workers = [asyncio.create_task(worker(queue)) for _ in range(5)]
for booksId in ids:
async for get_book in client.iter_messages(channel.chat, ids=booksId):
await queue.put(get_book)
await queue.join()
for the_worker in workers:
the_worker.cancel()
async def wait_until(dt):
now = datetime.now()
await asyncio.sleep((dt - now).total_seconds())
async def run_at(dt, coro):
await wait_until(dt)
return await coro
client = TelegramClient("Test", api_id, api_hash)
loop = asyncio.get_event_loop()
client.start()
try:
loop = loop.create_task(run_at(datetime(2021, 2, 19, 11,00),main()))
client.run_until_disconnected()
except KeyboardInterrupt as keyint:
print("KeyboardInterrupt..")
pass
run_until_disconnected() is just a helper function, you don't have an obligation to run it if you want to control when the event loop ends. The minimal change would be to replace client.run_until_disconnected() with loop.run_forever(), and call loop.stop() at the end of main().
A more idiomatic approach would be to leave main() as it is, but instead of starting it with create_task, await it from your top-level coroutine (normally called main by convention, but you already chose that name for the other function). Then you can just call asyncio.run(actual_main()), and the program will automatically exit when main finishes. For example (untested):
# main and other functions as in the question
async def actual_main():
global client
client = TelegramClient("Test", api_id, api_hash)
await client.start()
await run_at(datetime(2021, 2, 19, 11, 00), main())
asyncio.run(actual_main())

Categories

Resources