nonebot-plugin-bililive 2.0.2__tar.gz → 2.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/PKG-INFO +4 -32
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/README.md +3 -31
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/database/db.py +22 -1
- nonebot_plugin_bililive-2.0.4/bililive/libs/dynamic/web.py +79 -0
- nonebot_plugin_bililive-2.0.4/bililive/plugins/pusher/dynamic_pusher.py +283 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/pusher/live_pusher.py +10 -1
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/utils/__init__.py +1 -1
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/utils/browser.py +9 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/version.py +1 -1
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/pyproject.toml +1 -1
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/tests/test_maintenance.py +69 -1
- nonebot_plugin_bililive-2.0.2/bililive/plugins/pusher/dynamic_pusher.py +0 -151
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/LICENSE +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/__main__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/cli/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/cli/bot.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/cli/utils.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/compat.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/config.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/database/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/database/models.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/card.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/desc.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/display.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/user_profile.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/at/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/at/at_off.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/at/at_on.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/auto_agree.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/auto_delete.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/dynamic/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/dynamic/dynamic_off.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/dynamic/dynamic_on.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/help.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/live/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/live/live_now.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/live/live_off.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/live/live_on.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/permission/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/permission/permission_off.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/permission/permission_on.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/pusher/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/sub/__init__.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/sub/add_sub.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/sub/delete_sub.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/sub/sub_list.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/utils/captcha_solver.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/utils/fonts_provider.py +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/utils/mobile.js +0 -0
- {nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/nonebot_plugin_bililive/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nonebot-plugin-bililive
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary: Push bilibili dynamics and live notifications to QQ with NoneBot2.
|
|
5
5
|
Keywords: nonebot,nonebot2,nonebot-plugin,qqbot,bilibili
|
|
6
6
|
Author-Email: SK-415 <2967923486@qq.com>
|
|
@@ -51,42 +51,12 @@ _✨ 将 B 站 UP 主动态与直播推送到 QQ 的 NoneBot2 插件 ✨_
|
|
|
51
51
|
<img src="https://img.shields.io/pypi/v/nonebot-plugin-bililive.svg" alt="pypi">
|
|
52
52
|
</a>
|
|
53
53
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
|
54
|
-
<a href="https://jq.qq.com/?_wv=1027&k=sHPbCRAd">
|
|
55
|
-
<img src="https://img.shields.io/badge/QQ%E7%BE%A4-629574472-orange" alt="qq group">
|
|
56
|
-
</a>
|
|
57
54
|
|
|
58
55
|
</div>
|
|
59
56
|
|
|
60
|
-
> 当前仓库已按 NoneBot 插件模板整理,可直接作为插件包发布到 PyPI 并在 NoneBot2 项目中安装使用。
|
|
61
|
-
|
|
62
|
-
<details>
|
|
63
|
-
<summary>配置发布工作流</summary>
|
|
64
|
-
|
|
65
|
-
1. 前往 https://pypi.org/manage/account/#api-tokens 创建新的 PyPI API Token。
|
|
66
|
-
2. 打开当前 GitHub 仓库的 Settings - Secrets and variables - Actions。
|
|
67
|
-
3. 新建名为 PYPI_API_TOKEN 的 Repository Secret,并填入刚刚创建的 Token。
|
|
68
|
-
|
|
69
|
-
</details>
|
|
70
|
-
|
|
71
|
-
> [!IMPORTANT]
|
|
72
|
-
> 当前项目使用符合 PEP 621 的 pyproject.toml,并已补充基于 tag 触发的 PyPI 发布工作流。
|
|
73
|
-
|
|
74
|
-
<details>
|
|
75
|
-
<summary>触发发布</summary>
|
|
76
|
-
|
|
77
|
-
创建 tag:
|
|
78
|
-
|
|
79
|
-
git tag v1.6.0post5
|
|
80
|
-
|
|
81
|
-
推送 tag:
|
|
82
|
-
|
|
83
|
-
git push origin --tags
|
|
84
|
-
|
|
85
|
-
</details>
|
|
86
|
-
|
|
87
57
|
## 📖 介绍
|
|
88
58
|
|
|
89
|
-
BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直播与动态消息推送到 QQ
|
|
59
|
+
BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直播与动态消息推送到 QQ 群或私聊场景。
|
|
90
60
|
|
|
91
61
|
### 特性
|
|
92
62
|
|
|
@@ -142,6 +112,8 @@ BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直
|
|
|
142
112
|
| BILILIVE_DYNAMIC_BIG_IMAGE | 否 | false | 是否优先展示大图 |
|
|
143
113
|
| BILILIVE_COMMAND_PREFIX | 否 | 空字符串 | 命令额外前缀 |
|
|
144
114
|
|
|
115
|
+
动态抓取默认优先使用 gRPC 接口;当部分 UID 命中 B 站风控时,插件会自动回退到 Playwright 持久化浏览器中的 cookies 请求网页动态接口。通常不需要额外配置 Cookie 登录;如果某些 UID 仍持续抓取失败,建议在插件使用的浏览器数据目录中登录一个常用的 B 站账号,以提高动态抓取成功率。
|
|
116
|
+
|
|
145
117
|
## 🎉 使用
|
|
146
118
|
|
|
147
119
|
### 指令表
|
|
@@ -17,42 +17,12 @@ _✨ 将 B 站 UP 主动态与直播推送到 QQ 的 NoneBot2 插件 ✨_
|
|
|
17
17
|
<img src="https://img.shields.io/pypi/v/nonebot-plugin-bililive.svg" alt="pypi">
|
|
18
18
|
</a>
|
|
19
19
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
|
20
|
-
<a href="https://jq.qq.com/?_wv=1027&k=sHPbCRAd">
|
|
21
|
-
<img src="https://img.shields.io/badge/QQ%E7%BE%A4-629574472-orange" alt="qq group">
|
|
22
|
-
</a>
|
|
23
20
|
|
|
24
21
|
</div>
|
|
25
22
|
|
|
26
|
-
> 当前仓库已按 NoneBot 插件模板整理,可直接作为插件包发布到 PyPI 并在 NoneBot2 项目中安装使用。
|
|
27
|
-
|
|
28
|
-
<details>
|
|
29
|
-
<summary>配置发布工作流</summary>
|
|
30
|
-
|
|
31
|
-
1. 前往 https://pypi.org/manage/account/#api-tokens 创建新的 PyPI API Token。
|
|
32
|
-
2. 打开当前 GitHub 仓库的 Settings - Secrets and variables - Actions。
|
|
33
|
-
3. 新建名为 PYPI_API_TOKEN 的 Repository Secret,并填入刚刚创建的 Token。
|
|
34
|
-
|
|
35
|
-
</details>
|
|
36
|
-
|
|
37
|
-
> [!IMPORTANT]
|
|
38
|
-
> 当前项目使用符合 PEP 621 的 pyproject.toml,并已补充基于 tag 触发的 PyPI 发布工作流。
|
|
39
|
-
|
|
40
|
-
<details>
|
|
41
|
-
<summary>触发发布</summary>
|
|
42
|
-
|
|
43
|
-
创建 tag:
|
|
44
|
-
|
|
45
|
-
git tag v1.6.0post5
|
|
46
|
-
|
|
47
|
-
推送 tag:
|
|
48
|
-
|
|
49
|
-
git push origin --tags
|
|
50
|
-
|
|
51
|
-
</details>
|
|
52
|
-
|
|
53
23
|
## 📖 介绍
|
|
54
24
|
|
|
55
|
-
BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直播与动态消息推送到 QQ
|
|
25
|
+
BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直播与动态消息推送到 QQ 群或私聊场景。
|
|
56
26
|
|
|
57
27
|
### 特性
|
|
58
28
|
|
|
@@ -108,6 +78,8 @@ BiliLive 是一个基于 NoneBot2 的 B 站推送插件,支持将 UP 主的直
|
|
|
108
78
|
| BILILIVE_DYNAMIC_BIG_IMAGE | 否 | false | 是否优先展示大图 |
|
|
109
79
|
| BILILIVE_COMMAND_PREFIX | 否 | 空字符串 | 命令额外前缀 |
|
|
110
80
|
|
|
81
|
+
动态抓取默认优先使用 gRPC 接口;当部分 UID 命中 B 站风控时,插件会自动回退到 Playwright 持久化浏览器中的 cookies 请求网页动态接口。通常不需要额外配置 Cookie 登录;如果某些 UID 仍持续抓取失败,建议在插件使用的浏览器数据目录中登录一个常用的 B 站账号,以提高动态抓取成功率。
|
|
82
|
+
|
|
111
83
|
## 🎉 使用
|
|
112
84
|
|
|
113
85
|
### 指令表
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import json
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
@@ -18,9 +19,12 @@ dynamic_offset = {}
|
|
|
18
19
|
class DB:
|
|
19
20
|
"""数据库交互类,与增删改查无关的部分不应该在这里面实现"""
|
|
20
21
|
|
|
22
|
+
_ready = False
|
|
23
|
+
|
|
21
24
|
@classmethod
|
|
22
25
|
async def init(cls):
|
|
23
26
|
"""初始化数据库"""
|
|
27
|
+
cls._ready = False
|
|
24
28
|
config = {
|
|
25
29
|
"connections": {
|
|
26
30
|
# "bililive": {
|
|
@@ -37,16 +41,33 @@ class DB:
|
|
|
37
41
|
},
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
await Tortoise.init(config)
|
|
44
|
+
await Tortoise.init(config, _enable_global_fallback=True)
|
|
41
45
|
|
|
42
46
|
await Tortoise.generate_schemas()
|
|
43
47
|
await cls.migrate()
|
|
44
48
|
await cls.update_uid_list()
|
|
49
|
+
cls._ready = True
|
|
45
50
|
|
|
46
51
|
@classmethod
|
|
47
52
|
async def close(cls):
|
|
53
|
+
cls._ready = False
|
|
48
54
|
await connections.close_all()
|
|
49
55
|
|
|
56
|
+
@classmethod
|
|
57
|
+
async def wait_until_ready(cls, timeout: float = 30) -> bool:
|
|
58
|
+
if cls._ready:
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
waited = 0.0
|
|
62
|
+
interval = 0.1
|
|
63
|
+
while waited < timeout:
|
|
64
|
+
if cls._ready:
|
|
65
|
+
return True
|
|
66
|
+
await asyncio.sleep(interval)
|
|
67
|
+
waited += interval
|
|
68
|
+
|
|
69
|
+
return cls._ready
|
|
70
|
+
|
|
50
71
|
@classmethod
|
|
51
72
|
async def get_user(cls, **kwargs):
|
|
52
73
|
"""获取 UP 主信息"""
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
WEB_DYNAMIC_URL = "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space"
|
|
6
|
+
DEFAULT_BROWSER_USER_AGENT = (
|
|
7
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
8
|
+
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WebDynamicError(RuntimeError):
|
|
13
|
+
def __init__(self, code, msg, data=None):
|
|
14
|
+
super().__init__(f"{code} {msg}")
|
|
15
|
+
self.code = code
|
|
16
|
+
self.msg = msg
|
|
17
|
+
self.data = data
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class WebDynamicItem:
|
|
22
|
+
dynamic_id: int
|
|
23
|
+
dynamic_type: str
|
|
24
|
+
author_name: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_web_dynamic_items(payload: dict) -> list[WebDynamicItem]:
|
|
28
|
+
items = payload.get("data", {}).get("items") or []
|
|
29
|
+
parsed_items = []
|
|
30
|
+
for item in items:
|
|
31
|
+
dynamic_id = item.get("id_str")
|
|
32
|
+
modules = item.get("modules") or {}
|
|
33
|
+
author_name = (modules.get("module_author") or {}).get("name")
|
|
34
|
+
dynamic_type = item.get("type") or ""
|
|
35
|
+
if not dynamic_id or not author_name:
|
|
36
|
+
continue
|
|
37
|
+
try:
|
|
38
|
+
parsed_items.append(
|
|
39
|
+
WebDynamicItem(
|
|
40
|
+
dynamic_id=int(dynamic_id),
|
|
41
|
+
dynamic_type=dynamic_type,
|
|
42
|
+
author_name=author_name,
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
continue
|
|
47
|
+
return parsed_items
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def get_user_dynamics_web(
|
|
51
|
+
uid: int,
|
|
52
|
+
cookies: dict[str, str],
|
|
53
|
+
*,
|
|
54
|
+
proxy: str | None = None,
|
|
55
|
+
user_agent: str | None = None,
|
|
56
|
+
timeout: int = 10,
|
|
57
|
+
) -> list[WebDynamicItem]:
|
|
58
|
+
headers = {
|
|
59
|
+
"User-Agent": user_agent or DEFAULT_BROWSER_USER_AGENT,
|
|
60
|
+
"Referer": f"https://space.bilibili.com/{uid}/dynamic",
|
|
61
|
+
"Accept": "application/json, text/plain, */*",
|
|
62
|
+
"Origin": "https://space.bilibili.com",
|
|
63
|
+
}
|
|
64
|
+
async with httpx.AsyncClient(
|
|
65
|
+
proxy=proxy,
|
|
66
|
+
headers=headers,
|
|
67
|
+
cookies=cookies,
|
|
68
|
+
timeout=timeout,
|
|
69
|
+
follow_redirects=True,
|
|
70
|
+
) as client:
|
|
71
|
+
response = await client.get(WEB_DYNAMIC_URL, params={"host_mid": uid})
|
|
72
|
+
payload = response.json()
|
|
73
|
+
if payload.get("code") != 0:
|
|
74
|
+
raise WebDynamicError(
|
|
75
|
+
payload.get("code"),
|
|
76
|
+
payload.get("message") or "unknown error",
|
|
77
|
+
payload.get("data"),
|
|
78
|
+
)
|
|
79
|
+
return parse_web_dynamic_items(payload)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from time import monotonic
|
|
4
|
+
|
|
5
|
+
from apscheduler.events import (
|
|
6
|
+
EVENT_JOB_ERROR,
|
|
7
|
+
EVENT_JOB_EXECUTED,
|
|
8
|
+
EVENT_JOB_MISSED,
|
|
9
|
+
EVENT_SCHEDULER_STARTED,
|
|
10
|
+
)
|
|
11
|
+
from bilireq.exceptions import GrpcError
|
|
12
|
+
from bilireq.grpc.dynamic import grpc_get_user_dynamics
|
|
13
|
+
from bilireq.grpc.protos.bilibili.app.dynamic.v2.dynamic_pb2 import DynamicType
|
|
14
|
+
from grpc import StatusCode
|
|
15
|
+
from grpc.aio import AioRpcError
|
|
16
|
+
from nonebot.adapters.onebot.v11.message import MessageSegment
|
|
17
|
+
from nonebot.log import logger
|
|
18
|
+
|
|
19
|
+
from ...config import plugin_config
|
|
20
|
+
from ...database import DB as db
|
|
21
|
+
from ...database import dynamic_offset as offset
|
|
22
|
+
from ...libs.dynamic.web import WebDynamicError, get_user_dynamics_web
|
|
23
|
+
from ...utils import (
|
|
24
|
+
get_bilibili_cookies,
|
|
25
|
+
get_dynamic_screenshot,
|
|
26
|
+
safe_send,
|
|
27
|
+
scheduler,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
RISK_CONTROL_RETRY_SECONDS = 3600
|
|
31
|
+
dynamic_risk_control_until = {}
|
|
32
|
+
dynamic_web_fallback_until = {}
|
|
33
|
+
WEB_SKIP_DYNAMIC_TYPES = {
|
|
34
|
+
"DYNAMIC_TYPE_LIVE_RCMD",
|
|
35
|
+
"DYNAMIC_TYPE_LIVE",
|
|
36
|
+
"DYNAMIC_TYPE_AD",
|
|
37
|
+
"DYNAMIC_TYPE_BANNER",
|
|
38
|
+
}
|
|
39
|
+
WEB_DYNAMIC_TYPE_MESSAGES = {
|
|
40
|
+
"DYNAMIC_TYPE_FORWARD": "转发了一条动态",
|
|
41
|
+
"DYNAMIC_TYPE_WORD": "发布了新文字动态",
|
|
42
|
+
"DYNAMIC_TYPE_DRAW": "发布了新图文动态",
|
|
43
|
+
"DYNAMIC_TYPE_AV": "发布了新投稿",
|
|
44
|
+
"DYNAMIC_TYPE_ARTICLE": "发布了新专栏",
|
|
45
|
+
"DYNAMIC_TYPE_MUSIC": "发布了新音频",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def throttle_dynamic_loop():
|
|
50
|
+
if plugin_config.bililive_dynamic_interval == 0:
|
|
51
|
+
await asyncio.sleep(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_dynamic_id(dynamic, use_web_fallback: bool) -> int:
|
|
55
|
+
if use_web_fallback:
|
|
56
|
+
return dynamic.dynamic_id
|
|
57
|
+
return int(dynamic.extend.dyn_id_str)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_dynamic_type(dynamic, use_web_fallback: bool):
|
|
61
|
+
if use_web_fallback:
|
|
62
|
+
return dynamic.dynamic_type
|
|
63
|
+
return dynamic.card_type
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_dynamic_author_name(dynamic, use_web_fallback: bool) -> str:
|
|
67
|
+
if use_web_fallback:
|
|
68
|
+
return dynamic.author_name
|
|
69
|
+
return dynamic.modules[0].module_author.author.name
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_dynamic_type_message(dynamic_type, use_web_fallback: bool) -> str:
|
|
73
|
+
if use_web_fallback:
|
|
74
|
+
return WEB_DYNAMIC_TYPE_MESSAGES.get(dynamic_type, "发布了新动态")
|
|
75
|
+
return {
|
|
76
|
+
0: "发布了新动态",
|
|
77
|
+
DynamicType.forward: "转发了一条动态",
|
|
78
|
+
DynamicType.word: "发布了新文字动态",
|
|
79
|
+
DynamicType.draw: "发布了新图文动态",
|
|
80
|
+
DynamicType.av: "发布了新投稿",
|
|
81
|
+
DynamicType.article: "发布了新专栏",
|
|
82
|
+
DynamicType.music: "发布了新音频",
|
|
83
|
+
}.get(dynamic_type, "发布了新动态")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def should_skip_dynamic(dynamic_type, use_web_fallback: bool) -> bool:
|
|
87
|
+
if use_web_fallback:
|
|
88
|
+
return dynamic_type in WEB_SKIP_DYNAMIC_TYPES
|
|
89
|
+
return dynamic_type in [
|
|
90
|
+
DynamicType.live_rcmd,
|
|
91
|
+
DynamicType.live,
|
|
92
|
+
DynamicType.ad,
|
|
93
|
+
DynamicType.banner,
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def get_user_dynamics_with_web_fallback(uid: int) -> tuple[list, bool]:
|
|
98
|
+
fallback_until = dynamic_web_fallback_until.get(uid)
|
|
99
|
+
if fallback_until is not None:
|
|
100
|
+
if fallback_until > monotonic():
|
|
101
|
+
logger.debug(f"动态 gRPC 接口仍在风控,继续使用 Web 接口:{uid}")
|
|
102
|
+
cookies = await get_bilibili_cookies()
|
|
103
|
+
if not cookies:
|
|
104
|
+
raise WebDynamicError(-1, "browser cookies unavailable")
|
|
105
|
+
dynamics = await get_user_dynamics_web(
|
|
106
|
+
uid,
|
|
107
|
+
cookies,
|
|
108
|
+
proxy=plugin_config.bililive_proxy,
|
|
109
|
+
user_agent=plugin_config.bililive_browser_ua or None,
|
|
110
|
+
timeout=plugin_config.bililive_dynamic_timeout,
|
|
111
|
+
)
|
|
112
|
+
return dynamics, True
|
|
113
|
+
del dynamic_web_fallback_until[uid]
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
dynamics = (
|
|
117
|
+
await grpc_get_user_dynamics(
|
|
118
|
+
uid,
|
|
119
|
+
timeout=plugin_config.bililive_dynamic_timeout,
|
|
120
|
+
proxy=plugin_config.bililive_proxy,
|
|
121
|
+
)
|
|
122
|
+
).list
|
|
123
|
+
dynamic_web_fallback_until.pop(uid, None)
|
|
124
|
+
return list(dynamics), False
|
|
125
|
+
except GrpcError as e:
|
|
126
|
+
if e.code != -352:
|
|
127
|
+
raise
|
|
128
|
+
logger.warning(
|
|
129
|
+
f"动态 gRPC 接口触发风控,切换 Web 接口:{uid} "
|
|
130
|
+
f"{e.code} {e.msg}"
|
|
131
|
+
)
|
|
132
|
+
dynamic_web_fallback_until[uid] = monotonic() + RISK_CONTROL_RETRY_SECONDS
|
|
133
|
+
cookies = await get_bilibili_cookies()
|
|
134
|
+
if not cookies:
|
|
135
|
+
raise WebDynamicError(-1, "browser cookies unavailable")
|
|
136
|
+
dynamics = await get_user_dynamics_web(
|
|
137
|
+
uid,
|
|
138
|
+
cookies,
|
|
139
|
+
proxy=plugin_config.bililive_proxy,
|
|
140
|
+
user_agent=plugin_config.bililive_browser_ua or None,
|
|
141
|
+
timeout=plugin_config.bililive_dynamic_timeout,
|
|
142
|
+
)
|
|
143
|
+
return dynamics, True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def dy_sched():
|
|
147
|
+
"""动态推送"""
|
|
148
|
+
if not await db.wait_until_ready():
|
|
149
|
+
logger.debug("数据库尚未初始化完成,跳过本轮动态推送")
|
|
150
|
+
await throttle_dynamic_loop()
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
uid = await db.next_uid("dynamic")
|
|
154
|
+
if not uid:
|
|
155
|
+
# 没有订阅先暂停一秒再跳过,不然会导致 CPU 占用过高
|
|
156
|
+
await throttle_dynamic_loop()
|
|
157
|
+
return
|
|
158
|
+
user = await db.get_user(uid=uid)
|
|
159
|
+
if user is None:
|
|
160
|
+
logger.warning(f"动态推送跳过异常订阅 UID:{uid}")
|
|
161
|
+
await throttle_dynamic_loop()
|
|
162
|
+
return
|
|
163
|
+
name = user.name
|
|
164
|
+
|
|
165
|
+
retry_at = dynamic_risk_control_until.get(uid)
|
|
166
|
+
if retry_at is not None:
|
|
167
|
+
if retry_at > monotonic():
|
|
168
|
+
logger.debug(f"动态接口风控冷却中,跳过 {name}({uid})")
|
|
169
|
+
await throttle_dynamic_loop()
|
|
170
|
+
return
|
|
171
|
+
del dynamic_risk_control_until[uid]
|
|
172
|
+
|
|
173
|
+
logger.debug(f"爬取动态 {name}({uid})")
|
|
174
|
+
use_web_fallback = False
|
|
175
|
+
try:
|
|
176
|
+
dynamics, use_web_fallback = await get_user_dynamics_with_web_fallback(uid)
|
|
177
|
+
except asyncio.CancelledError:
|
|
178
|
+
logger.debug(f"动态轮询任务已取消:{name}({uid})")
|
|
179
|
+
return
|
|
180
|
+
except AioRpcError as e:
|
|
181
|
+
if e.code() == StatusCode.DEADLINE_EXCEEDED:
|
|
182
|
+
logger.error(f"爬取动态超时,将在下个轮询中重试:{e.code()} {e.details()}")
|
|
183
|
+
else:
|
|
184
|
+
logger.error(f"爬取动态失败:{e.code()} {e.details()}")
|
|
185
|
+
await throttle_dynamic_loop()
|
|
186
|
+
return
|
|
187
|
+
except GrpcError as e:
|
|
188
|
+
logger.error(f"爬取动态失败:{e.code} {e.msg}")
|
|
189
|
+
await throttle_dynamic_loop()
|
|
190
|
+
return
|
|
191
|
+
except WebDynamicError as e:
|
|
192
|
+
dynamic_risk_control_until[uid] = monotonic() + RISK_CONTROL_RETRY_SECONDS
|
|
193
|
+
retry_minutes = RISK_CONTROL_RETRY_SECONDS // 60
|
|
194
|
+
logger.warning(
|
|
195
|
+
f"动态 Web 接口获取失败,{name}({uid})将在 "
|
|
196
|
+
f"{retry_minutes} 分钟后重试:{e.code} {e.msg}"
|
|
197
|
+
)
|
|
198
|
+
await throttle_dynamic_loop()
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
dynamic_risk_control_until.pop(uid, None)
|
|
202
|
+
|
|
203
|
+
if not dynamics: # 没发过动态
|
|
204
|
+
if uid in offset and offset[uid] == -1: # 不记录会导致第一次发动态不推送
|
|
205
|
+
offset[uid] = 0
|
|
206
|
+
return
|
|
207
|
+
name = get_dynamic_author_name(dynamics[0], use_web_fallback)
|
|
208
|
+
|
|
209
|
+
if uid not in offset: # 已删除
|
|
210
|
+
return
|
|
211
|
+
elif offset[uid] == -1: # 第一次爬取
|
|
212
|
+
if len(dynamics) == 1: # 只有一条动态
|
|
213
|
+
offset[uid] = get_dynamic_id(dynamics[0], use_web_fallback)
|
|
214
|
+
else: # 第一个可能是置顶动态,但置顶也可能是最新一条,所以取前两条的最大值
|
|
215
|
+
offset[uid] = max(
|
|
216
|
+
get_dynamic_id(dynamics[0], use_web_fallback),
|
|
217
|
+
get_dynamic_id(dynamics[1], use_web_fallback),
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
dynamic = None
|
|
222
|
+
for dynamic in sorted(
|
|
223
|
+
dynamics,
|
|
224
|
+
key=lambda x: get_dynamic_id(x, use_web_fallback), # 动态从旧到新排列
|
|
225
|
+
):
|
|
226
|
+
dynamic_id = get_dynamic_id(dynamic, use_web_fallback)
|
|
227
|
+
dynamic_type = get_dynamic_type(dynamic, use_web_fallback)
|
|
228
|
+
if dynamic_id > offset[uid]:
|
|
229
|
+
logger.info(f"检测到新动态({dynamic_id}):{name}({uid})")
|
|
230
|
+
image, err = await get_dynamic_screenshot(dynamic_id)
|
|
231
|
+
url = f"https://t.bilibili.com/{dynamic_id}"
|
|
232
|
+
if image is None:
|
|
233
|
+
logger.debug(f"动态不存在,已跳过:{url}")
|
|
234
|
+
return
|
|
235
|
+
elif should_skip_dynamic(dynamic_type, use_web_fallback):
|
|
236
|
+
logger.debug(f"无需推送的动态 {dynamic_type},已跳过:{url}")
|
|
237
|
+
offset[uid] = dynamic_id
|
|
238
|
+
return
|
|
239
|
+
message = (
|
|
240
|
+
f"{name} {get_dynamic_type_message(dynamic_type, use_web_fallback)}:\n"
|
|
241
|
+
+ str(f"动态图片可能截图异常:{err}\n" if err else "")
|
|
242
|
+
+ MessageSegment.image(image)
|
|
243
|
+
+ f"\n{url}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
push_list = await db.get_push_list(uid, "dynamic")
|
|
247
|
+
for sets in push_list:
|
|
248
|
+
await safe_send(
|
|
249
|
+
bot_id=sets.bot_id,
|
|
250
|
+
send_type=sets.type,
|
|
251
|
+
type_id=sets.type_id,
|
|
252
|
+
message=message,
|
|
253
|
+
at=bool(sets.at) and plugin_config.bililive_dynamic_at,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
offset[uid] = dynamic_id
|
|
257
|
+
|
|
258
|
+
if dynamic:
|
|
259
|
+
await db.update_user(uid, name)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def dynamic_lisener(event):
|
|
263
|
+
if hasattr(event, "job_id") and event.job_id != "dynamic_sched":
|
|
264
|
+
return
|
|
265
|
+
job = scheduler.get_job("dynamic_sched")
|
|
266
|
+
if not job:
|
|
267
|
+
scheduler.add_job(
|
|
268
|
+
dy_sched, id="dynamic_sched", next_run_time=datetime.now(scheduler.timezone)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if plugin_config.bililive_dynamic_interval == 0:
|
|
273
|
+
scheduler.add_listener(
|
|
274
|
+
dynamic_lisener,
|
|
275
|
+
EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_SCHEDULER_STARTED,
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
scheduler.add_job(
|
|
279
|
+
dy_sched,
|
|
280
|
+
"interval",
|
|
281
|
+
seconds=plugin_config.bililive_dynamic_interval,
|
|
282
|
+
id="dynamic_sched",
|
|
283
|
+
)
|
|
@@ -13,11 +13,20 @@ live_time = {}
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@scheduler.scheduled_job(
|
|
16
|
-
"interval",
|
|
16
|
+
"interval",
|
|
17
|
+
seconds=plugin_config.bililive_live_interval,
|
|
18
|
+
id="live_sched",
|
|
19
|
+
coalesce=True,
|
|
20
|
+
max_instances=1,
|
|
21
|
+
misfire_grace_time=5,
|
|
17
22
|
)
|
|
18
23
|
async def live_sched():
|
|
19
24
|
# sourcery skip: use-fstring-for-concatenation
|
|
20
25
|
"""直播推送"""
|
|
26
|
+
if not await db.wait_until_ready():
|
|
27
|
+
logger.debug("数据库尚未初始化完成,跳过本轮直播推送")
|
|
28
|
+
return
|
|
29
|
+
|
|
21
30
|
uids = await db.get_uid_list("live")
|
|
22
31
|
|
|
23
32
|
if not uids: # 订阅为空
|
|
@@ -304,4 +304,4 @@ PROXIES = {"all://": plugin_config.bililive_proxy}
|
|
|
304
304
|
require("nonebot_plugin_apscheduler")
|
|
305
305
|
from nonebot_plugin_apscheduler import scheduler # noqa
|
|
306
306
|
|
|
307
|
-
from .browser import get_dynamic_screenshot # noqa
|
|
307
|
+
from .browser import get_bilibili_cookies, get_dynamic_screenshot # noqa
|
|
@@ -63,6 +63,15 @@ async def get_browser() -> BrowserContext:
|
|
|
63
63
|
return _browser
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
async def get_bilibili_cookies() -> dict[str, str]:
|
|
67
|
+
browser = await get_browser()
|
|
68
|
+
cookies = await browser.cookies([
|
|
69
|
+
"https://www.bilibili.com/",
|
|
70
|
+
"https://api.bilibili.com/",
|
|
71
|
+
])
|
|
72
|
+
return {cookie["name"]: cookie["value"] for cookie in cookies}
|
|
73
|
+
|
|
74
|
+
|
|
66
75
|
async def get_dynamic_screenshot(
|
|
67
76
|
dynamic_id,
|
|
68
77
|
style=plugin_config.bililive_screenshot_style,
|
|
@@ -27,8 +27,10 @@ with patch("nonebot.get_driver", return_value=DummyDriver()), patch(
|
|
|
27
27
|
compat = import_module("bililive.compat")
|
|
28
28
|
Config = import_module("bililive.config").Config
|
|
29
29
|
core_version = import_module("bililive.version")
|
|
30
|
+
db_module = import_module("bililive.database.db")
|
|
31
|
+
web_dynamic = import_module("bililive.libs.dynamic.web")
|
|
30
32
|
plugin_entry = import_module("nonebot_plugin_bililive")
|
|
31
|
-
DB =
|
|
33
|
+
DB = db_module.DB
|
|
32
34
|
models = import_module("bililive.database.models")
|
|
33
35
|
Group = models.Group
|
|
34
36
|
|
|
@@ -88,7 +90,73 @@ class PluginEntryTests(unittest.TestCase):
|
|
|
88
90
|
self.assertEqual(plugin_entry.__version__, core_version.__version__)
|
|
89
91
|
|
|
90
92
|
|
|
93
|
+
class WebDynamicTests(unittest.TestCase):
|
|
94
|
+
def test_parse_web_dynamic_items_extracts_required_fields(self):
|
|
95
|
+
payload = {
|
|
96
|
+
"data": {
|
|
97
|
+
"items": [
|
|
98
|
+
{
|
|
99
|
+
"id_str": "1190297023030493193",
|
|
100
|
+
"type": "DYNAMIC_TYPE_DRAW",
|
|
101
|
+
"modules": {
|
|
102
|
+
"module_author": {"name": "玻啵莉Polly"},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
items = web_dynamic.parse_web_dynamic_items(payload)
|
|
110
|
+
|
|
111
|
+
self.assertEqual(len(items), 1)
|
|
112
|
+
self.assertEqual(items[0].dynamic_id, 1190297023030493193)
|
|
113
|
+
self.assertEqual(items[0].dynamic_type, "DYNAMIC_TYPE_DRAW")
|
|
114
|
+
self.assertEqual(items[0].author_name, "玻啵莉Polly")
|
|
115
|
+
|
|
116
|
+
def test_parse_web_dynamic_items_skips_invalid_items(self):
|
|
117
|
+
payload = {
|
|
118
|
+
"data": {
|
|
119
|
+
"items": [
|
|
120
|
+
{"id_str": "bad", "type": "DYNAMIC_TYPE_DRAW", "modules": {}},
|
|
121
|
+
{
|
|
122
|
+
"id_str": "1190297023030493193",
|
|
123
|
+
"type": "DYNAMIC_TYPE_DRAW",
|
|
124
|
+
"modules": {"module_author": {}},
|
|
125
|
+
},
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
self.assertEqual(web_dynamic.parse_web_dynamic_items(payload), [])
|
|
131
|
+
|
|
132
|
+
|
|
91
133
|
class DBPermissionTests(unittest.IsolatedAsyncioTestCase):
|
|
134
|
+
async def test_db_init_enables_global_fallback(self):
|
|
135
|
+
with (
|
|
136
|
+
patch.object(db_module.Tortoise, "init", new=AsyncMock()) as init_db,
|
|
137
|
+
patch.object(
|
|
138
|
+
db_module.Tortoise,
|
|
139
|
+
"generate_schemas",
|
|
140
|
+
new=AsyncMock(),
|
|
141
|
+
) as generate_schemas,
|
|
142
|
+
patch.object(DB, "migrate", new=AsyncMock()) as migrate,
|
|
143
|
+
patch.object(DB, "update_uid_list", new=AsyncMock()) as update_uid_list,
|
|
144
|
+
):
|
|
145
|
+
await DB.init()
|
|
146
|
+
|
|
147
|
+
self.assertTrue(init_db.await_args.kwargs["_enable_global_fallback"])
|
|
148
|
+
self.assertTrue(DB._ready)
|
|
149
|
+
generate_schemas.assert_awaited_once()
|
|
150
|
+
migrate.assert_awaited_once()
|
|
151
|
+
update_uid_list.assert_awaited_once()
|
|
152
|
+
|
|
153
|
+
async def test_wait_until_ready_returns_false_before_init(self):
|
|
154
|
+
DB._ready = False
|
|
155
|
+
|
|
156
|
+
ready = await DB.wait_until_ready(timeout=0)
|
|
157
|
+
|
|
158
|
+
self.assertFalse(ready)
|
|
159
|
+
|
|
92
160
|
async def test_set_permission_creates_group_when_missing(self):
|
|
93
161
|
with (
|
|
94
162
|
patch.object(DB, "get_group", new=AsyncMock(return_value=None)),
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
|
|
4
|
-
from apscheduler.events import (
|
|
5
|
-
EVENT_JOB_ERROR,
|
|
6
|
-
EVENT_JOB_EXECUTED,
|
|
7
|
-
EVENT_JOB_MISSED,
|
|
8
|
-
EVENT_SCHEDULER_STARTED,
|
|
9
|
-
)
|
|
10
|
-
from bilireq.exceptions import GrpcError
|
|
11
|
-
from bilireq.grpc.dynamic import grpc_get_user_dynamics
|
|
12
|
-
from bilireq.grpc.protos.bilibili.app.dynamic.v2.dynamic_pb2 import DynamicType
|
|
13
|
-
from grpc import StatusCode
|
|
14
|
-
from grpc.aio import AioRpcError
|
|
15
|
-
from nonebot.adapters.onebot.v11.message import MessageSegment
|
|
16
|
-
from nonebot.log import logger
|
|
17
|
-
|
|
18
|
-
from ...config import plugin_config
|
|
19
|
-
from ...database import DB as db
|
|
20
|
-
from ...database import dynamic_offset as offset
|
|
21
|
-
from ...utils import get_dynamic_screenshot, safe_send, scheduler
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
async def dy_sched():
|
|
25
|
-
"""动态推送"""
|
|
26
|
-
uid = await db.next_uid("dynamic")
|
|
27
|
-
if not uid:
|
|
28
|
-
# 没有订阅先暂停一秒再跳过,不然会导致 CPU 占用过高
|
|
29
|
-
await asyncio.sleep(1)
|
|
30
|
-
return
|
|
31
|
-
user = await db.get_user(uid=uid)
|
|
32
|
-
if user is None:
|
|
33
|
-
logger.warning(f"动态推送跳过异常订阅 UID:{uid}")
|
|
34
|
-
return
|
|
35
|
-
name = user.name
|
|
36
|
-
|
|
37
|
-
logger.debug(f"爬取动态 {name}({uid})")
|
|
38
|
-
try:
|
|
39
|
-
# 获取 UP 最新动态列表
|
|
40
|
-
dynamics = (
|
|
41
|
-
await grpc_get_user_dynamics(
|
|
42
|
-
uid,
|
|
43
|
-
timeout=plugin_config.bililive_dynamic_timeout,
|
|
44
|
-
proxy=plugin_config.bililive_proxy,
|
|
45
|
-
)
|
|
46
|
-
).list
|
|
47
|
-
except AioRpcError as e:
|
|
48
|
-
if e.code() == StatusCode.DEADLINE_EXCEEDED:
|
|
49
|
-
logger.error(f"爬取动态超时,将在下个轮询中重试:{e.code()} {e.details()}")
|
|
50
|
-
else:
|
|
51
|
-
logger.error(f"爬取动态失败:{e.code()} {e.details()}")
|
|
52
|
-
return
|
|
53
|
-
except GrpcError as e:
|
|
54
|
-
logger.error(f"爬取动态失败:{e.code} {e.msg}")
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
if not dynamics: # 没发过动态
|
|
58
|
-
if uid in offset and offset[uid] == -1: # 不记录会导致第一次发动态不推送
|
|
59
|
-
offset[uid] = 0
|
|
60
|
-
return
|
|
61
|
-
# 更新昵称
|
|
62
|
-
name = dynamics[0].modules[0].module_author.author.name
|
|
63
|
-
|
|
64
|
-
if uid not in offset: # 已删除
|
|
65
|
-
return
|
|
66
|
-
elif offset[uid] == -1: # 第一次爬取
|
|
67
|
-
if len(dynamics) == 1: # 只有一条动态
|
|
68
|
-
offset[uid] = int(dynamics[0].extend.dyn_id_str)
|
|
69
|
-
else: # 第一个可能是置顶动态,但置顶也可能是最新一条,所以取前两条的最大值
|
|
70
|
-
offset[uid] = max(
|
|
71
|
-
int(dynamics[0].extend.dyn_id_str), int(dynamics[1].extend.dyn_id_str)
|
|
72
|
-
)
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
dynamic = None
|
|
76
|
-
for dynamic in sorted(
|
|
77
|
-
dynamics,
|
|
78
|
-
key=lambda x: int(x.extend.dyn_id_str), # 动态从旧到新排列
|
|
79
|
-
):
|
|
80
|
-
dynamic_id = int(dynamic.extend.dyn_id_str)
|
|
81
|
-
if dynamic_id > offset[uid]:
|
|
82
|
-
logger.info(f"检测到新动态({dynamic_id}):{name}({uid})")
|
|
83
|
-
image, err = await get_dynamic_screenshot(dynamic_id)
|
|
84
|
-
url = f"https://t.bilibili.com/{dynamic_id}"
|
|
85
|
-
if image is None:
|
|
86
|
-
logger.debug(f"动态不存在,已跳过:{url}")
|
|
87
|
-
return
|
|
88
|
-
elif dynamic.card_type in [
|
|
89
|
-
DynamicType.live_rcmd,
|
|
90
|
-
DynamicType.live,
|
|
91
|
-
DynamicType.ad,
|
|
92
|
-
DynamicType.banner,
|
|
93
|
-
]:
|
|
94
|
-
logger.debug(f"无需推送的动态 {dynamic.card_type},已跳过:{url}")
|
|
95
|
-
offset[uid] = dynamic_id
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
type_msg = {
|
|
99
|
-
0: "发布了新动态",
|
|
100
|
-
DynamicType.forward: "转发了一条动态",
|
|
101
|
-
DynamicType.word: "发布了新文字动态",
|
|
102
|
-
DynamicType.draw: "发布了新图文动态",
|
|
103
|
-
DynamicType.av: "发布了新投稿",
|
|
104
|
-
DynamicType.article: "发布了新专栏",
|
|
105
|
-
DynamicType.music: "发布了新音频",
|
|
106
|
-
}
|
|
107
|
-
message = (
|
|
108
|
-
f"{name} {type_msg.get(dynamic.card_type, type_msg[0])}:\n"
|
|
109
|
-
+ str(f"动态图片可能截图异常:{err}\n" if err else "")
|
|
110
|
-
+ MessageSegment.image(image)
|
|
111
|
-
+ f"\n{url}"
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
push_list = await db.get_push_list(uid, "dynamic")
|
|
115
|
-
for sets in push_list:
|
|
116
|
-
await safe_send(
|
|
117
|
-
bot_id=sets.bot_id,
|
|
118
|
-
send_type=sets.type,
|
|
119
|
-
type_id=sets.type_id,
|
|
120
|
-
message=message,
|
|
121
|
-
at=bool(sets.at) and plugin_config.bililive_dynamic_at,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
offset[uid] = dynamic_id
|
|
125
|
-
|
|
126
|
-
if dynamic:
|
|
127
|
-
await db.update_user(uid, name)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def dynamic_lisener(event):
|
|
131
|
-
if hasattr(event, "job_id") and event.job_id != "dynamic_sched":
|
|
132
|
-
return
|
|
133
|
-
job = scheduler.get_job("dynamic_sched")
|
|
134
|
-
if not job:
|
|
135
|
-
scheduler.add_job(
|
|
136
|
-
dy_sched, id="dynamic_sched", next_run_time=datetime.now(scheduler.timezone)
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if plugin_config.bililive_dynamic_interval == 0:
|
|
141
|
-
scheduler.add_listener(
|
|
142
|
-
dynamic_lisener,
|
|
143
|
-
EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_SCHEDULER_STARTED,
|
|
144
|
-
)
|
|
145
|
-
else:
|
|
146
|
-
scheduler.add_job(
|
|
147
|
-
dy_sched,
|
|
148
|
-
"interval",
|
|
149
|
-
seconds=plugin_config.bililive_dynamic_interval,
|
|
150
|
-
id="dynamic_sched",
|
|
151
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/database/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/card.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/desc.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/libs/dynamic/display.py
RENAMED
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/at/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/at/at_off.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/at/at_on.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/auto_agree.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/auto_delete.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/dynamic/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/live/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/live/live_now.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/live/live_off.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/live/live_on.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/pusher/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/sub/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/sub/add_sub.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/sub/delete_sub.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/plugins/sub/sub_list.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/utils/captcha_solver.py
RENAMED
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/bililive/utils/fonts_provider.py
RENAMED
|
File without changes
|
|
File without changes
|
{nonebot_plugin_bililive-2.0.2 → nonebot_plugin_bililive-2.0.4}/nonebot_plugin_bililive/__init__.py
RENAMED
|
File without changes
|