Metadata-Version: 2.1
Name: nonebot-plugin-tgforwarder
Version: 0.1.0
Summary: Poll safewbot's public message API and dispatch processed messages to NoneBot
Author: EasiFlux
License: MIT
Requires-Python: >=3.9
Requires-Dist: nonebot2>=2.3.1
Requires-Dist: httpx<1.0.0,>=0.27.0
Description-Content-Type: text/markdown

# nonebot-plugin-tgforwarder

一个 NoneBot2 插件，通过轮询 `safewbot` 的公开消息 HTTP API 获取已经完成规则处理或审核发布的消息，并把它们交给 Python 回调或 NoneBot matcher。

## 实现边界

数据流：

```text
safewbot 公开消息 API
  -> HTTP 轮询
  -> cursor / delivery_id 去重与消息建模
  -> Python 回调或 NoneBot 自定义事件 matcher
  -> 业务自己的后续转发逻辑
```

本插件仅调用：

```http
GET /api/public/v1/messages?cursor=<int>&limit=<1..100>
Authorization: Bearer <API endpoint token>
```

不包含以下能力：

- SafeW 普通账号登录或私密群监听；
- SafeW Bot API 的 `getUpdates` / webhook；
- `safewbot` Web 管理 API；
- 媒体文件下载。

公开 API 当前只返回处理后的文本和媒体元数据，不返回本地文件路径、下载 URL 或媒体二进制。插件不会读取或修改 `safewbot` 仓库。

## 安装

```bash
pip install nonebot-plugin-tgforwarder
```

在 NoneBot 项目中加载插件：

```python
nonebot.load_plugin("nonebot_plugin_tgforwarder")
```

先在 `safewbot` 管理后台创建“对外 API”目标，绑定需要的来源，并保存只显示一次的 Token。

## 配置

在 NoneBot 的 `.env` 中配置：

```dotenv
TGFORWARDER_API_BASE_URL=http://127.0.0.1:8000
TGFORWARDER_API_TOKEN=tgf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

| 配置项 | 默认值 | 说明 |
| --- | --- | --- |
| `TGFORWARDER_ENABLED` | `true` | 是否启动轮询器 |
| `TGFORWARDER_API_BASE_URL` | `http://127.0.0.1:8000` | `safewbot` Web 服务根地址 |
| `TGFORWARDER_API_TOKEN` | 空 | 公开 API 目标 Token；为空时记录错误且不启动 |
| `TGFORWARDER_CURSOR_FILE` | `data/tgforwarder-cursor.json` | 跨重启游标文件 |
| `TGFORWARDER_WHITELIST_FILE` | `data/tgforwarder-whitelist.json` | 来源群白名单文件 |
| `TGFORWARDER_INITIAL_CURSOR` | `0` | 游标文件不存在或损坏时的起始游标 |
| `TGFORWARDER_PAGE_LIMIT` | `50` | 单页条数，范围 `1..100` |
| `TGFORWARDER_POLL_INTERVAL` | `2.0` | 无更多消息时的轮询间隔（秒） |
| `TGFORWARDER_REQUEST_TIMEOUT` | `30.0` | HTTP 请求超时（秒） |
| `TGFORWARDER_RETRY_INITIAL` | `1.0` | 首次重试等待（秒） |
| `TGFORWARDER_RETRY_MAX` | `60.0` | 最大退避等待（秒） |
| `TGFORWARDER_RETRY_JITTER` | `0.2` | 退避随机抖动比例，范围 `0..1` |
| `TGFORWARDER_DEDUP_CACHE_SIZE` | `2048` | 内存中的 `delivery_id` 去重容量 |
| `TGFORWARDER_DISPATCH_MATCHER` | `true` | 是否向 NoneBot matcher 分发事件 |
| `TGFORWARDER_MATCHER_PRIORITY` | `1` | 自定义事件 matcher 优先级 |
| `TGFORWARDER_SHUTDOWN_TIMEOUT` | `5.0` | 关闭时等待当前处理完成的最长时间 |

游标文件与一个公开 API 目标配套使用。切换到另一个 API 目标时，应更换或删除游标文件；Token 仅在同一目标上轮换时可以保留原游标。

## 群白名单

插件默认拒绝所有来源群。只有 `PublicMessage.source_chat_id` 已加入白名单的消息才会分发给 Python 回调或 NoneBot matcher。

将 Bot 主人的账号配置为 NoneBot 超级用户：

```dotenv
SUPERUSERS=["123456789"]
```

Bot 主人可以在与 Bot 的私聊中管理白名单：

```text
/tgfwd add 群号
/tgfwd del 群号
/tgfwd list
```

非白名单群的消息会跳过分发并提交游标，防止阻塞同一 API 队列中的其他群。之后再加入白名单，也不会补发此前已经跳过的历史消息。

## 消费消息

### Python 回调

回调不依赖已连接的 NoneBot Bot，适合把消息交给自己的服务层：

```python
from nonebot_plugin_tgforwarder import PublicMessage, on_public_message


@on_public_message
async def consume(message: PublicMessage) -> None:
    print(message.delivery_id, message.source_chat_id, message.text)
```

### NoneBot matcher

自定义 matcher 需要至少一个已连接的 NoneBot Bot，用于进入 NoneBot 的事件处理管线：

```python
from nonebot_plugin_tgforwarder import PublicMessageEvent, public_message_matcher


@public_message_matcher.handle()
async def consume_event(event: PublicMessageEvent) -> None:
    message = event.message
    print(message.delivery_id, message.media_items)
```

如果白名单内的消息没有任何已注册回调或 matcher handler，插件不会推进该条游标，避免静默丢弃消息。

## 交付、重试与去重

- `delivery_id` 是 API 目标内可见记录的增量游标，不要求连续；
- 每条消息分发成功后立即原子写入游标，因此同页后续消息失败不会重放此前已提交条目；
- 网络错误、超时、HTTP `408`、`429` 和 `5xx` 使用原 cursor 指数退避重试；
- HTTP `401`、`422` 及响应契约错误会记录完整的非敏感诊断，并按最大间隔继续探测；日志不会输出 Token；
- 关闭时先通知轮询器停止，超时后取消任务，并关闭 HTTP 客户端；
- API 没有业务 ACK，所以端到端语义是“至少一次”。回调中的外部副作用应以 `delivery_id` 做幂等；
- 一个回调失败时不提交当前消息，后续会重试。此前已执行但未完成整条确认的回调也可能再次执行。

## 测试

```bash
pdm install -G dev
pdm test
```

测试覆盖公开 API 鉴权与响应解析、错误分类、响应游标约束、游标和白名单原子持久化、白名单命令，以及逐条提交和失败重放行为。

## 发布

版本号、`CHANGELOG.md`、Git 标签和 GitHub Release 由 release-please 管理。提交信息遵循 Conventional Commits：

- `fix:` 触发补丁版本；
- `feat:` 触发次版本；
- `feat!:`、`fix!:` 或提交正文中的 `BREAKING CHANGE:` 触发主版本。

合并 release-please 创建的发布 PR 后，工作流会创建 Release、构建发行包并通过 PyPI Trusted Publishing 发布。
