Merge branch 'dev' into richardchien-patch-1

This commit is contained in:
Ju4tCode 2021-05-29 18:15:32 +08:00 committed by GitHub
commit 4795f01583
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 1492 additions and 823 deletions

View File

@ -28,6 +28,11 @@ A clear and concise description of what you expected to happen.
- Python Version: [e.g. 3.8]
- Nonebot Version: [e.g. 2.0.0]
**截图**
**协议端信息:**
- 协议端: [e.g. go-cqhttp]
- 协议端版本: [e.g. 1.0.0]
**截图或日志**
If applicable, add screenshots to help explain your problem.

View File

@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/nonebot/discussions/discussions/new
about: Ask questions about nonebot
- name: Plugin Publish
url: https://v2.nonebot.dev/store.html
about: Publish your plugin to nonebot homepage and nb-cli

17
.github/ISSUE_TEMPLATE/document.md vendored Normal file
View File

@ -0,0 +1,17 @@
---
name: Document improvement
about: Feedback on documentation, including errors and ideas
title: 'Docs: some description'
labels: documentation
assignees: ''
---
**描述问题或主题:**
**需做出的修改:**
* [ ] 一些修改
* [ ] 一些修改
* [ ] 一些修改

2
.gitignore vendored
View File

@ -189,3 +189,5 @@ dev
docs_build/_build
!tests/.env
*.xmind
yarn.lock
.DS_Store

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"tabWidth": 2,
"useTabs": false,
"endOfLine": "lf",
"arrowParens": "always",
"singleQuote": false,
"trailingComma": "es5",
"semi": true
}

View File

