Merge branch 'main' into feat/db

This commit is contained in:
felinae98 2022-05-29 00:19:40 +08:00
commit 861441eb2b
No known key found for this signature in database
GPG Key ID: 00C8B010587FF610
47 changed files with 7271 additions and 8830 deletions

14
.github/actions/build-docs/action.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Build Docs
description: Build Docs
runs:
using: "composite"
steps:
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- name: Build Frontend
shell: bash
run: |
yarn install
yarn docs:build

View File

@ -0,0 +1,15 @@
name: Build Frontend
description: Build Frontend
runs:
using: "composite"
steps:
- name: Setup Node Environment
uses: ./.github/actions/setup-node
- name: Build Frontend
shell: bash
working-directory: ./admin-frontend
run: |
yarn install
yarn build

18
.github/actions/setup-node/action.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Setup Node
description: Setup Node
runs:
using: "composite"
steps:
- uses: actions/setup-node@v2
with:
node-version: "16"
- id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
shell: bash
- uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}

40
.github/actions/setup-python/action.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Setup Python
description: Setup Python
inputs:
python-version:
description: Python version
required: false
default: "3.9"
runs:
using: "composite"
steps:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ inputs.python-version }}
- name: Install poetry
uses: Gr1N/setup-poetry@v7
- name: Cache Windows dependencies
uses: actions/cache@v2
if: ${{ runner.os == 'Windows' }}
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
if: ${{ runner.os == 'Linux' }}
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ inputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
- name: Cache macOS dependencies
uses: actions/cache@v2
if: ${{ runner.os == 'macOS' }}
with:
path: ~/Library/Caches/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ inputs.python-version }}-${{ hashFiles('**/poetry.lock') }}

36
.github/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,36 @@
template: $CHANGES
category-template: "### $TITLE"
name-template: "Release v$RESOLVED_VERSION 🌈"
tag-template: "v$RESOLVED_VERSION"
change-template: "- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) ([#$NUMBER]($URL))"
change-title-escapes: '\<&'
exclude-labels:
- "dependencies"
- "skip-changelog"
categories:
- title: "破坏性变更"
labels:
- "Breaking"
- title: "新功能"
labels:
- "feature"
- "enhancement"
- title: "Bug 修复"
labels:
- "fix"
- "bugfix"
- "bug"
- title: "文档"
labels:
- "documentation"
version-resolver:
major:
labels:
- "major"
minor:
labels:
- "minor"
patch:
labels:
- "patch"
default: patch

137
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,137 @@
name: test-build
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-frontend:
name: Build Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Frontend
uses: ./.github/actions/build-frontend
- name: Upload dist
uses: actions/upload-artifact@v3
with:
name: frontend
path: ./admin-frontend/build/
test:
name: Test Coverage
runs-on: ${{ matrix.os }}
needs: build-frontend
strategy:
matrix:
python-version: ["3.9", "3.10"]
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
env:
OS: ${{ matrix.os }}
PYTHON_VERSION: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v3
- name: Download frontend files
uses: actions/download-artifact@v2
with:
name: frontend
path: ./src/plugins/nonebot_bison/admin_page/dist
- name: Setup Python environment
uses: ./.github/actions/setup-python
with:
python-version: ${{ matrix.python-version }}
- name: Install prerequisites
run: poetry install
- name: Run Pytest
run: poetry run pytest --cov-report xml --cov=./src/plugins/nonebot_bison -k 'not compare and not render'
- name: Upload coverage report
uses: codecov/codecov-action@v3
with:
env_vars: OS,PYTHON_VERSION
docker-main:
name: Docker main
runs-on: ubuntu-latest
needs: [build-frontend, test]
steps:
- uses: actions/checkout@v3
- name: Download frontend files
uses: actions/download-artifact@v2
with:
name: frontend
path: ./src/plugins/nonebot_bison/admin_page/dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile_with_frontend
push: ${{ github.event_name != 'pull_request' }}
tags: felinae98/nonebot-bison:main
cache-from: type=gha
cache-to: type=gha,mode=max
docker-main-sentry:
name: Docker main sentry
runs-on: ubuntu-latest
needs: [build-frontend, test]
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v3
- name: Download frontend files
uses: actions/download-artifact@v2
with:
name: frontend
path: ./src/plugins/nonebot_bison/admin_page/dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile_with_frontend_sentry
push: ${{ github.event_name != 'pull_request' }}
tags: felinae98/nonebot-bison:main-sentry
cache-from: type=gha
cache-to: type=gha,mode=max

39
.github/workflows/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Release Drafter
on:
pull_request_target:
branches:
- main
types:
- closed
jobs:
update-release-drafter:
runs-on: ubuntu-latest
concurrency:
group: pull-request-changelog
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: release-drafter/release-drafter@v5
id: release-drafter
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update Changelog
uses: docker://ghcr.io/nonebot/auto-changelog:master
with:
latest_changes_position: '# Change Log\n\n'
latest_changes_title: '## 最近更新'
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'
changelog_body: ${{ steps.release-drafter.outputs.body }}
commit_and_push: false
- name: Commit and Push
run: |
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git add .
git diff-index --quiet HEAD || git commit -m ":memo: Update changelog"
git push

35
.github/workflows/release-trigger.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Trigger Release
on:
workflow_dispatch:
jobs:
archive:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: main
token: ${{ secrets.GH_PAT }}
- name: Setup Python environment
uses: ./.github/actions/setup-python
- run: echo "TAG_NAME=v$(poetry version -s)" >> $GITHUB_ENV
- name: Archive Changelog
uses: docker://ghcr.io/nonebot/auto-changelog:master
with:
archive_regex: '(?<=## )最近更新(?=\n)'
archive_title: ${{ env.TAG_NAME }}
commit_and_push: false
- name: Push Tag
run: |
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git add .
git commit -m ":bookmark: Release $(poetry version -s)"
git tag ${{ env.TAG_NAME }}
git push && git push --tags

