nonebot-plugin-shitbot 0.1.0__py3-none-any.whl
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/__init__.py +21 -0
- nonebot_plugin_shitbot/aux.py +71 -0
- nonebot_plugin_shitbot/command.py +154 -0
- nonebot_plugin_shitbot/commands/__init__.py +27 -0
- nonebot_plugin_shitbot/commands/advrandpic_cmd.py +129 -0
- nonebot_plugin_shitbot/commands/autoreply_cmd.py +71 -0
- nonebot_plugin_shitbot/commands/autoreply_main_cmd.py +67 -0
- nonebot_plugin_shitbot/commands/convert_cmd.py +371 -0
- nonebot_plugin_shitbot/commands/help_cmd.py +92 -0
- nonebot_plugin_shitbot/commands/md2pic_cmd.py +195 -0
- nonebot_plugin_shitbot/commands/perm_cmd.py +392 -0
- nonebot_plugin_shitbot/commands/pixiv_cmd.py +144 -0
- nonebot_plugin_shitbot/commands/randpic_cmd.py +75 -0
- nonebot_plugin_shitbot/commands/session_cmd.py +79 -0
- nonebot_plugin_shitbot/commands/shitpost_cmd.py +189 -0
- nonebot_plugin_shitbot/config.py +186 -0
- nonebot_plugin_shitbot/config.yaml +23 -0
- nonebot_plugin_shitbot/css/github-markdown-dark-dimmed.css +1220 -0
- nonebot_plugin_shitbot/default_config.yaml +23 -0
- nonebot_plugin_shitbot/docs/help/advrandpic.md +61 -0
- nonebot_plugin_shitbot/docs/help/convert.md +37 -0
- nonebot_plugin_shitbot/docs/help/help.md +17 -0
- nonebot_plugin_shitbot/docs/help/imgs/md2picex1.png +0 -0
- nonebot_plugin_shitbot/docs/help/imgs/md2picex2.png +0 -0
- nonebot_plugin_shitbot/docs/help/index.md +22 -0
- nonebot_plugin_shitbot/docs/help/md2pic.md +57 -0
- nonebot_plugin_shitbot/docs/help/perm.md +110 -0
- nonebot_plugin_shitbot/docs/help/pixiv.md +42 -0
- nonebot_plugin_shitbot/docs/help/randpic.md +34 -0
- nonebot_plugin_shitbot/docs/help/session.md +38 -0
- nonebot_plugin_shitbot/docs/help/shitpost.md +39 -0
- nonebot_plugin_shitbot/docs//346/235/203/351/231/220/347/263/273/347/273/237.md +220 -0
- nonebot_plugin_shitbot/handlers.py +150 -0
- nonebot_plugin_shitbot/msgutils.py +442 -0
- nonebot_plugin_shitbot/parser.py +284 -0
- nonebot_plugin_shitbot/permissions.py +198 -0
- nonebot_plugin_shitbot/scripts/p2png.sh +38 -0
- nonebot_plugin_shitbot/scripts/png2fr.sh +19 -0
- nonebot_plugin_shitbot/scripts/png2v.sh +24 -0
- nonebot_plugin_shitbot/session.py +76 -0
- nonebot_plugin_shitbot/tasks.py +25 -0
- nonebot_plugin_shitbot-0.1.0.dist-info/METADATA +17 -0
- nonebot_plugin_shitbot-0.1.0.dist-info/RECORD +46 -0
- nonebot_plugin_shitbot-0.1.0.dist-info/WHEEL +5 -0
- nonebot_plugin_shitbot-0.1.0.dist-info/licenses/LICENSE +661 -0
- nonebot_plugin_shitbot-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import shutil
|
|
5
|
+
import uuid
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent, MessageSegment
|
|
11
|
+
from nonebot.log import logger
|
|
12
|
+
|
|
13
|
+
from ..aux import rm_cache, stuff_download
|
|
14
|
+
from ..command import BotCommand
|
|
15
|
+
from ..config import config
|
|
16
|
+
from ..msgutils import dump_message, get_multimedias_url
|
|
17
|
+
from ..parser import BotArgParser
|
|
18
|
+
from ..tasks import EndOfQueue, prod_cons
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..session import BotSession
|
|
22
|
+
|
|
23
|
+
bash = shutil.which("bash") or "/bin/bash"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BotCommandConvert(BotCommand):
|
|
27
|
+
_name = "convert"
|
|
28
|
+
|
|
29
|
+
def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
|
|
30
|
+
super().__init__(bot, session, _pid=_pid, _internal=_internal)
|
|
31
|
+
self._mode = "video"
|
|
32
|
+
self._runlock = asyncio.Lock()
|
|
33
|
+
self._if_accept_pic = False
|
|
34
|
+
self._prod_lock = asyncio.Lock()
|
|
35
|
+
self._convert_lock = asyncio.Lock()
|
|
36
|
+
self._urls: asyncio.Queue[str | EndOfQueue] = asyncio.Queue()
|
|
37
|
+
self._downloads: asyncio.Queue[str | EndOfQueue] = asyncio.Queue()
|
|
38
|
+
self._pngs: asyncio.Queue[str | EndOfQueue] = asyncio.Queue()
|
|
39
|
+
self._outputs: asyncio.Queue[str | EndOfQueue] = asyncio.Queue()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def outputs(self):
|
|
43
|
+
return self._outputs
|
|
44
|
+
|
|
45
|
+
def _init_parser(self):
|
|
46
|
+
parser = BotArgParser()
|
|
47
|
+
start = parser.add_subparser("start")
|
|
48
|
+
stop = parser.add_subparser("stop")
|
|
49
|
+
start.set_rule(max=0)
|
|
50
|
+
start.add_opt("-m", required=True, choice=["video", "frame"], default=["video"])
|
|
51
|
+
stop.set_rule(max=0)
|
|
52
|
+
parser.set_rule(max=0, need_subcmd=True)
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
async def _guard_state(self, subcmd=None):
|
|
56
|
+
if subcmd is None:
|
|
57
|
+
return False
|
|
58
|
+
if self._argv is not None and subcmd == "start":
|
|
59
|
+
if (
|
|
60
|
+
self._session is None
|
|
61
|
+
or (command := self._session.commands.get(self._pid)) is None
|
|
62
|
+
):
|
|
63
|
+
return False
|
|
64
|
+
tip = "错误:会话被占用\n"
|
|
65
|
+
tip += f"命令 {command.name} 正在运行,进行下一步前请先终止它或等待其完成。"
|
|
66
|
+
await self.send_msg(tip)
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
if self._argv is None and subcmd == "stop":
|
|
70
|
+
tip = "错误:会话未开始\n"
|
|
71
|
+
tip += "你还没有开始收集图片,请先使用 /convert start 。"
|
|
72
|
+
await self.send_msg(tip)
|
|
73
|
+
self.unlock()
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
async def _url_to_download(
|
|
80
|
+
download_url: str, client: httpx.AsyncClient, download_dir: Path
|
|
81
|
+
):
|
|
82
|
+
try:
|
|
83
|
+
filename = f"{uuid.uuid4().hex}"
|
|
84
|
+
save_path = download_dir / filename
|
|
85
|
+
await stuff_download(client, download_url, save_path)
|
|
86
|
+
logger.info(f"下载图片成功: {save_path}")
|
|
87
|
+
return str(save_path)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"下载图片失败 {download_url}: {e}")
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
async def _download_to_png(download_path: str, png_dir: Path):
|
|
94
|
+
if download_path == "":
|
|
95
|
+
return ""
|
|
96
|
+
png_path = png_dir / f"{uuid.uuid4().hex}.png"
|
|
97
|
+
logger.info(
|
|
98
|
+
f"执行图片转png脚本: {config.script_p2png_path} {download_path} {png_path}"
|
|
99
|
+
)
|
|
100
|
+
proc = await asyncio.create_subprocess_exec(
|
|
101
|
+
bash,
|
|
102
|
+
str(config.script_p2png_path),
|
|
103
|
+
str(download_path),
|
|
104
|
+
str(png_path),
|
|
105
|
+
stdout=asyncio.subprocess.PIPE,
|
|
106
|
+
stderr=asyncio.subprocess.PIPE,
|
|
107
|
+
)
|
|
108
|
+
stdout, stderr = await proc.communicate()
|
|
109
|
+
if stdout:
|
|
110
|
+
logger.info(f"脚本 stdout:\n{stdout.decode()}")
|
|
111
|
+
if stderr:
|
|
112
|
+
logger.warning(f"脚本 stderr:\n{stderr.decode()}")
|
|
113
|
+
if proc.returncode != 0:
|
|
114
|
+
logger.error(f"转换失败: {stderr.decode()[:]}")
|
|
115
|
+
return ""
|
|
116
|
+
logger.info(f"转换图片成功: {png_path}")
|
|
117
|
+
return str(png_path)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
async def _png_to_video(png_path: str, video_dir: Path):
|
|
121
|
+
if png_path == "":
|
|
122
|
+
return ""
|
|
123
|
+
video_path = video_dir / f"{uuid.uuid4().hex}.mp4"
|
|
124
|
+
logger.info(
|
|
125
|
+
f"执行图片转视频脚本: {config.script_png2v_path} {png_path} {video_path}"
|
|
126
|
+
)
|
|
127
|
+
proc = await asyncio.create_subprocess_exec(
|
|
128
|
+
bash,
|
|
129
|
+
str(config.script_png2v_path),
|
|
130
|
+
str(png_path),
|
|
131
|
+
str(video_path),
|
|
132
|
+
stdout=asyncio.subprocess.PIPE,
|
|
133
|
+
stderr=asyncio.subprocess.PIPE,
|
|
134
|
+
)
|
|
135
|
+
stdout, stderr = await proc.communicate()
|
|
136
|
+
if stdout:
|
|
137
|
+
logger.info(f"脚本 stdout:\n{stdout.decode()}")
|
|
138
|
+
if stderr:
|
|
139
|
+
logger.warning(f"脚本 stderr:\n{stderr.decode()}")
|
|
140
|
+
if proc.returncode != 0:
|
|
141
|
+
logger.error(f"转换失败: {stderr.decode()[:]}")
|
|
142
|
+
return ""
|
|
143
|
+
logger.info(f"转换视频成功: {video_path}")
|
|
144
|
+
return str(video_path)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
async def _png_to_frame(png_path: str, frame_dir: Path):
|
|
148
|
+
if png_path == "":
|
|
149
|
+
return ""
|
|
150
|
+
frame_path = frame_dir / f"{uuid.uuid4().hex}.png"
|
|
151
|
+
logger.info(
|
|
152
|
+
f"执行图片加框脚本: {config.script_png2fr_path} {png_path} {frame_path}"
|
|
153
|
+
)
|
|
154
|
+
proc = await asyncio.create_subprocess_exec(
|
|
155
|
+
bash,
|
|
156
|
+
str(config.script_png2fr_path),
|
|
157
|
+
str(png_path),
|
|
158
|
+
str(frame_path),
|
|
159
|
+
stdout=asyncio.subprocess.PIPE,
|
|
160
|
+
stderr=asyncio.subprocess.PIPE,
|
|
161
|
+
)
|
|
162
|
+
stdout, stderr = await proc.communicate()
|
|
163
|
+
if stdout:
|
|
164
|
+
logger.info(f"脚本 stdout:\n{stdout.decode()}")
|
|
165
|
+
if stderr:
|
|
166
|
+
logger.warning(f"脚本 stderr:\n{stderr.decode()}")
|
|
167
|
+
if proc.returncode != 0:
|
|
168
|
+
logger.error(f"转换失败: {stderr.decode()[:]}")
|
|
169
|
+
return ""
|
|
170
|
+
logger.info(f"转换方形图片成功: {frame_path}")
|
|
171
|
+
return str(frame_path)
|
|
172
|
+
|
|
173
|
+
async def _send_outputs(self):
|
|
174
|
+
if not self.session:
|
|
175
|
+
return
|
|
176
|
+
outputs = []
|
|
177
|
+
while True:
|
|
178
|
+
output = await self._outputs.get()
|
|
179
|
+
if isinstance(output, EndOfQueue):
|
|
180
|
+
break
|
|
181
|
+
if output == "":
|
|
182
|
+
continue
|
|
183
|
+
outputs.append(output)
|
|
184
|
+
|
|
185
|
+
logger.info(f"用户 {self.session.user_id} 结束收集,共收到 {len(outputs)} 张")
|
|
186
|
+
|
|
187
|
+
if len(outputs) == 0:
|
|
188
|
+
await self.send_msg("没有输出被生成。")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
await self.send_msg(f"共生成 {len(outputs)} 个输出,逐个发送…")
|
|
192
|
+
|
|
193
|
+
for i, output in enumerate(outputs):
|
|
194
|
+
output_file = Path(output)
|
|
195
|
+
if not output_file.exists():
|
|
196
|
+
logger.error(f"输出文件不存在: {output_file}")
|
|
197
|
+
continue
|
|
198
|
+
container_path = config.client_base / output_file.relative_to(
|
|
199
|
+
config.bot_base
|
|
200
|
+
)
|
|
201
|
+
try:
|
|
202
|
+
if self._mode == "video":
|
|
203
|
+
await self.send_msg(
|
|
204
|
+
Message(MessageSegment.video(f"file://{container_path}"))
|
|
205
|
+
)
|
|
206
|
+
elif self._mode == "frame":
|
|
207
|
+
await self.send_msg(
|
|
208
|
+
Message(MessageSegment.image(f"file://{container_path}"))
|
|
209
|
+
)
|
|
210
|
+
logger.info(f"发送第 {i + 1} 个输出成功: {output_file.name}")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"发送 {output_file.name} 失败: {e}")
|
|
213
|
+
await self.send_msg(
|
|
214
|
+
f"发送 {output_file.name} 失败: {e}\n尝试以文件形式发送..."
|
|
215
|
+
)
|
|
216
|
+
try:
|
|
217
|
+
msg = Message(
|
|
218
|
+
MessageSegment("file", {"file": f"file://{container_path}"})
|
|
219
|
+
)
|
|
220
|
+
await self.send_msg(msg)
|
|
221
|
+
logger.info(
|
|
222
|
+
f"以文件形式发送第 {i + 1} 个输出成功: {output_file.name}"
|
|
223
|
+
)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(f"发送 {output_file.name} 失败: {e}")
|
|
226
|
+
await self.send_msg(
|
|
227
|
+
f"发送第 {i + 1} 个输出 {output_file.name} 失败: {e}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
await asyncio.sleep(0.25)
|
|
231
|
+
|
|
232
|
+
await self.send_msg("输出发送完毕。")
|
|
233
|
+
|
|
234
|
+
async def _convert_start(self):
|
|
235
|
+
if not self.session:
|
|
236
|
+
return
|
|
237
|
+
downloads_dir = (
|
|
238
|
+
config.cache
|
|
239
|
+
/ self.session.group_id
|
|
240
|
+
/ self.session.user_id
|
|
241
|
+
/ str(self._pid)
|
|
242
|
+
/ "downloads"
|
|
243
|
+
)
|
|
244
|
+
pngs_dir = (
|
|
245
|
+
config.cache
|
|
246
|
+
/ self.session.group_id
|
|
247
|
+
/ self.session.user_id
|
|
248
|
+
/ str(self._pid)
|
|
249
|
+
/ "pngs"
|
|
250
|
+
)
|
|
251
|
+
outputs_dir = (
|
|
252
|
+
config.cache
|
|
253
|
+
/ self.session.group_id
|
|
254
|
+
/ self.session.user_id
|
|
255
|
+
/ str(self._pid)
|
|
256
|
+
/ "outputs"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
downloads_dir.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
pngs_dir.mkdir(parents=True, exist_ok=True)
|
|
261
|
+
outputs_dir.mkdir(parents=True, exist_ok=True)
|
|
262
|
+
|
|
263
|
+
async def _urls_to_downloads():
|
|
264
|
+
try:
|
|
265
|
+
async with httpx.AsyncClient() as client:
|
|
266
|
+
await prod_cons(
|
|
267
|
+
self._urls,
|
|
268
|
+
self._downloads,
|
|
269
|
+
self._url_to_download,
|
|
270
|
+
client,
|
|
271
|
+
downloads_dir,
|
|
272
|
+
)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.exception(f"urls_to_downloads 管道异常退出: {e}")
|
|
275
|
+
|
|
276
|
+
async def _downloads_to_pngs():
|
|
277
|
+
try:
|
|
278
|
+
await prod_cons(
|
|
279
|
+
self._downloads, self._pngs, self._download_to_png, pngs_dir
|
|
280
|
+
)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.exception(f"downloads_to_pngs 管道异常退出: {e}")
|
|
283
|
+
|
|
284
|
+
async def _pngs_to_outputs():
|
|
285
|
+
try:
|
|
286
|
+
async with self._convert_lock:
|
|
287
|
+
if self._mode == "video":
|
|
288
|
+
await prod_cons(
|
|
289
|
+
self._pngs, self._outputs, self._png_to_video, outputs_dir
|
|
290
|
+
)
|
|
291
|
+
elif self._mode == "frame":
|
|
292
|
+
await prod_cons(
|
|
293
|
+
self._pngs, self._outputs, self._png_to_frame, outputs_dir
|
|
294
|
+
)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.exception(f"pngs_to_outputs 管道异常退出: {e}")
|
|
297
|
+
|
|
298
|
+
asyncio.create_task(_urls_to_downloads())
|
|
299
|
+
asyncio.create_task(_downloads_to_pngs())
|
|
300
|
+
asyncio.create_task(_pngs_to_outputs())
|
|
301
|
+
|
|
302
|
+
logger.info(f"用户 {self.session.user_id} 开始了图片收集")
|
|
303
|
+
await self.send_msg(
|
|
304
|
+
"图片收集已开始, Bot 会收集本条信息后你发送的所有图片,直到你发送 /convert stop 完成收集。"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
self._if_accept_pic = True
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
async def _convert_stop(self):
|
|
311
|
+
if not self.session:
|
|
312
|
+
return
|
|
313
|
+
self._if_accept_pic = False
|
|
314
|
+
|
|
315
|
+
await self.send_msg("转换图片中...")
|
|
316
|
+
async with self._prod_lock:
|
|
317
|
+
await self._urls.put(EndOfQueue())
|
|
318
|
+
|
|
319
|
+
async with self._convert_lock:
|
|
320
|
+
pass
|
|
321
|
+
await self.send_msg("转换完毕。")
|
|
322
|
+
|
|
323
|
+
await self._send_outputs()
|
|
324
|
+
await rm_cache(self.session.group_id, self.session.user_id, str(self._pid))
|
|
325
|
+
self.unlock()
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
async def roger(self, event: MessageEvent):
|
|
329
|
+
async with self._prod_lock:
|
|
330
|
+
if not self._if_accept_pic:
|
|
331
|
+
return
|
|
332
|
+
msg = await dump_message(self.bot, event.get_message())
|
|
333
|
+
url_list = get_multimedias_url(msg, basetypes=["image"])
|
|
334
|
+
for url in url_list:
|
|
335
|
+
await self._urls.put(url)
|
|
336
|
+
|
|
337
|
+
async def run(self, args: Message):
|
|
338
|
+
async with self._runlock:
|
|
339
|
+
if not self.session:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
new_argv = args.extract_plain_text().strip().split()
|
|
343
|
+
if not await self._legal_case(new_argv):
|
|
344
|
+
if self._argv is None:
|
|
345
|
+
self.unlock()
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
self._parser.parse_argv(new_argv)
|
|
349
|
+
subcmd = self._parser.subcmd
|
|
350
|
+
|
|
351
|
+
if not await self._guard_state(subcmd):
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
self._argv = new_argv
|
|
355
|
+
|
|
356
|
+
if self._pid >= 0 and not self._check_perm("convert"):
|
|
357
|
+
await self.send_msg("权限不足")
|
|
358
|
+
self.unlock()
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
if subcmd == "start":
|
|
362
|
+
new_mode = self._parser.subparsers[subcmd].opts_value.get(
|
|
363
|
+
"-m", ["video"]
|
|
364
|
+
)[0]
|
|
365
|
+
self._mode = new_mode
|
|
366
|
+
await self._convert_start()
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
if subcmd == "stop":
|
|
370
|
+
await self._convert_stop()
|
|
371
|
+
return
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from nonebot.adapters.onebot.v11 import Bot, Message
|
|
8
|
+
|
|
9
|
+
from ..command import BotCommand
|
|
10
|
+
from ..parser import BotArgParser
|
|
11
|
+
from .md2pic_cmd import BotCommandMd2pic
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..session import BotSession
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BotCommandHelp(BotCommand):
|
|
18
|
+
_name = "help"
|
|
19
|
+
|
|
20
|
+
def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
|
|
21
|
+
super().__init__(bot, session, _pid=_pid, _internal=_internal)
|
|
22
|
+
|
|
23
|
+
def _init_parser(self):
|
|
24
|
+
parser = BotArgParser()
|
|
25
|
+
parser.add_subparser("help")
|
|
26
|
+
parser.add_subparser("session")
|
|
27
|
+
parser.add_subparser("perm")
|
|
28
|
+
parser.add_subparser("convert")
|
|
29
|
+
parser.add_subparser("randpic")
|
|
30
|
+
parser.add_subparser("shitpost")
|
|
31
|
+
parser.add_subparser("advrandpic")
|
|
32
|
+
parser.add_subparser("md2pic")
|
|
33
|
+
parser.add_subparser("pixiv")
|
|
34
|
+
return parser
|
|
35
|
+
|
|
36
|
+
async def run(self, args: Message):
|
|
37
|
+
if not self.session:
|
|
38
|
+
return
|
|
39
|
+
new_argv = args.extract_plain_text().strip().split()
|
|
40
|
+
# I know it's impossible to be illegal. Just for formalism :)
|
|
41
|
+
if not await self._legal_case(new_argv):
|
|
42
|
+
if self._argv is None:
|
|
43
|
+
self.unlock()
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
self._argv = new_argv
|
|
47
|
+
self._parser.parse_argv(self._argv)
|
|
48
|
+
subcmd = self._parser.subcmd
|
|
49
|
+
|
|
50
|
+
if not self._check_perm("help"):
|
|
51
|
+
await self.send_msg("权限不足")
|
|
52
|
+
self.unlock()
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
help_dir = Path(__file__).resolve().parent.parent / "docs" / "help"
|
|
56
|
+
tip = "错误: 帮助文档目录不存在"
|
|
57
|
+
if help_dir.exists():
|
|
58
|
+
tip = "错误: 帮助文档 index.md 不存在"
|
|
59
|
+
help_path = help_dir / "index.md"
|
|
60
|
+
if help_path.exists():
|
|
61
|
+
tip = help_path.read_text(encoding="utf-8")
|
|
62
|
+
if subcmd in (
|
|
63
|
+
"help",
|
|
64
|
+
"session",
|
|
65
|
+
"perm",
|
|
66
|
+
"convert",
|
|
67
|
+
"randpic",
|
|
68
|
+
"shitpost",
|
|
69
|
+
"advrandpic",
|
|
70
|
+
"md2pic",
|
|
71
|
+
"pixiv",
|
|
72
|
+
):
|
|
73
|
+
tip = f"错误: 帮助文档 {subcmd}.md 不存在"
|
|
74
|
+
help_path = help_dir / f"{subcmd}.md"
|
|
75
|
+
if help_path.exists():
|
|
76
|
+
tip = help_path.read_text(encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
tip = re.sub(
|
|
79
|
+
r"!\[([^\]]*)\]\((?!https?://|/)([^)]+)\)",
|
|
80
|
+
lambda m: f").resolve()})",
|
|
81
|
+
tip,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
_pid = -1
|
|
85
|
+
pids = self.session.commands.keys()
|
|
86
|
+
while _pid in pids:
|
|
87
|
+
_pid -= 1
|
|
88
|
+
md2pic = BotCommandMd2pic.make(self.bot, self.session, _pid=_pid)
|
|
89
|
+
tip = "-c\n" + tip
|
|
90
|
+
msg = Message(tip)
|
|
91
|
+
await md2pic.run(msg) # type: ignore
|
|
92
|
+
self.unlock()
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from nonebot import require
|
|
8
|
+
from nonebot.adapters.onebot.v11 import Bot, Message, MessageSegment
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
require("nonebot_plugin_htmlrender")
|
|
12
|
+
from nonebot_plugin_htmlrender import md_to_pic
|
|
13
|
+
|
|
14
|
+
from ..command import BotCommand
|
|
15
|
+
from ..parser import BotArgParser
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..session import BotSession
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BotCommandMd2pic(BotCommand):
|
|
22
|
+
_name = "md2pic"
|
|
23
|
+
|
|
24
|
+
def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
|
|
25
|
+
super().__init__(bot, session, _pid=_pid, _internal=_internal)
|
|
26
|
+
self._md = ""
|
|
27
|
+
self._scale = 2
|
|
28
|
+
self._css = ""
|
|
29
|
+
self._padding = 30
|
|
30
|
+
self._min_w = 20
|
|
31
|
+
self._max_w = 2000
|
|
32
|
+
self._min_h = 20
|
|
33
|
+
self._max_h = None
|
|
34
|
+
|
|
35
|
+
def _init_parser(self):
|
|
36
|
+
parser = BotArgParser()
|
|
37
|
+
parser.set_rule(min=1, max=1)
|
|
38
|
+
parser.add_opt("-c", necessary=True)
|
|
39
|
+
parser.add_opt("-s", required=True, type=float, default=[2])
|
|
40
|
+
parser.add_opt(
|
|
41
|
+
"-t",
|
|
42
|
+
required=True,
|
|
43
|
+
choice=["github-markdown-dark-dimmed"],
|
|
44
|
+
default=["github-markdown-dark-dimmed"],
|
|
45
|
+
)
|
|
46
|
+
parser.add_opt("--padding", required=True, type=int, default=[30])
|
|
47
|
+
parser.add_opt("--min_w", required=True, type=int, default=[20])
|
|
48
|
+
parser.add_opt("--max_w", required=True, type=int, default=[2000])
|
|
49
|
+
parser.add_opt("--min_h", required=True, type=int, default=[20])
|
|
50
|
+
parser.add_opt("--max_h", required=True, type=int, default=[])
|
|
51
|
+
|
|
52
|
+
return parser
|
|
53
|
+
|
|
54
|
+
async def _render(self) -> bytes:
|
|
55
|
+
|
|
56
|
+
min_w = int(self._scale * self._min_w)
|
|
57
|
+
max_w = self._max_w
|
|
58
|
+
min_h = int(self._scale * self._min_h)
|
|
59
|
+
max_h = (
|
|
60
|
+
int(self._scale * self._max_h) if self._max_h is not None else self._max_h
|
|
61
|
+
)
|
|
62
|
+
padding = int(self._scale * self._padding)
|
|
63
|
+
|
|
64
|
+
raw = await md_to_pic(
|
|
65
|
+
self._md, width=max_w, css_path=self._css, device_scale_factor=self._scale
|
|
66
|
+
)
|
|
67
|
+
img = Image.open(io.BytesIO(raw))
|
|
68
|
+
|
|
69
|
+
# Get the background color (the upper-left color)
|
|
70
|
+
bgpx: Any = img.getpixel((0, 0))
|
|
71
|
+
bgr, bgg, bgb = int(bgpx[0]), int(bgpx[1]), int(bgpx[2])
|
|
72
|
+
|
|
73
|
+
def is_bg(r: int, g: int, b: int, *, delta: int = 6) -> bool:
|
|
74
|
+
return (
|
|
75
|
+
abs(r - bgr) < delta and abs(g - bgg) < delta and abs(b - bgb) < delta
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def is_column_has_content(x: int):
|
|
79
|
+
for y in range(0, h, 4):
|
|
80
|
+
pixel: Any = img.getpixel((x, y))
|
|
81
|
+
r, g, b = int(pixel[0]), int(pixel[1]), int(pixel[2])
|
|
82
|
+
if not is_bg(r, g, b):
|
|
83
|
+
return True
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def is_row_has_content(y: int):
|
|
87
|
+
for x in range(0, w, 4):
|
|
88
|
+
pixel: Any = img.getpixel((x, y))
|
|
89
|
+
r, g, b = int(pixel[0]), int(pixel[1]), int(pixel[2])
|
|
90
|
+
if not is_bg(r, g, b):
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
w, h = img.size
|
|
95
|
+
content_left, content_right = 0, w
|
|
96
|
+
content_top, content_bottom = 0, h
|
|
97
|
+
|
|
98
|
+
# Find out the leftmost column that has content
|
|
99
|
+
for x in range(w):
|
|
100
|
+
if is_column_has_content(x):
|
|
101
|
+
content_left = x
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
# Find out the rightmost column that has content
|
|
105
|
+
for x in range(w - 1, content_left - 1, -1):
|
|
106
|
+
if is_column_has_content(x):
|
|
107
|
+
content_right = x + 1
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
# Find out the top row that has content
|
|
111
|
+
for y in range(h):
|
|
112
|
+
if is_row_has_content(y):
|
|
113
|
+
content_top = y
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
# Find out the bottom row that has content
|
|
117
|
+
for y in range(h - 1, content_top - 1, -1):
|
|
118
|
+
if is_row_has_content(y):
|
|
119
|
+
content_bottom = y + 1
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
crop_left = max(0, content_left - padding)
|
|
123
|
+
crop_right = max(crop_left + min_w + 1, content_right + padding)
|
|
124
|
+
crop_top = max(0, content_top - padding)
|
|
125
|
+
crop_bottom = max(crop_top + min_h + 1, content_bottom + padding)
|
|
126
|
+
if max_h is not None:
|
|
127
|
+
crop_bottom = min(crop_bottom, crop_top + max_h)
|
|
128
|
+
|
|
129
|
+
crop_right = min(crop_right, w)
|
|
130
|
+
crop_bottom = min(crop_bottom, h)
|
|
131
|
+
img = img.crop((crop_left, crop_top, crop_right, crop_bottom))
|
|
132
|
+
|
|
133
|
+
buf = io.BytesIO()
|
|
134
|
+
img.save(buf, format="PNG")
|
|
135
|
+
return buf.getvalue()
|
|
136
|
+
|
|
137
|
+
async def run(self, args: Message):
|
|
138
|
+
if not self.session:
|
|
139
|
+
return
|
|
140
|
+
new_argss = args.extract_plain_text().split("\n", 1)
|
|
141
|
+
if len(new_argss) < 2:
|
|
142
|
+
await self._send_format_error()
|
|
143
|
+
if self._argv is None:
|
|
144
|
+
self.unlock()
|
|
145
|
+
return
|
|
146
|
+
new_argv = new_argss[0].strip().split()
|
|
147
|
+
new_argv.append(new_argss[1])
|
|
148
|
+
if not await self._legal_case(new_argv):
|
|
149
|
+
if self._argv is None:
|
|
150
|
+
self.unlock()
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if not await self._guard_state():
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
self._argv = new_argv
|
|
157
|
+
self._parser.parse_argv(self._argv)
|
|
158
|
+
|
|
159
|
+
if self._pid >= 0 and not self._check_perm("md2pic"):
|
|
160
|
+
await self.send_msg("权限不足")
|
|
161
|
+
self.unlock()
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
self._scale = self._parser.opts_value["-s"][0]
|
|
165
|
+
theme = self._parser.opts_value["-t"][0]
|
|
166
|
+
self._css = str(Path(__file__).resolve().parent.parent / "css" / f"{theme}.css")
|
|
167
|
+
self._padding = self._parser.opts_value["--padding"][0]
|
|
168
|
+
self._min_w = self._parser.opts_value["--min_w"][0]
|
|
169
|
+
self._max_w = self._parser.opts_value["--max_w"][0]
|
|
170
|
+
self._min_h = self._parser.opts_value["--min_h"][0]
|
|
171
|
+
self._max_h = (
|
|
172
|
+
self._parser.opts_value["--max_h"][0]
|
|
173
|
+
if len(self._parser.opts_value["--max_h"]) > 0
|
|
174
|
+
else None
|
|
175
|
+
)
|
|
176
|
+
self._md = self._parser.value[0]
|
|
177
|
+
|
|
178
|
+
self._padding = max(0, self._padding)
|
|
179
|
+
self._padding = min(50, self._padding)
|
|
180
|
+
self._max_w = min(10000000, self._max_w)
|
|
181
|
+
self._max_w = max(0, self._max_w)
|
|
182
|
+
self._min_w = min(self._max_w, self._min_w)
|
|
183
|
+
self._min_w = max(0, self._min_w)
|
|
184
|
+
if self._max_h is not None:
|
|
185
|
+
self._max_h = min(10000000, self._max_h)
|
|
186
|
+
self._max_h = max(0, self._max_h)
|
|
187
|
+
self._min_h = min(self._max_h, self._min_h)
|
|
188
|
+
self._min_h = max(0, self._min_h)
|
|
189
|
+
|
|
190
|
+
img = await self._render()
|
|
191
|
+
|
|
192
|
+
msg = Message(MessageSegment.image(img))
|
|
193
|
+
await self.send_msg(msg)
|
|
194
|
+
|
|
195
|
+
self.unlock()
|