@ -17,7 +17,7 @@ _✨ Python 异步机器人框架 ✨_
<a href="https://pypi.python.org/pypi/nonebot2">
<img src="https://img.shields.io/pypi/v/nonebot2" alt="pypi">
</a>
<img src="https://img.shields.io/badge/python-3.7+-blue" alt="python"><br />
<img src="https://img.shields.io/badge/python-3.7.3+-blue" alt="python"><br />
<a href="https://github.com/howmanybots/onebot/blob/master/README.md">
<img src="https://img.shields.io/badge/OneBot-v11-black?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAAIVBMVEUAAAAAAAADAwMHBwceHh4UFBQNDQ0ZGRkoKCgvLy8iIiLWSdWYAAAAAXRSTlMAQObYZgAAAQVJREFUSMftlM0RgjAQhV+0ATYK6i1Xb+iMd0qgBEqgBEuwBOxU2QDKsjvojQPvkJ/ZL5sXkgWrFirK4MibYUdE3OR2nEpuKz1/q8CdNxNQgthZCXYVLjyoDQftaKuniHHWRnPh2GCUetR2/9HsMAXyUT4/3UHwtQT2AggSCGKeSAsFnxBIOuAggdh3AKTL7pDuCyABcMb0aQP7aM4AnAbc/wHwA5D2wDHTTe56gIIOUA/4YYV2e1sg713PXdZJAuncdZMAGkAukU9OAn40O849+0ornPwT93rphWF0mgAbauUrEOthlX8Zu7P5A6kZyKCJy75hhw1Mgr9RAUvX7A3csGqZegEdniCx30c3agAAAABJRU5ErkJggg==" alt="cqhttp">
</a>
@ -97,7 +97,13 @@ NoneBot2 的驱动框架 `Driver` 以及通信协议 `Adapter` 均可**自定义
nb create
```
## 插件
## 社区资源
### 教程/实际项目/经验分享
- [awesome-nonebot](https://github.com/nonebot/awesome-nonebot)
### 插件
此外NoneBot2 还有丰富的官方以及第三方现成的插件供大家使用:
@ -107,7 +113,9 @@ NoneBot2 的驱动框架 `Driver` 以及通信协议 `Adapter` 均可**自定义
nb plugin install nonebot_plugin_docs
```
或者尝试 [文档镜像](https://nonebot2-vercel-mirror.vercel.app)
或者尝试以下镜像:
- [文档镜像(中国境内)](https://nb2.baka.icu)
- [文档镜像(vercel)](https://nonebot2-vercel-mirror.vercel.app)
- 其他插件请查看 [商店](https://v2.nonebot.dev/store.html)

View File

@ -6,7 +6,7 @@
## 从 NoneBot v1 迁移
`APScheduler` 作为 `nonebot` v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。`nonebot2` 已将 `APScheduler` 独立为 `nonebot_plugin_apscheduler` 插件,你可以在 [插件广场](https://v2.nonebot.dev/plugin-store.html) 中找到它。
`APScheduler` 作为 `nonebot` v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。`nonebot2` 已将 `APScheduler` 独立为 `nonebot_plugin_apscheduler` 插件,你可以在 [插件广场](https://v2.nonebot.dev/store.html) 中找到它。
相比于 `nonebot` v1 ,只需要安装插件并修改 `scheduler` 的导入方式即可完成迁移。

View File

@ -6,9 +6,9 @@
## 从 NoneBot v1 迁移
`APScheduler` 作为 `nonebot` v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。`nonebot2` 已将 `APScheduler` 独立为 `nonebot_plugin_apscheduler` 插件,你可以在 [插件广场](https://v2.nonebot.dev/plugin-store.html) 中找到它。
`APScheduler` 作为 `nonebot` v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。`nonebot2` 已将 `APScheduler` 独立为 `nonebot_plugin_apscheduler` 插件,你可以在 [插件广场](https://v2.nonebot.dev/store.html) 中找到它。
相比于 `nonebot` v1`nonebot` v2只需要安装插件并修改 `scheduler` 的导入方式即可完成迁移。
相比于 `nonebot` v1`nonebot` v2 只需要安装插件并修改 `scheduler` 的导入方式即可完成迁移。
## 安装插件

View File

@ -6,7 +6,7 @@
## 从 v1 迁移
`APScheduler` 作为 `nonebot` v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。`nonebot2` 已将 `APScheduler` 独立为 `nonebot_plugin_apscheduler` 插件,你可以在 [插件广场](https://v2.nonebot.dev/plugin-store.html) 中找到它。
`APScheduler` 作为 `nonebot` v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。`nonebot2` 已将 `APScheduler` 独立为 `nonebot_plugin_apscheduler` 插件,你可以在 [插件广场](https://v2.nonebot.dev/store.html) 中找到它。
相比于 `nonebot` v1 ,只需要安装插件并修改 `scheduler` 的导入方式即可完成迁移。

View File

@ -1,7 +1,7 @@
<template>
<v-card flat class="adapters">
<v-row>
<v-col cols="12" sm="4">
<v-row class="justify-center">
<v-col cols="12" sm="6">
<v-text-field
v-model="filterText"
dense
@ -9,7 +9,7 @@
outlined
clearable
hide-details
label="Filter Adapter"
label="搜索适配器"
>
<template v-slot:prepend-inner>
<div class="v-input__icon v-input__icon--prepend-inner">
@ -18,16 +18,16 @@
</template>
</v-text-field>
</v-col>
<v-col cols="12" sm="4">
<v-col cols="12" sm="6">
<v-dialog v-model="dialog" max-width="600px">
<template v-slot:activator="{ on, attrs }">
<v-btn dark block color="primary" v-bind="attrs" v-on="on"
>Publish Your Adapter
>发布适配器
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="headline">Adapter Information</span>
<span class="headline">适配器信息</span>
</v-card-title>
<v-card-text>
<v-form ref="newAdapterForm" v-model="valid" lazy-validation>
@ -49,14 +49,14 @@
</v-col>
<v-col cols="12">
<v-text-field
v-model="newAdapter.id"
v-model="newAdapter.link"
label="PyPI 项目名"
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
v-model="newAdapter.link"
v-model="newAdapter.id"
label="协议 import 包名"
required
></v-text-field>
@ -75,7 +75,7 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="dialog = false">
Close
关闭
</v-btn>
<v-btn
:disabled="!valid"
@ -86,22 +86,23 @@
publishAdapter();
"
>
Publish
发布
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-col>
<v-col cols="12" sm="4">
</v-row>
<v-row>
<v-col cols="12">
<v-pagination
v-model="page"
:length="pageNum"
prev-icon="fa-caret-left"
next-icon="fa-caret-right"
></v-pagination>
</v-col>
></v-pagination
></v-col>
</v-row>
<hr />
<v-row>
<v-col
cols="12"
@ -138,7 +139,7 @@ import adapters from "../public/adapters.json";
export default {
name: "Adapters",
components: {
PublishCard
PublishCard,
},
data() {
return {
@ -152,8 +153,8 @@ export default {
desc: null,
id: null,
link: null,
repo: null
}
repo: null,
},
};
},
computed: {
@ -161,7 +162,7 @@ export default {
return Math.ceil(this.filteredAdapters.length / 10);
},
filteredAdapters() {
return this.adapters.filter(adapter => {
return this.adapters.filter((adapter) => {
return (
adapter.id.indexOf(this.filterText || "") != -1 ||
adapter.name.indexOf(this.filterText || "") != -1 ||
@ -173,7 +174,7 @@ export default {
displayAdapters() {
return this.filteredAdapters.slice((this.page - 1) * 10, this.page * 10);
},
publishPlugin() {
publishAdapter() {
if (!this.$refs.newAdapterForm.validate()) {
return;
}
@ -215,7 +216,7 @@ ${this.newAdapter.repo}
window.open(
`https://github.com/nonebot/nonebot2/issues/new?title=${title}&body=${body}&labels=Adapter`
);
}
}
},
},
};
</script>

View File

@ -1,7 +1,7 @@
<template>
<v-card flat class="bots">
<v-row>
<v-col cols="12" sm="4">
<v-col cols="12" sm="6">
<v-text-field
v-model="filterText"
dense
@ -9,7 +9,7 @@
outlined
clearable
hide-details
label="Filter Bot"
label="搜索机器人"
>
<template v-slot:prepend-inner>
<div class="v-input__icon v-input__icon--prepend-inner">
@ -18,16 +18,16 @@
</template>
</v-text-field>
</v-col>
<v-col cols="12" sm="4">
<v-col cols="12" sm="6">
<v-dialog v-model="dialog" max-width="600px">
<template v-slot:activator="{ on, attrs }">
<v-btn dark block color="primary" v-bind="attrs" v-on="on"
>Publish Your Bot
>发布机器人
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="headline">Bot Information</span>
<span class="headline">机器人信息</span>
</v-card-title>
<v-card-text>
<v-form ref="newBotForm" v-model="valid" lazy-validation>
@ -61,7 +61,7 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="dialog = false">
Close
关闭
</v-btn>
<v-btn
:disabled="!valid"
@ -72,13 +72,15 @@
publishBot();
"
>
Publish
发布
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-col>
<v-col cols="12" sm="4">
</v-row>
<v-row>
<v-col cols="12">
<v-pagination
v-model="page"
:length="pageNum"
@ -87,7 +89,6 @@
></v-pagination>
</v-col>
</v-row>
<hr />
<v-row>
<v-col cols="12" sm="6" v-for="(bot, index) in displayBots" :key="index">
<PublishCard
@ -118,7 +119,7 @@ import bots from "../public/bots.json";
export default {
name: "Bots",
components: {
PublishCard
PublishCard,
},
data() {
return {
@ -130,8 +131,8 @@ export default {
newBot: {
name: null,
desc: null,
repo: null
}
repo: null,
},
};
},
computed: {
@ -139,7 +140,7 @@ export default {
return Math.ceil(this.filteredBots.length / 10);
},
filteredBots() {
return this.bots.filter(bot => {
return this.bots.filter((bot) => {
return (
bot.name.indexOf(this.filterText || "") != -1 ||
bot.desc.indexOf(this.filterText || "") != -1 ||
@ -183,7 +184,7 @@ ${this.newBot.repo}
window.open(
`https://github.com/nonebot/nonebot2/issues/new?title=${title}&body=${body}&labels=Bot`
);
}
}
},
},
};
</script>

View File

@ -136,8 +136,8 @@ export default {
props: {
messages: {
type: Array,
default: () => []
}
default: () => [],
},
},
methods: {
initWOW: function() {
@ -146,13 +146,13 @@ export default {
animateClass: "animate__animated",
offset: 0,
mobile: true,
live: true
live: true,
}).init();
}
},
},
mounted() {
this.initWOW();
}
},
};
</script>

View File

@ -1,7 +1,7 @@
<template>
<v-card flat class="plugins">
<v-row>
<v-col cols="12" sm="4">
<v-col cols="12" sm="6">
<v-text-field
v-model="filterText"
dense
@ -9,7 +9,7 @@
outlined
clearable
hide-details
label="Filter Plugin"
label="搜索插件"
>
<template v-slot:prepend-inner>
<div class="v-input__icon v-input__icon--prepend-inner">
@ -18,16 +18,16 @@
</template>
</v-text-field>
</v-col>
<v-col cols="12" sm="4">
<v-col cols="12" sm="6">
<v-dialog v-model="dialog" max-width="600px">
<template v-slot:activator="{ on, attrs }">
<v-btn dark block color="primary" v-bind="attrs" v-on="on"
>Publish Your Plugin
>发布插件
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="headline">Plugin Information</span>
<span class="headline">插件信息</span>
</v-card-title>
<v-card-text>
<v-form ref="newPluginForm" v-model="valid" lazy-validation>
@ -75,7 +75,7 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="dialog = false">
Close
关闭
</v-btn>
<v-btn
:disabled="!valid"
@ -86,13 +86,15 @@
publishPlugin();
"
>
Publish
发布
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-col>
<v-col cols="12" sm="4">
</v-row>
<v-row>
<v-col cols="12">
<v-pagination
v-model="page"
:length="pageNum"
@ -101,7 +103,6 @@
></v-pagination>
</v-col>
</v-row>
<hr />
<v-row>
<v-col
cols="12"
@ -115,7 +116,7 @@
:id="plugin.id"
:author="plugin.author"
:link="plugin.repo"
text="copy nb install command"
text="点此复制安装命令"
:command="`nb plugin install ${plugin.id}`"
></PublishCard>
</v-col>
@ -140,7 +141,7 @@ import plugins from "../public/plugins.json";
export default {
name: "Plugins",
components: {
PublishCard
PublishCard,
},
data() {
return {
@ -154,8 +155,8 @@ export default {
desc: null,
id: null,
link: null,
repo: null
}
repo: null,
},
};
},
computed: {
@ -163,7 +164,7 @@ export default {
return Math.ceil(this.filteredPlugins.length / 10);
},
filteredPlugins() {
return this.plugins.filter(plugin => {
return this.plugins.filter((plugin) => {
return (
plugin.id.indexOf(this.filterText || "") != -1 ||
plugin.name.indexOf(this.filterText || "") != -1 ||
@ -217,7 +218,7 @@ ${this.newPlugin.repo}
window.open(
`https://github.com/nonebot/nonebot2/issues/new?title=${title}&body=${body}&labels=Plugin`
);
}
}
},
},
};
</script>

View File

@ -28,7 +28,7 @@
{{ text }}
<v-icon right small>fa-copy</v-icon>
</v-btn>
<v-snackbar v-model="snackbar">Copied!</v-snackbar>
<v-snackbar v-model="snackbar">复制成功</v-snackbar>
</v-card-actions>
</v-card>
</template>
@ -44,17 +44,17 @@ export default {
author: String,
link: String,
text: String,
command: String
command: String,
},
data() {
return {
snackbar: false
snackbar: false,
};
},
computed: {
showCommand() {
return this.text && this.command;
}
},
},
methods: {
repoLink(repo) {
@ -65,11 +65,11 @@ export default {
},
copyCommand() {
copy(this.command, {
format: "text/plain"
format: "text/plain",
});
this.snackbar = true;
}
}
},
},
};
</script>

View File

@ -10,7 +10,7 @@
}}</v-tab>
</v-tabs>
</v-toolbar>
<v-tabs-items class="sub-item" v-model="tab">
<v-tabs-items class="sub-item pt-1" v-model="tab">
<v-tab-item>
<Adapter></Adapter>
</v-tab-item>
@ -37,7 +37,7 @@ export default {
components: {
Adapter,
Plugin,
Bot
Bot,
},
data() {
return {
@ -45,12 +45,12 @@ export default {
tabs: {
0: "协议",
1: "插件",
2: "机器人"
}
2: "机器人",
},
};
},
computed: {},
methods: {}
methods: {},
};
</script>

View File

@ -1 +1,64 @@
# 事件处理函数重载
当我们在编写 `nonebot2` 应用时,常常会遇到这样一个问题:该怎么让同一类型的不同事件执行不同的响应逻辑?又或者如何让不同的 `adapter` 针对同一类型的事件作出不同响应?
针对这个问题, `nonebot2` 提供一个便捷而高效的解决方案:事件处理函数重载机制。简单地说,`handler` (事件处理函数) 会根据其参数的 `type hints` ([PEP484 类型标注](https://www.python.org/dev/peps/pep-0484/)) 来对相对应的 `adapter``Event` 进行响应,并且会忽略不符合其参数类型标注的情况。
必须要注意的是,该机制利用了 `inspect` 标准库获取到了事件处理函数的 `singnature` (签名) ,进一步获取到参数名称和类型标注。故而,我们在编写 `handler` 时,参数的名称和类型标注必须要符合 `T_Handler` 规定,详情可以参看 **指南** 中的[事件处理](../guide/creating-a-handler)。
::: tip 提示
如果想了解更多关于 `inspect` 标准库的信息,可以查看[官方文档](https://docs.python.org/zh-cn/3.9/library/inspect.html)。
:::
下面,我们会以 `CQHTTP` 中的 `群聊消息事件``私聊消息事件` 为例,对该机制的应用进行简单的介绍。
## 一个例子
首先,我们需要导入需要的方法、类型。
```python
from nonebot import on_command
from nonebot.adapters.cqhttp import Bot, GroupMessageEvent, PrivateMessageEvent
```
之后,我们可以注册一个 `Matcher` 来响应 `消息事件`
```python
matcher = on_command("testoverload")
```
最后, 我们编写不同的 `handler` 并编写不同的类型标注来实现事件处理函数重载:
```python
@matcher.handle()
async def _(bot: Bot, event: GroupMessageEvent):
await matcher.send("群聊消息事件响应成功!")
@matcher.handle()
async def _(bot: Bot, event: PrivateMessageEvent):
await matcher.send("私聊消息事件响应成功!")
```
此时,我们可以在群聊或私聊中对我们的机器人发送 `testoverload` ,它会在不同的场景做出不同的应答。
这样一个简单的事件处理函数重载就完成了。
## 进阶
事件处理函数重载机制同样支持被 `matcher.got` 等装饰器装饰的函数。 例如:
```python
@matcher.got("key1", prompt="群事件提问")
async def _(bot: Bot, event: GroupMessageEvent):
await matcher.send("群聊消息事件响应成功!")
@matcher.got("key2", prompt="私聊事件提问")
async def _(bot: Bot, event: PrivateMessageEvent):
await matcher.send("私聊消息事件响应成功!")
```
只有触发事件符合的函数才会触发装饰器。

View File

@ -1,2 +1,90 @@
# 权限控制
**权限控制**是机器人在实际应用中需要解决的重点问题之一,`Nonebot` 提供了十分完善且灵活的权限控制机制—— `Permission` 机制。接下来我们将对这个机制进行简单的说明。
## 应用
如同 `Rule` 一样, `Permission` 可以在[注册事件响应器](../guide/creating-a-matcher)时添加 `permission` 参数来加以应用,这样 `Nonebot` 会在事件响应时检测事件主体的权限。下面我们以 `SUPERUSER` 为例,对该机制的应用做一下介绍。
```python
from nonebot.permission import SUPERUSER
from nonebot.adapters import Bot
from nonebot import on_command
matcher = on_command("测试超管", permission=SUPERUSER)
@matcher.handle()
async def _(bot: Bot):
await matcher.send("超管命令测试成功")
@matcher.got("key1", "超管提问")
async def _(bot: Bot, event: Event):
await matcher.send("超管命令got成功")
```
在这段代码中,我们事件响应器指定了 `SUPERUSER` 这样一个权限,那么机器人只会响应超级管理员的 `测试超管` 命令,并且会响应该超级管理员的连续对话。
::: tip 提示
在这里需要强调的是,`Permission` 与 `Rule` 的表现并不相同, `Rule` 只会在初次响应时生效,在余下的对话中并没有限制事件;但是 `Permission` 会持续生效,在连续对话中会一直对事件主体加以限制。
:::
## 进阶
`Permission` 除了可以在注册事件响应器时加以应用,还可以在编写事件处理函数 `handler` 时主动调用,我们可以利用这个特性在一个 `handler` 里对不同权限的事件主体进行区别响应,下面我们以 `CQHTTP` 中的 `GROUP_ADMIN` (普通管理员非群主)和 `GROUP_OWNER` 为例,说明下怎么进行主动调用。
```python
from nonebot import on_command
from nonebot.adapters.cqhttp import Bot
from nonebot.adapters.cqhttp import GroupMessageEvent
from nonebot.adapters.cqhttp import GROUP_ADMIN, GROUP_OWNER
matcher = on_command("测试权限")
@matcher.handle()
async def _(bot: Bot, event: GroupMessageEvent):
if await GROUP_ADMIN(bot, event):
await matcher.send("管理员测试成功")
elif await GROUP_OWNER(bot, event):
await matcher.send("群主测试成功")
else:
await matcher.send("群员测试成功")
```
在这段代码里,我们并没有对命令的权限指定,这个命令会响应所有在群聊中的 `测试权限` 命令,但是在 `handler` 里,我们对两个 `Permission` 进行主动调用,从而可以对不同的角色进行不同的响应。
## 自定义
如同 `Rule` 一样, `Permission` 也是由非负数个 `PermissionChecker` 组成的,但只需其中一个返回 `True` 时就会匹配成功。下面则是 `PermissionChecker``Permission` 示例:
```python
from nonebot.adapters import Bot, Event
from nonebot.permission import Permission
async def async_checker(bot: Bot, event: Event) -> bool:
return True
def sync_checker(bot: Bot, event: Event) -> bool:
return True
def check(arg1, arg2):
async def _checker(bot: Bot, event: Event) -> bool:
return bool(arg1 + arg2)
return Permission(_checker)
```
`Permission``PermissionChecker` 之间可以使用 `或 |` 互相组合:
```python
from nonebot.permission import Permission
Permission(async_checker1) | sync_checker | async_checker2
```
同样地,如果想用 `Permission(*checkers)` 包裹构造 `Permission` ,函数必须是异步的;但是在利用 `或 |` 符号连接构造时, `Nonebot` 会自动包裹同步函数为异步函数。

View File

@ -6,9 +6,9 @@
## 从 NoneBot v1 迁移
`APScheduler` 作为 `nonebot` v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。`nonebot2` 已将 `APScheduler` 独立为 `nonebot_plugin_apscheduler` 插件,你可以在 [插件广场](https://v2.nonebot.dev/plugin-store.html) 中找到它。
`APScheduler` 作为 `nonebot` v1 的可选依赖,为众多 bot 提供了方便的定时任务功能。`nonebot2` 已将 `APScheduler` 独立为 `nonebot_plugin_apscheduler` 插件,你可以在 [商店](https://v2.nonebot.dev/store.html) 中找到它。
相比于 `nonebot` v1`nonebot` v2只需要安装插件并修改 `scheduler` 的导入方式即可完成迁移。
相比于 `nonebot` v1`nonebot` v2 只需要安装插件并修改 `scheduler` 的导入方式即可完成迁移。
## 安装插件

View File

@ -191,9 +191,6 @@ Adapter 类型
* `api: str`: API 名称
* `self_id: Optional[str]`: 指定调用 API 的机器人
* `**data`: API 数据
@ -256,7 +253,7 @@ await bot.send_msg(message="hello world")
## _class_ `Message`
基类:`list`, `abc.ABC`
基类:`List`[`nonebot.adapters._base.T_MessageSegment`], `abc.ABC`
消息数组

View File

@ -312,12 +312,108 @@ CQHTTP 协议 Bot 适配。继承属性参考 [BaseBot](./#class-basebot) 。
CQHTTP 协议 MessageSegment 适配。具体方法参考协议消息段类型或源码。
### `is_text()`
### _static_ `anonymous(ignore_failure=None)`
### _static_ `at(user_id)`
### _static_ `contact(type_, id)`
### _static_ `contact_group(group_id)`
### _static_ `contact_user(user_id)`
### _static_ `dice()`
### _static_ `face(id_)`
### _static_ `forward(id_)`
### _static_ `image(file, type_=None, cache=True, proxy=True, timeout=None)`
### _static_ `json(data)`
### _static_ `location(latitude, longitude, title=None, content=None)`
### _static_ `music(type_, id_)`
### _static_ `music_custom(url, audio, title, content=None, img_url=None)`
### _static_ `node(id_)`
### _static_ `node_custom(user_id, nickname, content)`
### _static_ `poke(type_, id_)`
### _static_ `record(file, magic=None, cache=None, proxy=None, timeout=None)`
### _static_ `reply(id_)`
### _static_ `rps()`
### _static_ `shake()`
### _static_ `share(url='', title='', content=None, image=None)`
### _static_ `text(text)`
### _static_ `video(file, cache=None, proxy=None, timeout=None)`
### _static_ `xml(data)`
### `type`
* 类型: `str`
* 说明: 消息段类型
### `data`
* 类型: `Dict[str, Union[str, list]]`
* 说明: 消息段数据
## _class_ `Message`
基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message)
基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message)[`nonebot.adapters.cqhttp.message.MessageSegment`]
CQHTTP 协议 Message 适配。
### `extract_plain_text()`
# NoneBot.adapters.cqhttp.permission 模块

View File

@ -292,7 +292,7 @@ message += MessageSegment.atDingtalkIds(event.senderId)
## _class_ `Message`
基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message)
基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message)[`nonebot.adapters.ding.message.MessageSegment`]
钉钉 协议 Message 适配。

View File

@ -963,7 +963,7 @@ Mirai-API-HTTP 协议 MessageSegment 适配。具体方法参考 [mirai-api-http
## _class_ `MessageChain`
基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message)
基类:[`nonebot.adapters._base.Message`](README.md#nonebot.adapters._base.Message)[`nonebot.adapters.mirai.message.MessageSegment`]
Mirai 协议 Message 适配

View File

@ -106,6 +106,30 @@ NoneBot 主要配置。大小写不敏感。
### `log_level`
* **类型**: `Union[int, str]`
* **默认值**: `None`
* **说明**
配置 NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称,参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。
* **示例**
```default
LOG_LEVEL=25
LOG_LEVEL=INFO
```
### `api_root`

View File

@ -14,7 +14,7 @@ sidebarDepth: 0
基类:`abc.ABC`
Driver 基类。将后端框架封装,以满足适配器使用。
Driver 基类。
### `_adapters`
@ -32,33 +32,33 @@ Driver 基类。将后端框架封装,以满足适配器使用。
### `_ws_connection_hook`
### `_bot_connection_hook`
* **类型**
`Set[T_WebSocketConnectionHook]`
`Set[T_BotConnectionHook]`
* **说明**
WebSocket 连接建立时执行的函数
Bot 连接建立时执行的函数
### `_ws_disconnection_hook`
### `_bot_disconnection_hook`
* **类型**
`Set[T_WebSocketDisconnectionHook]`
`Set[T_BotDisconnectionHook]`
* **说明**
WebSocket 连接断开时执行的函数
Bot 连接断开时执行的函数
@ -120,6 +120,21 @@ Driver 基类。将后端框架封装,以满足适配器使用。
### _property_ `bots`
* **类型**
`Dict[str, Bot]`
* **说明**
获取当前所有已连接的 Bot
### `register_adapter(name, adapter, **kwargs)`
@ -144,33 +159,33 @@ Driver 基类。将后端框架封装,以满足适配器使用。
驱动类型名称
### _abstract property_ `server_app`
驱动 APP 对象
### _abstract property_ `asgi`
驱动 ASGI 对象
### _abstract property_ `logger`
驱动专属 logger 日志记录器
### _property_ `bots`
* **类型**
`Dict[str, Bot]`
### _abstract_ `run(host=None, port=None, *args, **kwargs)`
* **说明**
获取当前所有已连接的 Bot
启动驱动框架
* **参数**
* `host: Optional[str]`: 驱动绑定 IP
* `post: Optional[int]`: 驱动绑定端口
* `*args`
* `**kwargs`
@ -226,41 +241,47 @@ Driver 基类。将后端框架封装,以满足适配器使用。
在 WebSocket 连接断开后,调用该函数来注销 bot 对象
### _abstract_ `run(host=None, port=None, *args, **kwargs)`
## _class_ `ReverseDriver`
基类:`nonebot.drivers.Driver`
Reverse Driver 基类。将后端框架封装,以满足适配器使用。
* **说明**
### _abstract property_ `server_app`
启动驱动框架
驱动 APP 对象
### _abstract property_ `asgi`
* **参数**
驱动 ASGI 对象
* `host: Optional[str]`: 驱动绑定 IP
* `post: Optional[int]`: 驱动绑定端口
* `*args`
* `**kwargs`
### _abstract async_ `_handle_http()`
### _abstract async_ `_handle_http(*args, **kwargs)`
用于处理 HTTP 类型请求的函数
### _abstract async_ `_handle_ws_reverse()`
### _abstract async_ `_handle_ws_reverse(*args, **kwargs)`
用于处理 WebSocket 类型请求的函数
## _class_ `HTTPRequest`
基类:`object`
HTTP 请求封装。参考 [asgi http scope](https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope)。
## _class_ `HTTPResponse`
基类:`object`
HTTP 响应封装。参考 [asgi http scope](https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope)。
## _class_ `WebSocket`
基类:`object`

View File

@ -79,7 +79,7 @@ FastAPI 驱动框架设置,详情参考 FastAPI 文档
## _class_ `Driver`
基类:[`nonebot.drivers.Driver`](README.md#nonebot.drivers.Driver)
基类:[`nonebot.drivers.ReverseDriver`](README.md#nonebot.drivers.ReverseDriver)
FastAPI 驱动框架

View File

@ -12,7 +12,7 @@ sidebarDepth: 0
## _class_ `Driver`
基类:[`nonebot.drivers.Driver`](README.md#nonebot.drivers.Driver)
基类:[`nonebot.drivers.ReverseDriver`](README.md#nonebot.drivers.ReverseDriver)
Quart 驱动框架

View File

@ -35,6 +35,21 @@ sidebarDepth: 0
### `module`
* **类型**
`Optional[ModuleType]`
* **说明**
事件响应器所在模块
### `plugin_name`
* **类型**
`Optional[str]`
@ -43,7 +58,37 @@ sidebarDepth: 0
* **说明**
事件响应器所在模块名称
事件响应器所在插件名
### `module_name`
* **类型**
`Optional[str]`
* **说明**
事件响应器所在模块名
### `module_prefix`
* **类型**
`Optional[str]`
* **说明**
事件响应器所在模块前缀

View File

@ -50,7 +50,7 @@ sidebarDepth: 0
* **说明**: 插件模块对象
### `export`
### _property_ `export`
* **类型**: `Export`
@ -282,7 +282,7 @@ sidebarDepth: 0
## `on_startswith(msg, rule=None, **kwargs)`
## `on_startswith(msg, rule=None, ignorecase=False, **kwargs)`
* **说明**
@ -294,12 +294,15 @@ sidebarDepth: 0
* **参数**
* `msg: str`: 指定消息开头内容
* `msg: Union[str, Tuple[str, ...]]`: 指定消息开头内容
* `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则
* `ignorecase: bool`: 是否忽略大小写
* `permission: Optional[Permission]`: 事件响应权限
@ -329,7 +332,7 @@ sidebarDepth: 0
## `on_endswith(msg, rule=None, **kwargs)`
## `on_endswith(msg, rule=None, ignorecase=False, **kwargs)`
* **说明**
@ -341,12 +344,15 @@ sidebarDepth: 0
* **参数**
* `msg: str`: 指定消息结尾内容
* `msg: Union[str, Tuple[str, ...]]`: 指定消息结尾内容
* `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则
* `ignorecase: bool`: 是否忽略大小写
* `permission: Optional[Permission]`: 事件响应权限
@ -663,7 +669,7 @@ sidebarDepth: 0
* `cmd: Union[str, Tuple[str, ...]]`: 命令前缀
* `**kwargs`: 其他传递给 `on_command` 的参数,将会覆盖命令组默认值
* `**kwargs`: 其他传递给 `on_shell_command` 的参数,将会覆盖命令组默认值
@ -940,7 +946,10 @@ sidebarDepth: 0
* **参数**
* `msg: str`: 指定消息开头内容
* `msg: Union[str, Tuple[str, ...]]`: 指定消息开头内容
* `ignorecase: bool`: 是否忽略大小写
* `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则
@ -987,7 +996,10 @@ sidebarDepth: 0
* **参数**
* `msg: str`: 指定消息结尾内容
* `msg: Union[str, Tuple[str, ...]]`: 指定消息结尾内容
* `ignorecase: bool`: 是否忽略大小写
* `rule: Optional[Union[Rule, T_RuleChecker]]`: 事件响应规则

View File

@ -91,7 +91,7 @@ Rule(async_function, run_sync(sync_function))
## `startswith(msg)`
## `startswith(msg, ignorecase=False)`
* **说明**
@ -107,7 +107,7 @@ Rule(async_function, run_sync(sync_function))
## `endswith(msg)`
## `endswith(msg, ignorecase=False)`
* **说明**

View File

@ -46,7 +46,7 @@ sidebarDepth: 0
## `T_WebSocketConnectionHook`
## `T_BotConnectionHook`
* **类型**
@ -57,12 +57,12 @@ sidebarDepth: 0
* **说明**
WebSocket 连接建立时执行的函数
Bot 连接建立时执行的函数
## `T_WebSocketDisconnectionHook`
## `T_BotDisconnectionHook`
* **类型**
@ -73,7 +73,7 @@ sidebarDepth: 0
* **说明**
WebSocket 连接断开时执行的函数
Bot 连接断开时执行的函数

View File

@ -13,10 +13,10 @@ pip install nonebot-adapter-cqhttp
QQ 协议端举例:
- [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) (基于 [MiraiGo](https://github.com/Mrs4s/MiraiGo))
- [cqhttp-mirai-embedded](https://github.com/yyuueexxiinngg/cqhttp-mirai/tree/embedded)
- [Mirai](https://github.com/mamoe/mirai) + [cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai)
- [onebot-kotlin](https://github.com/yyuueexxiinngg/onebot-kotlin)
- [Mirai](https://github.com/mamoe/mirai) + [onebot-mirai](https://github.com/yyuueexxiinngg/onebot-kotlin)
- [Mirai](https://github.com/mamoe/mirai) + [Mirai Native](https://github.com/iTXTech/mirai-native) + [CQHTTP](https://github.com/richardchien/coolq-http-api)
- [OICQ-http-api](https://github.com/takayama-lily/onebot) (基于 [OICQ](https://github.com/takayama-lily/oicq))
- [node-onebot](https://github.com/takayama-lily/node-onebot) (基于 [abot](https://github.com/takayama-lily/abot), [OICQ](https://github.com/takayama-lily/oicq))
这里以 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 为例
@ -24,59 +24,74 @@ QQ 协议端举例:
2. 运行 exe 文件或者使用 `./go-cqhttp` 启动
3. 生成默认配置文件并修改默认配置
```hjson{2,3,35-36,42}
{
```yml{2,3,18,57,58}
account:
uin: 机器人QQ号
password: 机器人密码
encrypt_password: false
password_encrypted: ""
enable_db: true
access_token: ""
relogin: {
enabled: true
relogin_delay: 3
max_relogin_times: 0
}
_rate_limit: {
password: "机器人密码"
encrypt: false
relogin:
disabled: false
delay: 3
interval: 0
max-times: 0
use-sso-address: true
heartbeat:
disabled: false
interval: 5
message:
post-format: array
ignore-invalid-cqcode: false
force-fragment: false
fix-url: false
proxy-rewrite: ""
report-self-message: false
remove-reply-at: false
extra-reply-data: false
output:
log-level: warn
debug: false
default-middlewares: &default
access-token: ""
filter: ""
rate-limit:
enabled: false
frequency: 1
bucket_size: 1
}
ignore_invalid_cqcode: false
force_fragmented: false
heartbeat_interval: 0
http_config: {
enabled: false
host: "0.0.0.0"
port: 5700
timeout: 0
post_urls: {}
}
ws_config: {
enabled: false
host: "0.0.0.0"
port: 6700
}
ws_reverse_servers: [
{
enabled: true
reverse_url: ws://127.0.0.1:8080/cqhttp/ws
reverse_api_url: ws://you_websocket_api.server
reverse_event_url: ws://you_websocket_event.server
reverse_reconnect_interval: 3000
}
]
post_message_format: array
use_sso_address: false
debug: false
log_level: ""
web_ui: {
enabled: false
bucket: 1
servers:
- http:
disabled: true
host: 127.0.0.1
web_ui_port: 9999
web_input: false
}
}
port: 5700
timeout: 5
middlewares:
<<: *default
post:
- ws:
disabled: true
host: 127.0.0.1
port: 6700
middlewares:
<<: *default
- ws-reverse:
disabled: false
universal: ws://127.0.0.1:8080/cqhttp/ws
api: ws://your_websocket_api.server
event: ws://your_websocket_event.server
reconnect-interval: 3000
middlewares:
<<: *default
database:
leveldb:
enable: true
```
其中 `ws://127.0.0.1:8080/cqhttp/ws` 中的 `127.0.0.1``8080` 应分别对应 nonebot 配置的 HOST 和 PORT。

View File

@ -8,7 +8,7 @@
nb plugin new
```
插件通常有两种形式,下面分别介绍
下面分别对两种通常的插件形式做具体介绍
## 单文件形式

View File

@ -37,7 +37,8 @@ AweSome-Bot
:::warning 提示
如果您使用如 `VSCode` / `PyCharm` 等 IDE 启动 nonebot请检查 IDE 当前工作空间目录是否与当前侧边栏打开目录一致。
* 在二者不一致的环境下可能导致 nonebot 读取配置文件和插件等不符合预期
- 注意:在二者不一致的环境下可能导致 nonebot 读取配置文件和插件等不符合预期
:::
通过 `nb-cli`

View File

@ -103,15 +103,15 @@ async def raw_handler(bot: DingBot, event: MessageEvent):
![机器人所在群的 Webhook 地址](./images/ding/webhook.png)
获取到Webhook地址后用户可以向这个地址发起HTTP POST 请求,即可实现给该钉钉群发送消息。
获取到 Webhook 地址后,用户可以向这个地址发起 HTTP POST 请求,即可实现给该钉钉群发送消息。
对于这种通过 Webhook 推送的消息钉钉需要开发者进行安全方面的设置目前有3种安全设置方式请根据需要选择一种如下
对于这种通过 Webhook 推送的消息,钉钉需要开发者进行安全方面的设置(目前有 3 种安全设置方式,请根据需要选择一种),如下:
1. **自定义关键词:** 最多可以设置10个关键词消息中至少包含其中1个关键词才可以发送成功。
1. **自定义关键词:** 最多可以设置 10 个关键词,消息中至少包含其中 1 个关键词才可以发送成功。
例如添加了一个自定义关键词:监控报警,则这个机器人所发送的消息,必须包含监控报警这个词,才能发送成功。
2. **加签:** 发送请求时带上验签的值,可以在机器人设置里看到密钥。
![加签密钥](./images/ding/jiaqian.png)
3. **IP地址(段):** 设定后只有来自IP地址范围内的请求才会被正常处理。支持两种设置方式IP地址和IP地址段暂不支持IPv6地址白名单。
3. **IP 地址(段):** 设定后,只有来自 IP 地址范围内的请求才会被正常处理。支持两种设置方式IP 地址和 IP 地址段,暂不支持 IPv6 地址白名单。
如果你选择 1/3 两种安全设置,你需要自己确认当前网络和发送的消息能被钉钉接受,然后使用 `bot.send` 的时候将 webhook 地址传入 webhook 参数即可。

View File

@ -72,7 +72,7 @@ pip install . # 不推荐
适配器可以通过 `nb-cli` 在创建项目时根据你的选择自动安装,也可以自行使用 `pip` 安装
```bash
pip install nonebot-adapter-<adapter-name>
pip install <adapter-name>
```
```bash
@ -88,9 +88,9 @@ nb adapter list
# 列出所有的插件
nb plugin list
# 搜索插件
nb plugin search xxx
nb plugin search <plugin-name>
# 安装插件
nb plugin install xxx
nb plugin install <plugin-name>
```
如果急于上线 Bot 或想要使用现成的插件,以下插件可作为参考:
@ -106,4 +106,10 @@ nb plugin install xxx
### 其他插件
还有更多的插件在 [这里](/plugin-store.md) 等着你发现~
还有更多的插件在 [这里](/store.html) 等着你发现~
## 安装开发环境(可选)
NoneBot v2 全程使用 `VSCode` 搭配 `Pylance` 的开发环境进行开发在严格的类型检查下NoneBot v2 具有完善的类型设计与声明。
在围绕 NoneBot v2 进行开发时,使用 `VSCode` 搭配 `Pylance` 进行类型检查是非常推荐的。这有利于统一代码风格及避免低级错误的发生。

View File

@ -39,10 +39,11 @@ if __name__ == "__main__":
`bot.py` 文件中添加以下行:
```python{5}
```python{6}
import nonebot
nonebot.init()
# 加载插件目录,该目录下为各插件,以下划线开头的插件将不会被加载
nonebot.load_plugins("awesome_bot/plugins")
@ -68,10 +69,11 @@ if __name__ == "__main__":
`bot.py` 文件中添加以下行:
```python{5,7}
```python{6,8}
import nonebot
nonebot.init()
# 加载一个 pip 安装的插件
nonebot.load_plugin("nonebot_plugin_status")
# 加载本地的单独插件
@ -83,6 +85,63 @@ if __name__ == "__main__":
nonebot.run()
```
## 从 json 文件中加载插件
`bot.py` 文件中添加以下行:
```python{6}
import nonebot
nonebot.init()
# 从 plugin.json 加载插件
nonebot.load_from_json("plugin.json")
app = nonebot.get_asgi()
if __name__ == "__main__":
nonebot.run()
```
**json 文件示例**
```json
{
"plugins": ["nonebot_plugin_status", "awesome_bot.plugins.xxx"],
"plugin_dirs": ["awesome_bot/plugins"]
}
```
## 从 toml 文件中加载插件
`bot.py` 文件中添加以下行:
```python{6}
import nonebot
nonebot.init()
# 从 pyproject.toml 加载插件
nonebot.load_from_toml("pyproject.toml")
app = nonebot.get_asgi()
if __name__ == "__main__":
nonebot.run()
```
**toml 文件示例:**
```toml
[nonebot.plugins]
plugins = ["nonebot_plugin_status", "awesome_bot.plugins.xxx"]
plugin_dirs = ["awesome_bot/plugins"]
```
::: tip
nb-cli 默认使用 `pyproject.toml` 加载插件。
:::
## 子插件(嵌套插件)
在插件中同样可以加载子插件,例如如下插件目录结构:

View File

@ -67,6 +67,12 @@ pip install nonebot-adapter-mirai
4. 修改配置文件
::: warning
由于NoneBot2的架构设计等原因, 部分功能的支持可能需要推迟到MAH 2.0正式发布后再完成
:::
::: tip
在此之前, 你可能需要了解我们为 MAH 设计的两种通信方式
@ -92,6 +98,13 @@ pip install nonebot-adapter-mirai
- 这是当使用正向 Websocket 时的配置举例
::: warning
在默认情况下, NoneBot和MAH会同时监听8080端口, 这会导致端口冲突的错误
请确保二者配置不在同一端口下
:::
- MAH 的`setting.yml`文件
- ```yaml
@ -106,9 +119,12 @@ pip install nonebot-adapter-mirai
- `.env`文件
- ```shell
PORT=2333
MIRAI_AUTH_KEY=1234567890
MIRAI_HOST=127.0.0.1 # 当MAH运行在本机时
MIRAI_PORT=8080 # MAH的监听端口
PORT=2333 # 防止与MAH接口冲突
```
- `bot.py`文件
@ -155,7 +171,7 @@ pip install nonebot-adapter-mirai
- ```shell
HOST=127.0.0.1 # 当MAH运行在本机时
PORT=2333
PORT=2333 # 防止与MAH接口冲突
MIRAI_AUTH_KEY=1234567890
MIRAI_HOST=127.0.0.1 # 当MAH运行在本机时

View File

@ -41,7 +41,7 @@ NoneBot.adapters.cqhttp.message 模块
.. automodule:: nonebot.adapters.cqhttp.message
:members:
:private-members:
:undoc-members:
:show-inheritance:
NoneBot.adapters.cqhttp.permission 模块

View File

@ -179,15 +179,17 @@ def init(*, _env_file: Optional[str] = None, **kwargs):
"""
global _driver
if not _driver:
logger.info("NoneBot is initializing...")
logger.success("NoneBot is initializing...")
env = Env()
logger.opt(
colors=True).info(f"Current <y><b>Env: {env.environment}</b></y>")
config = Config(**kwargs,
_common_config=env.dict(),
_env_file=_env_file or f".env.{env.environment}")
default_filter.level = "DEBUG" if config.debug else "INFO"
default_filter.level = (
"DEBUG" if config.debug else
"INFO") if config.log_level is None else config.log_level
logger.opt(
colors=True).info(f"Current <y><b>Env: {env.environment}</b></y>")
logger.opt(colors=True).debug(
f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}")
@ -223,7 +225,7 @@ def run(host: Optional[str] = None,
nonebot.run(host="127.0.0.1", port=8080)
"""
logger.info("Running NoneBot...")
logger.success("Running NoneBot...")
get_driver().run(host, port, *args, **kwargs)

View File

@ -11,8 +11,8 @@ from copy import copy
from functools import reduce, partial
from typing_extensions import Protocol
from dataclasses import dataclass, field
from typing import (Any, Set, Dict, Union, TypeVar, Mapping, Optional, Iterable,
Awaitable, TYPE_CHECKING)
from typing import (Any, Set, List, Dict, Union, TypeVar, Mapping, Optional,
Iterable, Awaitable, TYPE_CHECKING)
from pydantic import BaseModel
@ -152,7 +152,6 @@ class Bot(abc.ABC):
:参数:
* ``api: str``: API 名称
* ``self_id: Optional[str]``: 指定调用 API 的机器人
* ``**data``: API 数据
:示例:
@ -176,10 +175,6 @@ class Bot(abc.ABC):
result = None
try:
if "self_id" in data and data["self_id"]:
bot = self.driver.bots[str(data["self_id"])]
result = await bot._call_api(api, **data)
else:
result = await self._call_api(api, **data)
except Exception as e:
exception = e
@ -316,7 +311,7 @@ class MessageSegment(abc.ABC, Mapping):
raise NotImplementedError
class Message(list, abc.ABC):
class Message(List[T_MessageSegment], abc.ABC):
"""消息数组"""
def __init__(self,

View File

@ -15,10 +15,9 @@ NoneBot 使用 `pydantic`_ 以及 `python-dotenv`_ 来读取配置。
"""
import os
from pathlib import Path
from datetime import timedelta
from ipaddress import IPv4Address
from typing import Any, Set, Dict, Tuple, Mapping, Optional
from typing import Any, Set, Dict, Union, Tuple, Mapping, Optional
from pydantic import BaseSettings, IPvAnyAddress
from pydantic.env_settings import SettingsError, InitSettingsSource, EnvSettingsSource
@ -173,6 +172,25 @@ class Config(BaseConfig):
是否以调试模式运行 NoneBot
"""
log_level: Optional[Union[int, str]] = None
"""
- **类型**: ``Union[int, str]``
- **默认值**: ``None``
:说明:
配置 NoneBot 日志输出等级可以为 ``int`` 类型等级或等级名称参考 `loguru 日志等级`_
:示例:
.. code-block:: default
LOG_LEVEL=25
LOG_LEVEL=INFO
.. _loguru 日志等级:
https://loguru.readthedocs.io/en/stable/api/logger.html#levels
"""
# bot connection configs
api_root: Dict[str, str] = {}

View File

@ -7,11 +7,12 @@
import abc
import asyncio
from typing import Set, Dict, Type, Optional, Callable, TYPE_CHECKING
from typing import (Any, Set, List, Dict, Type, Tuple, Optional, Callable,
MutableMapping, TYPE_CHECKING)
from nonebot.log import logger
from nonebot.config import Env, Config
from nonebot.typing import T_WebSocketConnectionHook, T_WebSocketDisconnectionHook
from nonebot.typing import T_BotConnectionHook, T_BotDisconnectionHook
if TYPE_CHECKING:
from nonebot.adapters import Bot
@ -19,7 +20,7 @@ if TYPE_CHECKING:
class Driver(abc.ABC):
"""
Driver 基类将后端框架封装以满足适配器使用
Driver 基类
"""
_adapters: Dict[str, Type["Bot"]] = {}
@ -27,15 +28,15 @@ class Driver(abc.ABC):
:类型: ``Dict[str, Type[Bot]]``
:说明: 已注册的适配器列表
"""
_ws_connection_hook: Set[T_WebSocketConnectionHook] = set()
_bot_connection_hook: Set[T_BotConnectionHook] = set()
"""
:类型: ``Set[T_WebSocketConnectionHook]``
:说明: WebSocket 连接建立时执行的函数
:类型: ``Set[T_BotConnectionHook]``
:说明: Bot 连接建立时执行的函数
"""
_ws_disconnection_hook: Set[T_WebSocketDisconnectionHook] = set()
_bot_disconnection_hook: Set[T_BotDisconnectionHook] = set()
"""
:类型: ``Set[T_WebSocketDisconnectionHook]``
:说明: WebSocket 连接断开时执行的函数
:类型: ``Set[T_BotDisconnectionHook]``
:说明: Bot 连接断开时执行的函数
"""
@abc.abstractmethod
@ -62,6 +63,18 @@ class Driver(abc.ABC):
:说明: 已连接的 Bot
"""
@property
def bots(self) -> Dict[str, "Bot"]:
"""
:类型:
``Dict[str, Bot]``
:说明:
获取当前所有已连接的 Bot
"""
return self._clients
def register_adapter(self, name: str, adapter: Type["Bot"], **kwargs):
"""
:说明:
@ -88,108 +101,12 @@ class Driver(abc.ABC):
"""驱动类型名称"""
raise NotImplementedError
@property
@abc.abstractmethod
def server_app(self):
"""驱动 APP 对象"""
raise NotImplementedError
@property
@abc.abstractmethod
def asgi(self):
"""驱动 ASGI 对象"""
raise NotImplementedError
@property
@abc.abstractmethod
def logger(self):
"""驱动专属 logger 日志记录器"""
raise NotImplementedError
@property
def bots(self) -> Dict[str, "Bot"]:
"""
:类型:
``Dict[str, Bot]``
:说明:
获取当前所有已连接的 Bot
"""
return self._clients
@abc.abstractmethod
def on_startup(self, func: Callable) -> Callable:
"""注册一个在驱动启动时运行的函数"""
raise NotImplementedError
@abc.abstractmethod
def on_shutdown(self, func: Callable) -> Callable:
"""注册一个在驱动停止时运行的函数"""
raise NotImplementedError
def on_bot_connect(
self, func: T_WebSocketConnectionHook) -> T_WebSocketConnectionHook:
"""
:说明:
装饰一个函数使他在 bot 通过 WebSocket 连接成功时执行
:函数参数:
* ``bot: Bot``: 当前连接上的 Bot 对象
"""
self._ws_connection_hook.add(func)
return func
def on_bot_disconnect(
self,
func: T_WebSocketDisconnectionHook) -> T_WebSocketDisconnectionHook:
"""
:说明:
装饰一个函数使他在 bot 通过 WebSocket 连接断开时执行
:函数参数:
* ``bot: Bot``: 当前连接上的 Bot 对象
"""
self._ws_disconnection_hook.add(func)
return func
def _bot_connect(self, bot: "Bot") -> None:
"""在 WebSocket 连接成功后,调用该函数来注册 bot 对象"""
self._clients[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot), self._ws_connection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
"Running cancelled!</bg #f8bbd0></r>")
asyncio.create_task(_run_hook(bot))
def _bot_disconnect(self, bot: "Bot") -> None:
"""在 WebSocket 连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._clients:
del self._clients[bot.self_id]
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot), self._ws_disconnection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
"Running cancelled!</bg #f8bbd0></r>")
asyncio.create_task(_run_hook(bot))
@abc.abstractmethod
def run(self,
host: Optional[str] = None,
@ -212,17 +129,174 @@ class Driver(abc.ABC):
f"<g>Loaded adapters: {', '.join(self._adapters)}</g>")
@abc.abstractmethod
async def _handle_http(self):
def on_startup(self, func: Callable) -> Callable:
"""注册一个在驱动启动时运行的函数"""
raise NotImplementedError
@abc.abstractmethod
def on_shutdown(self, func: Callable) -> Callable:
"""注册一个在驱动停止时运行的函数"""
raise NotImplementedError
def on_bot_connect(self, func: T_BotConnectionHook) -> T_BotConnectionHook:
"""
:说明:
装饰一个函数使他在 bot 通过 WebSocket 连接成功时执行
:函数参数:
* ``bot: Bot``: 当前连接上的 Bot 对象
"""
self._bot_connection_hook.add(func)
return func
def on_bot_disconnect(
self, func: T_BotDisconnectionHook) -> T_BotDisconnectionHook:
"""
:说明:
装饰一个函数使他在 bot 通过 WebSocket 连接断开时执行
:函数参数:
* ``bot: Bot``: 当前连接上的 Bot 对象
"""
self._bot_disconnection_hook.add(func)
return func
def _bot_connect(self, bot: "Bot") -> None:
"""在 WebSocket 连接成功后,调用该函数来注册 bot 对象"""
self._clients[bot.self_id] = bot
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot), self._bot_connection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketConnection hook. "
"Running cancelled!</bg #f8bbd0></r>")
asyncio.create_task(_run_hook(bot))
def _bot_disconnect(self, bot: "Bot") -> None:
"""在 WebSocket 连接断开后,调用该函数来注销 bot 对象"""
if bot.self_id in self._clients:
del self._clients[bot.self_id]
async def _run_hook(bot: "Bot") -> None:
coros = list(map(lambda x: x(bot), self._bot_disconnection_hook))
if coros:
try:
await asyncio.gather(*coros)
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
"Running cancelled!</bg #f8bbd0></r>")
asyncio.create_task(_run_hook(bot))
class ForwardDriver(Driver):
pass
class ReverseDriver(Driver):
"""
Reverse Driver 基类将后端框架封装以满足适配器使用
"""
@property
@abc.abstractmethod
def server_app(self):
"""驱动 APP 对象"""
raise NotImplementedError
@property
@abc.abstractmethod
def asgi(self):
"""驱动 ASGI 对象"""
raise NotImplementedError
@abc.abstractmethod
async def _handle_http(self, *args, **kwargs):
"""用于处理 HTTP 类型请求的函数"""
raise NotImplementedError
@abc.abstractmethod
async def _handle_ws_reverse(self):
async def _handle_ws_reverse(self, *args, **kwargs):
"""用于处理 WebSocket 类型请求的函数"""
raise NotImplementedError
class WebSocket(object):
class HTTPRequest:
"""HTTP 请求封装。参考 `asgi http scope`_。
.. _asgi http scope:
https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
"""
def __init__(self, scope: MutableMapping[str, Any]):
self._scope = scope
@property
def type(self) -> str:
return "http"
@property
def scope(self) -> MutableMapping[str, Any]:
return self._scope
@property
def http_version(self) -> str:
raise self.scope["http_version"]
@property
def method(self) -> str:
raise self.scope["method"]
@property
def schema(self) -> str:
raise self.scope["schema"]
@property
def path(self) -> str:
return self.scope["path"]
@property
def query_string(self) -> bytes:
return self.scope["query_string"]
@property
def headers(self) -> List[Tuple[bytes, bytes]]:
return list(self.scope["headers"])
@property
def body(self) -> bytes:
return self.scope["body"]
class HTTPResponse:
"""HTTP 响应封装。参考 `asgi http scope`_。
.. _asgi http scope:
https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
"""
def __init__(self, status: int, headers: List[Tuple[bytes, bytes]],
body: bytes):
self.status: int = status
self.headers: List[Tuple[bytes, bytes]] = headers
self.body: bytes = body
@property
def type(self) -> str:
return "http"
class WebSocket:
"""WebSocket 连接封装,统一接口方便外部调用。"""
@abc.abstractmethod

View File

@ -24,7 +24,7 @@ from nonebot.typing import overrides
from nonebot.utils import DataclassEncoder
from nonebot.exception import RequestDenied
from nonebot.config import Env, Config as NoneBotConfig
from nonebot.drivers import Driver as BaseDriver, WebSocket as BaseWebSocket
from nonebot.drivers import ReverseDriver, WebSocket as BaseWebSocket
class Config(BaseSettings):
@ -76,7 +76,7 @@ class Config(BaseSettings):
extra = "ignore"
class Driver(BaseDriver):
class Driver(ReverseDriver):
"""
FastAPI 驱动框架
@ -106,40 +106,40 @@ class Driver(BaseDriver):
self._server_app.websocket("/{adapter}/ws/")(self._handle_ws_reverse)
@property
@overrides(BaseDriver)
@overrides(ReverseDriver)
def type(self) -> str:
"""驱动名称: ``fastapi``"""
return "fastapi"
@property
@overrides(BaseDriver)
@overrides(ReverseDriver)
def server_app(self) -> FastAPI:
"""``FastAPI APP`` 对象"""
return self._server_app
@property
@overrides(BaseDriver)
@overrides(ReverseDriver)
def asgi(self):
"""``FastAPI APP`` 对象"""
return self._server_app
@property
@overrides(BaseDriver)
@overrides(ReverseDriver)
def logger(self) -> logging.Logger:
"""fastapi 使用的 logger"""
return logging.getLogger("fastapi")
@overrides(BaseDriver)
@overrides(ReverseDriver)
def on_startup(self, func: Callable) -> Callable:
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
return self.server_app.on_event("startup")(func)
@overrides(BaseDriver)
@overrides(ReverseDriver)
def on_shutdown(self, func: Callable) -> Callable:
"""参考文档: `Events <https://fastapi.tiangolo.com/advanced/events/#startup-event>`_"""
return self.server_app.on_event("shutdown")(func)
@overrides(BaseDriver)
@overrides(ReverseDriver)
def run(self,
host: Optional[str] = None,
port: Optional[int] = None,
@ -176,7 +176,7 @@ class Driver(BaseDriver):
log_config=LOGGING_CONFIG,
**kwargs)
@overrides(BaseDriver)
@overrides(ReverseDriver)
async def _handle_http(self, adapter: str, request: Request):
data = await request.body()
data_dict = json.loads(data.decode())
@ -211,7 +211,7 @@ class Driver(BaseDriver):
asyncio.create_task(bot.handle_message(data_dict))
return Response("", 204)
@overrides(BaseDriver)
@overrides(ReverseDriver)
async def _handle_ws_reverse(self, adapter: str,
websocket: FastAPIWebSocket):
ws = WebSocket(websocket)

View File

@ -16,8 +16,7 @@ import uvicorn
from nonebot.config import Config as NoneBotConfig
from nonebot.config import Env
from nonebot.drivers import Driver as BaseDriver
from nonebot.drivers import WebSocket as BaseWebSocket
from nonebot.drivers import ReverseDriver, WebSocket as BaseWebSocket
from nonebot.exception import RequestDenied
from nonebot.log import logger
from nonebot.typing import overrides
@ -35,7 +34,7 @@ except ImportError:
_AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine])
class Driver(BaseDriver):
class Driver(ReverseDriver):
"""
Quart 驱动框架
@ -45,7 +44,7 @@ class Driver(BaseDriver):
* ``/{adapter name}/ws``: WebSocket 上报
"""
@overrides(BaseDriver)
@overrides(ReverseDriver)
def __init__(self, env: Env, config: NoneBotConfig):
super().__init__(env, config)
@ -57,40 +56,40 @@ class Driver(BaseDriver):
view_func=self._handle_ws_reverse)
@property
@overrides(BaseDriver)
@overrides(ReverseDriver)
def type(self) -> str:
"""驱动名称: ``quart``"""
return 'quart'
@property
@overrides(BaseDriver)
@overrides(ReverseDriver)
def server_app(self) -> Quart:
"""``Quart`` 对象"""
return self._server_app
@property
@overrides(BaseDriver)
@overrides(ReverseDriver)
def asgi(self):
"""``Quart`` 对象"""
return self._server_app
@property
@overrides(BaseDriver)
@overrides(ReverseDriver)
def logger(self):
"""fastapi 使用的 logger"""
return self._server_app.logger
@overrides(BaseDriver)
@overrides(ReverseDriver)
def on_startup(self, func: _AsyncCallable) -> _AsyncCallable:
"""参考文档: `Startup and Shutdown <https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html>`_"""
return self.server_app.before_serving(func) # type: ignore
@overrides(BaseDriver)
@overrides(ReverseDriver)
def on_shutdown(self, func: _AsyncCallable) -> _AsyncCallable:
"""参考文档: `Startup and Shutdown <https://pgjones.gitlab.io/quart/how_to_guides/startup_shutdown.html>`_"""
return self.server_app.after_serving(func) # type: ignore
@overrides(BaseDriver)
@overrides(ReverseDriver)
def run(self,
host: Optional[str] = None,
port: Optional[int] = None,
@ -126,7 +125,7 @@ class Driver(BaseDriver):
log_config=LOGGING_CONFIG,
**kwargs)
@overrides(BaseDriver)
@overrides(ReverseDriver)
async def _handle_http(self, adapter: str):
request: Request = _request
@ -157,7 +156,7 @@ class Driver(BaseDriver):
asyncio.create_task(bot.handle_message(data))
return Response('', 204)
@overrides(BaseDriver)
@overrides(ReverseDriver)
async def _handle_ws_reverse(self, adapter: str):
websocket: QuartWebSocket = _websocket
if adapter not in self._adapters:

View File

@ -12,11 +12,12 @@ NoneBot 使用 `loguru`_ 来记录日志信息。
import sys
import logging
from typing import Union
from loguru import logger as logger_
import loguru
# logger = logging.getLogger("nonebot")
logger = logger_
logger: "loguru.Logger" = loguru.logger
"""
:说明:
@ -44,11 +45,16 @@ logger = logger_
class Filter:
def __init__(self) -> None:
self.level = "DEBUG"
self.level: Union[int, str] = "DEBUG"
def __call__(self, record):
module = sys.modules.get(record["name"])
if module:
plugin_name = getattr(module, "__plugin_name__", record["name"])
record["name"] = plugin_name
record["name"] = record["name"].split(".")[0]
levelno = logger.level(self.level).no
levelno = logger.level(self.level).no if isinstance(self.level,
str) else self.level
return record["level"].no >= levelno

View File

@ -6,6 +6,7 @@
"""
from functools import wraps
from types import ModuleType
from datetime import datetime
from contextvars import ContextVar
from collections import defaultdict
@ -36,6 +37,9 @@ current_event: ContextVar = ContextVar("current_event")
class MatcherMeta(type):
if TYPE_CHECKING:
module: Optional[str]
plugin_name: Optional[str]
module_name: Optional[str]
module_prefix: Optional[str]
type: str
rule: Rule
permission: Permission
@ -46,7 +50,7 @@ class MatcherMeta(type):
expire_time: Optional[datetime]
def __repr__(self) -> str:
return (f"<Matcher from {self.module or 'unknow'}, "
return (f"<Matcher from {self.module_name or 'unknown'}, "
f"type={self.type}, priority={self.priority}, "
f"temp={self.temp}>")
@ -56,10 +60,28 @@ class MatcherMeta(type):
class Matcher(metaclass=MatcherMeta):
"""事件响应器类"""
module: Optional[str] = None
module: Optional[ModuleType] = None
"""
:类型: ``Optional[ModuleType]``
:说明: 事件响应器所在模块
"""
plugin_name: Optional[str] = module and getattr(module, "__plugin_name__",
None)
"""
:类型: ``Optional[str]``
:说明: 事件响应器所在模块名称
:说明: 事件响应器所在插件名
"""
module_name: Optional[str] = module and getattr(module, "__module_name__",
None)
"""
:类型: ``Optional[str]``
:说明: 事件响应器所在模块名
"""
module_prefix: Optional[str] = module and getattr(module,
"__module_prefix__", None)
"""
:类型: ``Optional[str]``
:说明: 事件响应器所在模块前缀
"""
type: str = ""
@ -136,7 +158,8 @@ class Matcher(metaclass=MatcherMeta):
self.state = self._default_state.copy()
def __repr__(self) -> str:
return (f"<Matcher from {self.module or 'unknown'}, type={self.type}, "
return (
f"<Matcher from {self.module_name or 'unknown'}, type={self.type}, "
f"priority={self.priority}, temp={self.temp}>")
def __str__(self) -> str:
@ -153,7 +176,7 @@ class Matcher(metaclass=MatcherMeta):
priority: int = 1,
block: bool = False,
*,
module: Optional[str] = None,
module: Optional[ModuleType] = None,
default_state: Optional[T_State] = None,
default_state_factory: Optional[T_StateFactory] = None,
expire_time: Optional[datetime] = None) -> Type["Matcher"]:
@ -185,6 +208,12 @@ class Matcher(metaclass=MatcherMeta):
"Matcher", (Matcher,), {
"module":
module,
"plugin_name":
module and getattr(module, "__plugin_name__", None),
"module_name":
module and getattr(module, "__module_name__", None),
"module_prefix":
module and getattr(module, "__module_prefix__", None),
"type":
type_,
"rule":

View File

@ -7,7 +7,7 @@ NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供
import asyncio
from datetime import datetime
from typing import Set, Type, TYPE_CHECKING
from typing import Set, Type, Optional, TYPE_CHECKING
from nonebot.log import logger
from nonebot.rule import TrieRule
@ -174,7 +174,7 @@ async def _run_matcher(Matcher: Type[Matcher], bot: "Bot", event: "Event",
return
async def handle_event(bot: "Bot", event: "Event"):
async def handle_event(bot: "Bot", event: "Event") -> Optional[Exception]:
"""
:说明:
@ -199,7 +199,7 @@ async def handle_event(bot: "Bot", event: "Event"):
except NoLogException:
show_log = False
if show_log:
logger.opt(colors=True).info(log_msg)
logger.opt(colors=True).success(log_msg)
state = {}
coros = list(map(lambda x: x(bot, event, state), _event_preprocessors))
@ -208,15 +208,15 @@ async def handle_event(bot: "Bot", event: "Event"):
if show_log:
logger.debug("Running PreProcessors...")
await asyncio.gather(*coros)
except IgnoredException:
except IgnoredException as e:
logger.opt(colors=True).info(
f"Event {event.get_event_name()} is <b>ignored</b>")
return
return e
except Exception as e:
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
"Event ignored!</bg #f8bbd0></r>")
return
return e
# Trie Match
_, _ = TrieRule.get_value(bot, event, state)
@ -237,14 +237,16 @@ async def handle_event(bot: "Bot", event: "Event"):
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
for result in results:
if not isinstance(result, Exception):
continue
if isinstance(result, StopPropagation):
if not break_flag:
break_flag = True
logger.debug("Stop event propagation")
elif isinstance(result, Exception):
else:
logger.opt(colors=True, exception=result).error(
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
)
return result
coros = list(map(lambda x: x(bot, event, state), _event_postprocessors))
if coros:
@ -256,3 +258,4 @@ async def handle_event(bot: "Bot", event: "Event"):
logger.opt(colors=True, exception=e).error(
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
)
return e

View File

@ -7,9 +7,10 @@
import re
import json
from types import ModuleType
from functools import reduce
from dataclasses import dataclass
from collections import defaultdict
from contextvars import Context, ContextVar, copy_context
from contextvars import Context, copy_context
from typing import Any, Set, List, Dict, Type, Tuple, Union, Optional, TYPE_CHECKING
import tomlkit
@ -49,11 +50,14 @@ class Plugin(object):
- **类型**: ``ModuleType``
- **说明**: 插件模块对象
"""
export: Export
@property
def export(self) -> Export:
"""
- **类型**: ``Export``
- **说明**: 插件内定义的导出内容
"""
return getattr(self.module, "__export__", Export())
@property
def matcher(self) -> Set[Type[Matcher]]:
@ -61,13 +65,16 @@ class Plugin(object):
- **类型**: ``Set[Type[Matcher]]``
- **说明**: 插件内定义的 ``Matcher``
"""
return _plugin_matchers[self.name]
# return reduce(
# lambda x, y: x | _plugin_matchers[y],
# filter(lambda x: x.startswith(self.name), _plugin_matchers.keys()),
# set())
return _plugin_matchers.get(self.name, set())
def _store_matcher(matcher: Type[Matcher]):
if matcher.module:
plugin_name = matcher.module.split(".", maxsplit=1)[0]
_plugin_matchers[plugin_name].add(matcher)
if matcher.plugin_name:
_plugin_matchers[matcher.plugin_name].add(matcher)
def on(type: str = "",
@ -282,8 +289,9 @@ def on_request(rule: Optional[Union[Rule, T_RuleChecker]] = None,
return matcher
def on_startswith(msg: str,
def on_startswith(msg: Union[str, Tuple[str, ...]],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = None,
ignorecase: bool = False,
**kwargs) -> Type[Matcher]:
"""
:说明:
@ -292,8 +300,9 @@ def on_startswith(msg: str,
:参数:
* ``msg: str``: 指定消息开头内容
* ``msg: Union[str, Tuple[str, ...]]``: 指定消息开头内容
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``ignorecase: bool``: 是否忽略大小写
* ``permission: Optional[Permission]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
@ -306,11 +315,12 @@ def on_startswith(msg: str,
- ``Type[Matcher]``
"""
return on_message(startswith(msg) & rule, **kwargs)
return on_message(startswith(msg, ignorecase) & rule, **kwargs)
def on_endswith(msg: str,
def on_endswith(msg: Union[str, Tuple[str, ...]],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = None,
ignorecase: bool = False,
**kwargs) -> Type[Matcher]:
"""
:说明:
@ -319,8 +329,9 @@ def on_endswith(msg: str,
:参数:
* ``msg: str``: 指定消息结尾内容
* ``msg: Union[str, Tuple[str, ...]]``: 指定消息结尾内容
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``ignorecase: bool``: 是否忽略大小写
* ``permission: Optional[Permission]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表
* ``temp: bool``: 是否为临时事件响应器仅执行一次
@ -333,7 +344,7 @@ def on_endswith(msg: str,
- ``Type[Matcher]``
"""
return on_message(endswith(msg) & rule, **kwargs)
return on_message(endswith(msg, ignorecase) & rule, **kwargs)
def on_keyword(keywords: Set[str],
@ -546,7 +557,7 @@ class CommandGroup:
:参数:
* ``cmd: Union[str, Tuple[str, ...]]``: 命令前缀
* ``**kwargs``: 其他传递给 ``on_command`` 的参数将会覆盖命令组默认值
* ``**kwargs``: 其他传递给 ``on_shell_command`` 的参数将会覆盖命令组默认值
:返回:
@ -631,6 +642,7 @@ class MatcherGroup:
final_kwargs = self.base_kwargs.copy()
final_kwargs.update(kwargs)
final_kwargs.pop("type", None)
final_kwargs.pop("permission", None)
matcher = on_metaevent(**final_kwargs)
self.matchers.append(matcher)
return matcher
@ -717,7 +729,8 @@ class MatcherGroup:
self.matchers.append(matcher)
return matcher
def on_startswith(self, msg: str, **kwargs) -> Type[Matcher]:
def on_startswith(self, msg: Union[str, Tuple[str, ...]],
**kwargs) -> Type[Matcher]:
"""
:说明:
@ -725,7 +738,8 @@ class MatcherGroup:
:参数:
* ``msg: str``: 指定消息开头内容
* ``msg: Union[str, Tuple[str, ...]]``: 指定消息开头内容
* ``ignorecase: bool``: 是否忽略大小写
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Permission]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表
@ -746,7 +760,8 @@ class MatcherGroup:
self.matchers.append(matcher)
return matcher
def on_endswith(self, msg: str, **kwargs) -> Type[Matcher]:
def on_endswith(self, msg: Union[str, Tuple[str, ...]],
**kwargs) -> Type[Matcher]:
"""
:说明:
@ -754,7 +769,8 @@ class MatcherGroup:
:参数:
* ``msg: str``: 指定消息结尾内容
* ``msg: Union[str, Tuple[str, ...]]``: 指定消息结尾内容
* ``ignorecase: bool``: 是否忽略大小写
* ``rule: Optional[Union[Rule, T_RuleChecker]]``: 事件响应规则
* ``permission: Optional[Permission]``: 事件响应权限
* ``handlers: Optional[List[Union[T_Handler, Handler]]]``: 事件处理函数列表
@ -928,11 +944,10 @@ def _load_plugin(manager: PluginManager, plugin_name: str) -> Optional[Plugin]:
try:
module = manager.load_plugin(plugin_name)
plugin = Plugin(plugin_name, module,
getattr(module, "__export__", Export()))
plugin = Plugin(plugin_name, module)
plugins[plugin_name] = plugin
logger.opt(
colors=True).info(f'Succeeded to import "<y>{plugin_name}</y>"')
colors=True).success(f'Succeeded to import "<y>{plugin_name}</y>"')
return plugin
except Exception as e:
logger.opt(colors=True, exception=e).error(

View File

@ -1,7 +1,7 @@
import re
from types import ModuleType
from contextvars import ContextVar
from typing import Any, Set, List, Dict, Type, Tuple, Union, Optional
from dataclasses import dataclass
from typing import Set, List, Dict, Type, Tuple, Union, Optional, TYPE_CHECKING
from nonebot.matcher import Matcher
from nonebot.handler import Handler
@ -9,32 +9,28 @@ from nonebot.permission import Permission
from nonebot.rule import Rule, ArgumentParser
from nonebot.typing import T_State, T_StateFactory, T_Handler, T_RuleChecker
from .export import Export, export
from .manager import PluginManager
plugins: Dict[str, "Plugin"] = ...
_export: ContextVar["Export"] = ...
_tmp_matchers: ContextVar[Set[Type[Matcher]]] = ...
class Export(dict):
def __call__(self, func, **kwargs):
...
def __setattr__(self, name, value):
...
def __getattr__(self, name):
...
PLUGIN_NAMESPACE: str = ...
@dataclass(eq=False)
class Plugin(object):
name: str
module: ModuleType
matcher: Set[Type[Matcher]]
export: Export
@property
def export(self) -> Export:
...
@property
def matcher(self) -> Set[Type[Matcher]]:
...
def on(type: str = ...,
def on(type: str = "",
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Permission] = ...,
*,
@ -94,8 +90,9 @@ def on_request(rule: Optional[Union[Rule, T_RuleChecker]] = ...,
def on_startswith(
msg: str,
msg: Union[str, Tuple[str, ...]],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
ignorecase: bool = ...,
*,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
@ -107,8 +104,9 @@ def on_startswith(
...
def on_endswith(msg: str,
def on_endswith(msg: Union[str, Tuple[str, ...]],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
ignorecase: bool = ...,
*,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
@ -121,7 +119,7 @@ def on_endswith(msg: str,
def on_keyword(keywords: Set[str],
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
*,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
@ -147,16 +145,24 @@ def on_command(cmd: Union[str, Tuple[str, ...]],
...
def on_shell_command(cmd: Union[str, Tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = None,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = None,
parser: Optional[ArgumentParser] = None,
**kwargs) -> Type[Matcher]:
def on_shell_command(
cmd: Union[str, Tuple[str, ...]],
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
parser: Optional[ArgumentParser] = ...,
*,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
state_factory: Optional[T_StateFactory] = ...) -> Type[Matcher]:
...
def on_regex(pattern: str,
flags: Union[int, re.RegexFlag] = 0,
flags: Union[int, re.RegexFlag] = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
*,
permission: Optional[Permission] = ...,
@ -169,67 +175,26 @@ def on_regex(pattern: str,
...
def load_plugin(module_path: str) -> Optional[Plugin]:
...
def load_plugins(*plugin_dir: str) -> Set[Plugin]:
...
def load_all_plugins(module_path: Set[str],
plugin_dir: Set[str]) -> Set[Plugin]:
...
def load_from_json(file_path: str, encoding: str = ...) -> Set[Plugin]:
...
def load_from_toml(file_path: str, encoding: str = ...) -> Set[Plugin]:
...
def load_builtin_plugins(name: str = ...):
...
def get_plugin(name: str) -> Optional[Plugin]:
...
def get_loaded_plugins() -> Set[Plugin]:
...
def export() -> Export:
...
def require(name: str) -> Export:
...
class CommandGroup:
def __init__(self,
cmd: Union[str, Tuple[str, ...]],
*,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Permission] = ...,
*,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...):
self.basecmd: Tuple[str, ...] = ...
self.base_kwargs: Dict[str, Any] = ...
state: Optional[T_State] = ...,
state_factory: Optional[T_StateFactory] = ...):
...
def command(self,
cmd: Union[str, Tuple[str, ...]],
*,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
@ -244,7 +209,7 @@ class CommandGroup:
cmd: Union[str, Tuple[str, ...]],
*,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]] = ...,
aliases: Optional[Set[Union[str, Tuple[str, ...]]]],
parser: Optional[ArgumentParser] = ...,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
@ -267,7 +232,8 @@ class MatcherGroup:
temp: bool = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...):
state: Optional[T_State] = ...,
state_factory: Optional[T_StateFactory] = ...):
...
def on(self,
@ -286,57 +252,58 @@ class MatcherGroup:
def on_metaevent(
self,
*,
rule: Optional[Union[Rule, T_RuleChecker]] = None,
handlers: Optional[List[Union[T_Handler, Handler]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
state_factory: Optional[T_StateFactory] = ...) -> Type[Matcher]:
...
def on_message(
self,
*,
rule: Optional[Union[Rule, T_RuleChecker]] = None,
permission: Optional[Permission] = None,
handlers: Optional[List[Union[T_Handler, Handler]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = True,
state: Optional[T_State] = None,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
state_factory: Optional[T_StateFactory] = ...) -> Type[Matcher]:
...
def on_notice(
self,
*,
rule: Optional[Union[Rule, T_RuleChecker]] = None,
handlers: Optional[List[Union[T_Handler, Handler]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
state_factory: Optional[T_StateFactory] = ...) -> Type[Matcher]:
...
def on_request(
self,
*,
rule: Optional[Union[Rule, T_RuleChecker]] = None,
handlers: Optional[List[Union[T_Handler, Handler]]] = None,
temp: bool = False,
priority: int = 1,
block: bool = False,
state: Optional[T_State] = None,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
priority: int = ...,
block: bool = ...,
state: Optional[T_State] = ...,
state_factory: Optional[T_StateFactory] = ...) -> Type[Matcher]:
...
def on_startswith(
self,
msg: str,
msg: Union[str, Tuple[str, ...]],
*,
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
ignorecase: bool = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
@ -348,9 +315,10 @@ class MatcherGroup:
def on_endswith(
self,
msg: str,
msg: Union[str, Tuple[str, ...]],
*,
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
ignorecase: bool = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
@ -364,7 +332,7 @@ class MatcherGroup:
self,
keywords: Set[str],
*,
rule: Optional[Optional[Union[Rule, T_RuleChecker]]] = ...,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Permission] = ...,
handlers: Optional[List[Union[T_Handler, Handler]]] = ...,
temp: bool = ...,
@ -408,7 +376,7 @@ class MatcherGroup:
def on_regex(
self,
pattern: str,
flags: Union[int, re.RegexFlag] = 0,
flags: Union[int, re.RegexFlag] = ...,
*,
rule: Optional[Union[Rule, T_RuleChecker]] = ...,
permission: Optional[Permission] = ...,
@ -419,3 +387,40 @@ class MatcherGroup:
state: Optional[T_State] = ...,
state_factory: Optional[T_StateFactory] = ...) -> Type[Matcher]:
...
def load_plugin(module_path: str) -> Optional[Plugin]:
...
def load_plugins(*plugin_dir: str) -> Set[Plugin]:
...
def load_all_plugins(module_path: Set[str],
plugin_dir: Set[str]) -> Set[Plugin]:
...
def load_from_json(file_path: str, encoding: str = ...) -> Set[Plugin]:
...
def load_from_toml(file_path: str, encoding: str = ...) -> Set[Plugin]:
...
def load_builtin_plugins(name: str = ...) -> Optional[Plugin]:
...
def get_plugin(name: str) -> Optional[Plugin]:
...
def get_loaded_plugins() -> Set[Plugin]:
...
def require(name: str) -> Optional[Export]:
...

View File

@ -12,8 +12,8 @@ from importlib.machinery import PathFinder, SourceFileLoader
from .export import _export, Export
_current_plugin: ContextVar[Optional[str]] = ContextVar("_current_plugin",
default=None)
_current_plugin: ContextVar[Optional[ModuleType]] = ContextVar(
"_current_plugin", default=None)
_internal_space = ModuleType(__name__ + "._internal")
_internal_space.__path__ = [] # type: ignore
@ -53,14 +53,13 @@ class _InternalModule(ModuleType):
class PluginManager:
def __init__(self,
namespace: Optional[str] = None,
namespace: str,
plugins: Optional[Iterable[str]] = None,
search_path: Optional[Iterable[str]] = None,
*,
id: Optional[str] = None):
self.namespace: Optional[str] = namespace
self.namespace_module: Optional[ModuleType] = self._setup_namespace(
namespace)
self.namespace: str = namespace
self.namespace_module: ModuleType = self._setup_namespace(namespace)
self.id: str = id or str(uuid.uuid4())
self.internal_id: str = md5(
@ -73,12 +72,7 @@ class PluginManager:
# ensure can be loaded
self.list_plugins()
def _setup_namespace(self,
namespace: Optional[str] = None
) -> Optional[ModuleType]:
if not namespace:
return None
def _setup_namespace(self, namespace: str) -> ModuleType:
try:
module = importlib.import_module(namespace)
except ImportError:
@ -156,14 +150,18 @@ class PluginManager:
def load_all_plugins(self) -> List[ModuleType]:
return [self.load_plugin(name) for name in self.list_plugins()]
def _rewrite_module_name(self, module_name) -> Optional[str]:
def _rewrite_module_name(self, module_name: str) -> Optional[str]:
prefix = f"{self.internal_module.__name__}."
if module_name.startswith(self.namespace + "."):
path = module_name.split(".")
length = self.namespace.count(".") + 1
return f"{prefix}{'.'.join(path[length:])}"
raw_name = module_name[len(self.namespace) +
1:] if module_name.startswith(self.namespace +
".") else None
# dir plugins
if raw_name and raw_name.split(".")[0] in self.search_plugins():
return f"{prefix}{raw_name}"
# third party plugin or renamed dir plugins
elif module_name in self.plugins or module_name.startswith(prefix):
return module_name
# dir plugins
elif module_name in self.search_plugins():
return f"{prefix}{module_name}"
return None
@ -194,36 +192,44 @@ class PluginLoader(SourceFileLoader):
def __init__(self, manager: PluginManager, fullname: str, path) -> None:
self.manager = manager
self.loaded = False
self._plugin_token = None
self._export_token = None
super().__init__(fullname, path)
def create_module(self, spec) -> Optional[ModuleType]:
if self.name in sys.modules:
self.loaded = True
return sys.modules[self.name]
prefix = self.manager.internal_module.__name__
plugin_name = self.name[len(prefix):] if self.name.startswith(
prefix) else self.name
self._plugin_token = _current_plugin.set(plugin_name.lstrip("."))
self._export_token = _export.set(Export())
# return None to use default module creation
return super().create_module(spec)
def exec_module(self, module: ModuleType) -> None:
if self.loaded:
return
# really need?
# setattr(module, "__manager__", self.manager)
if self._export_token:
setattr(module, "__export__", _export.get())
export = Export()
_export_token = _export.set(export)
prefix = self.manager.internal_module.__name__
is_dir_plugin = self.name.startswith(prefix + ".")
module_name = self.name[len(prefix) +
1:] if is_dir_plugin else self.name
_plugin_token = _current_plugin.set(module)
setattr(module, "__export__", export)
setattr(module, "__plugin_name__",
module_name.split(".")[0] if is_dir_plugin else module_name)
setattr(module, "__module_name__", module_name)
setattr(module, "__module_prefix__", prefix if is_dir_plugin else "")
# try:
# super().exec_module(module)
# except Exception as e:
# raise ImportError(
# f"Error when executing module {module_name} from {module.__file__}."
# ) from e
super().exec_module(module)
if self._plugin_token:
_current_plugin.reset(self._plugin_token)
if self._export_token:
_export.reset(self._export_token)
_current_plugin.reset(_plugin_token)
_export.reset(_export_token)
return

View File

@ -175,7 +175,8 @@ class TrieRule:
})
def startswith(msg: str) -> Rule:
def startswith(msg: Union[str, Tuple[str, ...]],
ignorecase: bool = False) -> Rule:
"""
:说明:
@ -185,17 +186,24 @@ def startswith(msg: str) -> Rule:
* ``msg: str``: 消息开头字符串
"""
if isinstance(msg, str):
msg = (msg,)
pattern = re.compile(
f"^(?:{'|'.join(re.escape(prefix) for prefix in msg)})",
re.IGNORECASE if ignorecase else 0)
async def _startswith(bot: "Bot", event: "Event", state: T_State) -> bool:
if event.get_type() != "message":
return False
text = event.get_plaintext()
return text.startswith(msg)
return bool(pattern.match(text))
return Rule(_startswith)
def endswith(msg: str) -> Rule:
def endswith(msg: Union[str, Tuple[str, ...]],
ignorecase: bool = False) -> Rule:
"""
:说明:
@ -205,11 +213,18 @@ def endswith(msg: str) -> Rule:
* ``msg: str``: 消息结尾字符串
"""
if isinstance(msg, str):
msg = (msg,)
pattern = re.compile(
f"(?:{'|'.join(re.escape(prefix) for prefix in msg)})$",
re.IGNORECASE if ignorecase else 0)
async def _endswith(bot: "Bot", event: "Event", state: T_State) -> bool:
if event.get_type() != "message":
return False
return event.get_plaintext().endswith(msg)
text = event.get_plaintext()
return bool(pattern.match(text))
return Rule(_endswith)

View File

@ -55,21 +55,21 @@ T_StateFactory = Callable[["Bot", "Event"], Awaitable[T_State]]
事件处理状态 State 类工厂函数
"""
T_WebSocketConnectionHook = Callable[["Bot"], Awaitable[None]]
T_BotConnectionHook = Callable[["Bot"], Awaitable[None]]
"""
:类型: ``Callable[[Bot], Awaitable[None]]``
:说明:
WebSocket 连接建立时执行的函数
Bot 连接建立时执行的函数
"""
T_WebSocketDisconnectionHook = Callable[["Bot"], Awaitable[None]]
T_BotDisconnectionHook = Callable[["Bot"], Awaitable[None]]
"""
:类型: ``Callable[[Bot], Awaitable[None]]``
:说明:
WebSocket 连接断开时执行的函数
Bot 连接断开时执行的函数
"""
T_CallingAPIHook = Callable[["Bot", str, Dict[str, Any]], Awaitable[None]]
"""

View File

@ -17,8 +17,8 @@
"scripts": {
"dev": "vuepress dev docs",
"build": "vuepress build docs",
"lint": "npx prettier -c docs/**/* !docs/api/**/*",
"lint:fix": "npx prettier --write docs/**/* !docs/api/**/*"
"lint": "npx prettier --config .prettierrc -c docs/**/* !docs/api/**/*",
"lint:fix": "npx prettier --config .prettierrc --write docs/**/* !docs/api/**/*"
},
"license": "MIT",
"devDependencies": {

View File

@ -100,14 +100,14 @@ def _check_at_me(bot: "Bot", event: "Event"):
# check the first segment
if event.message[0] == at_me_seg:
event.to_me = True
del event.message[0]
event.message.pop(0)
if event.message and event.message[0].type == "text":
event.message[0].data["text"] = event.message[0].data[
"text"].lstrip()
if not event.message[0].data["text"]:
del event.message[0]
if event.message and event.message[0] == at_me_seg:
del event.message[0]
event.message.pop(0)
if event.message and event.message[0].type == "text":
event.message[0].data["text"] = event.message[0].data[
"text"].lstrip()

View File

@ -68,8 +68,7 @@ class Bot(BaseBot):
async def handle_message(self, message: dict):
...
async def call_api(self, api: str, *, self_id: Optional[str],
**data) -> Any:
async def call_api(self, api: str, *, **data) -> Any:
...
async def send(self, event: Event, message: Union[str, Message,
@ -77,12 +76,13 @@ class Bot(BaseBot):
**kwargs) -> Any:
...
async def send_private_msg(self,
async def send_private_msg(
self,
*,
user_id: int,
message: Union[str, Message],
auto_escape: bool = ...,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -93,16 +93,17 @@ class Bot(BaseBot):
* ``user_id``: 对方 QQ
* ``message``: 要发送的内容
* ``auto_escape``: 消息内容是否作为纯文本发送即不解析 CQ 只在 ``message`` 字段是字符串时有效
* ``self_id``: 机器人 QQ
"""
...
async def send_group_msg(self,
async def send_group_msg(
self,
*,
group_id: int,
message: Union[str, Message],
auto_escape: bool = ...,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -113,18 +114,19 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``message``: 要发送的内容
* ``auto_escape``: 消息内容是否作为纯文本发送即不解析 CQ 只在 ``message`` 字段是字符串时有效
* ``self_id``: 机器人 QQ
"""
...
async def send_msg(self,
async def send_msg(
self,
*,
message_type: Optional[str] = ...,
user_id: Optional[int] = ...,
group_id: Optional[int] = ...,
message: Union[str, Message],
auto_escape: bool = ...,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -137,14 +139,15 @@ class Bot(BaseBot):
* ``group_id``: 群号消息类型为 ``group`` 时需要
* ``message``: 要发送的内容
* ``auto_escape``: 消息内容是否作为纯文本发送即不解析 CQ 只在 ``message`` 字段是字符串时有效
* ``self_id``: 机器人 QQ
"""
...
async def delete_msg(self,
async def delete_msg(
self,
*,
message_id: int,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -153,14 +156,15 @@ class Bot(BaseBot):
:参数:
* ``message_id``: 消息 ID
* ``self_id``: 机器人 QQ
"""
...
async def get_msg(self,
async def get_msg(
self,
*,
message_id: int,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -169,14 +173,15 @@ class Bot(BaseBot):
:参数:
* ``message_id``: 消息 ID
* ``self_id``: 机器人 QQ
"""
...
async def get_forward_msg(self,
async def get_forward_msg(
self,
*,
id: int,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -185,15 +190,16 @@ class Bot(BaseBot):
:参数:
* ``id``: 合并转发 ID
* ``self_id``: 机器人 QQ
"""
...
async def send_like(self,
async def send_like(
self,
*,
user_id: int,
times: int = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -203,16 +209,17 @@ class Bot(BaseBot):
* ``user_id``: 对方 QQ
* ``times``: 赞的次数每个好友每天最多 10
* ``self_id``: 机器人 QQ
"""
...
async def set_group_kick(self,
async def set_group_kick(
self,
*,
group_id: int,
user_id: int,
reject_add_request: bool = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -223,16 +230,17 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``user_id``: 要踢的 QQ
* ``reject_add_request``: 拒绝此人的加群请求
* ``self_id``: 机器人 QQ
"""
...
async def set_group_ban(self,
async def set_group_ban(
self,
*,
group_id: int,
user_id: int,
duration: int = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -243,17 +251,18 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``user_id``: 要禁言的 QQ
* ``duration``: 禁言时长单位秒``0`` 表示取消禁言
* ``self_id``: 机器人 QQ
"""
...
async def set_group_anonymous_ban(self,
async def set_group_anonymous_ban(
self,
*,
group_id: int,
anonymous: Optional[Dict[str, Any]] = ...,
anonymous_flag: Optional[str] = ...,
duration: int = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -265,15 +274,16 @@ class Bot(BaseBot):
* ``anonymous``: 可选要禁言的匿名用户对象群消息上报的 ``anonymous`` 字段
* ``anonymous_flag``: 可选要禁言的匿名用户的 flag需从群消息上报的数据中获得
* ``duration``: 禁言时长单位秒无法取消匿名用户禁言
* ``self_id``: 机器人 QQ
"""
...
async def set_group_whole_ban(self,
async def set_group_whole_ban(
self,
*,
group_id: int,
enable: bool = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -283,16 +293,17 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``enable``: 是否禁言
* ``self_id``: 机器人 QQ
"""
...
async def set_group_admin(self,
async def set_group_admin(
self,
*,
group_id: int,
user_id: int,
enable: bool = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -303,15 +314,16 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``user_id``: 要设置管理员的 QQ
* ``enable``: ``True`` 为设置``False`` 为取消
* ``self_id``: 机器人 QQ
"""
...
async def set_group_anonymous(self,
async def set_group_anonymous(
self,
*,
group_id: int,
enable: bool = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -321,16 +333,17 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``enable``: 是否允许匿名聊天
* ``self_id``: 机器人 QQ
"""
...
async def set_group_card(self,
async def set_group_card(
self,
*,
group_id: int,
user_id: int,
card: str = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -341,15 +354,16 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``user_id``: 要设置的 QQ
* ``card``: 群名片内容不填或空字符串表示删除群名片
* ``self_id``: 机器人 QQ
"""
...
async def set_group_name(self,
async def set_group_name(
self,
*,
group_id: int,
group_name: str,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -359,15 +373,16 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``group_name``: 新群名
* ``self_id``: 机器人 QQ
"""
...
async def set_group_leave(self,
async def set_group_leave(
self,
*,
group_id: int,
is_dismiss: bool = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -377,17 +392,18 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``is_dismiss``: 是否解散如果登录号是群主则仅在此项为 True 时能够解散
* ``self_id``: 机器人 QQ
"""
...
async def set_group_special_title(self,
async def set_group_special_title(
self,
*,
group_id: int,
user_id: int,
special_title: str = ...,
duration: int = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -399,16 +415,17 @@ class Bot(BaseBot):
* ``user_id``: 要设置的 QQ
* ``special_title``: 专属头衔不填或空字符串表示删除专属头衔
* ``duration``: 专属头衔有效期单位秒-1 表示永久不过此项似乎没有效果可能是只有某些特殊的时间长度有效有待测试
* ``self_id``: 机器人 QQ
"""
...
async def set_friend_add_request(self,
async def set_friend_add_request(
self,
*,
flag: str,
approve: bool = ...,
remark: str = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -419,17 +436,18 @@ class Bot(BaseBot):
* ``flag``: 加好友请求的 flag需从上报的数据中获得
* ``approve``: 是否同意请求
* ``remark``: 添加后的好友备注仅在同意时有效
* ``self_id``: 机器人 QQ
"""
...
async def set_group_add_request(self,
async def set_group_add_request(
self,
*,
flag: str,
sub_type: str,
approve: bool = ...,
reason: str = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -441,29 +459,25 @@ class Bot(BaseBot):
* ``sub_type``: ``add`` ``invite``请求类型需要和上报消息中的 ``sub_type`` 字段相符
* ``approve``: 是否同意请求邀请
* ``reason``: 拒绝理由仅在拒绝时有效
* ``self_id``: 机器人 QQ
"""
...
async def get_login_info(self,
*,
self_id: Optional[int] = ...) -> Dict[str, Any]:
async def get_login_info(self) -> Dict[str, Any]:
"""
:说明:
获取登录号信息
:参数:
* ``self_id``: 机器人 QQ
"""
...
async def get_stranger_info(self,
async def get_stranger_info(
self,
*,
user_id: int,
no_cache: bool = ...,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -473,30 +487,25 @@ class Bot(BaseBot):
* ``user_id``: QQ
* ``no_cache``: 是否不使用缓存使用缓存可能更新不及时但响应更快
* ``self_id``: 机器人 QQ
"""
...
async def get_friend_list(self,
*,
self_id: Optional[int] = ...
) -> List[Dict[str, Any]]:
async def get_friend_list(self) -> List[Dict[str, Any]]:
"""
:说明:
获取好友列表
:参数:
* ``self_id``: 机器人 QQ
"""
...
async def get_group_info(self,
async def get_group_info(
self,
*,
group_id: int,
no_cache: bool = ...,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -506,22 +515,16 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``no_cache``: 是否不使用缓存使用缓存可能更新不及时但响应更快
* ``self_id``: 机器人 QQ
"""
...
async def get_group_list(self,
*,
self_id: Optional[int] = ...
) -> List[Dict[str, Any]]:
async def get_group_list(self) -> List[Dict[str, Any]]:
"""
:说明:
获取群列表
:参数:
* ``self_id``: 机器人 QQ
"""
...
@ -531,7 +534,7 @@ class Bot(BaseBot):
group_id: int,
user_id: int,
no_cache: bool = ...,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -542,7 +545,7 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``user_id``: QQ
* ``no_cache``: 是否不使用缓存使用缓存可能更新不及时但响应更快
* ``self_id``: 机器人 QQ
"""
...
@ -550,7 +553,7 @@ class Bot(BaseBot):
self,
*,
group_id: int,
self_id: Optional[int] = ...) -> List[Dict[str, Any]]:
) -> List[Dict[str, Any]]:
"""
:说明:
@ -559,15 +562,15 @@ class Bot(BaseBot):
:参数:
* ``group_id``: 群号
* ``self_id``: 机器人 QQ
"""
...
async def get_group_honor_info(self,
async def get_group_honor_info(
self,
*,
group_id: int,
type: str = ...,
self_id: Optional[int] = ...
) -> Dict[str, Any]:
"""
:说明:
@ -578,14 +581,15 @@ class Bot(BaseBot):
* ``group_id``: 群号
* ``type``: 要获取的群荣誉类型可传入 ``talkative`` ``performer`` ``legend`` ``strong_newbie`` ``emotion`` 以分别获取单个类型的群荣誉数据或传入 ``all`` 获取所有数据
* ``self_id``: 机器人 QQ
"""
...
async def get_cookies(self,
async def get_cookies(
self,
*,
domain: str = ...,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -594,28 +598,24 @@ class Bot(BaseBot):
:参数:
* ``domain``: 需要获取 cookies 的域名
* ``self_id``: 机器人 QQ
"""
...
async def get_csrf_token(self,
*,
self_id: Optional[int] = ...) -> Dict[str, Any]:
async def get_csrf_token(self) -> Dict[str, Any]:
"""
:说明:
获取 CSRF Token
:参数:
* ``self_id``: 机器人 QQ
"""
...
async def get_credentials(self,
async def get_credentials(
self,
*,
domain: str = ...,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -624,15 +624,16 @@ class Bot(BaseBot):
:参数:
* ``domain``: 需要获取 cookies 的域名
* ``self_id``: 机器人 QQ
"""
...
async def get_record(self,
async def get_record(
self,
*,
file: str,
out_format: str,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -642,14 +643,15 @@ class Bot(BaseBot):
* ``file``: 收到的语音文件名CQ 码的 ``file`` 参数 ``0B38145AA44505000B38145AA4450500.silk``
* ``out_format``: 要转换到的格式目前支持 ``mp3````amr````wma````m4a````spx````ogg````wav````flac``
* ``self_id``: 机器人 QQ
"""
...
async def get_image(self,
async def get_image(
self,
*,
file: str,
self_id: Optional[int] = ...) -> Dict[str, Any]:
) -> Dict[str, Any]:
"""
:说明:
@ -658,70 +660,51 @@ class Bot(BaseBot):
:参数:
* ``file``: 收到的图片文件名CQ 码的 ``file`` 参数 ``6B4DE3DFD1BD271E3297859D41C530F5.jpg``
* ``self_id``: 机器人 QQ
"""
...
async def can_send_image(self,
*,
self_id: Optional[int] = ...) -> Dict[str, Any]:
async def can_send_image(self) -> Dict[str, Any]:
"""
:说明:
检查是否可以发送图片
:参数:
* ``self_id``: 机器人 QQ
"""
...
async def can_send_record(self,
*,
self_id: Optional[int] = ...) -> Dict[str, Any]:
async def can_send_record(self) -> Dict[str, Any]:
"""
:说明:
检查是否可以发送语音
:参数:
* ``self_id``: 机器人 QQ
"""
...
async def get_status(self,
*,
self_id: Optional[int] = ...) -> Dict[str, Any]:
async def get_status(self) -> Dict[str, Any]:
"""
:说明:
获取插件运行状态
:参数:
* ``self_id``: 机器人 QQ
"""
...
async def get_version_info(self,
*,
self_id: Optional[int] = ...) -> Dict[str, Any]:
async def get_version_info(self) -> Dict[str, Any]:
"""
:说明:
获取版本信息
:参数:
* ``self_id``: 机器人 QQ
"""
...
async def set_restart(self,
async def set_restart(
self,
*,
delay: int = ...,
self_id: Optional[int] = ...) -> None:
) -> None:
"""
:说明:
@ -730,18 +713,15 @@ class Bot(BaseBot):
:参数:
* ``delay``: 要延迟的毫秒数如果默认情况下无法重启可以尝试设置延迟为 2000 左右
* ``self_id``: 机器人 QQ
"""
...
async def clean_cache(self, *, self_id: Optional[int] = ...) -> None:
async def clean_cache(self) -> None:
"""
:说明:
清理数据目录
:参数:
* ``self_id``: 机器人 QQ
"""
...

View File

@ -1,6 +1,9 @@
import re
from io import BytesIO
from pathlib import Path
from base64 import b64encode
from functools import reduce
from typing import Any, Dict, Union, Tuple, Mapping, Iterable, Optional
from typing import Any, List, Dict, Union, Tuple, Mapping, Iterable, Optional
from nonebot.typing import overrides
from nonebot.adapters import Message as BaseMessage, MessageSegment as BaseMessageSegment
@ -79,17 +82,23 @@ class MessageSegment(BaseMessageSegment):
return MessageSegment("forward", {"id": id_})
@staticmethod
def image(file: str,
def image(file: Union[str, bytes, BytesIO, Path],
type_: Optional[str] = None,
cache: bool = True,
proxy: bool = True,
timeout: Optional[int] = None) -> "MessageSegment":
if isinstance(file, BytesIO):
file = file.read()
if isinstance(file, bytes):
file = f"base64://{b64encode(file).decode()}"
elif isinstance(file, Path):
file = f"file:///{file.resolve()}"
return MessageSegment(
"image", {
"file": file,
"type": type_,
"cache": cache,
"proxy": proxy,
"cache": _b2s(cache),
"proxy": _b2s(proxy),
"timeout": timeout
})
@ -148,17 +157,23 @@ class MessageSegment(BaseMessageSegment):
return MessageSegment("poke", {"type": type_, "id": id_})
@staticmethod
def record(file: str,
def record(file: Union[str, bytes, BytesIO, Path],
magic: Optional[bool] = None,
cache: Optional[bool] = None,
proxy: Optional[bool] = None,
timeout: Optional[int] = None) -> "MessageSegment":
if isinstance(file, BytesIO):
file = file.read()
if isinstance(file, bytes):
file = f"base64://{b64encode(file).decode()}"
elif isinstance(file, Path):
file = f"file:///{file.resolve()}"
return MessageSegment(
"record", {
"file": file,
"magic": _b2s(magic),
"cache": cache,
"proxy": proxy,
"cache": _b2s(cache),
"proxy": _b2s(proxy),
"timeout": timeout
})
@ -191,14 +206,21 @@ class MessageSegment(BaseMessageSegment):
return MessageSegment("text", {"text": text})
@staticmethod
def video(file: str,
def video(file: Union[str, bytes, BytesIO, Path],
cache: Optional[bool] = None,
proxy: Optional[bool] = None,
timeout: Optional[int] = None) -> "MessageSegment":
return MessageSegment("video", {
if isinstance(file, BytesIO):
file = file.read()
if isinstance(file, bytes):
file = f"base64://{b64encode(file).decode()}"
elif isinstance(file, Path):
file = f"file:///{file.resolve()}"
return MessageSegment(
"video", {
"file": file,
"cache": cache,
"proxy": proxy,
"cache": _b2s(cache),
"proxy": _b2s(proxy),
"timeout": timeout
})
@ -207,7 +229,7 @@ class MessageSegment(BaseMessageSegment):
return MessageSegment("xml", {"data": data})
class Message(BaseMessage):
class Message(BaseMessage[MessageSegment]):
"""
CQHTTP 协议 Message 适配
"""

View File

@ -167,7 +167,7 @@ reference = "aliyun"
[[package]]
name = "nonebot2"
version = "2.0.0-alpha.12"
version = "2.0.0a13.post1"
description = "An asynchronous python bot framework."
category = "main"
optional = false
@ -226,7 +226,7 @@ reference = "aliyun"
[[package]]
name = "python-dotenv"
version = "0.16.0"
version = "0.17.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
category = "main"
optional = false
@ -418,7 +418,7 @@ reference = "aliyun"
[metadata]
lock-version = "1.1"
python-versions = "^3.7.3"
content-hash = "b8ec196a78675b4098ab7509cbdbd311ffcbcf1ce8b625c589f1e95596801c71"
content-hash = "f1908ea0987f3c4a50d18c8d0350b73602fa57e9349e1aa95b3190064ebcc881"
[metadata.files]
certifi = [
@ -500,8 +500,8 @@ pygtrie = [
{file = "pygtrie-2.4.2.tar.gz", hash = "sha256:43205559d28863358dbbf25045029f58e2ab357317a59b11f11ade278ac64692"},
]
python-dotenv = [
{file = "python-dotenv-0.16.0.tar.gz", hash = "sha256:9fa413c37d4652d3fa02fea0ff465c384f5db75eab259c4fc5d0c5b8bf20edd4"},
{file = "python_dotenv-0.16.0-py2.py3-none-any.whl", hash = "sha256:31d752f5b748f4e292448c9a0cac6a08ed5e6f4cefab85044462dcad56905cec"},
{file = "python-dotenv-0.17.0.tar.gz", hash = "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a"},
{file = "python_dotenv-0.17.0-py2.py3-none-any.whl", hash = "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"},
]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "nonebot-adapter-cqhttp"
version = "2.0.0-alpha.12"
version = "2.0.0-alpha.13"
description = "OneBot(CQHTTP) adapter for nonebot2"
authors = ["yanyongyu <yyy@nonebot.dev>"]
license = "MIT"
@ -25,7 +25,7 @@ exclude = ["nonebot/__init__.py", "nonebot/adapters/__init__.py"]
[tool.poetry.dependencies]
python = "^3.7.3"
httpx = "^0.17.0"
nonebot2 = "^2.0.0-alpha.12"
nonebot2 = "^2.0.0-alpha.13"
[tool.poetry.dev-dependencies]
nonebot2 = { path = "../../", develop = true }

View File

@ -155,7 +155,7 @@ class MessageSegment(BaseMessageSegment):
return {"msgtype": self.type, self.type: copy(self.data)}
class Message(BaseMessage):
class Message(BaseMessage[MessageSegment]):
"""
钉钉 协议 Message 适配
"""

View File

@ -266,7 +266,7 @@ class MessageSegment(BaseMessageSegment):
return cls(type=MessageType.POKE, name=name)
class MessageChain(BaseMessage):
class MessageChain(BaseMessage[MessageSegment]):
"""
Mirai 协议 Message 适配

View File

@ -4,7 +4,15 @@ sidebar: auto
# 更新日志
## v2.0.0a12
## v2.0.0a14
- 修改日志等级,支持输出等级自定义
- 修复日志输出模块名错误
- 修改 `Matcher` 属性 `module` 类型
- 新增 `Matcher` 属性 `plugin_name` `module_name` `module_prefix`
- 移除 `bot.call_api` 参数 `self_id` 切换机器人支持
## v2.0.0a13.post1
- 分离 `handler``matcher`
- 修复 `cqhttp` secret 校验出错

View File

@ -1,3 +1,4 @@
ENVIRONMENT=dev
LOG_LEVEL=25
CUSTOM_CONFIG=common
FASTAPI_RELOAD_DIRS=["test_plugins"]

View File

@ -2,6 +2,7 @@ DRIVER=nonebot.drivers.fastapi
HOST=0.0.0.0
PORT=2333
DEBUG=true
LOG_LEVEL=DEBUG
SUPERUSERS=["123123123"]
NICKNAME=["bot"]