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.
Files changed (54) hide show
  1. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/PKG-INFO +1 -1
  2. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/README.md +17 -8
  3. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/aux.py +15 -0
  4. nonebot_plugin_shitbot-0.1.2/nonebot_plugin_shitbot/commands/autoreply_cmd.py +486 -0
  5. nonebot_plugin_shitbot-0.1.2/nonebot_plugin_shitbot/commands/autoreply_main_cmd.py +105 -0
  6. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/help_cmd.py +3 -1
  7. nonebot_plugin_shitbot-0.1.2/nonebot_plugin_shitbot/config.yaml +25 -0
  8. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/handlers.py +4 -1
  9. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/msgutils.py +15 -17
  10. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/tasks.py +2 -0
  11. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/PKG-INFO +1 -1
  12. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/SOURCES.txt +0 -13
  13. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/pyproject.toml +1 -1
  14. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/commands/autoreply_cmd.py +0 -71
  15. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/commands/autoreply_main_cmd.py +0 -67
  16. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/config.yaml +0 -23
  17. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/advrandpic.md +0 -61
  18. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/convert.md +0 -37
  19. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/help.md +0 -17
  20. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/imgs/md2picex1.png +0 -0
  21. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/imgs/md2picex2.png +0 -0
  22. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/index.md +0 -22
  23. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/md2pic.md +0 -57
  24. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/perm.md +0 -110
  25. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/pixiv.md +0 -42
  26. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/randpic.md +0 -34
  27. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/session.md +0 -38
  28. nonebot_plugin_shitbot-0.1.1/nonebot_plugin_shitbot/docs/help/shitpost.md +0 -39
  29. 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
  30. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/LICENSE +0 -0
  31. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/__init__.py +0 -0
  32. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/command.py +0 -0
  33. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/__init__.py +0 -0
  34. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/advrandpic_cmd.py +0 -0
  35. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/convert_cmd.py +0 -0
  36. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/md2pic_cmd.py +0 -0
  37. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/perm_cmd.py +0 -0
  38. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/pixiv_cmd.py +0 -0
  39. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/randpic_cmd.py +0 -0
  40. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/session_cmd.py +0 -0
  41. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/commands/shitpost_cmd.py +0 -0
  42. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/config.py +0 -0
  43. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/css/github-markdown-dark-dimmed.css +0 -0
  44. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/default_config.yaml +0 -0
  45. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/parser.py +0 -0
  46. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/permissions.py +0 -0
  47. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/scripts/p2png.sh +0 -0
  48. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/scripts/png2fr.sh +0 -0
  49. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/scripts/png2v.sh +0 -0
  50. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot/session.py +0 -0
  51. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/dependency_links.txt +0 -0
  52. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/requires.txt +0 -0
  53. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/nonebot_plugin_shitbot.egg-info/top_level.txt +0 -0
  54. {nonebot_plugin_shitbot-0.1.1 → nonebot_plugin_shitbot-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nonebot-plugin-shitbot
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: NoneBot shitpost plugin
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  # Shitbot
2
2
 
3
- 这项目目前是我自用的QQ机器人插件,正在逐步完善中
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
- - 给史搭一个数据库存起来管理 智能化/shitpost
34
- - 提一嘴我打算怎么搭建数据库吧 将史的message键保存为json 如果是forward类型的则将其转换为node列表后打包为{"type": forward, "data": {"content": [node]}} 将json中提及的富媒体信息全部下载后重命名为其0xsha256sum值保存 并将json富媒体信息的url替换为对应的本地路径 最后将json重命名为其0xsha256sum值 这样无论是富媒体还是代表史的json文件都可以通过计算哈希值与已有的文件名比对来去重
35
- - 添加一个记录json文件元数据的json文件(这算不算元元数据?)类型大概是Dict[str, Dict[str, Any]] 键是史的文件名 也就是哈希值 值中保存着史的:real_id列表 时间戳 好评数 发送失败次数 存储着已经发送过该史的群号的列表
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
- - 添加一个json文件用于记录各个real_id对应的史的哈希值
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 / "docs" / "help"
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"