Merge branch 'main' into feat/db

This commit is contained in:
felinae98 2022-05-31 21:11:11 +08:00
commit 05f1edd7fa
No known key found for this signature in database
GPG Key ID: 00C8B010587FF610
15 changed files with 473 additions and 122 deletions

View File

@ -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') }}

View File

@ -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 }}

View File

@ -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:

View File

@ -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

View File

@ -38,6 +38,8 @@
- 专栏 - 专栏
- 转发 - 转发
- 纯文字 - 纯文字
- Bilibili 直播
- 开播提醒
- RSS - RSS
- 富文本转换为纯文本 - 富文本转换为纯文本
- 提取出所有图片 - 提取出所有图片

View File

@ -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 的机制有一定了解,当然,
依葫芦画瓢也是足够的
:::
## 类的方法与成员变量
## 方法与变量的定义

View File

@ -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`时:
![simple1](/images/forward-msg-simple1.png) ![simple1](/images/forward-msg-simple1.png)
@ -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

View File

@ -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,
)

View File

@ -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):

View File

@ -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"

View File

@ -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:

View File

@ -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

View 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
}
}

View 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

View File

@ -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(