Metadata-Version: 2.4
Name: nonebot-plugin-uniref
Version: 0.3.0
Summary: Portable and serializable entity references for NoneBot
Author: Misty02600
Author-email: Misty02600 <xiao02600@gmail.com>
License-Expression: MIT
License-File: LICENSE
Requires-Dist: nonebot-plugin-alconna>=0.62.1,<0.63.0
Requires-Dist: nonebot-plugin-uninfo>=0.11.1,<0.12.0
Requires-Dist: nonebot2>=2.5.0,<3.0.0
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/Misty02600/nonebot-plugin-uniref
Project-URL: Issues, https://github.com/Misty02600/nonebot-plugin-uniref/issues
Project-URL: Repository, https://github.com/Misty02600/nonebot-plugin-uniref.git
Description-Content-Type: text/markdown

<div align="center">

  <a href="https://nonebot.dev/">
    <img src="https://nonebot.dev/logo.png" width="200" height="200" alt="nonebot">
  </a>

# nonebot-plugin-uniref

_✨ [NoneBot2](https://github.com/nonebot/nonebot2) 可持久化的跨平台实体引用插件 ✨_

<p align="center">
  <img src="https://img.shields.io/github/license/Misty02600/nonebot-plugin-uniref" alt="license">
  <img src="https://img.shields.io/badge/python-3.11+-blue.svg" alt="Python">
  <img src="https://img.shields.io/badge/nonebot-2.5.0+-red.svg" alt="NoneBot">
  <a href="https://pypi.org/project/nonebot-plugin-uniref">
    <img src="https://badgen.net/pypi/v/nonebot-plugin-uniref" alt="pypi">
  </a>
</p>

</div>

本插件提供可比较、可序列化的 `UserRef` 与 `SceneRef`，用于在 NoneBot 插件中持久化用户和场景身份。

## 安装

- 使用 nb-cli

```console
nb plugin install nonebot-plugin-uniref
```

- 使用 pip

```console
pip install nonebot-plugin-uniref
```

## 使用

### 获取事件中的 Ref

```python
from nonebot_plugin_uniref import EventSceneRef, EventUserRef


@matcher.handle()
async def handle(user_ref: EventUserRef, scene_ref: EventSceneRef): ...
```

或调用 `get_user_ref(bot, session, event=None)` 和
`get_scene_ref(bot, session, event=None)`。
无法建立 Ref 时抛出 `RefUnavailableError`。

### 构造 Ref

```python
from nonebot_plugin_alconna import At, UniMsg
from nonebot_plugin_uniref import RefContext


@matcher.handle()
async def handle(message: UniMsg, refs: RefContext):
    user_ids = [at.target for at in message[At] if at.flag == "user"]
    users = [refs.user(user_id) for user_id in user_ids]
```


### 编码与恢复 Ref

```python
from nonebot_plugin_uniref import UserRef, decode_ref, encode_ref

ref = UserRef(namespace="QQClient", id="123")
value = encode_ref(ref)

assert value == "user:QQClient:123"
assert decode_ref(value) == ref
```

### 构造 Alconna Target

```python
from nonebot_plugin_uniref import decode_ref, ref_to_target

ref = decode_ref("scene:QQClient:group:456")
target = ref_to_target(ref, bot=bot)
```

省略 `bot` 时由 Ref 限定候选；传入 `bot` 时严格绑定同一逻辑 Bot，不回退到其他账号。
`ref_to_target()` 只构造发送地址，不保证 Bot 当前在线、具有权限或能够触达目标。

## 模型定义

### `UserRef`

| 属性        | 类型 | 含义                       |
| ----------- | ---- | -------------------------- |
| `namespace` | str  | 用户 ID 所属的身份命名空间 |
| `id`        | str  | 用户在该范围内的 ID        |

### `SceneRef`

| 属性        | 类型 | 含义                       |
| ----------- | ---- | -------------------------- |
| `namespace` | str  | 场景 ID 所属的身份命名空间 |
| `type`      | `SceneKind` | UniRef 定义的场景种类 |
| `id`        | str  | 场景在该范围内的完整 ID    |

`SceneKind` 包含 `private`、`group`、`guild`、`channel_text`、`channel_category` 和
`channel_voice`；codec 仍将它们编码为对应的可读字符串。

`namespace` 表示 ID 的相等范围；同一真人在不同 namespace 中可以拥有不同的 `UserRef`。Ref 不绑定运行中
的 Bot，也不是数据库行引用。平台 ID 仅在某个 App 内有效时，对应平台的 AppID 会进入 `namespace`；
它表示身份作用域，不表示 Bot 实例。仅按 Bot 隔离业务数据时，仍由业务插件加入 Bot 维度。

## 支持的 Adapter

`{...}` 表示决定 ID 相等范围的平台值；`id=` 后才是实际的 `Ref.id`。Scene 类型已经由 `SceneRef.type`
保存，所以 namespace 不再重复 `private`、`group` 或 `channel`。

| 已验证 Adapter / 场景 | UserRef：namespace；ID                                   | SceneRef：namespace；type；ID                        |
| --------------------- | -------------------------------------------------------- | ---------------------------------------------------- |
| OneBot V11、Milky     | `QQClient`；id=QQ 号                                     | `QQClient`；`private`：QQ 号；`group`：群号          |
| Telegram              | `Telegram`；id=User ID                                   | `Telegram`；`private`、`group`；id=Chat ID           |
| Discord               | `Discord`；id=User Snowflake                             | `Discord`；`private`、`guild`、Channel；id=Snowflake |
| QQ C2C                | `QQAPI:c2c:{QQ AppID}`；id=`user_openid`                    | `QQAPI:{QQ AppID}`；`private`；id=`user_openid`     |
| QQ 群聊               | `QQAPI:group:{QQ AppID}:{group_openid}`；id=`member_openid` | `QQAPI:{QQ AppID}`；`group`；id=`group_openid`      |
| QQ 公开文字子频道     | `QQAPI:guild:{QQ AppID}:{guild_id}`；id=`author.id`         | `QQAPI:{QQ AppID}`；`channel_text`；id=`channel_id` |
| 飞书私聊              | `Feishu:{Feishu AppID}`；id=`open_id`                       | 同 namespace；`private`；id=`open_id`               |
| 飞书群聊              | `Feishu:{Feishu AppID}`；id=`open_id`                       | `Feishu`；`group`；id=`chat_id`                      |

QQ UserRef 用 `c2c` / `group` / `guild` 区分三套用户 ID。群成员按 App 和群隔离，不能直接转换为发送
Target；频道用户按 App 和 Guild 隔离，可以转换为频道私信 Target。
