Metadata-Version: 2.4
Name: nicemoe
Version: 0.3.0
Summary: OneBot v11 应用端框架
Author-email: nicemoe <255655@qq.com>
License: MIT
License-File: LICENSE
Keywords: asgi,bot,chatbot,onebot,onebot11,qq
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Chat
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: loguru>=0.7
Requires-Dist: starlette>=0.27
Provides-Extra: standard
Requires-Dist: uvicorn[standard]>=0.23; extra == 'standard'
Description-Content-Type: text/markdown

# nicemoe

OneBot v11 应用端框架。

严格按 [OneBot v11 规范](https://github.com/botuniverse/onebot-11) 实现，
第三方依赖只有 `starlette` 和 `loguru`。

> 术语：规范里的「OneBot」指**实现端**（NapCat / Lagrange / LLOneBot），
> 本框架是**应用端** —— 消费事件、调用 API。

## 安装

```bash
pip install nicemoe[standard]     # 带上 uvicorn
```

## 三分钟上手

```python
# config.py —— 和代码分开，进 .gitignore
import os

ACCESS_TOKEN = os.getenv('NICEMOE_TOKEN', '')   # 秘密从环境变量来
SUPERUSERS = [10001000]
NICKNAMES = ['萌萌']                # 「萌萌，查询 xxx」也能触发，昵称会被剥掉
COMMAND_PREFIXES = ('/', '')        # 带不带 / 都认
HOST, PORT = '127.0.0.1', 8080
```

```python
# main.py
import config
from nicemoe import GROUP_ADMIN, SUPERUSER, Finish, OneBot

app = OneBot.from_config(config)    # 大写配置项自动映射；不认识的安静忽略
asgi = app.asgi                     # 也可以 uvicorn 模块名:asgi


@app.command('查询', aliases=('查',), cooldown=5)
async def query(ctx):
    """查询角色信息。"""
    if not ctx.args:
        raise Finish('用法：查询 <区服> <角色名>')
    return f"{ctx.args[0]} 的 {ctx.args.rest(1)}"    # 返回非 None 自动发出去


@app.command('踢', permission=SUPERUSER | GROUP_ADMIN, denied='需要管理员权限')
async def kick(ctx):
    await ctx.bot.set_group_kick(group_id=ctx.group_id, user_id=ctx.args.int(0))


@app.command('绑定')
async def bind(ctx):
    reply = await ctx.prompt('要绑定哪个角色？', timeout=60)
    return f"已绑定 {reply.text}"


if __name__ == '__main__':
    app.run()                       # host/port 从 config 来
```

实现端的反向 WebSocket 地址填 `ws://你的地址:端口/onebot/v11/ws`。

> 配置用 **Python 文件**而不是 `.env`，图的是类型是真的：`ONLY_TO_ME = False`
> 就是布尔假，不用担心 `'false'` 这个字符串是真值那种坑；`COMMAND_PREFIXES`
> 直接是元组，不用编码成 `/,#,` 那种谁都看不懂的写法。秘密照样能从环境变量来
> —— 在 `config.py` 里读就行。
>
> `app` 是个**普通对象**，不是全局单例 —— 你能造第二个、能在测试里造一次性的。
> 插件想拿到它就 `from app import onebot`（应用对象单独放一个模块，别放 main.py，
> 否则会和 `load_plugins` 形成循环导入）。

## 特点

**连接是一等公民。** `Bot` 就是一条连接，`app.bots` 是公开的在线表。断线时
未完成的 API 调用**立刻**抛 `ConnectionClosed`，不用干等超时。

**收包不会被业务拖住。** 有界队列 + worker 池，handler 一进入挂起点就把
worker 放掉。2 个 worker 能同时撑住任意多局进行中的游戏，不会死锁。

**会话状态就是局部变量。**

```python
@app.command('猜成语')
async def guess(ctx):
    answer = pick_idiom()
    async with ctx.capture(scope='group', match=is_idiom, timeout=60) as stream:
        async for guess in stream:
            if guess.text == answer:
                return MessageSegment.at(guess.user_id) + ' 答对了'
    return f'没人猜出来，答案是 {answer}'
```

不需要全局的「谁在玩」表，超时和异常退出自动收尾。

**处理函数只有一种签名：`(ctx)`。** 事件和命令都一样 —— 区别只在于命令的上下文
多了 `args` / `command`。所以 `await ctx.send(...)` 两边都能用：

```python
@app.on('notice.group_increase')
async def welcome(ctx):
    await ctx.send(MessageSegment.at(ctx.user_id) + ' 欢迎进群～')
```

**消息只用数组格式。** 裸字符串一律当纯文本，不解析其中的 CQ 码 ——
`f"欢迎 {昵称} 加入"` 不会因为有人把昵称改成 `[CQ:at,qq=all]` 就 @全体成员。

**权限是可组合的谓词，不是固定等级表。** 库只给协议定义的那几个事实和
`|` `&` `~` 运算，等级体系你自己搭：

```python
VIP = Permission(lambda ctx: ctx.user_id in vip_set)

@app.command('特权', permission=SUPERUSER | VIP)
@app.command('群管', permission=GROUP_ADMIN & ~ANONYMOUS)
```

判角色不只读 `sender.role`（规范上它「不保证存在」），查不到就回落
`get_group_member_info`，带缓存，且在 `notice.group_admin` 时主动失效。

**`only_to_me` 是触发条件不是权限。** 不满足时当作**没命中这条命令**，
消息落到事件总线，而不是「命中了但拒绝」。at 和昵称会被剥掉，所以
`@机器人 /查询 天鹅坪` 里 `ctx.args.rest()` 就是 `天鹅坪`。

**能脱离真 QQ 跑测试。**

```python
from nicemoe.testing import FakeOneBot, running

async with running(app), FakeOneBot(app) as impl:
    await impl.send_group_message('/查询 天鹅坪 张三')
    call = await impl.expect_action('send_msg')
    assert call['params']['message'].extract_plain_text() == '天鹅坪 的 张三'
    await impl.reply(call, {'message_id': 1})
```

## 不做的事

不做 OneBot v12 抽象层、不做插件热重载、不做依赖注入、不做 NLP 意图路由。

冷却、功能开关、黑名单、授权、使用统计这些都**不在库里** —— 它们需要知道
「数据存在哪」，属于使用者的产品形态。库提供 `@app.middleware` 挂载点和
`ctx.command.extra` 元数据透传，够用了。

## 开发

```bash
python -m unittest discover -s tests -t .   # 263 个用例，不开端口
python -m tests.smoke_uvicorn                # 真 uvicorn + 真 WebSocket
```

## 许可

MIT
