返回

MadelineProto实战教程:获取消息所有图片(解决只下最后一张)

php

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 处理媒体消息的方式说起。

  1. 单张图片消息 (messageMediaPhoto) : 当你发送一张图片时,消息的 media 字段类型就是 messageMediaPhoto。这个结构体直接包含了这张图片的各种信息(不同尺寸、文件引用等)。你的原始代码恰好能处理这种情况,$this->MadelineProto->downloadToDir($message['media'], ...) 就能直接下载这张图。

  2. 图片集/媒体组 (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 结构内部通常会包含一个数组(键名可能是 photosdocuments 或一个更通用的 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 字段,就表示它是媒体组的一部分。这种情况下,更稳妥的方式可能是:
    1. 记录下这个 grouped_id
    2. 使用 $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_rvar_dump 打印消息对象,看看它到底长啥样,是解决这类问题的金钥匙。