My task is to create small bot which is a product list. When user sends products, it answers with the Inline keyboard and buttons, each single button is one of the products(initially there are red cross next to the text) When user press a button, it must change cross to a green check mark(name of product must stay).
How to implement it?
code that creates a list:
#dp.message_handler()
async def show_list(message: types.Message):
productlist = message.text.split(',')
keyboard = InlineKeyboardMarkup()
for product in productlist:
keyboard.row(InlineKeyboardButton(text=f'❌ {product}', callback_data=f'{product}'))
await message.answer("Список продуктов: ", reply_markup=keyboard)
that's how it works in telegram
I have a way to do this task.
Step one: Define a variable to record the selected products.
Step two: Define a callback query handler to access the callbacks.
selected_products = {}
#dp.message_handler()
async def show_list(message: types.Message):
global selected_products; selected_products[message.message_id] = []
productlist = message.text.split(',')
keyboard = InlineKeyboardMarkup()
for product in productlist:
keyboard.row(InlineKeyboardButton(text=f'❌ {product}', callback_data=f'{product}'))
await message.answer("Список продуктов: ", reply_markup=keyboard)
#dp.callback_query_handler()
async def update_productlist(call: types.CallbackQuery):
global selected_products
if call.message.message_id - 1 in selected_products.keys():
selected_products[call.message.message_id - 1].append(call.data)
else:
return 1
productlist = []
keyboard = call.message.reply_markup.inline_keyboard
for row in keyboard:
for col in row:
productlist.append(col.callback_data)
new_keyboard = InlineKeyboardMarkup()
for product in productlist:
if product in selected_products[call.message.message_id - 1]:
new_keyboard.row(InlineKeyboardButton(text=f'✔️ {product}', callback_data=f'{product}'))
else:
new_keyboard.row(InlineKeyboardButton(text=f'❌ {product}', callback_data=f'{product}'))
else:
await bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id, reply_markup=new_keyboard)
Note: This way has a bug. If the user enters another command before the product list response, the returned product list will not be interactive.
Related
I am trying to implement a "Back" button that will return to the previous dropdown.
I am using Pycord.
I have a dropdown with options to pick different food categories, after you pick a category, the dropdown menu changes to a new dropdown where see items in that category.
In addition, you have a "Back" button that should get you to the previous dropdown.
At the moment I get In components.0.components.0.options.1: The specified option value is already used Error, after I click the back button and click the same category again.
Here is where I recreate the issue, first I run the slash command /shop and I click the "Meats" category
Then I get to a new dropdown and I click the "Back" Button:
I get to the original dropdown.
and if I click the "Meats" cateogry again it crushes.
main.py
import discord
from example1 import CategoryView
bot = discord.Bot()
#bot.command()
async def shop(ctx):
await ctx.respond("Choose a category from the dropdown below! ✨", view=CategoryView())
#bot.event
async def on_ready():
print("Ready!")
bot.run("TOKEN")
example1.py
import discord
from example2 import CategoryPriceView
class CategoryView(discord.ui.View):
def __init__(self):
super().__init__()
#discord.ui.select(
placeholder = "Choose a food category!",
min_values = 1,
max_values = 1,
options = [
discord.SelectOption(
label="Meats",
emoji='🍖'
),
discord.SelectOption(
label="Salads",
emoji='🥗'
)
]
)
async def select_callback(self, select, interaction):
category = select.values[0]
await interaction.response.edit_message(content="Choose your item!", view=CategoryPriceView(category, self))
example2.py
import discord
options = []
class CategoryPriceView(discord.ui.View):
def __init__(self, category, father):
super().__init__()
global options
self.category = category
self.father = father
if self.category == 'Meats':
options.append(discord.SelectOption(label='Steak', description="Price: 40$"))
elif self.category == 'Salads':
options.append(discord.SelectOption(label='Greek Salad', description="Price: 30$"))
#discord.ui.button(label="Back", row=1, style=discord.ButtonStyle.blurple)
async def button_callback(self, button, interaction):
await interaction.response.edit_message(content="Choose a category from the dropdown below! ✨", view=self.father)
#discord.ui.select(
placeholder = "Choose an item!",
min_values = 1,
max_values = 1,
options = options
)
async def select_callback(self, select, interaction):
item = select.values[0]
await interaction.response.edit_message(content=f"You chose {item}! ", view=None)
EDIT: My answer was completely wrong. Edited to provide actual answer.
The issue is the global options. Every time you select an option from the original CategoryView, you're creating a new instance of CategoryPriceView and adding to options. As options is global and never gets cleared - there are duplicates in there - hence the duplicate value error. You can resolve this by subclassing Select and creating a custom CategoryPriceSelect that has the options logic you want. See below.
# example2.py
import discord
class CategoryPriceSelect(discord.ui.Select):
def __init__(self, category: str) -> None:
options = []
if category == 'Meats':
options.append(discord.SelectOption(label='Steak', description="Price: 40$"))
elif category == 'Salads':
options.append(discord.SelectOption(label='Greek Salad', description="Price: 30$"))
super().__init__(
placeholder="Choose an item!",
min_values=1,
max_values=1,
options=options
)
async def callback(self, interaction: discord.Interaction):
item = self.values[0]
await interaction.response.edit_message(content=f"You chose {item}! ", view=None)
class CategoryPriceView(discord.ui.View):
def __init__(self, category, father):
super().__init__()
self.category = category
self.father = father
select = CategoryPriceSelect(self.category)
self.add_item(select)
#discord.ui.button(label="Back", row=1, style=discord.ButtonStyle.blurple)
async def button_callback(self, button, interaction):
await interaction.response.edit_message(content="Choose a category from the dropdown below! ✨", view=self.father)
I've tested this and this works now without throwing the error and I believe this has your desired functionality.
I'm trying to create a bot that my friends and I can do predictions through for the upcoming Soccer World Championship.
Now I want this bot to be able to ask the result of each match as a question, with a button for answer.
Creating one question works perfectly, but then moving on to the next question is what I have issues with.
I found the following online:
"Each message is a different view, but you don't need to program each view. You can create a class that inherits from discord.ui.button where it takes a team name as a string, then pass that button (plus another for the 2nd team) to a class that inherits from discord.ui.View"
I'm a beginner when it comes to python and I have no clue how I actually do this. Any tips?
What I want:
Message 1: "Team A vs Team B"
Button 1: Team A
Button 2: Team B
Message 2: "Team C vs Team C"
Button 1: Team C
Button 2: Team D
The code I currently already have:
class WorldsGeneral(discord.ui.View):
def __init__(self, *, timeout=10):
super().__init__(timeout=timeout)
#discord.ui.button(label="Groups", style=discord.ButtonStyle.red)
async def Finals(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.send_message()
# The messages for predictions would have to be send by pressing this button ^
#discord.ui.button(label="Standings")
async def Standings(
self, interaction: discord.Interaction, button: discord.ui.Button
):
await interaction.response.send_message()
#bot.command(aliases=["poule", "predictions])
async def worlds(ctx):
embed = discord.Embed(
colour=0x0433f3,
title=":trophy: Worlds Poule 2022 :trophy:",
)
embed.set_image(
url=f"https://thumbs.gfycat.com/AccomplishedAdorableClumber-size_restricted.gif"
)
view = WorldsGeneral()
msg = await ctx.send(
":construction: Major Work In Progress :construction:", view=view
)
view.orig_mes = msg
You have an issue with your worlds command: there's a missing quotation at the very top. Here's how the fixed code should look:
#bot.command(aliases=["poule", "predictions"]) # Missing quotation right here
async def worlds(ctx):
embed = discord.Embed(
colour=0x0433f3,
title=":trophy: Worlds Poule 2022 :trophy:",
)
embed.set_image(
url=f"https://thumbs.gfycat.com/AccomplishedAdorableClumber-size_restricted.gif"
)
view = WorldsGeneral()
msg = await ctx.send(
":construction: Major Work In Progress :construction:", view=view
)
view.orig_mes = msg
I'm trying to edit an embed by adding the user's name into the embed field. It works, however if there are more than one field it edits all of them. User can add and remove embeds using a command so I don't know what names these fields have.
I want it so that users can react by emojis and depending on which emoji they use, it adds their name to a field.
#bot.event
async def on_raw_reaction_add(payload):
msg = await bot.get_channel(payload.channel_id).fetch_message(payload.message_id)
if not msg.author.bot:
return
if not msg.embeds:
return
embed1 = msg.embeds[0]
embed_dict = embed1.to_dict()
for field in embed_dict["fields"]:
field["value"] += f" {payload.member.name},"
embed2 = discord.Embed.from_dict(embed_dict)
await msg.edit(embed=embed2)
You are iterating through every field, but only one of them needs to be changed.
You can check the PartialEmoji, which can be both standard or custom, used in the reaction through payload.emoji.
embed_dict = embed1.to_dict()
to_modify = {
discord.PartialEmoji.from_str('😂'): 0,
discord.PartialEmoji.from_str('😢'): 1
# etc
}
emoji_used = payload.emoji # returns PartialEmoji containing the emoji
number = to_modify[emoji_used] # select field to modify
embed_dict[number]["value"] += f" {payload.member.name},"
embed2 = discord.Embed.from_dict(embed_dict)
I would like to display via bot how many times a command has been executed.
For this I have already inquired here on the platform and have tried different things. About my code:
e = discord.Embed(color=discord.Colour.green())
user = ctx.author
e.title = f "New suggestion!"
e.set_footer(text=f"{user} | {user.id}")
e.description = f "**__Submitter:__**\n {ctx.author}"
e.add_field(name="__Suggestion:__", value=f"{text}")
e.set_thumbnail(url=ctx.message.author.avatar_url)
e.timestamp = datetime.utcnow()
await channel.send(embed=e)
await ctx.message.add_reaction("✅")
The bot should add Suggestion no. #number in the title, which will go up one at a time when the command is executed. I have already tried the following:
def __init__(self, bot):
self.bot = bot
self.counter = 0
#Shortened
e.title = f "Suggestion no. {self.counter}"
Or just as well:
e.title = f "Suggestion no. {self.counter + 1}
But that didn't help, so how do I make sure that a correct number is displayed and that in case of a bot restart this number continues to rise and doesn't start again from the beginning.
Note that a global event did not work for me somehow!
You actually need to update the counter variable
self.counter += 1
e.title = f"Suggestion no. {self.counter}"
You can save it in a JSON or text file if you want the variable to continue after the bot restarts
Context:
I am using PyTelegramBotAPi or Python Telegram Bot
I have a code I am running when a user starts the conversation.
When the user starts the conversation I need to send him the first picture and a question if He saw something in the picture, the function needs to wait for the user input and return whether he saw it or not.
After that, I will need to keep sending the picture in a loop and wait for the answer and run a bisection algorithm on it.
What I have tried so far:
I tried to use reply markup that waits for a response or an inline keyboard with handlers but I am stuck because my code is running without waiting for the user input.
The code:
#bot.message_handler(func=lambda msg: msg in ['Yes', 'No'])
#bot.message_handler(commands=['start', 'help'])
def main(message):
"""
This is my main function
"""
chat_id = message.chat.id
try:
reply_answer = message.reply_to_message.text
except AttributeError:
reply_answer = '0'
# TODO : should wait for the answer asynchnonossly
def tester(n, reply_answer):
"""
Displays the current candidate to the user and asks them to
check if they see wildfire damages.
"""
print('call......')
bisector.index = n
bot.send_photo(
chat_id=chat_id,
photo=bisector.image.save_image(),
caption=f"Did you see it Yes or No {bisector.date}",
reply_markup=types.ForceReply(selective=True))
# I SHOUL WAIT FOR THE INPUT HERE AND RETURN THE USER INPUT
return eval(reply_answer)
culprit = bisect(bisector.count, lambda x: x, partial(tester, reply_answer=reply_answer) )
bisector.index = culprit
bot.send_message(chat_id, f"Found! First apparition = {bisector.date}")
bot.polling(none_stop=True)
The algorithm I am running on the user input is something like this :
def bisect(n, mapper, tester):
"""
Runs a bisection.
- `n` is the number of elements to be bisected
- `mapper` is a callable that will transform an integer from "0" to "n"
into a value that can be tested
- `tester` returns true if the value is within the "right" range
"""
if n < 1:
raise ValueError('Cannot bissect an empty array')
left = 0
right = n - 1
while left + 1 < right:
mid = int((left + right) / 2)
val = mapper(mid)
tester_values = tester(val) # Here is where I am using the ouput from Telegram bot
if tester_values:
right = mid
else:
left = mid
return mapper(right)
I hope I was clear explaining the problem, feel free to ask any clarification.
If you know something that can point me in the right direction in order to solve this problem, let me know.
I have tried a similar question but I am not getting answers.
You should save your user info in a database. Basic fields would be:
(id, first_name, last_name, username, menu)
What is menu?
Menu keeps user's current state. When a user sends a message to your bot, you check the database to find out about user's current sate.
So if the user doesn't exist, you add them to your users table with menu set to MainMenu or WelcomeMenu or in your case PictureMenu.
Now you're going to have a listener for update function, let's assume each of these a menu.
#bot.message_handler(commands=['start', 'help'])
so when the user sends start you're going to check user's menu field inside the function.
#bot.message_handler(commands=['start', 'help'])
def main(message):
user = fetch_user_from_db(chat_id)
if user.menu == "PictureMenu":
if message.photo is Not None:
photo = message.photo[0].file_id
photo_file = download_photo_from_telegram(photo)
do_other_things()
user.menu = "Picture2Menu";
user.save();
else:
send_message("Please send a photo")
if user.menu == "Picture2Menu":
if message.photo is Not None:
photo = message.photo[0].file_id
photo_file = download_photo_from_telegram(photo)
do_other_things()
user.menu = "Picture3Menu";
user.save();
else:
send_message("Please send a photo")
...
I hope you got it.
I have found the answer:
the trick was to use next_step_handler, and message_handler_function to handle command starting with start and help
Then as suggested by #ALi in his answer, I will be saving the user input answer as well as the question id he replied to in a dictionary where keys are questions and id are the answer.
Once the user has answered all questions, I can run the algorithms on his answer
Here is how it looks like in the code :
user_dict = {}
# Handle '/start' and '/help'
#bot.message_handler(commands=['help', 'start'])
def send_welcome(message):
# initialise the the bisector and
bisector = LandsatBisector(LON, LAT)
indice = 0
message = send_current_candidate(bot, message, bisector, indice)
bot.register_next_step_handler(
message, partial(
process_step, indice, bisector))
def process_step(indice, bisector, message):
# this run a while loop and will that send picture and will stop when the count is reached
response = message.text
user = User.create_get_user(message, bisector=bisector)
if indice < bisector.count - 1:
indice += 1
try:
# get or create
user.responses[bisector.date] = response # save the response
message = send_current_candidate(bot, message, bisector, indice)
bot.register_next_step_handler(
message, partial(
process_step, indice, bisector))
except Exception as e:
print(e)
bot.reply_to(message, 'oooops')
else:
culprit = bisect(bisector.count,
lambda x: x,
partial(
tester_function,
responses=list(user.responses.values())))
bisector.index = culprit
bot.reply_to(message, f"Found! First apparition = {bisector.date}")