详解如何使用 Python 操作 Telegram(电报)机器人(二)

简介: 详解如何使用 Python 操作 Telegram(电报)机器人(二)

接上篇: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 张图片。

b6def82170950d006986c6e8b5ed4f68.png

结果没有问题,并且这 4 张图片是整体作为一条消息发送的。然后我们在代码中还指定了一个 caption 参数,它是做什么的呢?我们点击一下图片就知道了。

e6861ec6365c4d3f74065768c73aad0d.png

点击图片放大查看时,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 参数,我们看看效果。

c33455a975830d72a629553985ff53f7.png

此时图片会和文字一起显示,当然你也可以不指定 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()

测试一下:

76a13642cafd00df2f1208a544a17735.png

结果正常,只是因为视频和图片是一起返回的,所以没有预览功能,需要点击之后才会播放。并且我们只给视频指定了 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()

测试一下:

92c44029b024758d8e252aa36744cf58.png

怎么样,是不是很有趣呢?另外 caption 还可以是富文本,只需将 parse_mode 参数指定为 "HTML"、"Markdown" 或 "MarkdownV2" 即可。

关于机器人如何回复不同种类的消息,以及同时回复多条消息,相关内容我们就说完了。有了这些功能,我们的机器人就已经很强大了,你也可以把它和公司的业务结合起来。

比如创建一个命令:/get,它的功能如下。

d0228ec7406a529855469c34719f0d75.png

然后在代码中添加一个 CommandHandler("get", get_table),便可让用户通过 Telegram 查询数据库表,当然这里只是打个比方,具体怎么做取决于你的想法。另外多说一句,如果你希望输入 / 之后能像上面那样有提示,那么需要通过 BotFather 进行设置。

1c76d05adc765b952d61d695bab7d233.png

要强调的是,这种方式只是起到一个提示作用,提示机器人支持 /get 命令。但机器人实际上是否支持,取决于代码中是否为机器人实现了 /get。所以当我们在代码中为机器人添加完命令之后,可以再通过 Edit Commands 进行设置,这样当用户输入 / 之后,机器人有哪些命令以及描述都会显示出来。

当然啦,如果你不通过 Edit Commands 进行设置的话,也是可以的,只是用户输入 / 之后不会有提示罢了,但命令是会回复的,只要在代码中实现了。同理,如果通过 Edit Commands 设置了,但代码中没实现,那么该命令也不会有效果。


自定义按钮



虽然目前的机器人已经很强大了,但是还不够,我们看一下 BotFather。

4210a09a6b81488460c566cfab3fc037.png

你会发现它下面带了很多的按钮,点击按钮之后会执行相应的逻辑,那我们要怎么实现这些按钮呢?

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()

测试一下:

d30e164585486fa53fb4aaa96f8ee422.png

此时按钮就实现了,由于在 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()

测试一下效果:

7ff82439e95c02dd0435fa0cf51f95d8.png

点击油管和 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()

测试一下:

98626c4147a0b16fb391357c9965e2b3.png

然后点击按钮,看看文字内容有没有发生改变。

6d925ad47f819b91577f429513e591b6.png

点击按钮,文字的内容被替换了。所以当机器人回复一条消息时,只需知道 chat_id 即可。但如果是修改某条消息,那么除了 chat_id 之外,还要知道 message_id。

修改文字调用的方法是 edit_message_text,但除了修改文字之外,还可以修改其它内容。

8b800240b9dfc78b14db6bfba5e13669.png

比如修改媒体文件,修改媒体文件的 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()

测试一下:

38734b401f314c66e477a492dfb0f674.png

此时点击按钮下一张,就会返回下一张图片,同理也可以返回上一张图片。如果已经是最后一张图片了,那么点击下一张,会返回第一张图片。

但问题来了,程序要如何得知用户正在浏览的是第几张图片呢?显然要借助于按钮。在创建按钮时,参数 callback_data 里面保存了 index,当点击下一张或上一张时,更新 index,返回新的图片,同时刷新按钮。

以上返回的是图片,你也可以换成视频,并增加一些点赞、是否喜欢等按钮。


小结



以上就是 Python 操作 Telegram 相关的内容,当然这里只介绍了一部分,还有一些更复杂的功能没有说,比如按钮的嵌套等等。另外目前是用户和机器人一对一私聊,但我们还可以创建一个组,让机器人回复组成员的消息。而关于这些内容,后续有空补上,本文就先到这儿,写的有点累了。

不知道什么原因,对技术的热情越来越冷淡,不像前几年刚毕业那会儿,看到有意思的,就会花时间去研究。但现在似乎没有当初的那种激情了,以前一篇文章三五万字轻轻松松,而现在刚写两万多字就感觉没有动力了,不过这也算是久违的长文了。

相关文章
|
21天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
17天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2564 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
15天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
13天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
17天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1556 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
19天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
830 14
|
14天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
622 7
|
8天前
|
Docker 容器
Docker操作 (五)
Docker操作 (五)
170 69
|
8天前
|
Docker 容器
Docker操作 (三)
Docker操作 (三)
167 69
|
19天前
|
人工智能 自动驾驶 机器人
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
过去22个月,AI发展速度超过任何历史时期,但我们依然还处于AGI变革的早期。生成式AI最大的想象力,绝不是在手机屏幕上做一两个新的超级app,而是接管数字世界,改变物理世界。
629 53
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界