how to stop a while loop in python-telegram from client side - python

How can I stop a while loop inside a function which is passed as a parameter for a telegram bot CommandHandler, using a command or text from the client side?
I have this bot:
from telegram import *
from datetime import datetime
import time
from telegram.ext import *
import ast #ignore this. i'll use it later
RUN_FUNC = True
def func_starter(update: Update, context):
update.effective_chat.send_message('Function innitiated.')
counter = 0
RUN_FUNC = True
while RUN_FUNC:
print(f'function running for {counter} seconds.')
counter += 1
time.sleep(1)
def func_stopper(update: Update, context):
update.effective_chat.send_message('function stopped')
RUN_FUNC = False
def main():
updater = Updater('*********:***********************************', use_context=True)
dp = updater.dispatcher
dp.add_handler(CommandHandler("stop", func_stopper))
dp.add_handler(CommandHandler("start", func_starter))
updater.start_polling(0)
updater.idle()
main()
So the bot gets the /start command from the client and it starts the func_starter function which has conditional while loop in it. But because the program never makes it past the while loop, any other command / text from the client, never gets registered by the python code therefore the loop goes on forever. I want to be able to stop the func_starter function, using a command from the client side.
I even made a global variable but obviously to no avail lol. It is logical I think that the program never makes it past the while loop, so is there any way to listen for new commands while in the loop?

I see two options:
pass run_async=True to CommandHandler("start", …), which will make func_starter run in its own thread.
Instead of using a while loop inside callbacks, use JobQueue to schedule the next steps

Related

Message from a telegram bot without a command(python)