128
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,128 @@
name: Release
on:
push:
tags:
- v*
jobs:
build-frontend:
name: Build Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Frontend
uses: ./.github/actions/build-frontend
- name: Upload dist
uses: actions/upload-artifact@v3
with:
name: frontend
path: ./admin-frontend/build/
publish-pypi-github:
runs-on: ubuntu-latest
needs: build-frontend
steps:
- uses: actions/checkout@v3
- name: Download frontend files
uses: actions/download-artifact@v2
with:
name: frontend
path: ./src/plugins/nonebot_bison/admin_page/dist
- name: Setup Python environment
uses: ./.github/actions/setup-python
- run: |
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- uses: release-drafter/release-drafter@v5
with:
name: Release ${{ env.TAG_NAME }}
tag: ${{ env.TAG_NAME }}
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish PyPI and Github
run: |
poetry publish --build -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} || echo "Already pushed to pypi"
gh release upload --clobber ${{ env.TAG_NAME }} dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-docker:
runs-on: ubuntu-latest
needs: build-frontend
steps:
- uses: actions/checkout@v3
- name: Download frontend files
uses: actions/download-artifact@v2
with:
name: frontend
path: ./src/plugins/nonebot_bison/admin_page/dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- run: |
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile_with_frontend
push: true
tags: |
felinae98/nonebot-bison:latest
felinae98/nonebot-bison:${{ env.TAG_NAME }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build Sentry and push
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile_with_frontend_sentry
push: true
tags: felinae98/nonebot-bison:${{ env.TAG_NAME }}-sentry
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docs
uses: ./.github/actions/build-docs
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1
with:
publish-dir: "./docs/.vuepress/dist"
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy ${{ env.TAG_NAME }}"
enable-commit-comment: false
alias: ${{ env.TAG_NAME }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

36
.github/workflows/website-preview.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Site Deploy(Preview)
on:
pull_request_target:
jobs:
preview:
runs-on: ubuntu-latest
concurrency:
group: pull-request-preview-${{ github.event.number }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Build Docs
uses: ./.github/actions/build-docs
- name: Get Deploy Name
run: |
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1
with:
publish-dir: "./docs/.vuepress/dist"
production-deploy: false
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
enable-commit-comment: false
alias: ${{ env.DEPLOY_NAME }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

View File

@ -20,3 +20,4 @@ repos:
hooks:
- id: prettier
types_or: [markdown, ts, tsx]
exclude: 'CHANGELOG.md'

View File

@ -1,47 +1,46 @@
# Change Log
## [0.2.11] - 2021-06-17
## 最近更新
- 增加了简单的单元测试
- 增加了管理员直接管理订阅的能力
### 新功能
## [0.3.0] - 2021-07-06
- 增加代理设置 [@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))
- 微博 tag 支持
- 修复 bug
- 增加微博超话和纯文字支持
- 更改浏览器配置
- 将“来源”移动到文末
- 使用组合来构建新的 platform新增状态改变类型订阅
## v0.5.3
## [0.3.1] - 2021-07-10
- on_command 设置 block=True (#63) @MeetWq
- 修复 issue#51 存在的问题并修改相应的单元测试 (#52) @AzideCupric
- 增添关于添加订阅命令中中止订阅的相关文档 (#45) @AzideCupric
- 修复不发送来源
- 发送 RSS 订阅的 title
- 修复浏览器渲染问题
### Bug 修复
## [0.3.2] - 2021-09-28
- 修复#66 (#69) @felinae98
- 增加 NoTargetGroup
- 增加 1x3 拼图的支持
- 增加网易云
## [0.5.2]
## [0.3.3] - 2021-09-28
- 修复了微博获取全文时接口失效的问题
- 修复了 bilibili 空列表时的报错
- 修复拼图问题
## [0.5.1]
## [0.4.0] - 2021-11-18
- 使用了新的在私聊中进行群管理的方式:从`管理-*`替换为`群管理`命令
- 默认关闭自动重发功能
- 添加了 [推送消息合并转发功能](https://nonebot-bison.vercel.app/usage/#%E9%85%8D%E7%BD%AE)
- 添加了`添加订阅`命令事件的中途取消功能
- 优化了`添加订阅`命令的聊天处理逻辑
- 项目更名为 nonebot-bison
## [0.5.0]
## [0.4.1] - 2021-11-31
- 添加了 FF14
- 去掉了自己维护的 playwright转向[nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender)
- 支持了 nonebot 2.0.0beta
- 加入了管理后台
## [0.4.4]
## [0.4.2]
并没有做什么只是为了修复前端文件没有正确打包的问题开了个新的版本号
推上 pypi
- 又双叒叕重构了一下
- 修复了 Docker 中 Playwright 下载的浏览器版本不正确问题
- 加入了猴子补丁,使 Windows 里能运行 Playwright
## [0.4.3]
@ -52,27 +51,47 @@
- 前端可以刷新了(之前居然不可以)
- 在镜像里塞进了浏览器(导致镜像体积起飞)
## [0.4.4]
## [0.4.2]
- 又双叒叕重构了一下
- 修复了 Docker 中 Playwright 下载的浏览器版本不正确问题
- 加入了猴子补丁,使 Windows 里能运行 Playwright
并没有做什么只是为了修复前端文件没有正确打包的问题开了个新的版本号
推上 pypi
## [0.5.0]
## [0.4.1] - 2021-11-31
- 添加了 FF14
- 去掉了自己维护的 playwright转向[nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender)
- 支持了 nonebot 2.0.0beta
- 加入了管理后台
## [0.5.1]
## [0.4.0] - 2021-11-18
- 使用了新的在私聊中进行群管理的方式:从`管理-*`替换为`群管理`命令
- 默认关闭自动重发功能
- 添加了 [推送消息合并转发功能](https://nonebot-bison.vercel.app/usage/#%E9%85%8D%E7%BD%AE)
- 添加了`添加订阅`命令事件的中途取消功能
- 优化了`添加订阅`命令的聊天处理逻辑
- 项目更名为 nonebot-bison
## [0.5.2]
## [0.3.3] - 2021-09-28
- 修复了微博获取全文时接口失效的问题
- 修复了 bilibili 空列表时的报错
- 修复拼图问题
## [0.3.2] - 2021-09-28
- 增加 NoTargetGroup
- 增加 1x3 拼图的支持
- 增加网易云
## [0.3.1] - 2021-07-10
- 修复不发送来源
- 发送 RSS 订阅的 title
- 修复浏览器渲染问题
## [0.3.0] - 2021-07-06
- 微博 tag 支持
- 修复 bug
- 增加微博超话和纯文字支持
- 更改浏览器配置
- 将“来源”移动到文末
- 使用组合来构建新的 platform新增状态改变类型订阅
## [0.2.11] - 2021-06-17
- 增加了简单的单元测试
- 增加了管理员直接管理订阅的能力

View File

@ -3,7 +3,7 @@
[![pypi](https://badgen.net/pypi/v/nonebot-bison)](https://pypi.org/project/nonebot-bison/)
[![license](https://img.shields.io/github/license/felinae98/nonebot-bison)](https://github.com/felinae98/nonebot-bison/blob/main/LICENSE)
[![felinae98](https://circleci.com/gh/felinae98/nonebot-bison.svg?style=shield)](https://circleci.com/gh/felinae98/nonebot-bison)
[![action](https://img.shields.io/github/workflow/status/felinae98/nonebot-bison/test-build)](https://github.com/felinae98/nonebot-bison/actions/workflows/main.yml)
[![docker](https://img.shields.io/docker/image-size/felinae98/nonebot-bison)](https://hub.docker.com/r/felinae98/nonebot-bison)
[![codecov](https://codecov.io/gh/felinae98/nonebot-bison/branch/main/graph/badge.svg?token=QCFIODJOOA)](https://codecov.io/gh/felinae98/nonebot-bison)
[![qq group](https://img.shields.io/badge/QQ%E7%BE%A4-868610060-orange)](https://qm.qq.com/cgi-bin/qm/qr?k=pXYMGB_e8b6so3QTqgeV6lkKDtEeYE4f&jump_from=webapi)
@ -31,6 +31,7 @@
- 图文
- 视频
- 纯文字
- 转发
- Bilibili
- 视频
- 图文
@ -44,6 +45,7 @@
- 塞壬唱片新闻
- 游戏内公告
- 版本更新等通知
- 泰拉记事社漫画
- 网易云音乐
- 歌手发布新专辑
- 电台更新

View File

@ -22,7 +22,7 @@
"react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"react-scripts": "^5.0.0",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1"
},

File diff suppressed because it is too large Load Diff

View File

@ -136,6 +136,8 @@ sidebar: auto
启用此功能时,可能会因为待推送图片过大/过多而导致文字消息与合并转发图片消息推送间隔过大(选择模式`1`时),请谨慎考虑开启。或者选择模式`2`,使图文消息一同合并转发(可能会使消息推送延迟过长)
:::
- `BISON_PROXY`: 使用的代理连接,形如`http://<ip>:<port>`(可选)
## 使用
::: warning

1070
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "nonebot-bison"
version = "0.5.2"
version = "0.5.3"
description = "Subscribe message from social medias"
authors = ["felinae98 <felinae225@qq.com>"]
license = "MIT"
@ -25,11 +25,11 @@ classifiers = [
[tool.poetry.dependencies]
python = "^3.9"
nonebot2 = "^2.0.0-beta.2"
httpx = ">=0.16.1 <1.0.0"
httpx = ">=0.16.1"
bs4 = "^0.0.1"
tinydb = "^4.3.0"
feedparser = "^6.0.2"
pillow = "^8.1.0"
pillow = ">=8.1,<10.0"
apscheduler = "^3.7.0"
expiringdict = "^1.2.1"
pyjwt = "^2.1.0"

View File

@ -129,8 +129,7 @@ def register_router_fastapi(driver: Driver, socketio):
def init():
driver = get_driver()
if driver.type == "fastapi":
assert isinstance(driver, Driver)
if isinstance(driver, Driver):
register_router_fastapi(driver, socket_app)
else:
logger.warning(f"Driver {driver.type} not supported")

View File

@ -9,7 +9,6 @@ from nonebot.adapters.onebot.v11.message import Message
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
from nonebot.internal.params import ArgStr
from nonebot.internal.rule import Rule
from nonebot.log import logger
from nonebot.matcher import Matcher
from nonebot.params import Depends, EventPlainText, EventToMe
from nonebot.permission import SUPERUSER
@ -17,7 +16,7 @@ from nonebot.rule import to_me
from nonebot.typing import T_State
from .config import config
from .platform import check_sub_target, platform_manager
from .platform import Platform, check_sub_target, platform_manager
from .plugin_config import plugin_config
from .types import Category, Target, User
from .utils import parse_text
@ -108,8 +107,13 @@ def do_add_sub(add_sub: Type[Matcher]):
"platform", _gen_prompt_template("{_prompt}"), [Depends(parse_platform)]
)
async def init_id(state: T_State):
if platform_manager[state["platform"]].has_target:
state["_prompt"] = "请输入订阅用户的id:\n查询id获取方法请回复:“查询”"
cur_platform = platform_manager[state["platform"]]
if cur_platform.has_target:
state["_prompt"] = (
("1." + cur_platform.parse_target_promot + "\n2.")
if cur_platform.parse_target_promot
else ""
) + "请输入订阅用户的id\n查询id获取方法请回复:“查询”"
else:
state["id"] = "default"
state["name"] = await platform_manager[state["platform"]].get_target_name(
@ -125,6 +129,8 @@ def do_add_sub(add_sub: Type[Matcher]):
raise LookupError
if target == "取消":
raise KeyboardInterrupt
platform = platform_manager[state["platform"]]
target = await platform.parse_target(target)
name = await check_sub_target(state["platform"], target)
if not name:
raise ValueError
@ -141,6 +147,8 @@ def do_add_sub(add_sub: Type[Matcher]):
await add_sub.finish("已中止订阅")
except (ValueError):
await add_sub.reject("id输入错误")
except (Platform.ParseTargetException):
await add_sub.reject("不能从你的输入中提取出id请检查你输入的内容是否符合预期")
else:
await add_sub.send(
"即将订阅的用户为:{} {} {}\n如有错误请输入“取消”重新订阅".format(
@ -244,41 +252,46 @@ def do_del_sub(del_sub: Type[Matcher]):
async def send_list(bot: Bot, event: Event, state: T_State):
user_info = state["target_user_info"]
assert isinstance(user_info, User)
sub_list = await config.list_subscribe(
# state.get("_user_id") or event.group_id, "group"
user_info.user,
user_info.user_type,
)
res = "订阅的帐号为:\n"
state["sub_table"] = {}
for index, sub in enumerate(sub_list, 1):
state["sub_table"][index] = {
"platform_name": sub.target.platform_name,
"target": sub.target.target,
}
res += "{} {} {} {}\n".format(
index,
sub.target.platform_name,
sub.target.target_name,
sub.target.target,
try:
sub_list = await config.list_subscribe(
# state.get("_user_id") or event.group_id, "group"
user_info.user,
user_info.user_type,
)
platform = platform_manager[sub.target.platform_name]
if platform.categories:
res += " [{}]".format(
", ".join(
map(lambda x: platform.categories[Category(x)], sub.categories)
)
assert sub_list
except AssertionError:
await del_sub.finish("暂无已订阅账号\n请使用“添加订阅”命令添加订阅")
else:
res = "订阅的帐号为:\n"
state["sub_table"] = {}
for index, sub in enumerate(sub_list, 1):
state["sub_table"][index] = {
"target_type": sub["target_type"],
"target": sub["target"],
}
res += "{} {} {} {}\n".format(
index, sub["target_type"], sub["target_name"], sub["target"]
)
if platform.enable_tag:
res += " {}".format(", ".join(sub.tags))
res += "\n"
res += "请输入要删除的订阅的序号"
await bot.send(event=event, message=Message(await parse_text(res)))
platform = platform_manager[sub["target_type"]]
if platform.categories:
res += " [{}]".format(
", ".join(
map(lambda x: platform.categories[Category(x)], sub["cats"])
)
)
if platform.enable_tag:
res += " {}".format(", ".join(sub["tags"]))
res += "\n"
res += "请输入要删除的订阅的序号\n输入'取消'中止"
await bot.send(event=event, message=Message(await parse_text(res)))
@del_sub.receive()
async def do_del(event: Event, state: T_State):
user_msg = str(event.get_message()).strip()
if user_msg == "取消":
await del_sub.finish("删除中止")
try:
index = int(str(event.get_message()).strip())
index = int(user_msg)
user_info = state["target_user_info"]
assert isinstance(user_info, User)
await config.del_subscribe(
@ -299,12 +312,13 @@ add_sub_matcher = on_command(
rule=configurable_to_me,
permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER,
priority=5,
block=True,
)
add_sub_matcher.handle()(set_target_user_info)
do_add_sub(add_sub_matcher)
query_sub_matcher = on_command("查询订阅", rule=configurable_to_me, priority=5)
query_sub_matcher = on_command("查询订阅", rule=configurable_to_me, priority=5, block=True)
query_sub_matcher.handle()(set_target_user_info)
do_query_sub(query_sub_matcher)
@ -314,11 +328,14 @@ del_sub_matcher = on_command(
rule=configurable_to_me,
permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER,
priority=5,
block=True,
)
del_sub_matcher.handle()(set_target_user_info)
do_del_sub(del_sub_matcher)
group_manage_matcher = on_command("群管理", rule=to_me(), permission=SUPERUSER, priority=4)
group_manage_matcher = on_command(
"群管理", rule=to_me(), permission=SUPERUSER, priority=4, block=True
)
@group_manage_matcher.handle()

View File

@ -1,12 +1,12 @@
import json
from typing import Any
import httpx
from bs4 import BeautifulSoup as bs
from nonebot.plugin import require
from ..post import Post
from ..types import Category, RawPost, Target
from ..utils import http_client
from .platform import CategoryNotSupport, NewMessage, StatusChange
@ -26,7 +26,7 @@ class Arknights(NewMessage):
return "明日方舟游戏信息"
async def get_sub_list(self, _) -> list[RawPost]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
raw_data = await client.get(
"https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/announcement.meta.json"
)
@ -44,7 +44,7 @@ class Arknights(NewMessage):
async def parse(self, raw_post: RawPost) -> Post:
announce_url = raw_post["webUrl"]
text = ""
async with httpx.AsyncClient() as client:
async with http_client() as client:
raw_html = await client.get(announce_url)
soup = bs(raw_html.text, "html.parser")
pics = []
@ -99,7 +99,7 @@ class AkVersion(StatusChange):
return "明日方舟游戏信息"
async def get_status(self, _):
async with httpx.AsyncClient() as client:
async with http_client() as client:
res_ver = await client.get(
"https://ak-conf.hypergryph.com/config/prod/official/IOS/version"
)
@ -155,7 +155,7 @@ class MonsterSiren(NewMessage):
return "明日方舟游戏信息"
async def get_sub_list(self, _) -> list[RawPost]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
raw_data = await client.get("https://monster-siren.hypergryph.com/api/news")
return raw_data.json()["data"]["list"]
@ -170,7 +170,7 @@ class MonsterSiren(NewMessage):
async def parse(self, raw_post: RawPost) -> Post:
url = f'https://monster-siren.hypergryph.com/info/{raw_post["cid"]}'
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.get(
f'https://monster-siren.hypergryph.com/api/news/{raw_post["cid"]}'
)
@ -189,3 +189,47 @@ class MonsterSiren(NewMessage):
compress=True,
override_use_pic=False,
)
class TerraHistoricusComic(NewMessage):
categories = {4: "泰拉记事社漫画"}
platform_name = "arknights"
name = "明日方舟游戏信息"
enable_tag = False
enabled = True
is_common = False
schedule_type = "interval"
schedule_kw = {"seconds": 30}
has_target = False
async def get_target_name(self, _: Target) -> str:
return "明日方舟游戏信息"
async def get_sub_list(self, _) -> list[RawPost]:
async with http_client() as client:
raw_data = await client.get(
"https://terra-historicus.hypergryph.com/api/recentUpdate"
)
return raw_data.json()["data"]
def get_id(self, post: RawPost) -> Any:
return f'{post["comicCid"]}/{post["episodeCid"]}'
def get_date(self, _) -> None:
return None
def get_category(self, _) -> Category:
return Category(4)
async def parse(self, raw_post: RawPost) -> Post:
url = f'https://terra-historicus.hypergryph.com/comic/{raw_post["comicCid"]}/episode/{raw_post["episodeCid"]}'
return Post(
"terra-historicus",
text=f'{raw_post["title"]} - {raw_post["episodeShortTitle"]}',
pics=[raw_post["coverUrl"]],
url=url,
target_name="泰拉记事社漫画",
compress=True,
override_use_pic=False,
)

View File

@ -1,10 +1,10 @@
import json
import re
from typing import Any, Optional
import httpx
from ..post import Post
from ..types import Category, RawPost, Tag, Target
from ..utils import http_client
from .platform import CategoryNotSupport, NewMessage
@ -26,9 +26,10 @@ class Bilibili(NewMessage):
schedule_kw = {"seconds": 10}
name = "B站"
has_target = True
parse_target_promot = "请输入用户主页的链接"
async def get_target_name(self, target: Target) -> Optional[str]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.get(
"https://api.bilibili.com/x/space/acc/info", params={"mid": target}
)
@ -37,8 +38,18 @@ class Bilibili(NewMessage):
return None
return res_data["data"]["name"]
async def parse_target(self, target_text: str) -> Target:
if re.match(r"\d+", target_text):
return Target(target_text)
elif match := re.match(
r"(?:https?://)?space\.bilibili\.com/(\d+)", target_text
):
return Target(match.group(1))
else:
raise self.ParseTargetException()
async def get_sub_list(self, target: Target) -> list[RawPost]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
params = {"host_uid": target, "offset": 0, "need_top": 0}
res = await client.get(
"https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history",

View File

@ -1,9 +1,8 @@
from typing import Any
import httpx
from ..post import Post
from ..types import RawPost, Target
from ..utils import http_client
from .platform import NewMessage
@ -23,7 +22,7 @@ class FF14(NewMessage):
return "最终幻想XIV官方公告"
async def get_sub_list(self, _) -> list[RawPost]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
raw_data = await client.get(
"https://ff.web.sdo.com/inc/newdata.ashx?url=List?gameCode=ff&category=5309,5310,5311,5312,5313&pageIndex=0&pageSize=5"
)

View File

@ -1,9 +1,9 @@
import re
from typing import Any, Optional
import httpx
from ..post import Post
from ..types import RawPost, Target
from ..utils import http_client
from .platform import NewMessage
@ -18,9 +18,10 @@ class NcmArtist(NewMessage):
schedule_kw = {"minutes": 1}
name = "网易云-歌手"
has_target = True
parse_target_promot = "请输入歌手主页包含数字ID的链接"
async def get_target_name(self, target: Target) -> Optional[str]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.get(
"https://music.163.com/api/artist/albums/{}".format(target),
headers={"Referer": "https://music.163.com/"},
@ -30,8 +31,18 @@ class NcmArtist(NewMessage):
return
return res_data["artist"]["name"]
async def parse_target(self, target_text: str) -> Target:
if re.match(r"^\d+$", target_text):
return Target(target_text)
elif match := re.match(
r"(?:https?://)?music\.163\.com/#/artist\?id=(\d+)", target_text
):
return Target(match.group(1))
else:
raise self.ParseTargetException()
async def get_sub_list(self, target: Target) -> list[RawPost]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.get(
"https://music.163.com/api/artist/albums/{}".format(target),
headers={"Referer": "https://music.163.com/"},

View File

@ -1,9 +1,9 @@
import re
from typing import Any, Optional
import httpx
from ..post import Post
from ..types import RawPost, Target
from ..utils import http_client
from .platform import NewMessage
@ -18,9 +18,10 @@ class NcmRadio(NewMessage):
schedule_kw = {"minutes": 10}
name = "网易云-电台"
has_target = True
parse_target_promot = "请输入主播电台主页包含数字ID的链接"
async def get_target_name(self, target: Target) -> Optional[str]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.post(
"http://music.163.com/api/dj/program/byradio",
headers={"Referer": "https://music.163.com/"},
@ -31,8 +32,18 @@ class NcmRadio(NewMessage):
return
return res_data["programs"][0]["radio"]["name"]
async def parse_target(self, target_text: str) -> Target:
if re.match(r"^\d+$", target_text):
return Target(target_text)
elif match := re.match(
r"(?:https?://)?music\.163\.com/#/djradio\?id=(\d+)", target_text
):
return Target(match.group(1))
else:
raise self.ParseTargetException()
async def get_sub_list(self, target: Target) -> list[RawPost]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.post(
"http://music.163.com/api/dj/program/byradio",
headers={"Referer": "https://music.163.com/"},

View File

@ -47,6 +47,7 @@ class Platform(metaclass=RegistryABCMeta, base=True):
enable_tag: bool
store: dict[Target, Any]
platform_name: str
parse_target_promot: Optional[str] = None
@abstractmethod
async def get_target_name(self, target: Target) -> Optional[str]:
@ -73,6 +74,12 @@ class Platform(metaclass=RegistryABCMeta, base=True):
self.reverse_category[val] = key
self.store = dict()
class ParseTargetException(Exception):
pass
async def parse_target(self, target_string: str) -> Target:
return Target(target_string)
@abstractmethod
def get_tags(self, raw_post: RawPost) -> Optional[Collection[Tag]]:
"Return Tag list of given RawPost"

View File

@ -2,11 +2,11 @@ import calendar
from typing import Any, Optional
import feedparser
import httpx
from bs4 import BeautifulSoup as bs
from ..post import Post
from ..types import RawPost, Target
from ..utils import http_client
from .platform import NewMessage
@ -23,7 +23,7 @@ class Rss(NewMessage):
has_target = True
async def get_target_name(self, target: Target) -> Optional[str]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.get(target, timeout=10.0)
feed = feedparser.parse(res.text)
return feed["feed"]["title"]
@ -35,7 +35,7 @@ class Rss(NewMessage):
return post.id
async def get_sub_list(self, target: Target) -> list[RawPost]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.get(target, timeout=10.0)
feed = feedparser.parse(res)
entries = feed.entries

View File

@ -3,12 +3,12 @@ import re
from datetime import datetime
from typing import Any, Optional
import httpx
from bs4 import BeautifulSoup as bs
from nonebot.log import logger
from ..post import Post
from ..types import *
from ..utils import http_client
from .platform import NewMessage
@ -28,9 +28,10 @@ class Weibo(NewMessage):
schedule_type = "interval"
schedule_kw = {"seconds": 3}
has_target = True
parse_target_promot = "请输入用户主页包含数字UID的链接"
async def get_target_name(self, target: Target) -> Optional[str]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
param = {"containerid": "100505" + target}
res = await client.get(
"https://m.weibo.cn/api/container/getIndex", params=param
@ -41,8 +42,17 @@ class Weibo(NewMessage):
else:
return None
async def parse_target(self, target_text: str) -> Target:
if re.match(r"\d+", target_text):
return Target(target_text)
elif match := re.match(r"(?:https?://)?weibo\.com/u/(\d+)", target_text):
# 都2202年了应该不会有http了吧不过还是防一手
return Target(match.group(1))
else:
raise self.ParseTargetException()
async def get_sub_list(self, target: Target) -> list[RawPost]:
async with httpx.AsyncClient() as client:
async with http_client() as client:
params = {"containerid": "107603" + target}
res = await client.get(
"https://m.weibo.cn/api/container/getIndex?", params=params, timeout=4.0
@ -123,15 +133,19 @@ class Weibo(NewMessage):
"Mobile Safari/537.36",
}
info = raw_post["mblog"]
if info["isLongText"] or info["pic_num"] > 9:
async with httpx.AsyncClient() as client:
retweeted = False
if info.get("retweeted_status"):
retweeted = True
pic_num = info["retweeted_status"]["pic_num"] if retweeted else info["pic_num"]
if info["isLongText"] or pic_num > 9:
async with http_client() as client:
res = await client.get(
"https://m.weibo.cn/detail/{}".format(info["mid"]), headers=header
)
try:
full_json_text = re.search(
r'"status": ([\s\S]+),\s+"call"', res.text
).group(1)
match = re.search(r'"status": ([\s\S]+),\s+"call"', res.text)
assert match
full_json_text = match.group(1)
info = json.loads(full_json_text)
except:
logger.info(
@ -140,7 +154,12 @@ class Weibo(NewMessage):
)
)
parsed_text = self._get_text(info["text"])
pic_urls = [img["large"]["url"] for img in info.get("pics", [])]
raw_pics_list = (
info["retweeted_status"].get("pics", [])
if retweeted
else info.get("pics", [])
)
pic_urls = [img["large"]["url"] for img in raw_pics_list]
detail_url = "https://weibo.com/{}/{}".format(info["user"]["id"], info["bid"])
# return parsed_text, detail_url, pic_urls
return Post(

View File

@ -1,3 +1,5 @@
from typing import Optional
import nonebot
from pydantic import BaseSettings
@ -12,9 +14,10 @@ class PlugConfig(BaseSettings):
bison_filter_log: bool = False
bison_to_me: bool = True
bison_skip_browser_check: bool = False
bison_use_pic_merge: int = 0 # 多图片时启用图片合并转发(仅限群),当bison_use_queue为False时该配置不会生效
bison_use_pic_merge: int = 0 # 多图片时启用图片合并转发(仅限群)
# 0不启用1首条消息单独发送剩余照片合并转发2以及以上所有消息全部合并转发
bison_resend_times: int = 0
bison_proxy: Optional[str]
class Config:
extra = "ignore"

View File

@ -3,13 +3,12 @@ from functools import reduce
from io import BytesIO
from typing import Optional, Union
import httpx
from nonebot.adapters.onebot.v11.message import Message, MessageSegment
from nonebot.log import logger
from PIL import Image
from .plugin_config import plugin_config
from .utils import parse_text
from .utils import http_client, parse_text
@dataclass
@ -34,7 +33,7 @@ class Post:
async def _pic_url_to_image(self, data: Union[str, bytes]) -> Image.Image:
pic_buffer = BytesIO()
if isinstance(data, str):
async with httpx.AsyncClient() as client:
async with http_client() as client:
res = await client.get(data)
pic_buffer.write(res.content)
else:

View File

@ -0,0 +1,93 @@
import logging
import nonebot
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from nonebot import get_driver
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.log import LoguruHandler, logger
from .config import config
from .platform import platform_manager
from .plugin_config import plugin_config
from .send import do_send_msgs, send_msgs
from .types import UserSubInfo
scheduler = AsyncIOScheduler(timezone="Asia/Shanghai")
@get_driver().on_startup
async def _start():
for platform_name, platform in platform_manager.items():
if platform.schedule_type in ["cron", "interval", "date"]:
logger.info(
f"start scheduler for {platform_name} with {platform.schedule_type} {platform.schedule_kw}"
)
scheduler.add_job(
fetch_and_send,
platform.schedule_type,
**platform.schedule_kw,
args=(platform_name,),
)
scheduler.configure({"apscheduler.timezone": "Asia/Shanghai"})
scheduler.start()
# get_driver().on_startup(_start)
async def fetch_and_send(target_type: str):
target = config.get_next_target(target_type)
if not target:
return
logger.debug(
"try to fecth new posts from {}, target: {}".format(target_type, target)
)
send_user_list = config.target_user_cache[target_type][target]
send_userinfo_list = list(
map(
lambda user: UserSubInfo(
user,
lambda target: config.get_sub_category(
target_type, target, user.user_type, user.user
),
lambda target: config.get_sub_tags(
target_type, target, user.user_type, user.user
),
),
send_user_list,
)
)
to_send = await platform_manager[target_type].fetch_new_post(
target, send_userinfo_list
)
if not to_send:
return
bot = nonebot.get_bot()
assert isinstance(bot, Bot)
for user, send_list in to_send:
for send_post in send_list:
logger.info("send to {}: {}".format(user, send_post))
if not bot:
logger.warning("no bot connected")
else:
await send_msgs(
bot, user.user, user.user_type, await send_post.generate_messages()
)
class CustomLogHandler(LoguruHandler):
def filter(self, record: logging.LogRecord):
return record.msg != (
'Execution of job "%s" '
"skipped: maximum number of running instances reached (%d)"
)
if plugin_config.bison_use_queue:
scheduler.add_job(do_send_msgs, "interval", seconds=0.3, coalesce=True)
aps_logger = logging.getLogger("apscheduler")
aps_logger.setLevel(30)
aps_logger.handlers.clear()
aps_logger.addHandler(CustomLogHandler())

View File

@ -8,7 +8,10 @@ from nonebot.adapters.onebot.v11.message import MessageSegment
from nonebot.log import default_format, logger
from nonebot.plugin import require
from .plugin_config import plugin_config
from ..plugin_config import plugin_config
from .http import http_client
__all__ = ["http_client", "Singleton", "parse_text", "html_to_text"]
class Singleton(type):

View File

@ -0,0 +1,12 @@
import functools
import httpx
from ..plugin_config import plugin_config
if plugin_config.bison_proxy:
http_client = functools.partial(
httpx.AsyncClient, proxies=plugin_config.bison_proxy
)
else:
http_client = httpx.AsyncClient

View File

@ -0,0 +1 @@
{"code":0,"message":"0","ttl":1,"data":{"mid":161775300,"name":"明日方舟","sex":"保密","face":"http://i0.hdslb.com/bfs/face/89154378c06a5ed332c40c2ca56f50cd641c0c90.jpg","face_nft":0,"sign":"重铸未来 方舟启航","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":3,"title":"明日方舟官方账号","desc":"","type":1},"vip":{"type":2,"status":1,"due_date":1648828800000,"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":5305,"name":"明日方舟音律系列","image":"http://i0.hdslb.com/bfs/garb/item/615a1653281141ddf64cbb98c792ddaee78f7f40.png","expire":0,"image_enhance":"http://i0.hdslb.com/bfs/garb/item/516ecdf2d495a62f1bac31497c831b711823140c.webp","image_enhance_frame":"http://i0.hdslb.com/bfs/garb/item/c0751afbf950373c260254d02768eabf30ff3906.png"},"nameplate":{"nid":0,"name":"","image":"","image_small":"","level":"","condition":""},"user_honour_info":{"mid":0,"colour":null,"tags":[]},"is_followed":true,"top_photo":"http://i1.hdslb.com/bfs/space/6c6084808ec5bdff1985acc05ce0e126c49ad76e.png","theme":{},"sys_notice":{},"live_room":{"roomStatus":1,"liveStatus":0,"url":"https://live.bilibili.com/5555734?broadcast_type=0\u0026is_room_feed=1","title":"《明日方舟》2022新春前瞻特辑","cover":"http://i0.hdslb.com/bfs/live/new_room_cover/79af83a27f6001c1acfb47d1c0b879290f7c3308.jpg","roomid":5555734,"roundStatus":1,"broadcast_type":0,"watched_show":{"switch":true,"num":13033,"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":"","school":null,"profession":{"name":"","department":"","title":"","is_show":0},"tags":null,"series":{"user_upgrade_status":3,"show_upgrade_window":false},"is_senior_member":0}}

View File

@ -0,0 +1 @@
{"code":0,"msg":"","data":[{"coverUrl":"https://web.hycdn.cn/comic/pic/20220507/c4da4fb95587101f1867a30fe85cb557.png","comicCid":"6253","title":"123罗德岛","subtitle":"你可能不知道的罗德岛小剧场!","episodeCid":"5771","episodeType":1,"episodeShortTitle":"「流明」篇","updateTime":1652025600},{"coverUrl":"https://web.hycdn.cn/comic/pic/20220505/5c296539f5dc808603f20dda86879c9c.png","comicCid":"0696","title":"A1行动预备组","subtitle":"","episodeCid":"2897","episodeType":1,"episodeShortTitle":"01","updateTime":1651852800},{"coverUrl":"https://web.hycdn.cn/comic/pic/20220428/f4dc1e91420d18eccb80d60bba322d42.png","comicCid":"6253","title":"123罗德岛","subtitle":"你可能不知道的罗德岛小剧场!","episodeCid":"6346","episodeType":1,"episodeShortTitle":"「艾丽妮」篇","updateTime":1651507200}]}

View File

@ -0,0 +1 @@
{"code":0,"msg":"","data":[{"coverUrl":"https://web.hycdn.cn/comic/pic/20220507/ab8a2ff408ec7d587775aed70b178ec0.png","comicCid":"6253","title":"123罗德岛","subtitle":"你可能不知道的罗德岛小剧场!","episodeCid":"4938","episodeType":1,"episodeShortTitle":"「掠风」篇","updateTime":1652112000},{"coverUrl":"https://web.hycdn.cn/comic/pic/20220507/c4da4fb95587101f1867a30fe85cb557.png","comicCid":"6253","title":"123罗德岛","subtitle":"你可能不知道的罗德岛小剧场!","episodeCid":"5771","episodeType":1,"episodeShortTitle":"「流明」篇","updateTime":1652025600},{"coverUrl":"https://web.hycdn.cn/comic/pic/20220505/5c296539f5dc808603f20dda86879c9c.png","comicCid":"0696","title":"A1行动预备组","subtitle":"","episodeCid":"2897","episodeType":1,"episodeShortTitle":"01","updateTime":1651852800},{"coverUrl":"https://web.hycdn.cn/comic/pic/20220428/f4dc1e91420d18eccb80d60bba322d42.png","comicCid":"6253","title":"123罗德岛","subtitle":"你可能不知道的罗德岛小剧场!","episodeCid":"6346","episodeType":1,"episodeShortTitle":"「艾丽妮」篇","updateTime":1651507200}]}

View File

@ -61,6 +61,7 @@ async def test_fetch_new(
"https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/preannouncement.meta.json"
)
monster_siren_router = respx.get("https://monster-siren.hypergryph.com/api/news")
terra_list = respx.get("https://terra-historicus.hypergryph.com/api/recentUpdate")
ak_list_router.mock(return_value=Response(200, json=arknights_list__1))
detail_router.mock(
return_value=Response(200, text=get_file("arknights-detail-807"))
@ -72,6 +73,7 @@ async def test_fetch_new(
return_value=Response(200, json=get_json("arknights-pre-0.json"))
)
monster_siren_router.mock(return_value=Response(200, json=monster_siren_list_0))
terra_list.mock(return_value=Response(200, json=get_json("terra-hist-0.json")))
target = ""
res = await arknights.fetch_new_post(target, [dummy_user_subinfo])
assert ak_list_router.called
@ -90,7 +92,17 @@ async def test_fetch_new(
assert len(post.pics) == 1
# assert(post.pics == ['https://ak-fs.hypergryph.com/announce/images/20210623/e6f49aeb9547a2278678368a43b95b07.jpg'])
print(res3[0][1])
r = await post.generate_messages()
await post.generate_messages()
terra_list.mock(return_value=Response(200, json=get_json("terra-hist-1.json")))
res = await arknights.fetch_new_post(target, [dummy_user_subinfo])
assert len(res) == 1
post = res[0][1][0]
assert post.target_type == "terra-historicus"
assert post.text == "123罗德岛 - 「掠风」篇"
assert post.url == "https://terra-historicus.hypergryph.com/comic/6253/episode/4938"
assert post.pics == [
"https://web.hycdn.cn/comic/pic/20220507/ab8a2ff408ec7d587775aed70b178ec0.png"
]
@pytest.mark.render
@ -116,6 +128,7 @@ async def test_send_with_render(
"https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/preannouncement.meta.json"
)
monster_siren_router = respx.get("https://monster-siren.hypergryph.com/api/news")
terra_list = respx.get("https://terra-historicus.hypergryph.com/api/recentUpdate")
ak_list_router.mock(return_value=Response(200, json=arknights_list_0))
detail_router.mock(
return_value=Response(200, text=get_file("arknights-detail-805"))
@ -127,6 +140,7 @@ async def test_send_with_render(
return_value=Response(200, json=get_json("arknights-pre-0.json"))
)
monster_siren_router.mock(return_value=Response(200, json=monster_siren_list_0))
terra_list.mock(return_value=Response(200, json=get_json("terra-hist-0.json")))
target = ""
res = await arknights.fetch_new_post(target, [dummy_user_subinfo])
assert ak_list_router.called

View File

@ -1,3 +1,5 @@
import typing
import pytest
from httpx import Response
from nonebug.app import App
@ -10,6 +12,10 @@ def bing_dy_list():
return get_json("bilibili_bing_list.json")["data"]["cards"]
if typing.TYPE_CHECKING:
from nonebot_bison.platform.bilibili import Bilibili
@pytest.fixture
def bilibili(app: App):
from nonebot_bison.platform import platform_manager
@ -46,3 +52,20 @@ async def test_dynamic_forward(bilibili, bing_dy_list):
+ "\n--------------\n"
+ "#明日方舟#\n【新增服饰】\n//殿堂上的游禽 - 星极\n塞壬唱片偶像企划《闪耀阶梯》特供服饰/殿堂上的游禽。星极自费参加了这项企划,尝试着用大众能接受的方式演绎天空之上的故事。\n\n_____________\n谦逊留给观众,骄傲发自歌喉,此夜,唯我璀璨。 "
)
async def test_parse_target(bilibili: "Bilibili"):
from nonebot_bison.platform.platform import Platform
res = await bilibili.parse_target(
"https://space.bilibili.com/161775300?from=search&seid=130517740606234234234&spm_id_from=333.337.0.0"
)
assert res == "161775300"
res2 = await bilibili.parse_target(
"space.bilibili.com/161775300?from=search&seid=130517740606234234234&spm_id_from=333.337.0.0"
)
assert res2 == "161775300"
with pytest.raises(Platform.ParseTargetException):
await bilibili.parse_target(
"https://www.bilibili.com/video/BV1qP4y1g738?spm_id_from=333.999.0.0"
)

View File

@ -1,4 +1,5 @@
import time
import typing
import pytest
import respx
@ -7,6 +8,9 @@ from nonebug.app import App
from .utils import get_json
if typing.TYPE_CHECKING:
from nonebot_bison.platform.ncm_artist import NcmArtist
@pytest.fixture
def ncm_artist(app: App):
@ -48,3 +52,16 @@ async def test_fetch_new(ncm_artist, ncm_artist_0, ncm_artist_1, dummy_user_subi
assert post.target_type == "ncm-artist"
assert post.text == "新专辑发布Y1K"
assert post.url == "https://music.163.com/#/album?id=131074504"
async def test_parse_target(ncm_artist: "NcmArtist"):
from nonebot_bison.platform.platform import Platform
res = await ncm_artist.parse_target("32540734")
assert res == "32540734"
res = await ncm_artist.parse_target("https://music.163.com/#/artist?id=32540734")
assert res == "32540734"
res = await ncm_artist.parse_target("music.163.com/#/artist?id=32540734")
assert res == "32540734"
with pytest.raises(Platform.ParseTargetException):
await ncm_artist.parse_target("music.163.com/#/rad?id=32540734")

View File

@ -1,4 +1,5 @@
import time
import typing
import pytest
import respx
@ -7,6 +8,9 @@ from nonebug.app import App
from .utils import get_json
if typing.TYPE_CHECKING:
from nonebot_bison.platform.ncm_radio import NcmRadio
@pytest.fixture
def ncm_radio(app: App):
@ -53,3 +57,14 @@ async def test_fetch_new(ncm_radio, ncm_radio_0, ncm_radio_1, dummy_user_subinfo
"http://p1.music.126.net/H5em5xUNIYXcjJhOmeaSqQ==/109951166647436789.jpg"
]
assert post.target_name == "《明日方舟》游戏原声OST"
async def test_parse_target(ncm_radio: "NcmRadio"):
res = await ncm_radio.parse_target("https://music.163.com/#/djradio?id=793745436")
assert res == "793745436"
res = await ncm_radio.parse_target("music.163.com/#/djradio?id=793745436")
assert res == "793745436"
res = await ncm_radio.parse_target("793745436")
assert res == "793745436"
with pytest.raises(ncm_radio.ParseTargetException):
await ncm_radio.parse_target("music.163.com/#/alm?id=793745436")

View File

@ -1,3 +1,4 @@
import typing
from datetime import datetime
import feedparser
@ -9,6 +10,9 @@ from pytz import timezone
from .utils import get_file, get_json
if typing.TYPE_CHECKING:
from nonebot_bison.platform.weibo import Weibo
@pytest.fixture
def weibo(app: App):
@ -23,7 +27,14 @@ def weibo_ak_list_1():
@pytest.mark.asyncio
@respx.mock
async def test_get_name(weibo):
profile_router = respx.get(
"https://m.weibo.cn/api/container/getIndex?containerid=1005056279793937"
)
profile_router.mock(
return_value=Response(200, json=get_json("weibo_ak_profile.json"))
)
name = await weibo.get_target_name("6279793937")
assert name == "明日方舟Arknights"
@ -125,3 +136,16 @@ def test_chaohua_tag(weibo):
tags = weibo.get_tags(test_post)
assert "刚出生的小羊驼长啥样" in tags
assert "小羊驼三三超话" in tags
async def test_parse_target(weibo: "Weibo"):
from nonebot_bison.platform.platform import Platform
res = await weibo.parse_target("https://weibo.com/u/6441489862")
assert res == "6441489862"
res = await weibo.parse_target("weibo.com/u/6441489862")
assert res == "6441489862"
res = await weibo.parse_target("6441489862")
assert res == "6441489862"
with pytest.raises(Platform.ParseTargetException):
await weibo.parse_target("https://weibo.com/arknights")

View File

@ -62,6 +62,7 @@ async def test_abort_add_on_id(app: App, db_migration):
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.config_manager import add_sub_matcher, common_platform
from nonebot_bison.platform import platform_manager
from nonebot_bison.platform.weibo import Weibo
ak_list_router = respx.get(
"https://m.weibo.cn/api/container/getIndex?containerid=1005056279793937"
@ -95,7 +96,7 @@ async def test_abort_add_on_id(app: App, db_migration):
ctx.receive_event(bot, event_2)
ctx.should_call_send(
event_2,
Message(BotReply.add_reply_on_id),
Message(BotReply.add_reply_on_id(Weibo)),
True,
)
event_abort = fake_group_message_event(
@ -118,6 +119,7 @@ async def test_abort_add_on_cats(app: App, db_migration):
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.config_manager import add_sub_matcher, common_platform
from nonebot_bison.platform import platform_manager
from nonebot_bison.platform.weibo import Weibo
ak_list_router = respx.get(
"https://m.weibo.cn/api/container/getIndex?containerid=1005056279793937"
@ -155,7 +157,7 @@ async def test_abort_add_on_cats(app: App, db_migration):
ctx.receive_event(bot, event_2)
ctx.should_call_send(
event_2,
Message(BotReply.add_reply_on_id),
Message(BotReply.add_reply_on_id(Weibo)),
True,
)
event_3 = fake_group_message_event(
@ -194,6 +196,7 @@ async def test_abort_add_on_tag(app: App, db_migration):
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.config_manager import add_sub_matcher, common_platform
from nonebot_bison.platform import platform_manager
from nonebot_bison.platform.weibo import Weibo
ak_list_router = respx.get(
"https://m.weibo.cn/api/container/getIndex?containerid=1005056279793937"
@ -231,7 +234,7 @@ async def test_abort_add_on_tag(app: App, db_migration):
ctx.receive_event(bot, event_2)
ctx.should_call_send(
event_2,
Message(BotReply.add_reply_on_id),
Message(BotReply.add_reply_on_id(Weibo)),
True,
)
event_3 = fake_group_message_event(
@ -265,3 +268,49 @@ async def test_abort_add_on_tag(app: App, db_migration):
True,
)
ctx.should_finished()
# 删除订阅阶段中止
@pytest.mark.asyncio
async def test_abort_del_sub(app: App):
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.config import Config
from nonebot_bison.config_manager import del_sub_matcher
from nonebot_bison.platform import platform_manager
config = Config()
config.user_target.truncate()
config.add_subscribe(
10000,
"group",
"6279793937",
"明日方舟Arknights",
"weibo",
[platform_manager["weibo"].reverse_category["图文"]],
["明日方舟"],
)
async with app.test_matcher(del_sub_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
assert isinstance(bot, Bot)
event = fake_group_message_event(
message=Message("删除订阅"), to_me=True, sender=fake_admin_user
)
ctx.receive_event(bot, event)
ctx.should_pass_rule()
ctx.should_pass_permission()
ctx.should_call_send(
event,
Message(
"订阅的帐号为:\n1 weibo 明日方舟Arknights 6279793937\n [图文] 明日方舟\n请输入要删除的订阅的序号\n输入'取消'中止"
),
True,
)
event_abort = fake_group_message_event(
message=Message("取消"), sender=fake_admin_user
)
ctx.receive_event(bot, event_abort)
ctx.should_call_send(event_abort, "删除中止", True)
ctx.should_finished()
subs = config.list_subscribe(10000, "group")
assert subs

View File

@ -64,6 +64,7 @@ async def test_add_with_target(app: App, db_migration):
from nonebot_bison.config import config
from nonebot_bison.config_manager import add_sub_matcher, common_platform
from nonebot_bison.platform import platform_manager
from nonebot_bison.platform.weibo import Weibo
ak_list_router = respx.get(
"https://m.weibo.cn/api/container/getIndex?containerid=1005056279793937"
@ -112,7 +113,7 @@ async def test_add_with_target(app: App, db_migration):
ctx.receive_event(bot, event_3)
ctx.should_call_send(
event_3,
Message(BotReply.add_reply_on_id),
Message(BotReply.add_reply_on_id(Weibo)),
True,
)
event_4_err = fake_group_message_event(
@ -178,6 +179,7 @@ async def test_add_with_target_no_cat(app: App, db_migration):
from nonebot_bison.config import config
from nonebot_bison.config_manager import add_sub_matcher, common_platform
from nonebot_bison.platform import platform_manager
from nonebot_bison.platform.ncm_artist import NcmArtist
ncm_router = respx.get("https://music.163.com/api/artist/albums/32540734")
ncm_router.mock(return_value=Response(200, json=get_json("ncm_siren.json")))
@ -202,7 +204,7 @@ async def test_add_with_target_no_cat(app: App, db_migration):
ctx.receive_event(bot, event_3)
ctx.should_call_send(
event_3,
Message(BotReply.add_reply_on_id),
Message(BotReply.add_reply_on_id(NcmArtist)),
True,
)
event_4_ok = fake_group_message_event(
@ -320,6 +322,7 @@ async def test_add_with_get_id(app: App, db_migration):
from nonebot_bison.config import config
from nonebot_bison.config_manager import add_sub_matcher, common_platform
from nonebot_bison.platform import platform_manager
from nonebot_bison.platform.weibo import Weibo
ak_list_router = respx.get(
"https://m.weibo.cn/api/container/getIndex?containerid=1005056279793937"
@ -358,7 +361,7 @@ async def test_add_with_get_id(app: App, db_migration):
ctx.receive_event(bot, event_3)
ctx.should_call_send(
event_3,
Message(BotReply.add_reply_on_id),
Message(BotReply.add_reply_on_id(Weibo)),
True,
)
event_4_query = fake_group_message_event(
@ -372,7 +375,7 @@ async def test_add_with_get_id(app: App, db_migration):
True,
)
"""
line 362:
关于Message([MessageSegment(*BotReply.add_reply_on_id_input_search())])
鬼知道为什么要在这里这样写
没有[]的话assert不了(should_call_send使用[MessageSegment(...)]的格式进行比较)
不在这里MessageSegment()的话也assert不了(指不能让add_reply_on_id_input_search直接返回一个MessageSegment对象)
@ -390,3 +393,125 @@ async def test_add_with_get_id(app: App, db_migration):
ctx.should_finished()
subs = await config.list_subscribe(10000, "group")
assert len(subs) == 0
@pytest.mark.asyncio
@respx.mock
async def test_add_with_bilibili_target_parser(app: App):
from nonebot.adapters.onebot.v11.event import Sender
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.config import Config
from nonebot_bison.config_manager import add_sub_matcher, common_platform
from nonebot_bison.platform import platform_manager
from nonebot_bison.platform.bilibili import Bilibili
config = Config()
config.user_target.truncate()
ak_list_router = respx.get(
"https://api.bilibili.com/x/space/acc/info?mid=161775300"
)
ak_list_router.mock(
return_value=Response(200, json=get_json("bilibili_arknights_profile.json"))
)
async with app.test_matcher(add_sub_matcher) as ctx:
bot = ctx.create_bot()
event_1 = fake_group_message_event(
message=Message("添加订阅"),
sender=Sender(card="", nickname="test", role="admin"),
to_me=True,
)
ctx.receive_event(bot, event_1)
ctx.should_pass_rule()
ctx.should_call_send(
event_1,
Message(
BotReply.add_reply_on_platform(
platform_manager=platform_manager, common_platform=common_platform
)
),
True,
)
event_2 = fake_group_message_event(
message=Message("全部"), sender=Sender(card="", nickname="test", role="admin")
)
ctx.receive_event(bot, event_2)
ctx.should_rejected()
ctx.should_call_send(
event_2,
BotReply.add_reply_on_platform_input_allplatform(platform_manager),
True,
)
event_3 = fake_group_message_event(
message=Message("bilibili"), sender=fake_admin_user
)
ctx.receive_event(bot, event_3)
assert Bilibili.parse_target_promot
ctx.should_call_send(
event_3,
Message(BotReply.add_reply_on_id(Bilibili)),
True,
)
event_4_err1 = fake_group_message_event(
message=Message(
"https://live.bilibili.com/5555734?broadcast_type=0&is_room_feed=1&spm_id_from=333.999.0.0"
),
sender=fake_admin_user,
)
ctx.receive_event(bot, event_4_err1)
ctx.should_call_send(
event_4_err1, BotReply.add_reply_on_target_parse_input_error, True
)
ctx.should_rejected()
event_4_err1 = fake_group_message_event(
message=Message(
"https://space.bilibili.com/ark161775300?from=search&seid=13051774060625135297&spm_id_from=333.337.0.0"
),
sender=fake_admin_user,
)
ctx.receive_event(bot, event_4_err1)
ctx.should_call_send(
event_4_err1, BotReply.add_reply_on_target_parse_input_error, True
)
ctx.should_rejected()
event_4_ok = fake_group_message_event(
message=Message(
"https://space.bilibili.com/161775300?from=search&seid=13051774060625135297&spm_id_from=333.337.0.0"
),
sender=fake_admin_user,
)
ctx.receive_event(bot, event_4_ok)
ctx.should_call_send(
event_4_ok,
BotReply.add_reply_on_target_confirm("bilibili", "明日方舟", "161775300"),
True,
)
ctx.should_call_send(
event_4_ok,
Message(BotReply.add_reply_on_cats(platform_manager, "bilibili")),
True,
)
event_5_ok = fake_group_message_event(
message=Message("视频"), sender=fake_admin_user
)
ctx.receive_event(bot, event_5_ok)
ctx.should_call_send(event_5_ok, Message(BotReply.add_reply_on_tags), True)
event_6 = fake_group_message_event(
message=Message("全部标签"), sender=fake_admin_user
)
ctx.receive_event(bot, event_6)
ctx.should_call_send(
event_6, BotReply.add_reply_subscribe_success("明日方舟"), True
)
ctx.should_finished()
subs = config.list_subscribe(10000, "group")
assert len(subs) == 1
sub = subs[0]
assert sub["target"] == "161775300"
assert sub["tags"] == []
assert sub["cats"] == [platform_manager["bilibili"].reverse_category["视频"]]
assert sub["target_type"] == "bilibili"
assert sub["target_name"] == "明日方舟"

View File

@ -65,7 +65,7 @@ async def test_del_sub(app: App, db_migration):
ctx.should_call_send(
event,
Message(
"订阅的帐号为:\n1 weibo 明日方舟Arknights 6279793937\n [图文] 明日方舟\n请输入要删除的订阅的序号"
"订阅的帐号为:\n1 weibo 明日方舟Arknights 6279793937\n [图文] 明日方舟\n请输入要删除的订阅的序号\n输入'取消'中止"
),
True,
)
@ -83,3 +83,30 @@ async def test_del_sub(app: App, db_migration):
ctx.should_finished()
subs = await config.list_subscribe(10000, "group")
assert len(subs) == 0
@pytest.mark.asyncio
async def test_del_empty_sub(app: App):
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.config import Config
from nonebot_bison.config_manager import del_sub_matcher
from nonebot_bison.platform import platform_manager
config = Config()
config.user_target.truncate()
async with app.test_matcher(del_sub_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
assert isinstance(bot, Bot)
event = fake_group_message_event(
message=Message("删除订阅"), to_me=True, sender=fake_admin_user
)
ctx.receive_event(bot, event)
ctx.should_pass_rule()
ctx.should_pass_permission()
ctx.should_finished()
ctx.should_call_send(
event,
"暂无已订阅账号\n请使用“添加订阅”命令添加订阅",
True,
)

20
tests/test_proxy.py Normal file
View File

@ -0,0 +1,20 @@
import pytest
from nonebug import App
from nonebug.fixture import nonebug_init
async def test_without_proxy(app: App):
from nonebot_bison.utils import http_client
c = http_client()
assert not c._mounts
@pytest.mark.parametrize(
"nonebug_init", [{"bison_proxy": "http://example.com"}], indirect=True
)
async def test_with_proxy(app: App):
from nonebot_bison.utils import http_client
c = http_client()
assert c._mounts

View File

@ -1,3 +1,4 @@
from ast import Str
from typing import TYPE_CHECKING
from typing_extensions import Literal
@ -130,8 +131,18 @@ class BotReply:
def add_reply_subscribe_success(name):
return "添加 {} 成功".format(name)
@staticmethod
def add_reply_on_id(platform: object) -> Str:
base_text = "请输入订阅用户的id\n查询id获取方法请回复:“查询”"
extra_text = (
("1." + platform.parse_target_promot + "\n2.")
if platform.parse_target_promot
else ""
)
return extra_text + base_text
add_reply_on_id_input_error = "id输入错误"
add_reply_on_target_parse_input_error = "不能从你的输入中提取出id请检查你输入的内容是否符合预期"
add_reply_on_platform_input_error = "平台输入错误"
add_reply_on_id = "请输入订阅用户的id:\n查询id获取方法请回复:“查询”"
add_reply_on_tags = '请输入要订阅的tag订阅所有tag输入"全部标签"'
add_reply_abort = "已中止订阅"

View File

@ -414,9 +414,9 @@
vue-demi "*"
ansi-regex@^5.0.0:
version "5.0.0"
resolved "https://registry.nlark.com/ansi-regex/download/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
integrity sha1-OIU59VF5vzkznIGvMKZU1p+Hy3U=
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^4.1.0:
version "4.3.0"