<!-- Source: https://telegram-bot.io.nexus/telegram4j/04 -->

# 过滤旧消息

::: danger 不推荐使用
笔者不推荐使用 Telegram4J：它有并发问题，服务总是挂掉。新项目请使用 [telegram-bot-base](<https://telegram-bot.io.nexus/telegram-bot-base/>)。本章仅作为历史记录保留。
:::


[[toc]]
在开发 Telegram 机器人时，机器人上线后可能会接收到大量旧消息。为了提升性能和用户体验，过滤这些旧消息是非常必要的。本文将详细介绍如何通过分析消息结构和实现代码来过滤机器人自身发送的旧消息。

## 问题描述

当 Telegram 机器人启动后，它可能会接收到大量历史消息。这些旧消息不仅会占用资源，还可能导致不必要的处理。因此，如何有效地过滤这些旧消息成为一个重要的问题。

### 示例旧消息

以下是一个示例的旧消息 JSON 结构：

```json
{
  "message": {
    "Message": {
      "data": {
        "Variant2": {
          "t1": {
            "BaseMessage": {
              "id": 1461,
              "flags": "110000010",
              "fromId": {
                "PeerUser": {
                  "userId": 7847170133
                }
              },
              "peerId": {
                "PeerUser": {
                  "userId": 5937448956
                }
              },
              "fwdFrom": null,
              "viaBotId": null,
              "replyTo": null,
              "date": 1733216667,
              "message": "/Start",
              "media": null,
              "replyMarkup": null,
              "entities": [
                {
                  "MessageEntityBotCommand": {
                    "offset": 0,
                    "length": 6,
                    "id": "6cef8ac7"
                  }
                }
              ],
              "views": null,
              "forwards": null,
              "replies": null,
              "editDate": null,
              "postAuthor": "null",
              "groupedId": null,
              "reactions": null,
              "restrictionReason": null,
              "ttlPeriod": null
            }
          }
        }
      }
    }
  },
  "chat": {
    "PrivateChat": {
      "user": {
        "User": {
          "minData": {
            "BaseUser": {
              "flags": "10010000000000000001101111",
              "flags2": "10000",
              "id": 5937448956,
              "accessHash": "6641851758459618002",
              "firstName": "Dora",
              "lastName": "Moss",
              "username": "Dora_moss",
              "phone": "null",
              "photo": {
                "BaseUserProfilePhoto": {
                  "flags": 0,
                  "photoId": "6053103839722190113",
                  "strippedThumb": null,
                  "dcId": 5
                }
              },
              "status": {
                "UserStatusRecently": {
                  "id": "e26f42f1"
                }
              },
              "botInfoVersion": null,
              "restrictionReason": null,
              "botInlinePlaceholder": "null",
              "langCode": "zh-hans",
              "emojiStatus": null,
              "usernames": null,
              "storiesMaxId": null
            }
          },
          "fullData": null
        }
      },
      "selfUser": {
        "User": {
          "minData": {
            "BaseUser": {
              "flags": "10000000000100010000001011",
              "flags2": "10010",
              "id": 7847170133,
              "accessHash": "-4262855346709871929",
              "firstName": "my-translator",
              "lastName": "null",
              "username": "litongjava_bot",
              "phone": "null",
              "photo": null,
              "status": null,
              "botInfoVersion": 1,
              "restrictionReason": null,
              "botInlinePlaceholder": "null",
              "langCode": "null",
              "emojiStatus": null,
              "usernames": null,
              "storiesMaxId": null
            }
          },
          "fullData": null
        }
      }
    }
  },
  "author": {
    "User": {
      "minData": {
        "BaseUser": {
          "flags": "10000000000100010000001011",
          "flags2": "10010",
          "id": 7847170133,
          "accessHash": "-4262855346709871929",
          "firstName": "my-translator",
          "lastName": "null",
          "username": "litongjava_bot",
          "phone": "null",
          "photo": null,
          "status": null,
          "botInfoVersion": 1,
          "restrictionReason": null,
          "botInlinePlaceholder": "null",
          "langCode": "null",
          "emojiStatus": null,
          "usernames": null,
          "storiesMaxId": null
        }
      },
      "fullData": null
    }
  }
}
```

### JSON 结构说明

- **message**: 包含发送消息的详细信息。
  - **Message**: 表示消息对象。
    - **data**: 包含变体数据。
      - **Variant2**: 特定的变体类型。
        - **t1**: Variant2 中的特定字段。
          - **BaseMessage**: 基础消息详细信息。
            - 各种字段如 `id`、`flags`、`fromId`、`peerId` 等，表示消息的不同属性。
- **chat**: 有关消息发送的聊天信息。
  - **PrivateChat**: 表示这是一个私聊。
    - **user**: 聊天中的另一个用户的信息。
      - **User**: 用户详细信息。
        - **minData**: 用户的最小数据。
          - **BaseUser**: 基础用户属性。
            - 字段如 `id`、`firstName`、`lastName`、`username` 等。
    - **selfUser**: 机器人的自身用户信息。
      - **User**: 机器人用户详细信息。
        - 结构与其他用户类似。
- **author**: 消息的作者。
  - **User**: 作者详细信息，此处为机器人本身。

通过分析以上 JSON 结构，可以发现消息的 `author id` 为 `7847170133`，这是机器人的自身 ID。因此，可以基于此 ID 来过滤机器人自身发送的消息。

## 过滤思路

基于以上分析，可以采取以下过滤策略：

1. **获取机器人的自身 ID**：通过 Telegram 客户端获取机器人的 ID。
2. **监听消息事件**：监听 `SendMessageEvent` 事件。
3. **过滤消息**：
   - 如果消息的 `author id` 与机器人的自身 ID 相同，则忽略该消息。
   - 仅处理来自其他用户的消息。

## 代码实现

本文提供了两部分主要代码：`TelegramTranslateBot` 配置类和 `UserPredicate` 过滤器。

### TelegramTranslateBot 配置

该类负责配置和连接 Telegram 客户端，并设置消息事件的监听与过滤。

```java
package nexus.io.gpt.translator.config;

import java.time.Duration;

import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.gpt.translator.predicate.UserPredicate;
import nexus.io.gpt.translator.tgfunc.TranslateFunction;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.utils.environment.EnvUtils;

import lombok.extern.slf4j.Slf4j;
import reactor.util.retry.Retry;
import telegram4j.core.MTProtoTelegramClient;
import telegram4j.core.event.domain.message.SendMessageEvent;
import telegram4j.mtproto.RpcException;

@Slf4j
@AConfiguration
public class TelegramTranslateBot {

  @Initialization
  public void config() {
    // 从环境变量获取 Telegram API 配置信息
    int apiId = EnvUtils.getInt("telegram.api.id");
    String apiHash = EnvUtils.getStr("telegram.api.hash");
    String botAuthToken = EnvUtils.getStr("telegram.bot.auth.token");

    // 创建并连接 MTProto Telegram 客户端
    MTProtoTelegramClient client = MTProtoTelegramClient.create(apiId, apiHash, botAuthToken).connect().block();
    if (client == null) {
      log.error("Failed to connect to Telegram MTProto client.");
      return;
    }

    // 获取机器人的自身 ID
    long selfId = client.getSelfId().asLong();

    // 配置事件监听器：接收 SendMessageEvent，过滤用户消息，并应用翻译功能
    client.on(SendMessageEvent.class)
        .filter(new UserPredicate(selfId)::test) // 过滤机器人自身发送的消息
        .flatMap(new TranslateFunction()::apply)  // 应用翻译功能
        .delayElements(Duration.ofSeconds(1))     // 每秒最多发送一条消息
        .doOnError(e -> log.error("处理消息时发生错误", e))
        .retryWhen(Retry.backoff(5, Duration.ofSeconds(1)).filter(e -> e instanceof RpcException))
        .subscribe();

    // 在服务器关闭时，断开 Telegram 客户端连接
    HookCan.me().addDestroyMethod(client::disconnect);

    log.info("Telegram MTProto client configured and connected.");
  }
}
```

#### 代码说明

1. **环境变量读取**：从环境变量中读取 Telegram API 的 `apiId`、`apiHash` 和 `botAuthToken`。
2. **客户端连接**：创建并连接 MTProto Telegram 客户端。
3. **获取自身 ID**：通过 `client.getSelfId()` 获取机器人的自身 ID。
4. **事件监听与过滤**：
   - 监听 `SendMessageEvent` 事件。
   - 使用 `UserPredicate` 过滤器过滤机器人自身发送的消息。
   - 通过 `TranslateFunction` 应用翻译功能。
   - 设置消息发送速率限制，每秒最多发送一条消息。
   - 错误处理与重试机制，针对 `RpcException` 进行重试。
5. **客户端断开连接**：在服务器关闭时，确保断开 Telegram 客户端的连接。

### UserPredicate 过滤器

该类用于判断消息是否来自机器人自身，从而决定是否需要处理该消息。

```java
package nexus.io.gpt.translator.predicate;

import java.util.Optional;
import java.util.function.Predicate;

import lombok.extern.slf4j.Slf4j;
import telegram4j.core.event.domain.message.SendMessageEvent;
import telegram4j.core.object.MentionablePeer;
import telegram4j.core.object.chat.Chat;

@Slf4j
public class UserPredicate implements Predicate<SendMessageEvent> {
  private long botId;

  public UserPredicate(long selfId) {
    this.botId = selfId;
  }

  @Override
  public boolean test(SendMessageEvent event) {
    log.info("event:{}", event);
    Optional<MentionablePeer> optionalAuthor = event.getAuthor();
    Optional<Chat> optionalChat = event.getChat();

    // 防止机器人处理自己发送的消息
    // 如果消息的发送者信息不存在，说明可能是机器人自身发送的消息，忽略
    if (optionalAuthor.isEmpty()) {
      log.info("消息发送者信息缺失，可能是机器人自身发送的消息。");
      return false;
    }
    if (optionalChat.isEmpty()) {
      log.info("消息信息缺失，可能是机器人自身发送的消息。");
      return false;
    }

    long messageAuthorId = optionalAuthor.get().getId().asLong();
    if (botId == messageAuthorId) {
      log.info("获取到机器人自己的消息");
      return false;
    }
    return true;
  }
}
```

#### 代码说明

1. **构造函数**：接收机器人的自身 ID，用于后续的比较。
2. **测试方法** `test`：
   - **日志记录**：记录接收到的事件信息，便于调试。
   - **作者信息检查**：
     - 如果 `optionalAuthor` 为空，可能是机器人自身发送的消息，返回 `false`。
     - 如果 `optionalChat` 为空，同样可能是机器人自身发送的消息，返回 `false`。
   - **ID 比较**：
     - 获取消息的作者 ID。
     - 如果作者 ID 与机器人的自身 ID 相同，说明是机器人自身发送的消息，返回 `false`。
     - 否则，返回 `true`，表示该消息需要进一步处理。

## 总结

通过以上分析和实现，我们成功地配置了 Telegram 机器人，使其在上线后能够有效地过滤掉自身发送的旧消息。关键步骤包括：

1. **理解消息结构**：通过分析消息的 JSON 结构，确定如何识别机器人自身发送的消息。
2. **获取自身 ID**：通过 Telegram 客户端获取机器人的唯一标识。
3. **实现过滤逻辑**：利用 `Predicate` 接口，实现基于作者 ID 的消息过滤。
4. **集成与测试**：将过滤器与事件监听器集成，确保机器人仅处理来自其他用户的消息。

这种过滤机制不仅提升了机器人的性能，还避免了不必要的资源消耗，为开发高效的 Telegram 机器人奠定了基础。
