MadelineProto实战教程:获取消息所有图片(解决只下最后一张)
2025-03-28 02:34:55
MadelineProto 实战:获取消息中的所有图片,告别只拿到最后一张的烦恼
遇到个有点挠头的问题:用 $MadelineProto->messages->getHistory()
获取了频道里的最新消息,想把消息里的所有图片都扒拉下来,结果试了半天,咋只拿到了最后一张图?
原先的代码瞅着是这样的:
<?php
// 假设 $this->MadelineProto 已经初始化好了
// $channels 是一个包含频道 ID 或用户名的数组
$messs = [];
foreach ($channels as $channel) {
// 获取每个频道的最新一条消息
$messs[$channel] = $this->MadelineProto->messages->getHistory(['peer' => $channel, 'limit' => 1]);
}
$posts = [];
foreach ($messs as $key => $messages) {
// $messages 包含 'messages', 'chats', 'users' 等键
foreach ($messages['messages'] as $message) {
$text = $message['message'] ?? ''; // 消息文本
$files = null; // 存放下载的文件路径
// 问题就出在这:只检查了 messageMediaPhoto
if (isset($message['media']) && $message['media']['_'] === 'messageMediaPhoto') {
// downloadToDir 只会下载 $message['media'] 代表的那一个媒体
$file = $this->MadelineProto->downloadToDir($message['media'], '../../img');
$files[] = $file; // 这里永远只有一个文件被加入
}
// 如果消息文本不为空(或者按需调整逻辑)
if ($text !== '') {
// $posts 的结构似乎也有点问题,这里只是沿用原逻辑示意
$posts[] = ['media' => [$key => $files] ];
}
}
}
// 最后 $posts 里的 'media' 值总是只有一个文件路径(如果消息是单图)或者 null/空数组
?>
上面这段代码的问题挺明显的:它只考虑了消息媒体类型是 messageMediaPhoto
的情况。这在处理单张图片的消息时没问题,但要是消息里包含了一个图片集(就是咱们平时在 Telegram 里看到的那种几张图组合在一起的“相册”),那麻烦就来了。
为什么只拿到一张图?深挖 Telegram 的媒体消息
这事儿得从 Telegram 处理媒体消息的方式说起。
-
单张图片消息 (
messageMediaPhoto
) : 当你发送一张图片时,消息的media
字段类型就是messageMediaPhoto
。这个结构体直接包含了这张图片的各种信息(不同尺寸、文件引用等)。你的原始代码恰好能处理这种情况,$this->MadelineProto->downloadToDir($message['media'], ...)
就能直接下载这张图。 -
图片集/媒体组 (
messageMediaGrouped
) : 当你一次性发送多张图片或视频(通常会组合成一个相册显示)时,Telegram 的处理方式就变了。它不会为每张图片单独发送一条消息。相反,它会发送一组消息,这些消息共享一个grouped_id
。getHistory
返回的消息列表中,对于一个图片集,可能只返回其中一条消息(通常是最后那条?或者是包含标题的那条?行为可能变化),这条消息的media
字段可能是messageMediaPhoto
(代表相册里的某一张,经常是最后一张),但同时会有一个grouped_id
字段。- 更关键的是, 有时
getHistory
返回的主消息,其media
字段类型直接就是messageMediaGrouped
!这个messageMediaGrouped
结构里面,才包含了组成这个相册的所有媒体项(图片或视频)的引用或基本信息。
你的代码只检查了 messageMediaPhoto
,完全忽略了 messageMediaGrouped
的可能性,也忽略了即使是 messageMediaPhoto
也可能只是媒体组一部分的情况(通过 grouped_id
判断)。当遇到图片集时,$message['media']
如果恰好是最后那张图的 messageMediaPhoto
,你就只下载了它;如果 getHistory
返回的消息其 media
类型是 messageMediaGrouped
,你的 if
条件根本就不满足,自然一张图都下载不了(或者 files
为空)。
所以,要想拿到所有图片,咱们得对症下药,专门处理媒体组的情况。
解决方案:揪出所有图片,一个都不能少
要解决这个问题,核心思路就是改造代码,让它能识别并正确处理包含多张图片的 messageMediaGrouped
类型,同时也要考虑到用户可能把图片作为文件发送的情况 (messageMediaDocument
)。
方案一:优先处理 messageMediaGrouped
这是最直接的办法,专门针对 Telegram 的“相册”功能。
原理与作用:
检查返回消息中的 media
字段。如果它的类型 (_
键) 是 messageMediaGrouped
,就说明这是个媒体集。messageMediaGrouped
结构内部通常会包含一个数组(键名可能是 photos
、documents
或一个更通用的 media
列表,具体看 MadelineProto 的解析结果,但思路是遍历这个列表),里面有构成相册的每一项媒体信息。咱们需要遍历这个内部列表,对列表中的每一个媒体项调用下载方法。
代码示例:
<?php
// ... (前面的代码,获取 $messs 保持不变) ...
$posts = [];
foreach ($messs as $channel_id => $messages) {
foreach ($messages['messages'] as $message) {
$text = $message['message'] ?? '';
$downloaded_files = []; // 用于收集当前消息下载的所有文件路径
if (isset($message['media'])) {
$media_type = $message['media']['_'] ?? 'unknown';
// 情况一:处理媒体组 (相册)
if ($media_type === 'messageMediaGrouped') {
// 检查 'grouped_media' 是否存在并且是个数组
// 注意: MadelineProto 不同版本或 Telegram API 更新可能导致这里的具体结构名变化
// 可能需要打印 $message['media'] 确认实际结构
// 假设 grouped media 信息在 $message['media']['grouped_media'] 数组里
if (isset($message['media']['grouped_media']) && is_array($message['media']['grouped_media'])) {
echo "检测到媒体组 (Grouped Media),准备下载...\n";
foreach ($message['media']['grouped_media'] as $grouped_item_media) {
// $grouped_item_media 可能直接是 Photo 或 Document 对象
try {
// 对组内的每个媒体项进行下载
$file_path = $this->MadelineProto->downloadToDir($grouped_item_media, '../../img');
if ($file_path) {
$downloaded_files[] = $file_path;
echo "下载媒体组成员成功: " . $file_path . "\n";
} else {
echo "下载媒体组成员失败 (可能不是可下载类型或出错)。\n";
}
} catch (\Throwable $e) {
// 捕获下载过程中可能发生的异常
echo "下载媒体组成员时发生错误: " . $e->getMessage() . "\n";
}
}
} else {
// 如果 messageMediaGrouped 结构不是预期的,打印出来看看
echo "警告: 发现 messageMediaGrouped,但内部结构未知或不包含 'grouped_media' 数组。\n";
// 你可能需要根据实际打印出的 $message['media'] 结构调整代码
// print_r($message['media']);
// 有时候,可能需要使用 grouped_id 配合其他方法获取所有消息
}
}
// 情况二:处理单张图片
elseif ($media_type === 'messageMediaPhoto') {
echo "检测到单张图片 (messageMediaPhoto),准备下载...\n";
try {
$file_path = $this->MadelineProto->downloadToDir($message['media'], '../../img');
if ($file_path) {
$downloaded_files[] = $file_path;
echo "下载单张图片成功: " . $file_path . "\n";
} else {
echo "下载单张图片失败。\n";
}
} catch (\Throwable $e) {
echo "下载单张图片时发生错误: " . $e->getMessage() . "\n";
}
}
// 情况三:处理作为文件发送的图片 (可选,但建议加上)
elseif ($media_type === 'messageMediaDocument') {
// 检查文档是不是图片类型
$mime_type = $message['media']['document']['mime_type'] ?? '';
if (strpos($mime_type, 'image/') === 0) { // 判断 MIME 类型是否以 'image/' 开头
echo "检测到作为文件发送的图片 (messageMediaDocument),准备下载...\n";
try {
$file_path = $this->MadelineProto->downloadToDir($message['media'], '../../img');
if ($file_path) {
$downloaded_files[] = $file_path;
echo "下载文件图片成功: " . $file_path . "\n";
} else {
echo "下载文件图片失败。\n";
}
} catch (\Throwable $e) {
echo "下载文件图片时发生错误: " . $e->getMessage() . "\n";
}
} else {
echo "找到一个文档,但不是图片类型 (MIME: " . $mime_type . "),跳过。\n";
}
}
// 其他媒体类型... 可以根据需要添加更多 elseif
else {
echo "未处理的媒体类型: " . $media_type . "\n";
}
} else {
// 消息没有媒体字段
echo "消息无媒体内容。\n";
}
// 如果下载到了文件,才加入 $posts 数组
if (!empty($downloaded_files)) {
// 这里可以根据你的需求调整 $posts 的结构
// 例如,直接关联频道 ID 和文件列表
$posts[] = [
'channel_id' => $channel_id, // 使用频道ID作为标识
'message_id' => $message['id'], // 记录消息ID,方便追踪
'text' => $text, // 消息文本
'files' => $downloaded_files // 包含本次下载的所有文件路径
];
} else if ($text !== '') {
// 如果没有下载到文件,但有文本,根据需要决定是否记录
// $posts[] = ['channel_id' => $channel_id, 'message_id' => $message['id'], 'text' => $text, 'files' => []];
echo "消息有文本但无符合条件的图片媒体。\n";
}
}
}
// 现在 $posts 包含了处理过的消息信息,其中 'files' 是个数组,可能有多个路径
// print_r($posts);
?>
注意点与 grouped_id
:
- 上面代码假设
messageMediaGrouped
内部直接包含了可供下载的媒体项数组 (暂定为grouped_media
)。你需要打印$message['media']
确认实际的数组键名。MadelineProto 可能会简化结构,例如直接提供一个media
对象的列表。 - 另一种可能是,
getHistory
返回的消息即使media._
不是messageMediaGrouped
,但只要存在grouped_id
字段,就表示它是媒体组的一部分。这种情况下,更稳妥的方式可能是:- 记录下这个
grouped_id
。 - 使用
$MadelineProto->messages->getMessages(['id' => [...IDs...]]);
或者专门处理 grouped media 的方法(如果 MadelineProto 提供的话)来获取这个组里的所有消息。然后遍历这些消息下载媒体。不过这种方式会增加 API 请求次数。
- 记录下这个
- 上面的示例代码优先采用了检查
messageMediaGrouped
的直接方式,因为它更简洁。如果这种方式在你的场景下获取不全,再考虑基于grouped_id
的方案。
安全建议:
- 检查下载路径 (
../../img
) : 确保这个路径是合法且你有写入权限的。避免路径穿越漏洞,最好使用绝对路径或经过严格校验的相对路径。使用realpath()
或类似函数解析路径。 - 文件名冲突 :
downloadToDir
默认可能会覆盖同名文件。如果需要保留所有文件,考虑在下载前检查文件是否存在,或者给下载的文件生成唯一的文件名(比如用 消息ID + 媒体ID + 原始文件名)。MadelineProto 的downloadToFile
方法允许你指定完整的文件路径和名称。 - 文件大小限制 : 下载大文件可能消耗大量时间和资源。可以考虑检查
$media['photo']['sizes']
(对 Photo) 或$media['document']['size']
(对 Document) 来设置一个下载大小上限。
进阶使用技巧:
- 选择图片尺寸 :
messageMediaPhoto
里的photo
对象通常包含一个sizes
数组,里面有不同尺寸的图片信息(缩略图、中等尺寸、原始大图)。你可以根据需要选择特定尺寸下载,而不是总是下载最大的原图。遍历sizes
数组,找到type
(例如 'x' 代表最大尺寸,其他字母代表不同规格的缩略图) 符合你要求的那个 Size 对象进行下载。 - 错误处理 : 在
downloadToDir
外面包一个try-catch
块,捕获可能发生的网络错误、文件系统错误或 API 错误,让脚本更健壮。记录下失败的消息 ID,方便后续重试。 - 异步下载 : 如果你需要处理大量消息或频道,下载会成为瓶颈。可以利用 MadelineProto 基于 Amp 的异步特性,并发执行下载任务,大幅提升效率。这需要对 PHP 的异步编程有所了解。
方案二:别忘了 messageMediaDocument
有时候用户会选择“作为文件发送”图片,而不是通过相册。这种情况下,消息的 media
类型会是 messageMediaDocument
。
原理与作用:
当 media._
是 messageMediaDocument
时,我们需要进一步检查这个文档的 mime_type
属性,看看它是不是图片类型(例如 image/jpeg
, image/png
, image/gif
等)。如果是,就按处理普通文件的方式下载它。
代码示例:
在上面方案一的代码里,我们已经加入了对 messageMediaDocument
的处理逻辑:
// ... (接上面代码) ...
// 情况三:处理作为文件发送的图片
elseif ($media_type === 'messageMediaDocument') {
// 检查文档是不是图片类型
$mime_type = $message['media']['document']['mime_type'] ?? '';
if (strpos($mime_type, 'image/') === 0) { // 更可靠地判断是否为图片 MIME
echo "检测到作为文件发送的图片 (messageMediaDocument),准备下载...\n";
try {
// downloadToDir 同样适用于 Document
$file_path = $this->MadelineProto->downloadToDir($message['media'], '../../img');
if ($file_path) {
$downloaded_files[] = $file_path;
echo "下载文件图片成功: " . $file_path . "\n";
} else {
echo "下载文件图片失败。\n";
}
} catch (\Throwable $e) {
echo "下载文件图片时发生错误: " . $e->getMessage() . "\n";
}
} else {
echo "找到一个文档,但不是图片类型 (MIME: " . $mime_type . "),跳过。\n";
}
}
// ...
安全建议:
- 严格校验 MIME 类型 : 不能仅仅依赖文件扩展名(如果文件名可获取的话)。
mime_type
是更可靠的依据。但即使mime_type
是图片类型,文件内容也可能被恶意构造。如果应用场景对安全要求很高,下载后可能还需要用图像处理库(如 GD 或 Imagick)尝试打开图片,验证其有效性。 - 提防伪装文件 : 不要完全信任
mime_type
。攻击者可能上传一个恶意脚本,但将其mime_type
设置为image/jpeg
。确保服务器配置不会执行上传目录中的任何文件。
进阶使用技巧:
- 处理大型文件 : 对于
messageMediaDocument
发送的大尺寸原图,考虑前面提到的文件大小限制和异步下载策略。 - 获取文件名 :
messageMediaDocument
结构中通常包含文件名信息 ($message['media']['document']['attributes']
数组中查找_
为documentAttributeFilename
的项)。可以在下载时使用这个原始文件名(注意处理特殊字符和重名)。
方案三:整合与优化(就是上面的示例代码)
其实最佳实践就是将上述情况整合起来处理。一个 if-elseif-else
结构或者 switch
语句可以清晰地根据 media._
的值分发到不同的处理逻辑:优先处理 messageMediaGrouped
,然后是 messageMediaPhoto
,再是 messageMediaDocument
(并检查 MIME type),最后可以有个 else
处理未知或不关心的媒体类型。
上面给出的 方案一的代码示例 实际上已经是一个整合了方案一和方案二的较完整版本,它能应对单图、图片集以及作为文件发送的图片这几种最常见的情况。
跑起来试试
现在,把你的代码替换成上面提供的【方案一:优先处理 messageMediaGrouped
】里的那个整合了各种情况的示例代码。运行一下,再次获取频道消息并尝试下载图片。你会发现,无论是单独一张图,还是一个包含多张图片的相册,甚至是作为文件发送的图片,只要符合条件,都能被正确识别并下载下来,$downloaded_files
数组里会包含所有从单条消息(或其代表的媒体组)中下载的图片路径。
记住,实际操作中一定要根据你使用的 MadelineProto 版本和遇到的具体消息结构,微调代码中访问数组的键名(特别是 messageMediaGrouped
内部的结构)。多用 print_r
或 var_dump
打印消息对象,看看它到底长啥样,是解决这类问题的金钥匙。