I'm building a Telegram bot that interacts with users to edit and upload videos using the python-telegram-bot
library. The bot retrieves videos from Reddit, edits them using moviepy
, and then asks the user for additional input through a conversation handler. However, I'm encountering a couple of issues:
AttributeError: 'NoneType' object has no attribute 'reply_text': I receive this error when the bot tries to call reply_text on update.message.reply_text("test")
Conversation Skipping Input: When I use update.effective_message.reply_text
(which isnt None) to ask a question, the bot skips waiting for the user's answer and starts re-uploading the next video in the loop.
Here is the relevant code snippet for the start_editor_questions
function, which is used in a conversation handler:
async def start_editor_questions(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Starts the conversation by sending the video and asking the first question."""
output_filename = context.user_data.get('output_filename')
await context.bot.send_video(chat_id=update.effective_chat.id, video=open(output_filename, 'rb'))
await update.effective_message.reply_text("What height do you want? (h1/h2)")
return HEIGHT
Here is my main:
if __name__ == '__main__':
application = Application.builder().token('YOUR_TOKEN').build()
# Register handlers
start_handler = CommandHandler('start', start)
application.add_handler(start_handler)
application.add_handler(CallbackQueryHandler(button_handler))
conv_handler = ConversationHandler(
entry_points=[CommandHandler("start", start_editor_questions)],
states={
HEIGHT: [MessageHandler(filters.TEXT & ~filters.COMMAND, height_response)],
TRIM: [MessageHandler(filters.TEXT & ~filters.COMMAND, trim_response)],
WATERMARK_COLOR: [MessageHandler(filters.TEXT & ~filters.COMMAND, watermark_color_response)],
WATERMARK_OPACITY: [MessageHandler(filters.TEXT & ~filters.COMMAND, watermark_opacity_response)],
TITLE: [MessageHandler(filters.TEXT & ~filters.COMMAND, title_response)],
TITLE_COLOR: [MessageHandler(filters.TEXT & ~filters.COMMAND, title_color_response)],
CAPTION: [MessageHandler(filters.TEXT & ~filters.COMMAND, caption_response)],
},
fallbacks=[CommandHandler("cancel", cancel)],
)
application.add_handler(conv_handler)
application.run_polling(allowed_updates=Update.ALL_TYPES)
I’ve included just the most relevant parts of the code. The complete setup includes additional functions and handlers. For example, here is the function that calls start_editor_questions
:
async def process_account_videos(update: Update, context: ContextTypes.DEFAULT_TYPE, account_name, link, remaining_posts):
while remaining_posts < 3:
for video in videos:
await start_editor_questions(update, context)
Questions:
I would greatly appreciate any help or suggestions on how to fix these issues. Please let me know if you need any more code.
EDIT: Added process_account_videos function
async def process_account_videos(update: Update, context: ContextTypes.DEFAULT_TYPE, account_name, link, remaining_posts):
print("process_account_videos")
limit = 1
after = None
retrieved_list_file = 'retrieved_list.csv'
file_path = retrieved_list_file
required_columns = ['time', 'accountname', 'special_name', 'after_retr']
if remaining_posts == 0:
return
if not os.path.exists(file_path):
df = pd.DataFrame(columns=required_columns)
df.to_csv(file_path, index=False)
print(f"Created new CSV file: {file_path}")
else:
df = pd.read_csv(file_path)
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
print(f"Warning: Missing columns {missing_columns}. Recreating the file.")
df = pd.DataFrame(columns=required_columns)
df.to_csv(file_path, index=False)
print(f"Recreated CSV file: {file_path}")
retrieved_list = pd.read_csv(retrieved_list_file)
account_data = retrieved_list[retrieved_list['accountname'] == account_name]
i = 1
while loop_number < 3:
account_data = retrieved_list[retrieved_list['accountname'] == account_name]
check = False
if not account_data.empty:
check = True
account_data['time'] = pd.to_datetime(account_data['time'], errors='coerce')
account_data = account_data.sort_values(by='time', ascending=False)
special_name_list = account_data['special_name'].values
after = account_data.iloc[0]['after_retr']
url = f"{link}.json?limit={limit}"
if after:
url += f"&after={after}"
print(url)
response = requests.get(url, headers={"User-Agent": "username"})
if response.status_code == 200:
data = response.json()
posts = data['data']['children']
account_folder = f"./downloads/{account_name}"
os.makedirs(account_folder, exist_ok=True)
output_folder = f"./downloads/{account_name}/output"
os.makedirs(output_folder, exist_ok=True)
for post in posts:
if check is True:
if post['data']['name'] in special_name_list:
continue
if post['data']['secure_media'] is None or post['data']['over_18'] is False:
new_row = {
'time': pd.Timestamp.now(),
'accountname': account_name,
'special_name': post['data']['name'],
'after_retr': data['data'].get("after")
}
retrieved_list = pd.concat([retrieved_list, pd.DataFrame([new_row])], ignore_index=True)
retrieved_list.to_csv(retrieved_list_file, index=False)
continue
video_url = post['data']['secure_media']['reddit_video']['fallback_url']
base_video_url = video_url.split("/DASH_")[0]
audio_url = f"{base_video_url}/DASH_AUDIO_128.mp4"
audio_response = requests.get(audio_url, stream=True)
if audio_response.status_code != 200:
audio_url = f"{base_video_url}/DASH_audio.mp4"
audio_response = requests.get(audio_url, stream=True)
video_filename = os.path.join(account_folder, f"video{i}.mp4")
with open(video_filename, "wb") as f:
video_response = requests.get(video_url, stream=True)
f.write(video_response.content)
if audio_response.status_code == 200:
audio_filename = os.path.join(account_folder, f"audio{i}.mp3")
with open(audio_filename, "wb") as f:
f.write(audio_response.content)
output_filename = os.path.join(output_folder, f"output{i}.mp4")
# Using subprocess to merge video and audio
subprocess.run(["ffmpeg", "-i", video_filename, "-i", audio_filename, "-c", "copy", output_filename])
else:
output_filename = os.path.join(output_folder, f"output{i}.mp4")
os.rename(video_filename, output_filename)
new_row = {
'time': pd.Timestamp.now(),
'accountname': account_name,
'special_name': post['data']['name'],
'after_retr': data['data'].get("after")
}
retrieved_list = pd.concat([retrieved_list, pd.DataFrame([new_row])], ignore_index=True)
retrieved_list.to_csv(retrieved_list_file, index=False)
# Increment the loop_number after each iteration
loop_number += 1
i += 1
context.user_data['account_name'] = account_name
context.user_data['output_filename'] = output_filename
await start_editor_questions(update, context)
output_folder = context.user_data.get('output_folder')
caption_name = context.user_data.get('caption')
await upload(output_folder, caption_name) # Implement this function according to your needs
if loop_number >= remaining_posts:
break
else:
print(f"Failed to connect to Reddit. Status code: {response.status_code}")
break
Answering both of your questions:
application.run_polling(allowed_updates=Update.ALL_TYPES)
, so do not expect to have the update.message
available on every update.process_account_videos
is being used, because this function seems to be handling every single update: Update
(either a message, message reaction, edited message etc.) and repeating the video upload routine - while remaining_posts < 3
- for each of those updates, that is: yes, it would be looping the upload routine in a row if the user send a single message or any other update