I want to send a Message(call a function) every day at a given Time. Sadly this is not possible with message.reply_text('Test'). Is there any way i can do this? I could not find anything.
This is my current code:
import telegram.ext
from telegram.ext import CommandHandler, MessageHandler, Filters
import schedule
import time
API_KEY = 'XXXXXXXXXXX'
updater = telegram.ext.Updater(API_KEY)
dispatcher = updater.dispatcher
def start(update, context):
update.message.reply_text('Welcome!')
# problem:
def Test(update, context):
update.message.reply_text('Works!!!')
# running special functions every Day at a given Time
schedule.every().day.at("10:00").do(Test)
while True:
schedule.run_pending()
time.sleep(1)
def main():
# add handlers for start and help commands
dispatcher.add_handler(CommandHandler("start", start))
# start your bot
updater.start_polling()
# run the bot until Ctrl-C
updater.idle()
The schedule part works, I just don`t know how to send this Message.
Thanks for your help!
Update object, inside of the message field, has the from field which is a User Telegram object containing the user's ID.
Once you have the user's ID, you can use the sendMessage method in order to reply him easily.
To conclude, instead of:
update.message.reply_text('Welcome!')
You could do like so:
user_id = update.message.from.id
updater.sendmessage(chat_id=user_id, text="Welcome!")

How can I run a command from another command?

I'm trying to write a Discord bot, where if I run a certain command, a part of the program will run, but the other part won't.
For example: In my case, when they type in "?m", they will get a message saying "xy", but then for 90 seconds, they won't be able to call that command again. If they do call it in this 90 second period, another command called "?x" will run.
Can someone help me out?
You can do it using bot.invoke() method:
#bot.command()
async def x(ctx):
pass # whatever here
#bot.command()
async def m(ctx):
if something:
ctx.command = bot.get_command("x")
await bot.invoke(ctx) # invoke a command called x
With Python's time module, you can check how long it's been since the command was last successfully called. time.time() returns the current time (expressed as a number of seconds since January 1st, 1970).
Calling one command from within another can be as simple as calling it with await, like any other coroutine, and passing the same context to it.
import time
from discord.ext.commands import Bot
from tokens import TOKEN
bot = Bot(command_prefix='?')
# Set the initial time as 0 (effectly 1970) so it was definitely long
# ago for the command to now be called.
last_called = 0
#bot.command()
async def hi(ctx):
# Compare the current time to when the command was last successfully
# called.
global last_called
now = time.time()
if now - last_called <= 90:
await not_now(ctx)
else:
last_called = now
await ctx.send('Hello')
#bot.command()
async def not_now(ctx):
await ctx.send('Please wait a minute.')
bot.run(TOKEN)
Using global variables is not generally the best idea, but unfortunately subclassing Bot for tidier encapsulation of data into a single object is quite clunky.

Run other commands alongside selenium

I am currently trying to make a discord bot that starts a selenium thread. It works but the only issue is that I cannot use other commands if selenium takes too long. It will eventually respond to the next command but it's only responds after selenium is finished.
This is what I have:
import threading
import discord
import time
from selenium import webdriver
from discord.ext import tasks, commands
client = commands.Bot(command_prefix='!')
def start(url):
driver = webdriver.Firefox()
driver.get(url)
time.sleep(10)
driver.close()
#client.command()
async def rq(ctx):
#if link == None:
#await ctx.send("Please send a link!")
await ctx.send("Started!")
threading(target=start("https://google.com/")).start()
#client.command()
async def sc(ctx):
await ctx.send("some command")
if __name__ == "__main__":
client.run(token)
Any solutions would be helpful!
The way you invoke the thread is incorrect:
threading(target=start("https://google.com/")).start()
What this does is:
Call the start function on the main thread, passing it the URL.
Wait for the function to finish.
Take the returned value (None) and pass it as the target function to the thread constructor (I think you meant threading.Thread there by the way).
So by the time the thread starts, the actual work has already completed on the main thread, and the thread itself does nothing.
The correct way to start a thread and pass it some arguments would be:
threading.Thread(target=start, args=("https://google.com/",)).start()
Notice how start is not being followed by (), so we're not calling the function directly; we're passing the function itself to the Thread constructor. The arguments are given as a tuple to the args argument (hence the trailing comma).

Pinging someone at an interval discord.py

I am making a bot and want it to ping a user every ten minutes. Here is what I have right now
#client.event
while True:
channel = client.get_channel(717346417592762457)
await message.channel.send(<#!561464966427705374>)
time.sleep(600)
But this results in a lot of errors.
One easy way to do it would be to use discord's tasks extension. You can easily set the amount of time for each loop. The only main drawback is that the loop restarts every time your bot restarts, or rather everytime you run the command, in this example. In the example below, I added the method to start the loop in a command, but you can have it start in an on_ready event or similar.
# New Import
from discord.ext import tasks, commands
# ... Other code ...
#tasks.loop(minutes=10) # #tasks.loop(seconds=600) <-- does the same amount of time
async def ping_loop():
channel = client.get_channel(717346417592762457)
await channel.send("<#!561464966427705374>")
# 1. Don't use message.channel, as you only defined channel and this isn't an event
# 2. As mentioned by Tim Roberts in the comments
# add speech marks to your send() since it's a string
# somewhere else in your code
#client.command()
async def loop(ctx):
try:
ping_loop.start() # start the loop if it isn't currently running
await ctx.send("Started pinging <#!561464966427705374>")
except: # happens if the loop is already running
ping_loop.cancel() # if so, cancel the loop
await ctx.send("Stopped pinging <#!561464966427705374>")
For the specific errors you mentioned:
#client.event
async def somefunction(ctx):
# you need to define a function to call (pass to discord.py)
# also the function has to be async, for the usage of `await`
while True:
channel = client.get_channel(717346417592762457)
await message.channel.send(<#!561464966427705374>)
# not sure if you can mention user like this
await asyncio.sleep(600)
# time.sleep(600) # prefer non-blocking asyncio sleep. make sure `asyncio` is imported
To mention a user, you need to first get user with
user_instance = ctx.guild.get_member(YOUR_USER_discord_id)
Note: You may need to enable the privilege to use the .get_member() function. If you have not enabled it yet, just follow the instructions in the exception.
Then you can sent message with
await ctx.send(f"THE_TEXT_YOU_WANT_TO_SPAM {user_instance.mention}!")
Also, if you are using it in a bot command like me, you may want to add some validation, so that not everyone can cast/invoke the command. I did this with has_role():
#commands.has_role(YOUR_ROLE_ID_THAT_CAN_CAST_THIS_COMMAND)
So in the end, it looks like this:
#bot.command('name': 'spam', 'help':'use at # to mention a user')
#commands.has_role(YOUR_ROLE_ID_THAT_CAN_CAST_THIS_COMMAND)
async def spamMention(ctx, spam_duration_second: int = 3):
user_instance = ctx.guild.get_member(YOUR_USER_discord_id)
for cntr in range(spam_duration_second):
await ctx.send(f"THE_TEXT_YOU_WANT_TO_SPAM {user_instance.mention}!")
await asyncio.sleep(THE_INTERVAL_YOU_WANT_IN_SECOND)
return
It's part of my code so I know it works (my python version: 3.7.9).

Python Asyncio run_forever() and Tasks

I adapted this code for using Google Cloud PubSub in Async Python: https://github.com/cloudfind/google-pubsub-asyncio
import asyncio
import datetime
import functools
import os
from google.cloud import pubsub
from google.gax.errors import RetryError
from grpc import StatusCode
async def message_producer():
""" Publish messages which consist of the current datetime """
while True:
await asyncio.sleep(0.1)
async def proc_message(message):
await asyncio.sleep(0.1)
print(message)
message.ack()
def main():
""" Main program """
loop = asyncio.get_event_loop()
topic = "projects/{project_id}/topics/{topic}".format(
project_id=PROJECT, topic=TOPIC)
subscription_name = "projects/{project_id}/subscriptions/{subscription}".format(
project_id=PROJECT, subscription=SUBSCRIPTION)
subscription = make_subscription(
topic, subscription_name)
def create_proc_message_task(message):
""" Callback handler for the subscription; schedule a task on the event loop """
print("Task created!")
task = loop.create_task(proc_message(message))
subscription.open(create_proc_message_task)
# Produce some messages to consume
loop.create_task(message_producer())
print("Subscribed, let's do this!")
loop.run_forever()
def make_subscription(topic, subscription_name):
""" Make a publisher and subscriber client, and create the necessary resources """
subscriber = pubsub.SubscriberClient()
try:
subscriber.create_subscription(subscription_name, topic)
except:
pass
subscription = subscriber.subscribe(subscription_name)
return subscription
if __name__ == "__main__":
main()
I basically removed the publishing code and only use the subscription code.
However, initially I did not include the loop.create_task(message_producer()) line. I figured that tasks were created as they were supposed to however they never actually run themselves. Only if I add said line the code properly executes and all created Tasks run. What causes this behaviour?
PubSub is calling the create_proc_message_task callback from a different thread. Since create_task is not thread-safe, it must only be called from the thread that runs the event loop (typically the main thread). To correct the issue, replace loop.create_task(proc_message(message)) with asyncio.run_coroutine_threadsafe(proc_message(message), loop) and message_producer will no longer be needed.
As for why message_producer appeared to fix the code, consider that run_coroutine_threadsafe does two additional things compared to create_task:
It operates in a thread-safe fashion, so the event loop data structures are not corrupted when this is done concurrently.
It ensures that the event loop wakes up at the soonest possible opportunity, so that it can process the new task.
In your case create_task added the task to the loop's runnable queue (without any locking), but failed to ensure the wakeup, because that is not needed when running in the event loop thread. The message_producer then served to force the loop to wake up in regular intervals, which is when it also checks and executes the runnable tasks.

Categories

Resources