mirror of
https://github.com/suyiiyii/nonebot-bison.git
synced 2025-06-08 04:43:00 +08:00
Merge branch 'main' into feat/db
This commit is contained in:
commit
05f1edd7fa
25
.github/actions/setup-python/action.yml
vendored
25
.github/actions/setup-python/action.yml
vendored
@ -18,23 +18,14 @@ runs:
|
|||||||
- name: Install poetry
|
- name: Install poetry
|
||||||
uses: Gr1N/setup-poetry@v7
|
uses: Gr1N/setup-poetry@v7
|
||||||
|
|
||||||
- name: Cache Windows dependencies
|
- id: poetry-cache
|
||||||
uses: actions/cache@v2
|
run: echo "::set-output name=dir::$(poetry config virtualenvs.path)"
|
||||||
if: ${{ runner.os == 'Windows' }}
|
shell: bash
|
||||||
with:
|
|
||||||
path: ~/AppData/Local/pypoetry/Cache/virtualenvs
|
|
||||||
key: ${{ runner.os }}-poetry-${{ inputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
|
||||||
|
|
||||||
- name: Cache Linux dependencies
|
- uses: actions/cache@v2
|
||||||
uses: actions/cache@v2
|
|
||||||
if: ${{ runner.os == 'Linux' }}
|
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ${{ steps.poetry-cache.outputs.dir }}
|
||||||
key: ${{ runner.os }}-poetry-${{ inputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
key: ${{ runner.os }}-poetry-${{ steps.python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||||
|
|
||||||
- name: Cache macOS dependencies
|
- run: poetry install
|
||||||
uses: actions/cache@v2
|
shell: bash
|
||||||
if: ${{ runner.os == 'macOS' }}
|
|
||||||
with:
|
|
||||||
path: ~/Library/Caches/pypoetry/virtualenvs
|
|
||||||
key: ${{ runner.os }}-poetry-${{ inputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
|
||||||
|
14
.github/workflows/main.yml
vendored
14
.github/workflows/main.yml
vendored
@ -4,7 +4,21 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- admin-frontend/**
|
||||||
|
- docker/**
|
||||||
|
- src/**
|
||||||
|
- tests/**
|
||||||
|
- pyproject.toml
|
||||||
|
- poetry.lock
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- admin-frontend/**
|
||||||
|
- docker/**
|
||||||
|
- src/**
|
||||||
|
- tests/**
|
||||||
|
- pyproject.toml
|
||||||
|
- poetry.lock
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
4
.github/workflows/website-preview.yml
vendored
4
.github/workflows/website-preview.yml
vendored
@ -2,6 +2,10 @@ name: Site Deploy(Preview)
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
|
paths:
|
||||||
|
- docs/**
|
||||||
|
- package.json
|
||||||
|
- yarn.lock
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
preview:
|
preview:
|
||||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -4,9 +4,20 @@
|
|||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|
||||||
|
- 添加bilibili开播提醒 [@Sichongzou](https://github.com/Sichongzou) ([#60](https://github.com/felinae98/nonebot-bison/pull/60))
|
||||||
|
- 添加User-Agent配置 [@felinae98](https://github.com/felinae98) ([#78](https://github.com/felinae98/nonebot-bison/pull/78))
|
||||||
- 增加代理设置 [@felinae98](https://github.com/felinae98) ([#71](https://github.com/felinae98/nonebot-bison/pull/71))
|
- 增加代理设置 [@felinae98](https://github.com/felinae98) ([#71](https://github.com/felinae98/nonebot-bison/pull/71))
|
||||||
- 增加Parse Target功能 [@felinae98](https://github.com/felinae98) ([#72](https://github.com/felinae98/nonebot-bison/pull/72))
|
- 增加Parse Target功能 [@felinae98](https://github.com/felinae98) ([#72](https://github.com/felinae98/nonebot-bison/pull/72))
|
||||||
|
|
||||||
|
### Bug 修复
|
||||||
|
|
||||||
|
- 捕获 JSONDecodeError [@felinae98](https://github.com/felinae98) ([#82](https://github.com/felinae98/nonebot-bison/pull/82))
|
||||||
|
- 捕获SSL异常 [@felinae98](https://github.com/felinae98) ([#75](https://github.com/felinae98/nonebot-bison/pull/75))
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
|
||||||
|
- 完善开发文档 [@AzideCupric](https://github.com/AzideCupric) ([#80](https://github.com/felinae98/nonebot-bison/pull/80))
|
||||||
|
|
||||||
## v0.5.3
|
## v0.5.3
|
||||||
|
|
||||||
- on_command 设置 block=True (#63) @MeetWq
|
- on_command 设置 block=True (#63) @MeetWq
|
||||||
|
@ -38,6 +38,8 @@
|
|||||||
- 专栏
|
- 专栏
|
||||||
- 转发
|
- 转发
|
||||||
- 纯文字
|
- 纯文字
|
||||||
|
- Bilibili 直播
|
||||||
|
- 开播提醒
|
||||||
- RSS
|
- RSS
|
||||||
- 富文本转换为纯文本
|
- 富文本转换为纯文本
|
||||||
- 提取出所有图片
|
- 提取出所有图片
|
||||||
|
@ -31,6 +31,11 @@ sidebar: auto
|
|||||||
|
|
||||||
本插件需要你的帮助!只需要会写简单的爬虫,就能给本插件适配新的网站。
|
本插件需要你的帮助!只需要会写简单的爬虫,就能给本插件适配新的网站。
|
||||||
|
|
||||||
|
::: danger
|
||||||
|
Nonebot 项目使用了全异步的处理方式,所以你需要对异步,Python asyncio 的机制有一定了解,当然,
|
||||||
|
依葫芦画瓢也是足够的
|
||||||
|
:::
|
||||||
|
|
||||||
## 基本概念
|
## 基本概念
|
||||||
|
|
||||||
- `nonebot_bison.post.Post`: 可以理解为推送内容,其中包含需要发送的文字,图片,链接,平台信息等
|
- `nonebot_bison.post.Post`: 可以理解为推送内容,其中包含需要发送的文字,图片,链接,平台信息等
|
||||||
@ -53,7 +58,7 @@ sidebar: auto
|
|||||||
例如:微博,Bilibili
|
例如:微博,Bilibili
|
||||||
- `nonebot_bison.platform.platform.StatusChange` 每次爬虫获取一个状态,在状态改变时发布推送
|
- `nonebot_bison.platform.platform.StatusChange` 每次爬虫获取一个状态,在状态改变时发布推送
|
||||||
例如:游戏开服提醒,主播上播提醒
|
例如:游戏开服提醒,主播上播提醒
|
||||||
- `nonebot_bison.platform.platform.SimplePost` 与`NewMessage`相似,但是不过滤新的消息
|
- `nonebot_bison.platform.platform.SimplePost` 与`NewMessage`相似,但是不过滤之前发过的
|
||||||
,每次发送全部消息
|
,每次发送全部消息
|
||||||
例如:每日榜单定时发送
|
例如:每日榜单定时发送
|
||||||
|
|
||||||
@ -64,10 +69,61 @@ sidebar: auto
|
|||||||
- 没有账号的概念
|
- 没有账号的概念
|
||||||
例如:游戏公告,教务处公告
|
例如:游戏公告,教务处公告
|
||||||
|
|
||||||
|
## 实现方法
|
||||||
|
|
||||||
现在你需要在`src/plugins/nonebot_bison/platform`下新建一个 py 文件,
|
现在你需要在`src/plugins/nonebot_bison/platform`下新建一个 py 文件,
|
||||||
在里面新建一个类,继承推送类型的基类,重载一些关键的函数,然后……就完成了,不需要修改别的东西了。
|
在里面新建一个类,继承推送类型的基类,重载一些关键的函数,然后……就完成了,不需要修改别的东西了。
|
||||||
|
|
||||||
任何一种订阅类型需要实现的方法/字段如下:
|
### 不同类型 Platform 的实现适配以及逻辑
|
||||||
|
|
||||||
|
- `nonebot_bison.platform.platform.NewMessage`
|
||||||
|
需要实现:
|
||||||
|
|
||||||
|
- `async get_sub_list(Target) -> list[RawPost]`
|
||||||
|
- `get_id(RawPost)`
|
||||||
|
- `get_date(RawPost)` (可选)
|
||||||
|
|
||||||
|
::: details 大致流程
|
||||||
|
|
||||||
|
1. 调用`get_sub_list`拿到 RawPost 列表
|
||||||
|
2. 调用`get_id`判断是否重复,如果没有重复就说明是新的 RawPost
|
||||||
|
3. 如果有`get_category`和`get_date`,则调用判断 RawPost 是否满足条件
|
||||||
|
4. 调用`parse`生成正式推文
|
||||||
|
:::
|
||||||
|
|
||||||
|
参考[nonebot_bison.platform.Weibo](https://github.com/felinae98/nonebot-bison/blob/v0.5.3/src/plugins/nonebot_bison/platform/weibo.py)
|
||||||
|
|
||||||
|
- `nonebot_bison.platform.platform.StatusChange`
|
||||||
|
需要实现:
|
||||||
|
|
||||||
|
- `async get_status(Target) -> Any`
|
||||||
|
- `compare_status(Target, old_status, new_status) -> list[RawPost]`
|
||||||
|
|
||||||
|
:::details 大致流程
|
||||||
|
|
||||||
|
1. `get_status`获取当前状态
|
||||||
|
2. 传入`compare_status`比较前状态
|
||||||
|
3. 通过则进入`parser`生成 Post
|
||||||
|
:::
|
||||||
|
|
||||||
|
参考[nonenot_bison.platform.AkVersion](https://github.com/felinae98/nonebot-bison/blob/v0.5.3/src/plugins/nonebot_bison/platform/arknights.py#L86)
|
||||||
|
|
||||||
|
- `nonebot_bison.platform.platform.SimplePost`
|
||||||
|
需要实现:
|
||||||
|
|
||||||
|
- `async get_sub_list(Target) -> list[RawPost]`
|
||||||
|
- `get_date(RawPost)` (可选)
|
||||||
|
|
||||||
|
::: details 大致流程
|
||||||
|
|
||||||
|
1. 调用`get_sub_list`拿到 RawPost 列表
|
||||||
|
2. 如果有`get_category`和`get_date`,则调用判断 RawPost 是否满足条件
|
||||||
|
3. 调用`parse`生成正式推文
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 公共方法/成员
|
||||||
|
|
||||||
|
任何一种订阅类型需要实现的方法/成员如下:
|
||||||
|
|
||||||
- `schedule_type`, `schedule_kw` 调度的参数,本质是使用 apscheduler 的[trigger 参数](https://apscheduler.readthedocs.io/en/3.x/userguide.html?highlight=trigger#choosing-the-right-scheduler-job-store-s-executor-s-and-trigger-s),`schedule_type`可以是`date`,`interval`和`cron`,
|
- `schedule_type`, `schedule_kw` 调度的参数,本质是使用 apscheduler 的[trigger 参数](https://apscheduler.readthedocs.io/en/3.x/userguide.html?highlight=trigger#choosing-the-right-scheduler-job-store-s-executor-s-and-trigger-s),`schedule_type`可以是`date`,`interval`和`cron`,
|
||||||
`schedule_kw`是对应的参数,一个常见的配置是`schedule_type=interval`, `schedule_kw={'seconds':30}`
|
`schedule_kw`是对应的参数,一个常见的配置是`schedule_type=interval`, `schedule_kw={'seconds':30}`
|
||||||
@ -80,11 +136,40 @@ sidebar: auto
|
|||||||
- `enable_tag` 平台发布内容是否带 Tag,例如微博
|
- `enable_tag` 平台发布内容是否带 Tag,例如微博
|
||||||
- `platform_name` 唯一的,英文的识别标识,比如`weibo`
|
- `platform_name` 唯一的,英文的识别标识,比如`weibo`
|
||||||
- `async get_target_name(Target) -> Optional[str]` 通常用于获取帐号的名称,如果平台没有帐号概念,可以直接返回平台的`name`
|
- `async get_target_name(Target) -> Optional[str]` 通常用于获取帐号的名称,如果平台没有帐号概念,可以直接返回平台的`name`
|
||||||
- `async parse(RawPost) -> Post`将获取到的 RawPost 处理成 Post
|
|
||||||
- `get_tags(RawPost) -> Optional[Collection[Tag]]` (可选) 从 RawPost 中提取 Tag
|
- `get_tags(RawPost) -> Optional[Collection[Tag]]` (可选) 从 RawPost 中提取 Tag
|
||||||
- `get_category(RawPos) -> Optional[Category]` (可选)从 RawPost 中提取 Category
|
- `get_category(RawPos) -> Optional[Category]` (可选)从 RawPost 中提取 Category
|
||||||
|
- `async parse(RawPost) -> Post` 将获取到的 RawPost 处理成 Post
|
||||||
|
- `async parse_target(str) -> Target` (可选)定制化处理传入用户输入的 Target 字符串,返回 Target(一般是把用户的主页链接解析为 Target),如果输入本身就是 Target,则直接返回 Target
|
||||||
|
- `parse_target_promot` (可选)在要求用户输入 Target 的时候显示的提示文字
|
||||||
|
|
||||||
例如要适配微博,我希望 bot 搬运新的消息,所以微博的类应该这样定义:
|
### 特有的方法/成员
|
||||||
|
|
||||||
|
- `async get_sub_list(Target) -> list[RawPost]` 输入一个`Target`,输出一个`RawPost`的 list
|
||||||
|
- 对于`nonebot_bison.platform.platform.NewMessage`
|
||||||
|
`get_sub_list(Target) -> list[RawPost]` 用于获取对应 Target 的 RawPost 列表,与上一次`get_sub_list`获取的列表比较,过滤出新的 RawPost
|
||||||
|
- 对于`nonebot_bison.platform.platform.SimplePost`
|
||||||
|
`get_sub_list` 用于获取对应 Target 的 RawPost 列表,但不会与上次获取的结果进行比较,而是直接进行发送
|
||||||
|
- `get_id(RawPost) -> Any` 输入一个`RawPost`,从`RawPost`中获取一个唯一的 ID,这个 ID 会用来判断这条`RawPost`是不是之前收到过
|
||||||
|
- `get_date(RawPost) -> Optional[int]` 输入一个`RawPost`,如果可以从`RawPost`中提取出发文的时间,返回发文时间的 timestamp,否则返回`None`
|
||||||
|
- `async get_status(Target) -> Any`
|
||||||
|
- 对于`nonebot_bison.platform.platform.StatusChange`
|
||||||
|
`get_status`用于获取对应 Target 当前的状态,随后将获取的状态作为参数`new_status`传入`compare_status`中
|
||||||
|
- `compare_status(self, target: Target, old_status, new_status) -> list[RawPost]`
|
||||||
|
- 对于`nonebot_bison.platform.platform.StatusChange`
|
||||||
|
`compare_status` 用于比较储存的`old_status`与新传入的`new_status`,并返回发生变更的 RawPost 列表
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
|
||||||
|
当然我们非常希望你对自己适配的平台写一些单元测试
|
||||||
|
|
||||||
|
你可以参照`tests/platforms/test_*.py`中的内容对单元测试进行编写。
|
||||||
|
|
||||||
|
为保证多次运行测试的一致性,可以 mock http 的响应,测试的内容应包括[获取 RawPost](https://github.com/felinae98/nonebot-bison/blob/v0.5.3/tests/platforms/test_weibo.py#L59),处理成 Post
|
||||||
|
,测试分类以及提取 tag 等,当然最好和 rsshub 做一个交叉验证。
|
||||||
|
|
||||||
|
## 一些例子
|
||||||
|
|
||||||
|
例如要适配微博,我希望 bot 搬运新的消息,所以微博的类应该这样实现:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class Weibo(NewMessage):
|
class Weibo(NewMessage):
|
||||||
@ -103,17 +188,26 @@ class Weibo(NewMessage):
|
|||||||
schedule_type = "interval"
|
schedule_type = "interval"
|
||||||
schedule_kw = {"seconds": 3}
|
schedule_kw = {"seconds": 3}
|
||||||
has_target = True
|
has_target = True
|
||||||
|
|
||||||
|
async def get_target_name(self, target: Target) -> Optional[str]:
|
||||||
|
#获取Target对应的用户名
|
||||||
|
...
|
||||||
|
async def get_sub_list(self, target: Target) -> list[RawPost]:
|
||||||
|
#获取对应Target的RawPost列表,会与上一次get_sub_list获取的列表比较,过滤出新的RawPost
|
||||||
|
...
|
||||||
|
def get_id(self, post: RawPost) -> Any:
|
||||||
|
#获取可以标识每个Rawpost的,不与之前RawPost重复的id,用于过滤出新的RawPost
|
||||||
|
...
|
||||||
|
def get_date(self, raw_post: RawPost) -> float:
|
||||||
|
#获取RawPost的发布时间,若bot过滤出的新RawPost发布时间与当前时间差超过2小时,该RawPost将被忽略,可以返回None
|
||||||
|
...
|
||||||
|
def get_tags(self, raw_post: RawPost) -> Optional[list[Tag]]:
|
||||||
|
#获取RawPost中包含的微博话题(#xxx#中的内容)
|
||||||
|
...
|
||||||
|
def get_category(self, raw_post: RawPost) -> Category:
|
||||||
|
#获取该RawPost在该类定义categories的具体分类(转发?视频?图文?...?)
|
||||||
|
...
|
||||||
|
async def parse(self, raw_post: RawPost) -> Post:
|
||||||
|
#将需要bot推送的RawPost处理成正式推送的Post
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
当然我们非常希望你对自己适配的平台写一些单元测试,你可以模仿`tests/platforms/test_*.py`中的内容写
|
|
||||||
一些单元测试。为保证多次运行测试的一致性,可以 mock http 的响应,测试的内容包括获取 RawPost,处理成 Post
|
|
||||||
,测试分类以及提取 tag 等,当然最好和 rsshub 做一个交叉验证。
|
|
||||||
|
|
||||||
::: danger
|
|
||||||
Nonebot 项目使用了全异步的处理方式,所以你需要对异步,Python asyncio 的机制有一定了解,当然,
|
|
||||||
依葫芦画瓢也是足够的
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 类的方法与成员变量
|
|
||||||
|
|
||||||
## 方法与变量的定义
|
|
||||||
|
@ -94,23 +94,27 @@ sidebar: auto
|
|||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
可参考[源文件](https://github.com/felinae98/nonebot-bison/blob/main/src/plugins/nonebot_bison/plugin_config.py)
|
::: tip INFO
|
||||||
|
|
||||||
|
- 所有配置项可参考[源文件](https://github.com/felinae98/nonebot-bison/blob/main/src/plugins/nonebot_bison/plugin_config.py)
|
||||||
|
- **配置项的配置方法** 请参考[NoneBot 配置方式](https://v2.nonebot.dev/docs/tutorial/configuration#%E9%85%8D%E7%BD%AE%E6%96%B9%E5%BC%8F),在`.env`/`.env.*`文件中写入希望配置的 Bison 配置项
|
||||||
|
:::
|
||||||
|
|
||||||
- `BISON_CONFIG_PATH`: 插件存放配置文件的位置,如果不设定默认为项目目录下的`data`目录
|
- `BISON_CONFIG_PATH`: 插件存放配置文件的位置,如果不设定默认为项目目录下的`data`目录
|
||||||
- `BISON_USE_PIC`: 将文字渲染成图片后进行发送,多用于规避风控
|
- `BISON_USE_PIC`: 将文字渲染成图片后进行发送,多用于规避风控
|
||||||
- `BISON_BROWSER`: 本插件使用 Chrome 来渲染图片
|
- `BISON_BROWSER`: 本插件使用 Chrome 来渲染图片
|
||||||
- 使用 browserless 提供的 Chrome 管理服务,设置为`ws://xxxxxxxx`,值为 Chrome Endpoint(推荐)
|
|
||||||
- 使用 cdp 连接相关服务,设置为`wsc://xxxxxxxxx`
|
|
||||||
- 使用本地安装的 Chrome,设置为`local:<chrome path>`,例如`local:/usr/bin/google-chrome-stable`
|
|
||||||
- 如果不进行配置,那么会在启动时候自动进行安装,在官方的 docker 镜像中已经安装了浏览器
|
- 如果不进行配置,那么会在启动时候自动进行安装,在官方的 docker 镜像中已经安装了浏览器
|
||||||
|
- 使用本地安装的 Chrome,设置为`local:<chrome path>`,例如`local:/usr/bin/google-chrome-stable`
|
||||||
|
- 使用 cdp 连接相关服务,设置为`wsc://xxxxxxxxx`
|
||||||
|
- 使用 browserless 提供的 Chrome 管理服务,设置为`ws://xxxxxxxx`,值为 Chrome Endpoint
|
||||||
::: warning
|
::: warning
|
||||||
截止发布时,本项目尚不能完全与 browserless 兼容,目前建议使用镜像内自带的浏览器,即
|
截止发布时,本项目尚不能完全与 browserless 兼容,目前建议使用镜像内自带的浏览器,即
|
||||||
不要配置这个变量
|
不要配置这个变量
|
||||||
:::
|
:::
|
||||||
- `BISON_SKIP_BROWSER_CHECK`: 是否在启动时自动下载浏览器,如果选择`False`会在用到浏览器时自动下载,
|
- `BISON_SKIP_BROWSER_CHECK`: 是否在启动时自动下载浏览器,如果选择`False`会在用到浏览器时自动下载,
|
||||||
默认`True`
|
默认`True`
|
||||||
- `BISON_OUTER_URL`: 从外部访问服务器的地址,默认为`http://localhost:8080/bison`,如果你的插件部署
|
- `BISON_OUTER_URL`: 从外部访问服务器的地址,默认为`http://localhost:8080/bison/`,如果你的插件部署
|
||||||
在服务器上,建议配置为`http://<你的服务器ip>:8080/bison`
|
在服务器上,建议配置为`http://<你的服务器ip>:8080/bison/`
|
||||||
::: warning
|
::: warning
|
||||||
如果需要从外网或者 Docker 容器外访问后台页面,请确保`HOST=0.0.0.0`
|
如果需要从外网或者 Docker 容器外访问后台页面,请确保`HOST=0.0.0.0`
|
||||||
:::
|
:::
|
||||||
@ -124,7 +128,7 @@ sidebar: auto
|
|||||||
- `1`: 首条消息单独发送,剩余图片合并转发
|
- `1`: 首条消息单独发送,剩余图片合并转发
|
||||||
- `2`: 所有消息全部合并转发
|
- `2`: 所有消息全部合并转发
|
||||||
|
|
||||||
::: details 配置项示例
|
::: details BISON_USE_PIC_MERGE 配置项示例
|
||||||
|
|
||||||
- 当`BISON_USE_PIC_MERGE=1`时:
|
- 当`BISON_USE_PIC_MERGE=1`时:
|
||||||

|

|
||||||
@ -137,6 +141,7 @@ sidebar: auto
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
- `BISON_PROXY`: 使用的代理连接,形如`http://<ip>:<port>`(可选)
|
- `BISON_PROXY`: 使用的代理连接,形如`http://<ip>:<port>`(可选)
|
||||||
|
- `BISON_UA`: 使用的 User-Agent,默认为 Chrome
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
@ -154,12 +159,16 @@ sidebar: auto
|
|||||||
所有命令都需要@bot 触发
|
所有命令都需要@bot 触发
|
||||||
|
|
||||||
- 添加订阅(仅管理员和群主和 SUPERUSER):`添加订阅`
|
- 添加订阅(仅管理员和群主和 SUPERUSER):`添加订阅`
|
||||||
::: tip 关于中止订阅
|
::: details 关于中止添加订阅
|
||||||
对于[**v0.5.1**](https://github.com/felinae98/nonebot-bison/releases/tag/v0.5.1)及以上的版本中,已经为`添加订阅`命令添加了中止订阅的功能。
|
对于[**v0.5.1**](https://github.com/felinae98/nonebot-bison/releases/tag/v0.5.1)及以上的版本中,已经为`添加订阅`命令添加了中止添加功能。
|
||||||
在添加订阅命令的~~几乎~~各个阶段,都可以向 Bot 发送`取消`消息来中止订阅过程(需要订阅发起者本人发送)
|
在`添加订阅`命令的~~几乎~~各个阶段,都可以向 Bot 发送`取消`消息来中止订阅过程(需要发起者本人发送)
|
||||||
:::
|
:::
|
||||||
- 查询订阅:`查询订阅`
|
- 查询订阅:`查询订阅`
|
||||||
- 删除订阅(仅管理员和群主和 SUPERUSER):`删除订阅`
|
- 删除订阅(仅管理员和群主和 SUPERUSER):`删除订阅`
|
||||||
|
::: details 关于中止删除订阅
|
||||||
|
对于[**v0.5.3**](https://github.com/felinae98/nonebot-bison/releases/tag/v0.5.3)及以上的版本中,已经为`删除订阅`命令添加了中止删除功能。
|
||||||
|
在`删除订阅`命令的~~几乎~~各个阶段,都可以向 Bot 发送`取消`消息来中止订阅过程(需要发起者本人发送)
|
||||||
|
:::
|
||||||
|
|
||||||
#### 私聊机器人获取后台地址
|
#### 私聊机器人获取后台地址
|
||||||
|
|
||||||
@ -178,8 +187,8 @@ sidebar: auto
|
|||||||
#### 私聊机器人进行配置(需要 SUPERUER 权限)
|
#### 私聊机器人进行配置(需要 SUPERUER 权限)
|
||||||
|
|
||||||
请私聊 bot`群管理`
|
请私聊 bot`群管理`
|
||||||
::: tip 关于中止订阅
|
::: details 关于中止订阅
|
||||||
与普通的[`添加订阅`](#在本群中进行配置)命令一样,在`群管理`命令中使用的`添加订阅`命令也可以使用`取消`来中止订阅过程
|
与普通的[`添加订阅`/`删除订阅`](#在本群中进行配置)命令一样,在`群管理`命令中使用的`添加订阅`/`删除订阅`命令也可以使用`取消`来中止订阅过程
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 所支持平台的 uid
|
### 所支持平台的 uid
|
||||||
|
@ -5,7 +5,7 @@ from typing import Any, Optional
|
|||||||
from ..post import Post
|
from ..post import Post
|
||||||
from ..types import Category, RawPost, Tag, Target
|
from ..types import Category, RawPost, Tag, Target
|
||||||
from ..utils import http_client
|
from ..utils import http_client
|
||||||
from .platform import CategoryNotSupport, NewMessage
|
from .platform import CategoryNotSupport, NewMessage, StatusChange
|
||||||
|
|
||||||
|
|
||||||
class Bilibili(NewMessage):
|
class Bilibili(NewMessage):
|
||||||
@ -155,3 +155,73 @@ class Bilibili(NewMessage):
|
|||||||
else:
|
else:
|
||||||
raise CategoryNotSupport(post_type)
|
raise CategoryNotSupport(post_type)
|
||||||
return Post("bilibili", text=text, url=url, pics=pic, target_name=target_name)
|
return Post("bilibili", text=text, url=url, pics=pic, target_name=target_name)
|
||||||
|
|
||||||
|
|
||||||
|
class Bilibililive(StatusChange):
|
||||||
|
# Author : Sichongzou
|
||||||
|
# Date : 2022-5-18 8:54
|
||||||
|
# Description : bilibili开播提醒
|
||||||
|
# E-mail : 1557157806@qq.com
|
||||||
|
categories = {}
|
||||||
|
platform_name = "bilibili-live"
|
||||||
|
enable_tag = True
|
||||||
|
enabled = True
|
||||||
|
is_common = True
|
||||||
|
schedule_type = "interval"
|
||||||
|
schedule_kw = {"seconds": 10}
|
||||||
|
name = "Bilibili直播"
|
||||||
|
has_target = True
|
||||||
|
|
||||||
|
async def get_target_name(self, target: Target) -> Optional[str]:
|
||||||
|
async with http_client() as client:
|
||||||
|
res = await client.get(
|
||||||
|
"https://api.bilibili.com/x/space/acc/info", params={"mid": target}
|
||||||
|
)
|
||||||
|
res_data = json.loads(res.text)
|
||||||
|
if res_data["code"]:
|
||||||
|
return None
|
||||||
|
return res_data["data"]["name"]
|
||||||
|
|
||||||
|
async def get_status(self, target: Target):
|
||||||
|
async with http_client() as client:
|
||||||
|
params = {"mid": target}
|
||||||
|
res = await client.get(
|
||||||
|
"https://api.bilibili.com/x/space/acc/info",
|
||||||
|
params=params,
|
||||||
|
timeout=4.0,
|
||||||
|
)
|
||||||
|
res_dict = json.loads(res.text)
|
||||||
|
if res_dict["code"] == 0:
|
||||||
|
info = {}
|
||||||
|
info["uid"] = res_dict["data"]["mid"]
|
||||||
|
info["uname"] = res_dict["data"]["name"]
|
||||||
|
info["live_state"] = res_dict["data"]["live_room"]["liveStatus"]
|
||||||
|
info["room_id"] = res_dict["data"]["live_room"]["roomid"]
|
||||||
|
info["title"] = res_dict["data"]["live_room"]["title"]
|
||||||
|
info["cover"] = res_dict["data"]["live_room"]["cover"]
|
||||||
|
return info
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def compare_status(self, target: Target, old_status, new_status) -> list[RawPost]:
|
||||||
|
if (
|
||||||
|
new_status["live_state"] != old_status["live_state"]
|
||||||
|
and new_status["live_state"] == 1
|
||||||
|
):
|
||||||
|
return [new_status]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def parse(self, raw_post: RawPost) -> Post:
|
||||||
|
url = "https://live.bilibili.com/{}".format(raw_post["room_id"])
|
||||||
|
pic = [raw_post["cover"]]
|
||||||
|
target_name = raw_post["uname"]
|
||||||
|
title = raw_post["title"]
|
||||||
|
return Post(
|
||||||
|
self.name,
|
||||||
|
text=title,
|
||||||
|
url=url,
|
||||||
|
pics=pic,
|
||||||
|
target_name=target_name,
|
||||||
|
compress=True,
|
||||||
|
)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import json
|
||||||
|
import ssl
|
||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@ -59,6 +61,25 @@ class Platform(metaclass=RegistryABCMeta, base=True):
|
|||||||
) -> list[tuple[User, list[Post]]]:
|
) -> list[tuple[User, list[Post]]]:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
async def do_fetch_new_post(
|
||||||
|
self, target: Target, users: list[UserSubInfo]
|
||||||
|
) -> list[tuple[User, list[Post]]]:
|
||||||
|
try:
|
||||||
|
return await self.fetch_new_post(target, users)
|
||||||
|
except httpx.RequestError as err:
|
||||||
|
logger.warning(
|
||||||
|
"network connection error: {}, url: {}".format(
|
||||||
|
type(err), err.request.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
except ssl.SSLError as err:
|
||||||
|
logger.warning(f"ssl error: {err}")
|
||||||
|
return []
|
||||||
|
except json.JSONDecodeError as err:
|
||||||
|
logger.warning(f"json error, parsing: {err.doc}")
|
||||||
|
return []
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def parse(self, raw_post: RawPost) -> Post:
|
async def parse(self, raw_post: RawPost) -> Post:
|
||||||
...
|
...
|
||||||
@ -226,7 +247,6 @@ class NewMessage(MessageProcess, abstract=True):
|
|||||||
async def fetch_new_post(
|
async def fetch_new_post(
|
||||||
self, target: Target, users: list[UserSubInfo]
|
self, target: Target, users: list[UserSubInfo]
|
||||||
) -> list[tuple[User, list[Post]]]:
|
) -> list[tuple[User, list[Post]]]:
|
||||||
try:
|
|
||||||
post_list = await self.get_sub_list(target)
|
post_list = await self.get_sub_list(target)
|
||||||
new_posts = await self.filter_common_with_diff(target, post_list)
|
new_posts = await self.filter_common_with_diff(target, post_list)
|
||||||
if not new_posts:
|
if not new_posts:
|
||||||
@ -243,13 +263,6 @@ class NewMessage(MessageProcess, abstract=True):
|
|||||||
res = await self.dispatch_user_post(target, new_posts, users)
|
res = await self.dispatch_user_post(target, new_posts, users)
|
||||||
self.parse_cache = {}
|
self.parse_cache = {}
|
||||||
return res
|
return res
|
||||||
except httpx.RequestError as err:
|
|
||||||
logger.warning(
|
|
||||||
"network connection error: {}, url: {}".format(
|
|
||||||
type(err), err.request.url
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class StatusChange(Platform, abstract=True):
|
class StatusChange(Platform, abstract=True):
|
||||||
@ -270,7 +283,6 @@ class StatusChange(Platform, abstract=True):
|
|||||||
async def fetch_new_post(
|
async def fetch_new_post(
|
||||||
self, target: Target, users: list[UserSubInfo]
|
self, target: Target, users: list[UserSubInfo]
|
||||||
) -> list[tuple[User, list[Post]]]:
|
) -> list[tuple[User, list[Post]]]:
|
||||||
try:
|
|
||||||
new_status = await self.get_status(target)
|
new_status = await self.get_status(target)
|
||||||
res = []
|
res = []
|
||||||
if old_status := self.get_stored_data(target):
|
if old_status := self.get_stored_data(target):
|
||||||
@ -287,13 +299,6 @@ class StatusChange(Platform, abstract=True):
|
|||||||
res = await self.dispatch_user_post(target, diff, users)
|
res = await self.dispatch_user_post(target, diff, users)
|
||||||
self.set_stored_data(target, new_status)
|
self.set_stored_data(target, new_status)
|
||||||
return res
|
return res
|
||||||
except httpx.RequestError as err:
|
|
||||||
logger.warning(
|
|
||||||
"network connection error: {}, url: {}".format(
|
|
||||||
type(err), err.request.url
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class SimplePost(MessageProcess, abstract=True):
|
class SimplePost(MessageProcess, abstract=True):
|
||||||
@ -302,7 +307,6 @@ class SimplePost(MessageProcess, abstract=True):
|
|||||||
async def fetch_new_post(
|
async def fetch_new_post(
|
||||||
self, target: Target, users: list[UserSubInfo]
|
self, target: Target, users: list[UserSubInfo]
|
||||||
) -> list[tuple[User, list[Post]]]:
|
) -> list[tuple[User, list[Post]]]:
|
||||||
try:
|
|
||||||
new_posts = await self.get_sub_list(target)
|
new_posts = await self.get_sub_list(target)
|
||||||
if not new_posts:
|
if not new_posts:
|
||||||
return []
|
return []
|
||||||
@ -318,13 +322,6 @@ class SimplePost(MessageProcess, abstract=True):
|
|||||||
res = await self.dispatch_user_post(target, new_posts, users)
|
res = await self.dispatch_user_post(target, new_posts, users)
|
||||||
self.parse_cache = {}
|
self.parse_cache = {}
|
||||||
return res
|
return res
|
||||||
except httpx.RequestError as err:
|
|
||||||
logger.warning(
|
|
||||||
"network connection error: {}, url: {}".format(
|
|
||||||
type(err), err.request.url
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class NoTargetGroup(Platform, abstract=True):
|
class NoTargetGroup(Platform, abstract=True):
|
||||||
|
@ -18,6 +18,7 @@ class PlugConfig(BaseSettings):
|
|||||||
# 0:不启用;1:首条消息单独发送,剩余照片合并转发;2以及以上:所有消息全部合并转发
|
# 0:不启用;1:首条消息单独发送,剩余照片合并转发;2以及以上:所有消息全部合并转发
|
||||||
bison_resend_times: int = 0
|
bison_resend_times: int = 0
|
||||||
bison_proxy: Optional[str]
|
bison_proxy: Optional[str]
|
||||||
|
bison_ua: str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
@ -58,7 +58,7 @@ async def fetch_and_send(target_type: str):
|
|||||||
send_user_list,
|
send_user_list,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
to_send = await platform_manager[target_type].fetch_new_post(
|
to_send = await platform_manager[target_type].do_fetch_new_post(
|
||||||
target, send_userinfo_list
|
target, send_userinfo_list
|
||||||
)
|
)
|
||||||
if not to_send:
|
if not to_send:
|
||||||
|
@ -4,9 +4,8 @@ import httpx
|
|||||||
|
|
||||||
from ..plugin_config import plugin_config
|
from ..plugin_config import plugin_config
|
||||||
|
|
||||||
if plugin_config.bison_proxy:
|
http_client = functools.partial(
|
||||||
http_client = functools.partial(
|
httpx.AsyncClient,
|
||||||
httpx.AsyncClient, proxies=plugin_config.bison_proxy
|
proxies=plugin_config.bison_proxy or None,
|
||||||
)
|
headers={"user-agent": plugin_config.bison_ua},
|
||||||
else:
|
)
|
||||||
http_client = httpx.AsyncClient
|
|
||||||
|
114
tests/platforms/static/bili_live_status.json
Normal file
114
tests/platforms/static/bili_live_status.json
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "0",
|
||||||
|
"ttl": 1,
|
||||||
|
"data": {
|
||||||
|
"mid": 13164144,
|
||||||
|
"name": "魔法Zc目录",
|
||||||
|
"sex": "男",
|
||||||
|
"face": "http://i0.hdslb.com/bfs/face/a84fa10f90f7060d0336384954ee1cde7a8e9bc6.jpg",
|
||||||
|
"face_nft": 0,
|
||||||
|
"sign": "每日18:00~22:00欢乐直播!请勿在任何乌有相关内容中刷Zc,尊重角色;商务合作qq271374252",
|
||||||
|
"rank": 10000,
|
||||||
|
"level": 6,
|
||||||
|
"jointime": 0,
|
||||||
|
"moral": 0,
|
||||||
|
"silence": 0,
|
||||||
|
"coins": 0,
|
||||||
|
"fans_badge": true,
|
||||||
|
"fans_medal": {
|
||||||
|
"show": false,
|
||||||
|
"wear": false,
|
||||||
|
"medal": null
|
||||||
|
},
|
||||||
|
"official": {
|
||||||
|
"role": 1,
|
||||||
|
"title": "bilibili 2021百大UP主、知名游戏UP主、直播高能主播",
|
||||||
|
"desc": "",
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
"vip": {
|
||||||
|
"type": 2,
|
||||||
|
"status": 1,
|
||||||
|
"due_date": 1702051200000,
|
||||||
|
"vip_pay_type": 0,
|
||||||
|
"theme_type": 0,
|
||||||
|
"label": {
|
||||||
|
"path": "",
|
||||||
|
"text": "年度大会员",
|
||||||
|
"label_theme": "annual_vip",
|
||||||
|
"text_color": "#FFFFFF",
|
||||||
|
"bg_style": 1,
|
||||||
|
"bg_color": "#FB7299",
|
||||||
|
"border_color": ""
|
||||||
|
},
|
||||||
|
"avatar_subscript": 1,
|
||||||
|
"nickname_color": "#FB7299",
|
||||||
|
"role": 3,
|
||||||
|
"avatar_subscript_url": "http://i0.hdslb.com/bfs/vip/icon_Certification_big_member_22_3x.png"
|
||||||
|
},
|
||||||
|
"pendant": {
|
||||||
|
"pid": 3399,
|
||||||
|
"name": "2233幻星集",
|
||||||
|
"image": "http://i0.hdslb.com/bfs/garb/item/20c07ded13498a5b12db99660c766ddd92ecfe31.png",
|
||||||
|
"expire": 0,
|
||||||
|
"image_enhance": "http://i0.hdslb.com/bfs/garb/item/20c07ded13498a5b12db99660c766ddd92ecfe31.png",
|
||||||
|
"image_enhance_frame": ""
|
||||||
|
},
|
||||||
|
"nameplate": {
|
||||||
|
"nid": 1,
|
||||||
|
"name": "黄金殿堂",
|
||||||
|
"image": "http://i2.hdslb.com/bfs/face/82896ff40fcb4e7c7259cb98056975830cb55695.png",
|
||||||
|
"image_small": "http://i0.hdslb.com/bfs/face/627e342851dfda6fe7380c2fa0cbd7fae2e61533.png",
|
||||||
|
"level": "稀有勋章",
|
||||||
|
"condition": "单个自制视频总播放数\u003e=100万"
|
||||||
|
},
|
||||||
|
"user_honour_info": {
|
||||||
|
"mid": 0,
|
||||||
|
"colour": null,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"is_followed": true,
|
||||||
|
"top_photo": "http://i2.hdslb.com/bfs/space/853fea2728651588a2cdef0a1e586bcefff8e3d8.png",
|
||||||
|
"theme": {},
|
||||||
|
"sys_notice": {},
|
||||||
|
"live_room": {
|
||||||
|
"roomStatus": 1,
|
||||||
|
"liveStatus": 0,
|
||||||
|
"url": "https://live.bilibili.com/3044248?broadcast_type=0\u0026is_room_feed=1",
|
||||||
|
"title": "【Zc】早朝危机合约!",
|
||||||
|
"cover": "http://i0.hdslb.com/bfs/live/new_room_cover/cf7d4d3b2f336c6dba299644c3af952c5db82612.jpg",
|
||||||
|
"roomid": 3044248,
|
||||||
|
"roundStatus": 1,
|
||||||
|
"broadcast_type": 0,
|
||||||
|
"watched_show": {
|
||||||
|
"switch": true,
|
||||||
|
"num": 13753,
|
||||||
|
"text_small": "1.3万",
|
||||||
|
"text_large": "1.3万人看过",
|
||||||
|
"icon": "https://i0.hdslb.com/bfs/live/a725a9e61242ef44d764ac911691a7ce07f36c1d.png",
|
||||||
|
"icon_location": "",
|
||||||
|
"icon_web": "https://i0.hdslb.com/bfs/live/8d9d0f33ef8bf6f308742752d13dd0df731df19c.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"birthday": "07-21",
|
||||||
|
"school": {
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
|
"profession": {
|
||||||
|
"name": "",
|
||||||
|
"department": "",
|
||||||
|
"title": "",
|
||||||
|
"is_show": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"评论区UP主",
|
||||||
|
"目标是星辰大海"
|
||||||
|
],
|
||||||
|
"series": {
|
||||||
|
"user_upgrade_status": 3,
|
||||||
|
"show_upgrade_window": false
|
||||||
|
},
|
||||||
|
"is_senior_member": 1
|
||||||
|
}
|
||||||
|
}
|
44
tests/platforms/test_bilibili_live.py
Normal file
44
tests/platforms/test_bilibili_live.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import feedparser
|
||||||
|
import pytest
|
||||||
|
import respx
|
||||||
|
from httpx import Response
|
||||||
|
from nonebug.app import App
|
||||||
|
from pytz import timezone
|
||||||
|
|
||||||
|
from .utils import get_file, get_json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bili_live(app: App):
|
||||||
|
from nonebot_bison.platform import platform_manager
|
||||||
|
|
||||||
|
return platform_manager["bilibili-live"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@respx.mock
|
||||||
|
async def test_fetch_bilibili_live_status(bili_live, dummy_user_subinfo):
|
||||||
|
mock_bili_live_status = get_json("bili_live_status.json")
|
||||||
|
|
||||||
|
bili_live_router = respx.get(
|
||||||
|
"https://api.bilibili.com/x/space/acc/info?mid=13164144"
|
||||||
|
)
|
||||||
|
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
|
||||||
|
target = "13164144"
|
||||||
|
res = await bili_live.fetch_new_post(target, [dummy_user_subinfo])
|
||||||
|
assert bili_live_router.called
|
||||||
|
assert len(res) == 0
|
||||||
|
mock_bili_live_status["data"]["live_room"]["liveStatus"] = 1
|
||||||
|
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
|
||||||
|
res2 = await bili_live.fetch_new_post(target, [dummy_user_subinfo])
|
||||||
|
post = res2[0][1][0]
|
||||||
|
assert post.target_type == "Bilibili直播"
|
||||||
|
assert post.text == "【Zc】早朝危机合约!"
|
||||||
|
assert post.url == "https://live.bilibili.com/3044248"
|
||||||
|
assert post.target_name == "魔法Zc目录"
|
||||||
|
assert post.pics == [
|
||||||
|
"http://i0.hdslb.com/bfs/live/new_room_cover/cf7d4d3b2f336c6dba299644c3af952c5db82612.jpg"
|
||||||
|
]
|
||||||
|
assert post.compress == True
|
@ -1,6 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from nonebug import App
|
from nonebug import App
|
||||||
from nonebug.fixture import nonebug_init
|
|
||||||
|
|
||||||
|
|
||||||
async def test_without_proxy(app: App):
|
async def test_without_proxy(app: App):
|
||||||
@ -8,6 +7,8 @@ async def test_without_proxy(app: App):
|
|||||||
|
|
||||||
c = http_client()
|
c = http_client()
|
||||||
assert not c._mounts
|
assert not c._mounts
|
||||||
|
req = c.build_request("GET", "http://example.com")
|
||||||
|
assert "Chrome" in req.headers["User-Agent"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user