nonebot-plugin-shitbot 0.1.1__tar.gz → 0.1.2__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_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/PKG-INFO +1 -1
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/README.md +17 -8
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/aux.py +15 -0
- nonebot_plugin_shitbot-0.1.2/nonebot_plugin_shitbot/commands/autoreply_cmd.py +486 -0
- nonebot_plugin_shitbot-0.1.2/nonebot_plugin_shitbot/commands/autoreply_main_cmd.py +105 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/help_cmd.py +3 -1
- nonebot_plugin_shitbot-0.1.2/nonebot_plugin_shitbot/config.yaml +25 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/handlers.py +4 -1
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/msgutils.py +15 -17
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/tasks.py +2 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/PKG-INFO +1 -1
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/SOURCES.txt +0 -13
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/pyproject.toml +1 -1
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/commands/autoreply_cmd.py +0 -71
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/commands/autoreply_main_cmd.py +0 -67
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/config.yaml +0 -23
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/advrandpic.md +0 -61
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/convert.md +0 -37
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/help.md +0 -17
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/imgs/md2picex1.png +0 -0
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/imgs/md2picex2.png +0 -0
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/index.md +0 -22
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/md2pic.md +0 -57
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/perm.md +0 -110
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/pixiv.md +0 -42
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/randpic.md +0 -34
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/session.md +0 -38
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/shitpost.md +0 -39
- nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs//346/235/203/351/231/220/347/263/273/347/273/237.md +0 -220
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/LICENSE +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/__init__.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/command.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/__init__.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/advrandpic_cmd.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/convert_cmd.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/md2pic_cmd.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/perm_cmd.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/pixiv_cmd.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/randpic_cmd.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/session_cmd.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/shitpost_cmd.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/config.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/css/github-markdown-dark-dimmed.css +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/default_config.yaml +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/parser.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/permissions.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/scripts/p2png.sh +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/scripts/png2fr.sh +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/scripts/png2v.sh +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/session.py +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/dependency_links.txt +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/requires.txt +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/top_level.txt +0 -0
- {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Shitbot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
这项目目前是我自用的基于Napcat端的QQ机器人插件,正在逐步完善中
|
|
4
4
|
|
|
5
5
|
## 已实现的功能
|
|
6
6
|
|
|
@@ -20,21 +20,30 @@
|
|
|
20
20
|
|
|
21
21
|
## TODO List(大致按优先级排序 但随缘想先解决哪个就解决哪个)
|
|
22
22
|
|
|
23
|
-
-
|
|
23
|
+
- B站视频解析并下载(支持链接块或者BV号)
|
|
24
24
|
|
|
25
25
|
- 添加单元测试
|
|
26
26
|
|
|
27
|
-
- B站视频解析并下载(支持链接块或者BV号)
|
|
28
|
-
|
|
29
27
|
- 实现 /stop 用于终止当前会话
|
|
30
28
|
|
|
31
29
|
- 为代码添加更多注释 并按照nonebot所说的插件规范修改代码(?)
|
|
32
30
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
31
|
+
- 手搓数据库 用于自动回复系统以及搬史系统
|
|
32
|
+
- 数据库由两部分组成: messages以及medias
|
|
33
|
+
- messages用于存储信息 通过将 Message 转化为 DumpedMsg 后以 yaml 格式存储
|
|
34
|
+
- 文件名为该文件的sha256sum值 通过比较文件名去重
|
|
35
|
+
- 维护一个映射表(的yaml文件) 用于记录信息的引用数(由数据库用户维护) 通常若元信息的引用数为0 则删除元信息
|
|
36
|
+
- medias用于messages中信息中可能存在的多媒体文件
|
|
37
|
+
- 文件名为该文件的sha256sum值 通过比较文件名去重
|
|
38
|
+
- 维护一个映射表(的yaml文件) 用于记录文件的引用数(只会被messages中的信息引用 因此仅由数据库本身维护) 通常若元信息的引用数为0 则删除元信息
|
|
39
|
+
|
|
40
|
+
- 基于上一点重构 /autoreply
|
|
41
|
+
|
|
42
|
+
- 智能化/shitpost
|
|
43
|
+
- 使用用上面提到的数据库
|
|
44
|
+
- 添加一个元信息映射表(yaml) 类型大概是Dict[str, Dict[str, Any]] 键是史的文件名 也就是哈希值 值中保存着史的:real_id列表 时间戳 好评数 发送失败次数 存储着已经发送过该史的群号的列表
|
|
36
45
|
|
|
37
|
-
-
|
|
46
|
+
- 维护一个yaml文件用于记录各个real_id对应的史的哈希值
|
|
38
47
|
|
|
39
48
|
- 根据以上信息我瞎编了一个可能有用的发史优先级公式:
|
|
40
49
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import shutil
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Any
|
|
@@ -50,6 +51,20 @@ async def rm_cache(group_id: str, user_id: str, pid: str):
|
|
|
50
51
|
logger.info(f"已删除缓存: {cache_path}")
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def rename_file_to_sha256(file_path: Path | str):
|
|
55
|
+
file = Path(file_path) if isinstance(file_path, str) else file_path
|
|
56
|
+
if not file.is_file():
|
|
57
|
+
raise FileNotFoundError(f"No such file or not a regular file: {file}")
|
|
58
|
+
# Compute sha256sum
|
|
59
|
+
with file.open("rb") as f:
|
|
60
|
+
hash_hex = hashlib.file_digest(f, "sha256").hexdigest()
|
|
61
|
+
# Rename
|
|
62
|
+
new_name = hash_hex
|
|
63
|
+
new_path = file.with_name(new_name)
|
|
64
|
+
file.rename(new_path)
|
|
65
|
+
return new_path
|
|
66
|
+
|
|
67
|
+
|
|
53
68
|
async def stuff_download(
|
|
54
69
|
client: httpx.AsyncClient,
|
|
55
70
|
url: str | httpx.URL,
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import uuid
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import yaml
|
|
9
|
+
from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent
|
|
10
|
+
from nonebot.log import logger
|
|
11
|
+
|
|
12
|
+
from ..aux import rename_file_to_sha256, rm_cache, stuff_download
|
|
13
|
+
from ..command import BotCommand
|
|
14
|
+
from ..config import config
|
|
15
|
+
from ..msgutils import (
|
|
16
|
+
DataVariables,
|
|
17
|
+
DumpedMsg,
|
|
18
|
+
DumpedSeg,
|
|
19
|
+
dump_message,
|
|
20
|
+
get_multimedias_url,
|
|
21
|
+
modify_msg_data,
|
|
22
|
+
msg_filter,
|
|
23
|
+
)
|
|
24
|
+
from ..parser import BotArgParser
|
|
25
|
+
from ..session import BotSession
|
|
26
|
+
from ..tasks import EndOfQueue, autoreply_lock, prod_cons
|
|
27
|
+
from .autoreply_main_cmd import BotCommandAutoReplyMain
|
|
28
|
+
|
|
29
|
+
# Will be reconstructed after I implement the database
|
|
30
|
+
class BotCommandAutoreply(BotCommand):
|
|
31
|
+
_name = "autoreply"
|
|
32
|
+
_autoreply_dir = BotCommandAutoReplyMain._autoreply_dir
|
|
33
|
+
_rule_path = BotCommandAutoReplyMain._rule_path
|
|
34
|
+
_msg_dir = BotCommandAutoReplyMain._msg_dir
|
|
35
|
+
_image_dir = BotCommandAutoReplyMain._image_dir
|
|
36
|
+
|
|
37
|
+
def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
|
|
38
|
+
super().__init__(bot, session, _pid=_pid, _internal=_internal)
|
|
39
|
+
self._k: list[str] = []
|
|
40
|
+
self._v: list[str] = []
|
|
41
|
+
self._vstate: str = ""
|
|
42
|
+
self._l: int = 1
|
|
43
|
+
self._sum: list[str] = []
|
|
44
|
+
self._st: list[int] = []
|
|
45
|
+
self._d: list[str] = []
|
|
46
|
+
self._tm: list[str] = []
|
|
47
|
+
self._m: str = "equal"
|
|
48
|
+
|
|
49
|
+
self._prod_lock = asyncio.Lock()
|
|
50
|
+
self._cons_lock = asyncio.Lock()
|
|
51
|
+
self._is_accept_pic = False
|
|
52
|
+
self._urls = asyncio.Queue()
|
|
53
|
+
self._downloads = asyncio.Queue()
|
|
54
|
+
self._pics = asyncio.Queue()
|
|
55
|
+
|
|
56
|
+
self._rule = {}
|
|
57
|
+
self._temp_msg: DumpedMsg = []
|
|
58
|
+
|
|
59
|
+
async def _guard_state(self, new_argv=None):
|
|
60
|
+
if new_argv is None:
|
|
61
|
+
return False
|
|
62
|
+
# If new_argv is not value --start or not value --stop, handle it as normal
|
|
63
|
+
self._parser.parse_argv(new_argv)
|
|
64
|
+
subcmd = self._parser.subcmd
|
|
65
|
+
if subcmd != "value":
|
|
66
|
+
return await super()._guard_state()
|
|
67
|
+
subparser = self._parser.subparsers[subcmd]
|
|
68
|
+
is_stop = subparser.opts_value["--stop"][0]
|
|
69
|
+
is_start = subparser.opts_value["--start"][0] if not is_stop else 0
|
|
70
|
+
if not is_start and not is_stop:
|
|
71
|
+
return await super()._guard_state()
|
|
72
|
+
|
|
73
|
+
# Handle the case where new_argv is value --start
|
|
74
|
+
if self._argv is not None and is_start:
|
|
75
|
+
return await super()._guard_state()
|
|
76
|
+
if self._argv is None and is_start:
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
# Handle the case where new_argv is value --stop and self._argv is None
|
|
80
|
+
if self._argv is None and is_stop:
|
|
81
|
+
tip = "错误:会话未开始\n"
|
|
82
|
+
tip += "尚未开始收集自动信息,请先使用 /autoreply value --start 开始收集。"
|
|
83
|
+
await self.send_msg(tip)
|
|
84
|
+
self.unlock()
|
|
85
|
+
return False
|
|
86
|
+
if self._argv is None:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# Now handle the case where new_argv is value --stop and self._argv is not None
|
|
90
|
+
if self._vstate != "start":
|
|
91
|
+
return await super()._guard_state()
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def _init_parser(self):
|
|
95
|
+
parser = BotArgParser()
|
|
96
|
+
parser.set_rule(max=0, need_subcmd=True)
|
|
97
|
+
|
|
98
|
+
start = parser.add_subparser("start")
|
|
99
|
+
start.set_rule(max=0)
|
|
100
|
+
|
|
101
|
+
stop = parser.add_subparser("stop")
|
|
102
|
+
stop.set_rule(max=0)
|
|
103
|
+
|
|
104
|
+
create = parser.add_subparser("create")
|
|
105
|
+
create.set_rule(max=0)
|
|
106
|
+
create.add_opt("-k", required=True, max_appeared=None)
|
|
107
|
+
create.add_opt("-v", required=True, max_appeared=None)
|
|
108
|
+
|
|
109
|
+
delete = parser.add_subparser("delete")
|
|
110
|
+
delete.set_rule(max=0)
|
|
111
|
+
delete.add_opt("-k", required=True, max_appeared=None)
|
|
112
|
+
delete.add_opt("-v", required=True, max_appeared=None)
|
|
113
|
+
|
|
114
|
+
value = parser.add_subparser("value")
|
|
115
|
+
value.set_rule(min=1, max=1)
|
|
116
|
+
value.add_opt("--stop", default=[0])
|
|
117
|
+
value.add_opt("--start", default=[0])
|
|
118
|
+
value.add_opt("-l", required=True, type=int, default=[1])
|
|
119
|
+
value.add_opt("--sum", required=True, max_appeared=None)
|
|
120
|
+
value.add_opt("--st", required=True, type=int, max_appeared=None)
|
|
121
|
+
|
|
122
|
+
key = parser.add_subparser("key")
|
|
123
|
+
key.set_rule(min=1, max=1)
|
|
124
|
+
key.add_opt(
|
|
125
|
+
"--tm",
|
|
126
|
+
required=True,
|
|
127
|
+
choice=["delspace", "delmarks", "uppercase", "lowercase"],
|
|
128
|
+
max_appeared=None,
|
|
129
|
+
default=[],
|
|
130
|
+
)
|
|
131
|
+
key.add_opt("-d", required=True, max_appeared=None, default=[])
|
|
132
|
+
key.add_opt("-m", required=True, choice=["equal", "contain"], default=["equal"])
|
|
133
|
+
key.add_opt("-v", required=True, max_appeared=None, default=[])
|
|
134
|
+
|
|
135
|
+
return parser
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _invert(flag: bool, exp: bool):
|
|
139
|
+
if flag:
|
|
140
|
+
return not exp
|
|
141
|
+
return exp
|
|
142
|
+
|
|
143
|
+
def _load_rule(self):
|
|
144
|
+
_rule = yaml.safe_load(self._rule_path.read_text(encoding="utf-8"))
|
|
145
|
+
if _rule is not None and not isinstance(_rule, dict):
|
|
146
|
+
raise ValueError
|
|
147
|
+
self._rule = _rule if _rule is not None else {}
|
|
148
|
+
|
|
149
|
+
def _update_rule(self):
|
|
150
|
+
with self._rule_path.open("w", encoding="utf-8") as f:
|
|
151
|
+
yaml.safe_dump(self._rule, f, allow_unicode=True)
|
|
152
|
+
arsession = BotSession.get_obj("public", "autoreply")
|
|
153
|
+
if arsession is None:
|
|
154
|
+
return
|
|
155
|
+
main_command = arsession.commands.get(arsession.curpid)
|
|
156
|
+
if main_command is None or not isinstance(
|
|
157
|
+
main_command, BotCommandAutoReplyMain
|
|
158
|
+
):
|
|
159
|
+
return
|
|
160
|
+
main_command.load_rule()
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
async def _url_to_download(
|
|
164
|
+
download_url: str, client: httpx.AsyncClient, download_dir: Path
|
|
165
|
+
):
|
|
166
|
+
try:
|
|
167
|
+
filename = f"{uuid.uuid4().hex}"
|
|
168
|
+
save_path = download_dir / filename
|
|
169
|
+
await stuff_download(client, download_url, save_path)
|
|
170
|
+
logger.info(f"下载图片成功: {save_path}")
|
|
171
|
+
return str(save_path)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"下载图片失败 {download_url}: {e}")
|
|
174
|
+
return ""
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
async def _download_to_pic(download_path: str, pic_dir: Path):
|
|
178
|
+
if download_path == "":
|
|
179
|
+
return ""
|
|
180
|
+
|
|
181
|
+
rename_path = rename_file_to_sha256(download_path)
|
|
182
|
+
pic_path = pic_dir / rename_path.name
|
|
183
|
+
async with autoreply_lock:
|
|
184
|
+
if not pic_path.exists():
|
|
185
|
+
rename_path.rename(pic_path)
|
|
186
|
+
return str(pic_path)
|
|
187
|
+
|
|
188
|
+
async def _start(self):
|
|
189
|
+
autoreply_session = BotSession.make("public", "autoreply")
|
|
190
|
+
main_command = BotCommandAutoReplyMain.make(
|
|
191
|
+
self.bot, autoreply_session, _pid=autoreply_session.curpid
|
|
192
|
+
)
|
|
193
|
+
if main_command is None:
|
|
194
|
+
await self.send_msg("警告: 自动回复正在运行中")
|
|
195
|
+
await self.send_msg("自动回复已开启")
|
|
196
|
+
self.unlock()
|
|
197
|
+
|
|
198
|
+
async def _stop(self):
|
|
199
|
+
async def _exe():
|
|
200
|
+
autoreply_session = BotSession.get_obj("public", "autoreply")
|
|
201
|
+
if autoreply_session is None:
|
|
202
|
+
await self.send_msg("警告: 自动回复未开启")
|
|
203
|
+
return
|
|
204
|
+
main_command = autoreply_session.commands.get(autoreply_session.curpid)
|
|
205
|
+
if main_command is None:
|
|
206
|
+
await self.send_msg("警告: 自动回复未开启")
|
|
207
|
+
return
|
|
208
|
+
main_command.unlock()
|
|
209
|
+
|
|
210
|
+
await _exe()
|
|
211
|
+
await self.send_msg("自动回复已关闭")
|
|
212
|
+
self.unlock()
|
|
213
|
+
|
|
214
|
+
async def _create_delete(self, mode):
|
|
215
|
+
if mode not in ["create", "delete"]:
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
verb = "创建" if mode == "create" else "删除"
|
|
219
|
+
flag = not mode == "create"
|
|
220
|
+
ptcl = "已" if mode == "create" else "不"
|
|
221
|
+
|
|
222
|
+
def _op1(flag: bool, dic: dict, key: str):
|
|
223
|
+
if flag:
|
|
224
|
+
return dic.pop(key)
|
|
225
|
+
dic[key] = [[], [], False, []]
|
|
226
|
+
|
|
227
|
+
def _op2(flag: bool, path: Path):
|
|
228
|
+
if flag:
|
|
229
|
+
return path.unlink()
|
|
230
|
+
path.touch(exist_ok=True)
|
|
231
|
+
|
|
232
|
+
async with autoreply_lock:
|
|
233
|
+
self._load_rule()
|
|
234
|
+
is_edited = False
|
|
235
|
+
for key in self._k:
|
|
236
|
+
if self._invert(flag, key in self._rule):
|
|
237
|
+
await self.send_msg(f"警告: {key} {ptcl}存在")
|
|
238
|
+
continue
|
|
239
|
+
_op1(flag, self._rule, key)
|
|
240
|
+
is_edited = True
|
|
241
|
+
if is_edited:
|
|
242
|
+
self._update_rule()
|
|
243
|
+
for value in self._v:
|
|
244
|
+
value_path = self._msg_dir / f"{value}.yaml"
|
|
245
|
+
if self._invert(flag, value_path.exists()):
|
|
246
|
+
await self.send_msg(f"警告: {value} {ptcl}存在")
|
|
247
|
+
continue
|
|
248
|
+
_op2(flag, value_path)
|
|
249
|
+
await self.send_msg(f"{verb}成功")
|
|
250
|
+
self.unlock()
|
|
251
|
+
|
|
252
|
+
async def _value_start(self, value_name: str):
|
|
253
|
+
if self._session is None:
|
|
254
|
+
return
|
|
255
|
+
value_path = self._msg_dir / f"{value_name}.yaml"
|
|
256
|
+
if not value_path.exists():
|
|
257
|
+
await self.send_msg(f"错误: {value_name} 不存在")
|
|
258
|
+
self.unlock()
|
|
259
|
+
return
|
|
260
|
+
downloads_dir = (
|
|
261
|
+
config.cache
|
|
262
|
+
/ self._session.group_id
|
|
263
|
+
/ self._session.user_id
|
|
264
|
+
/ str(self._pid)
|
|
265
|
+
/ "autoreply"
|
|
266
|
+
)
|
|
267
|
+
downloads_dir.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
|
|
269
|
+
async def _urls_to_downloads():
|
|
270
|
+
try:
|
|
271
|
+
async with httpx.AsyncClient() as client:
|
|
272
|
+
await prod_cons(
|
|
273
|
+
self._urls,
|
|
274
|
+
self._downloads,
|
|
275
|
+
self._url_to_download,
|
|
276
|
+
client,
|
|
277
|
+
downloads_dir,
|
|
278
|
+
)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.exception(f"urls_to_downloads 管道异常退出: {e}")
|
|
281
|
+
|
|
282
|
+
async def _downloads_to_pics():
|
|
283
|
+
try:
|
|
284
|
+
async with self._cons_lock:
|
|
285
|
+
await prod_cons(
|
|
286
|
+
self._downloads,
|
|
287
|
+
self._pics,
|
|
288
|
+
self._download_to_pic,
|
|
289
|
+
self._image_dir,
|
|
290
|
+
)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.exception(f"downloads_to_pics 管道异常退出: {e}")
|
|
293
|
+
|
|
294
|
+
asyncio.create_task(_urls_to_downloads())
|
|
295
|
+
asyncio.create_task(_downloads_to_pics())
|
|
296
|
+
|
|
297
|
+
logger.info(f"用户 {self._session.user_id} 开始为 {value_name} 设置具体信息")
|
|
298
|
+
await self.send_msg(
|
|
299
|
+
f"请发送仅包含图片或文字的信息, 或者发送 /autoreply value --stop {value_name} 停止设置"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self._is_accept_pic = True
|
|
303
|
+
self._value_name = value_name
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
async def _value_stop(self, value_name: str):
|
|
307
|
+
if not self.session:
|
|
308
|
+
return
|
|
309
|
+
self._is_accept_pic = False
|
|
310
|
+
|
|
311
|
+
async with self._prod_lock:
|
|
312
|
+
await self._urls.put(EndOfQueue())
|
|
313
|
+
async with self._cons_lock:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
pics = []
|
|
317
|
+
while True:
|
|
318
|
+
pic = await self._pics.get()
|
|
319
|
+
if isinstance(pic, EndOfQueue):
|
|
320
|
+
break
|
|
321
|
+
pics.append(pic)
|
|
322
|
+
|
|
323
|
+
self._temp_msg = modify_msg_data(
|
|
324
|
+
self._temp_msg,
|
|
325
|
+
{
|
|
326
|
+
"file": DataVariables([f"{Path(path).name}" for path in pics]),
|
|
327
|
+
"summary": DataVariables(self._sum),
|
|
328
|
+
"sub_type": DataVariables(self._st),
|
|
329
|
+
},
|
|
330
|
+
["image"],
|
|
331
|
+
replace=True,
|
|
332
|
+
)
|
|
333
|
+
if self._temp_msg == []:
|
|
334
|
+
await self.send_msg("错误: 未受到任何信息")
|
|
335
|
+
await rm_cache(self.session.group_id, self.session.user_id, str(self._pid))
|
|
336
|
+
self.unlock()
|
|
337
|
+
return
|
|
338
|
+
async with autoreply_lock:
|
|
339
|
+
value_path = self._msg_dir / f"{value_name}.yaml"
|
|
340
|
+
value_path.write_text(yaml.safe_dump(self._temp_msg))
|
|
341
|
+
|
|
342
|
+
await self.send_msg("设置成功")
|
|
343
|
+
await rm_cache(self.session.group_id, self.session.user_id, str(self._pid))
|
|
344
|
+
self.unlock()
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
async def _value_modify(self, value_name: str):
|
|
348
|
+
if not self.session:
|
|
349
|
+
return
|
|
350
|
+
value_path = self._msg_dir / f"{value_name}.yaml"
|
|
351
|
+
if not value_path.exists():
|
|
352
|
+
await self.send_msg(f"错误: {value_name} 不存在")
|
|
353
|
+
self.unlock()
|
|
354
|
+
return
|
|
355
|
+
async with autoreply_lock:
|
|
356
|
+
self._temp_msg = yaml.safe_load(value_path.read_text(encoding="utf-8"))
|
|
357
|
+
self._temp_msg = modify_msg_data(
|
|
358
|
+
self._temp_msg,
|
|
359
|
+
{
|
|
360
|
+
"summary": DataVariables(self._sum),
|
|
361
|
+
"sub_type": DataVariables(self._st),
|
|
362
|
+
},
|
|
363
|
+
["image"],
|
|
364
|
+
replace=False,
|
|
365
|
+
)
|
|
366
|
+
value_path.write_text(yaml.safe_dump(self._temp_msg))
|
|
367
|
+
await self.send_msg("修改成功")
|
|
368
|
+
self.unlock()
|
|
369
|
+
|
|
370
|
+
async def _key(self, key_name: str):
|
|
371
|
+
if not self.session:
|
|
372
|
+
return
|
|
373
|
+
key = key_name
|
|
374
|
+
async with autoreply_lock:
|
|
375
|
+
self._load_rule()
|
|
376
|
+
if key not in self._rule:
|
|
377
|
+
await self.send_msg(f"错误: {key} 不存在")
|
|
378
|
+
self.unlock()
|
|
379
|
+
return
|
|
380
|
+
self._rule[key][0] = self._tm[:]
|
|
381
|
+
self._rule[key][1] = self._d[:]
|
|
382
|
+
self._rule[key][2] = self._m == "contain"
|
|
383
|
+
self._rule[key][3] = self._v[:]
|
|
384
|
+
self._update_rule()
|
|
385
|
+
await self.send_msg("设置成功")
|
|
386
|
+
self.unlock()
|
|
387
|
+
|
|
388
|
+
async def roger(self, event: MessageEvent):
|
|
389
|
+
async with self._prod_lock:
|
|
390
|
+
if not self._is_accept_pic:
|
|
391
|
+
return
|
|
392
|
+
if self._l <= 0:
|
|
393
|
+
return
|
|
394
|
+
msg = await dump_message(self.bot, event.get_message())
|
|
395
|
+
|
|
396
|
+
def _filter(seg: DumpedSeg) -> bool:
|
|
397
|
+
if seg["type"] != "text" and seg["type"] != "image":
|
|
398
|
+
return False
|
|
399
|
+
if seg["type"] == "text" and seg["data"].get("text") is None:
|
|
400
|
+
return False
|
|
401
|
+
if seg["type"] == "image" and seg["data"].get("url") is None:
|
|
402
|
+
return False
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
msg = msg_filter(_filter, msg)
|
|
406
|
+
url_list = get_multimedias_url(msg, basetypes=["image"])
|
|
407
|
+
for url in url_list:
|
|
408
|
+
await self._urls.put(url)
|
|
409
|
+
self._temp_msg.extend(msg)
|
|
410
|
+
self._l -= 1
|
|
411
|
+
if self._l <= 0:
|
|
412
|
+
value_name = getattr(self, "_value_name", None)
|
|
413
|
+
if value_name is None:
|
|
414
|
+
raise ValueError
|
|
415
|
+
asyncio.create_task(self.run(Message(f"value --stop {value_name}")))
|
|
416
|
+
|
|
417
|
+
async def run(self, args: Message):
|
|
418
|
+
if not self.session:
|
|
419
|
+
return
|
|
420
|
+
new_argv = args.extract_plain_text().strip().split()
|
|
421
|
+
|
|
422
|
+
if not await self._legal_case(new_argv):
|
|
423
|
+
if self._argv is None:
|
|
424
|
+
self.unlock()
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
if not await self._guard_state(new_argv):
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
self._argv = new_argv
|
|
431
|
+
self._parser.parse_argv(self._argv)
|
|
432
|
+
subcmd = self._parser.subcmd
|
|
433
|
+
if subcmd is None:
|
|
434
|
+
self.unlock()
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
if not self._check_perm("autoreplymanager"):
|
|
438
|
+
await self.send_msg("权限不足")
|
|
439
|
+
self.unlock()
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
subparser = self._parser.subparsers[subcmd]
|
|
443
|
+
self._k = subparser.opts_value.get("-k", [])
|
|
444
|
+
self._v = subparser.opts_value.get("-v", [])
|
|
445
|
+
if subparser.opts_value.get("--start", [0])[0]:
|
|
446
|
+
self._vstate = "start"
|
|
447
|
+
if subparser.opts_value.get("--stop", [0])[0]:
|
|
448
|
+
self._vstate = "stop"
|
|
449
|
+
_sum = subparser.opts_value.get("--sum", [])
|
|
450
|
+
_st = subparser.opts_value.get("--st", [])
|
|
451
|
+
if _sum or self._vstate != "stop":
|
|
452
|
+
self._sum = _sum
|
|
453
|
+
if _st or self._vstate != "stop":
|
|
454
|
+
self._st = _st
|
|
455
|
+
self._l = subparser.opts_value.get("-l", [1])[0]
|
|
456
|
+
self._d = subparser.opts_value.get("-d", [])
|
|
457
|
+
self._tm = subparser.opts_value.get("--tm", [])
|
|
458
|
+
self._m = subparser.opts_value.get("-m", ["equal"])[0]
|
|
459
|
+
|
|
460
|
+
if subcmd == "start":
|
|
461
|
+
await self._start()
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
if subcmd == "stop":
|
|
465
|
+
await self._stop()
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
if subcmd in ["create", "delete"]:
|
|
469
|
+
await self._create_delete(subcmd)
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
if subcmd == "value":
|
|
473
|
+
value_name = subparser.value[0]
|
|
474
|
+
if self._vstate == "start":
|
|
475
|
+
await self._value_start(value_name)
|
|
476
|
+
return
|
|
477
|
+
if self._vstate == "stop":
|
|
478
|
+
await self._value_stop(value_name)
|
|
479
|
+
return
|
|
480
|
+
await self._value_modify(value_name)
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
if subcmd == "key":
|
|
484
|
+
key_name = subparser.value[0]
|
|
485
|
+
await self._key(key_name)
|
|
486
|
+
return
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent
|
|
9
|
+
|
|
10
|
+
from ..aux import validate_schema
|
|
11
|
+
from ..command import BotCommand
|
|
12
|
+
from ..config import config
|
|
13
|
+
from ..msgutils import MSG_SCHEMA, DumpedSeg, msg_map, undump_message
|
|
14
|
+
from ..tasks import autoreply_lock
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..session import BotSession
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_LIST_SCHEMA = [[str], [str], bool, [str], None]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Simple auto reply, reconstructing.
|
|
24
|
+
class BotCommandAutoReplyMain(BotCommand):
|
|
25
|
+
_name = "autoreply_main"
|
|
26
|
+
_autoreply_dir = config.data / "autoreply"
|
|
27
|
+
_rule_path = _autoreply_dir / "rule.yaml"
|
|
28
|
+
_msg_dir = _autoreply_dir / "messages"
|
|
29
|
+
_image_dir = _autoreply_dir / "images"
|
|
30
|
+
_contain_image_dir = config.client_base / _image_dir.relative_to(config.bot_base)
|
|
31
|
+
_rule: dict[str, list[Any]]
|
|
32
|
+
|
|
33
|
+
def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
|
|
34
|
+
super().__init__(bot, session, _pid=_pid, _internal=_internal)
|
|
35
|
+
self._rule = {}
|
|
36
|
+
|
|
37
|
+
def load_rule(self):
|
|
38
|
+
_rule = yaml.safe_load(self._rule_path.read_text(encoding="utf-8"))
|
|
39
|
+
if _rule is not None and not isinstance(_rule, dict):
|
|
40
|
+
raise ValueError
|
|
41
|
+
self._rule = _rule if _rule is not None else {}
|
|
42
|
+
|
|
43
|
+
async def roger(self, event: MessageEvent):
|
|
44
|
+
group_id = str(getattr(event, "group_id", "private"))
|
|
45
|
+
if event.message_type == "private":
|
|
46
|
+
group_id = "private"
|
|
47
|
+
user_id = str(event.user_id)
|
|
48
|
+
|
|
49
|
+
rule_keys = self._rule.keys()
|
|
50
|
+
raw = event.get_message().extract_plain_text()
|
|
51
|
+
|
|
52
|
+
for key in rule_keys:
|
|
53
|
+
if len(self._rule[key]) < 4 or not validate_schema(
|
|
54
|
+
self._rule[key], _LIST_SCHEMA
|
|
55
|
+
):
|
|
56
|
+
continue
|
|
57
|
+
text = raw
|
|
58
|
+
for trans in self._rule[key][0]:
|
|
59
|
+
if trans == "delspace":
|
|
60
|
+
text = text.replace(" ", "")
|
|
61
|
+
if trans == "delmarks":
|
|
62
|
+
text = re.sub(r"[^\w\s]", "", text)
|
|
63
|
+
if trans == "uppercase":
|
|
64
|
+
text = text.upper()
|
|
65
|
+
if trans == "lowercase":
|
|
66
|
+
text = text.lower()
|
|
67
|
+
for substr in self._rule[key][1]:
|
|
68
|
+
text = text.replace(substr, "")
|
|
69
|
+
if not (
|
|
70
|
+
(self._rule[key][2] and key in text)
|
|
71
|
+
or (not self._rule[key][2] and key == text)
|
|
72
|
+
):
|
|
73
|
+
continue
|
|
74
|
+
if len(self._rule[key][3]) == 0:
|
|
75
|
+
continue
|
|
76
|
+
index = random.randint(0, len(self._rule[key][3]) - 1)
|
|
77
|
+
msg_name = self._rule[key][3][index]
|
|
78
|
+
msg_path = self._msg_dir / f"{msg_name}.yaml"
|
|
79
|
+
if not msg_path.exists():
|
|
80
|
+
continue
|
|
81
|
+
dumped_msg = yaml.safe_load(msg_path.read_text(encoding="utf-8"))
|
|
82
|
+
if not validate_schema(dumped_msg, MSG_SCHEMA):
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
def _map(seg: DumpedSeg) -> DumpedSeg:
|
|
86
|
+
if seg["type"] != "image":
|
|
87
|
+
return seg
|
|
88
|
+
seg["data"]["file"] = (
|
|
89
|
+
f"file://{self._contain_image_dir / seg['data']['file']!s}"
|
|
90
|
+
)
|
|
91
|
+
return seg
|
|
92
|
+
|
|
93
|
+
dumped_msg = msg_map(_map, dumped_msg)
|
|
94
|
+
|
|
95
|
+
msg = undump_message(dumped_msg)
|
|
96
|
+
await self.send_msg(msg, group_id=group_id, user_id=user_id)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
async def run(self, args: Message):
|
|
100
|
+
async with autoreply_lock:
|
|
101
|
+
self._autoreply_dir.mkdir(exist_ok=True, parents=True)
|
|
102
|
+
self._image_dir.mkdir(exist_ok=True)
|
|
103
|
+
self._msg_dir.mkdir(exist_ok=True)
|
|
104
|
+
self._rule_path.touch(exist_ok=True)
|
|
105
|
+
self.load_rule()
|
|
@@ -31,6 +31,7 @@ class BotCommandHelp(BotCommand):
|
|
|
31
31
|
parser.add_subparser("advrandpic")
|
|
32
32
|
parser.add_subparser("md2pic")
|
|
33
33
|
parser.add_subparser("pixiv")
|
|
34
|
+
parser.add_subparser("autoreply")
|
|
34
35
|
return parser
|
|
35
36
|
|
|
36
37
|
async def run(self, args: Message):
|
|
@@ -52,7 +53,7 @@ class BotCommandHelp(BotCommand):
|
|
|
52
53
|
self.unlock()
|
|
53
54
|
return
|
|
54
55
|
|
|
55
|
-
help_dir = Path(__file__).resolve().parent.parent / "
|
|
56
|
+
help_dir = Path(__file__).resolve().parent.parent / "help"
|
|
56
57
|
tip = "错误: 帮助文档目录不存在"
|
|
57
58
|
if help_dir.exists():
|
|
58
59
|
tip = "错误: 帮助文档 index.md 不存在"
|
|
@@ -69,6 +70,7 @@ class BotCommandHelp(BotCommand):
|
|
|
69
70
|
"advrandpic",
|
|
70
71
|
"md2pic",
|
|
71
72
|
"pixiv",
|
|
73
|
+
"autoreply",
|
|
72
74
|
):
|
|
73
75
|
tip = f"错误: 帮助文档 {subcmd}.md 不存在"
|
|
74
76
|
help_path = help_dir / f"{subcmd}.md"
|