接上篇:https://developer.aliyun.com/article/1617543
媒体组
现在我们已经知道如何让机器人回复不同种类的消息了,但如果我想实现更复杂的功能,比如同时发送多张图片、多个视频,并且还配带文字,要怎么做呢?可能有人觉得这还不简单,写个循环不就行了,比如要发送 5 个视频,那么调用 5 次 send_video 方法不就好了。
首先这是一种方法,但循环 5 次,那么这 5 个视频是作为不同的消息分开发送的。更多时候,我们是希望作为一个整体发送,那么此时可以使用媒体组功能。
from telegram import Update, InputMediaPhoto from telegram.ext import ( ApplicationBuilder, ContextTypes, CommandHandler ) from proxy import PROXY BOT_API_TOKEN = "6485526535:AAEvGr9EDqtc4QPehkgohH6gczOTO5RIYRE" async def send_media_group(update: Update, context: ContextTypes.DEFAULT_TYPE): media_group = [ # 可以是 URL、bytes 对象、文件句柄、file_id InputMediaPhoto(open('satori1.png', "rb"), caption="古"), InputMediaPhoto(open('satori2.png', "rb"), caption="明"), InputMediaPhoto(open('satori3.png', "rb"), caption="地"), InputMediaPhoto(open('satori4.png', "rb"), caption="觉") ] # 发送媒体组 await context.bot.send_media_group( chat_id=update.message.chat.id, media=media_group ) application = ApplicationBuilder().token(BOT_API_TOKEN).proxy(PROXY).build() download_handler = CommandHandler("satori", send_media_group) application.add_handler(download_handler) application.run_polling()
我们输入命令 /satori,应该会返回 4 张图片。
结果没有问题,并且这 4 张图片是整体作为一条消息发送的。然后我们在代码中还指定了一个 caption 参数,它是做什么的呢?我们点击一下图片就知道了。
点击图片放大查看时,captaion 会显示在图片下方。另外,如果发送了多张图片,但只有一张图片指定了 caption 参数,那么该 caption 会和图片一起显示,我们举例说明。
async def send_media_group(update: Update, context: ContextTypes.DEFAULT_TYPE): caption = "+v ❥(^_-) 解锁地灵殿隐藏福利" media_group = [ # 可以是 URL、bytes 对象、文件句柄、file_id InputMediaPhoto(open('satori1.png', "rb")), InputMediaPhoto(open('satori2.png', "rb")), InputMediaPhoto(open('satori3.png', "rb"), caption=caption), InputMediaPhoto(open('satori4.png', "rb")) ] # 发送媒体组 await context.bot.send_media_group( chat_id=update.message.chat.id, media=media_group )
只有一张图片指定了 caption 参数,我们看看效果。
此时图片会和文字一起显示,当然你也可以不指定 caption 参数,而是在发送完图片之后,再调用一次 send_message。这种做法也是可以的,只不过此时图片和文字会作为两条消息分开显示。
以上是发送图片,除了图片之外还可以发送音频、视频、文档,并且只支持这 4 种。但要注意:它们不能混在一起发,只有图片和视频可以,我们测试一下。
from telegram import ( Update, InputMediaPhoto, InputMediaAudio, InputMediaVideo, InputMediaDocument ) from telegram.ext import ( ApplicationBuilder, ContextTypes, CommandHandler ) from proxy import PROXY BOT_API_TOKEN = "6485526535:AAEvGr9EDqtc4QPehkgohH6gczOTO5RIYRE" async def send_media_group(update: Update, context: ContextTypes.DEFAULT_TYPE): video_caption = ( "这游戏我玩不下去了,装备喂养和贴膜就算了," "但自定义词条我是真忍不了,洗不出来,根本洗不出来。" ) media_group = [ InputMediaPhoto(open("satori1.png", "rb")), InputMediaVideo(open("DNF 装备销毁.mp4", "rb"), caption=video_caption), # 也支持发送音频和文档,但不能混在一起 # InputMediaAudio(open("3rd eye.mp3", "rb")), # InputMediaDocument(open('OpenAI.pdf', 'rb')) ] # 发送媒体组 await context.bot.send_media_group( chat_id=update.message.chat.id, media=media_group ) application = ApplicationBuilder().token(BOT_API_TOKEN).proxy(PROXY).build() download_handler = CommandHandler("test_media_group", send_media_group) application.add_handler(download_handler) application.run_polling()
测试一下:
结果正常,只是因为视频和图片是一起返回的,所以没有预览功能,需要点击之后才会播放。并且我们只给视频指定了 caption 参数,所以文字直接显示在了下方,如果媒体组中有多个 caption,那么就不会单独显示了,需要点击放大之后才能看到。
当然啦,如果你不需要同时发送多个媒体文件,那么就没必要调用 send_media_group 方法了,直接使用之前的方法即可。
- send_photo;
- send_audio;
- send_video;
- send_document;
这些方法一次性只能发送一个媒体文件,比如发送视频。
async def send_video(update: Update, context: ContextTypes.DEFAULT_TYPE): video_caption = ( "这游戏我玩不下去了,装备喂养和贴膜就算了," "但自定义词条我是真忍不了,洗不出来,根本洗不出来。" ) await context.bot.send_video( chat_id=update.message.chat.id, video="DNF 装备销毁.mp4", caption=video_caption, # 让 caption 显示在上方,默认显示在下方 show_caption_above_media=True, ) application = ApplicationBuilder().token(BOT_API_TOKEN).proxy(PROXY).build() download_handler = CommandHandler("destroy", send_video) application.add_handler(download_handler) application.run_polling()
测试一下:
怎么样,是不是很有趣呢?另外 caption 还可以是富文本,只需将 parse_mode 参数指定为 "HTML"、"Markdown" 或 "MarkdownV2" 即可。
关于机器人如何回复不同种类的消息,以及同时回复多条消息,相关内容我们就说完了。有了这些功能,我们的机器人就已经很强大了,你也可以把它和公司的业务结合起来。
比如创建一个命令:/get,它的功能如下。
然后在代码中添加一个 CommandHandler("get", get_table),便可让用户通过 Telegram 查询数据库表,当然这里只是打个比方,具体怎么做取决于你的想法。另外多说一句,如果你希望输入 / 之后能像上面那样有提示,那么需要通过 BotFather 进行设置。
要强调的是,这种方式只是起到一个提示作用,提示机器人支持 /get 命令。但机器人实际上是否支持,取决于代码中是否为机器人实现了 /get。所以当我们在代码中为机器人添加完命令之后,可以再通过 Edit Commands 进行设置,这样当用户输入 / 之后,机器人有哪些命令以及描述都会显示出来。
当然啦,如果你不通过 Edit Commands 进行设置的话,也是可以的,只是用户输入 / 之后不会有提示罢了,但命令是会回复的,只要在代码中实现了。同理,如果通过 Edit Commands 设置了,但代码中没实现,那么该命令也不会有效果。
自定义按钮
虽然目前的机器人已经很强大了,但是还不够,我们看一下 BotFather。
你会发现它下面带了很多的按钮,点击按钮之后会执行相应的逻辑,那我们要怎么实现这些按钮呢?
from telegram import ( Update, InlineKeyboardMarkup, InlineKeyboardButton, ) from telegram.ext import ( ApplicationBuilder, ContextTypes, CommandHandler ) from proxy import PROXY BOT_API_TOKEN = "6485526535:AAEvGr9EDqtc4QPehkgohH6gczOTO5RIYRE" async def add_button(update: Update, context: ContextTypes.DEFAULT_TYPE): text = "作为<i>程序猿</i>,你最喜欢哪种编程语言呢?" # 设置按钮 reply_markup = InlineKeyboardMarkup([ # 第一行 [InlineKeyboardButton(text="Python", url="https://www.python.org")], # 第二行 [InlineKeyboardButton(text="Golang", url="https://golang.org")], # 第三行 [InlineKeyboardButton(text="Rust", url="https://www.rust-lang.org")], # 第四行 [InlineKeyboardButton(text="Zig", url="https://ziglang.org")], ]) await context.bot.send_message( chat_id=update.message.chat.id, text=text, parse_mode="HTML", reply_markup=reply_markup ) application = ApplicationBuilder().token(BOT_API_TOKEN).proxy(PROXY).build() download_handler = CommandHandler("language", add_button) application.add_handler(download_handler) application.run_polling()
测试一下:
此时按钮就实现了,由于在 InlineKeyboardButton 里面指定的是 url,所以这是跳转按钮,点击之后会打开指定的页面。并且按钮的右上角还有一个小箭头,表示按钮是跳转按钮。
但除了跳转按钮之外,还有回调按钮,也就是点击按钮之后会执行回调函数,我们举例说明。
from telegram import ( Update, InlineKeyboardMarkup, InlineKeyboardButton, ) from telegram.ext import ( ApplicationBuilder, ContextTypes, CommandHandler, CallbackQueryHandler, ) from proxy import PROXY BOT_API_TOKEN = "6485526535:AAEvGr9EDqtc4QPehkgohH6gczOTO5RIYRE" async def add_button(update: Update, context: ContextTypes.DEFAULT_TYPE): text = "o(╥﹏╥)o😂╭(╯^╰)╮" # 设置按钮 reply_markup = InlineKeyboardMarkup([ # 第一行,两个跳转按钮 [InlineKeyboardButton(text="百度", url="https://www.baidu.com"), InlineKeyboardButton(text="谷歌", url="https://www.google.com"),], # 第二行,两个回调按钮 [InlineKeyboardButton(text="油管", callback_data="youtube"), InlineKeyboardButton(text="B站", callback_data="bilibili"),], ]) await context.bot.send_message( chat_id=update.message.chat.id, text=text, reply_markup=reply_markup ) async def callback(update: Update, context: ContextTypes.DEFAULT_TYPE): # 当点击回调按钮时,会执行相应的回调函数 cb_data = update.callback_query.data # 回调按钮中指定的 callback_data if cb_data == "youtube": text = "欢迎来到油管" elif cb_data == "bilibili": text = "欢迎来到 B 站" else: text = "Unknown Website" await context.bot.send_message( # 注意:这里是 update.callback_query.message.chat.id chat_id=update.callback_query.message.chat.id, text=text ) application = ApplicationBuilder().token(BOT_API_TOKEN).proxy(PROXY).build() # 添加 Handler application.add_handler( CommandHandler("website", add_button) ) # 处理回调的 Handler,否则点击按钮不会有效果 application.add_handler( CallbackQueryHandler(callback) ) application.run_polling()
测试一下效果:
点击油管和 B站的时候会执行回调函数,结果没有问题。但是我们发现,这些文字是单独发送的,那可不可以本地修改呢,也就是将按钮上方的文字替换掉。答案是可以的,我们来测试一下。
from telegram import ( Update, InlineKeyboardMarkup, InlineKeyboardButton, ) from telegram.ext import ( ApplicationBuilder, ContextTypes, CommandHandler, CallbackQueryHandler, ) from proxy import PROXY BOT_API_TOKEN = "6485526535:AAEvGr9EDqtc4QPehkgohH6gczOTO5RIYRE" def get_reply_markup(): reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton(text="古明地觉", callback_data="satori")], [InlineKeyboardButton(text="古明地恋", callback_data="koishi")], [InlineKeyboardButton(text="雾雨魔理沙", callback_data="marisa")], [InlineKeyboardButton(text="琪露诺", callback_data="cirno")], ]) return reply_markup async def add_button(update: Update, context: ContextTypes.DEFAULT_TYPE): text = "点击想要攻略的角色" await context.bot.send_message( chat_id=update.message.chat.id, text=text, reply_markup=get_reply_markup() ) async def callback(update: Update, context: ContextTypes.DEFAULT_TYPE): cb_data = update.callback_query.data if cb_data == "satori": img = "你将要攻略古明地觉" elif cb_data == "koishi": img = "你将要攻略古明地恋" elif cb_data == "marisa": img = "你将要攻略雾雨魔理沙" elif cb_data == "cirno": img = "你将要攻略琪露诺" else: raise RuntimeError("Unreachable") # 点击按钮之后,要对上方的文字进行修改,替换成其它内容 # 所以这相当于编辑已有消息,既然要编辑,那么除了 chat_id 之外还要指定 message_id # 因为是回调,所以要多调用一次 callback_query message_id = update.callback_query.message.message_id chat_id = update.callback_query.message.chat.id # 调用 edit_message_media 方法,编辑消息 await context.bot.edit_message_text( text=img, chat_id=chat_id, message_id=message_id, reply_markup=get_reply_markup() ) application = ApplicationBuilder().token(BOT_API_TOKEN).proxy(PROXY).build() application.add_handler( CommandHandler("gogogo", add_button) ) application.add_handler( CallbackQueryHandler(callback) ) application.run_polling()
测试一下:
然后点击按钮,看看文字内容有没有发生改变。
点击按钮,文字的内容被替换了。所以当机器人回复一条消息时,只需知道 chat_id 即可。但如果是修改某条消息,那么除了 chat_id 之外,还要知道 message_id。
修改文字调用的方法是 edit_message_text,但除了修改文字之外,还可以修改其它内容。
比如修改媒体文件,修改媒体文件的 caption,修改按钮等等。
修改消息综合案例
关于修改消息我们已经知道怎么做了,下面来做一个综合案例。假设当前有 N 张图片,用户默认会看到第一张,然后点击按钮可以查看下一张图片,当然也可以查看上一张。那么这个需求怎么实现呢?
from telegram import ( Update, InlineKeyboardMarkup, InlineKeyboardButton, InputMediaPhoto ) from telegram.ext import ( ApplicationBuilder, ContextTypes, CommandHandler, CallbackQueryHandler, ) from proxy import PROXY BOT_API_TOKEN = "6485526535:AAEvGr9EDqtc4QPehkgohH6gczOTO5RIYRE" # 这里我就用 4 张图片为例 IMAGES = ["satori.png", "koishi.png", "marisa.png", "cirno.png"] def get_navigation_buttons(index): reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton(text="上一张", callback_data=f"prev:{index}"), InlineKeyboardButton(text="下一张", callback_data=f"next:{index}")], ]) return reply_markup async def get_pic(update: Update, context: ContextTypes.DEFAULT_TYPE): # 默认发送第一张图片 await context.bot.send_photo( chat_id=update.message.chat.id, photo=IMAGES[0], caption=f"正在浏览第 1 / {len(IMAGES)} 张图片", reply_markup=get_navigation_buttons(0) ) async def callback(update: Update, context: ContextTypes.DEFAULT_TYPE): # 点击按钮,触发回调 op, index = update.callback_query.data.split(":") if op == "prev": index = (int(index) - 1) % len(IMAGES) else: # op == "next" index = (int(index) + 1) % len(IMAGES) # int(index) 减 1 和加 1 之后,就是上一张图片和下一张图片的索引 # 但这里又对 len(IMAGES) 进行取模,主要是为了实现循环浏览 # 比如第一张的上一张会返回最后一张,最后一张的下一张会返回第一张 await context.bot.edit_message_media( chat_id=update.callback_query.message.chat.id, message_id=update.callback_query.message.message_id, media=InputMediaPhoto( open(IMAGES[index], "rb"), caption=f"正在浏览第 {index + 1} / {len(IMAGES)} 张图片" ), reply_markup=get_navigation_buttons(index) ) application = ApplicationBuilder().token(BOT_API_TOKEN).proxy(PROXY).build() application.add_handler( CommandHandler("get_pic", get_pic) ) application.add_handler( CallbackQueryHandler(callback) ) application.run_polling()
测试一下:
此时点击按钮下一张,就会返回下一张图片,同理也可以返回上一张图片。如果已经是最后一张图片了,那么点击下一张,会返回第一张图片。
但问题来了,程序要如何得知用户正在浏览的是第几张图片呢?显然要借助于按钮。在创建按钮时,参数 callback_data 里面保存了 index,当点击下一张或上一张时,更新 index,返回新的图片,同时刷新按钮。
以上返回的是图片,你也可以换成视频,并增加一些点赞、是否喜欢等按钮。
小结
以上就是 Python 操作 Telegram 相关的内容,当然这里只介绍了一部分,还有一些更复杂的功能没有说,比如按钮的嵌套等等。另外目前是用户和机器人一对一私聊,但我们还可以创建一个组,让机器人回复组成员的消息。而关于这些内容,后续有空补上,本文就先到这儿,写的有点累了。
不知道什么原因,对技术的热情越来越冷淡,不像前几年刚毕业那会儿,看到有意思的,就会花时间去研究。但现在似乎没有当初的那种激情了,以前一篇文章三五万字轻轻松松,而现在刚写两万多字就感觉没有动力了,不过这也算是久违的长文了。