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.
Files changed (46) hide show
  1. nonebot_plugin_shitbot/__init__.py +21 -0
  2. nonebot_plugin_shitbot/aux.py +71 -0
  3. nonebot_plugin_shitbot/command.py +154 -0
  4. nonebot_plugin_shitbot/commands/__init__.py +27 -0
  5. nonebot_plugin_shitbot/commands/advrandpic_cmd.py +129 -0
  6. nonebot_plugin_shitbot/commands/autoreply_cmd.py +71 -0
  7. nonebot_plugin_shitbot/commands/autoreply_main_cmd.py +67 -0
  8. nonebot_plugin_shitbot/commands/convert_cmd.py +371 -0
  9. nonebot_plugin_shitbot/commands/help_cmd.py +92 -0
  10. nonebot_plugin_shitbot/commands/md2pic_cmd.py +195 -0
  11. nonebot_plugin_shitbot/commands/perm_cmd.py +392 -0
  12. nonebot_plugin_shitbot/commands/pixiv_cmd.py +144 -0
  13. nonebot_plugin_shitbot/commands/randpic_cmd.py +75 -0
  14. nonebot_plugin_shitbot/commands/session_cmd.py +79 -0
  15. nonebot_plugin_shitbot/commands/shitpost_cmd.py +189 -0
  16. nonebot_plugin_shitbot/config.py +186 -0
  17. nonebot_plugin_shitbot/config.yaml +23 -0
  18. nonebot_plugin_shitbot/css/github-markdown-dark-dimmed.css +1220 -0
  19. nonebot_plugin_shitbot/default_config.yaml +23 -0
  20. nonebot_plugin_shitbot/docs/help/advrandpic.md +61 -0
  21. nonebot_plugin_shitbot/docs/help/convert.md +37 -0
  22. nonebot_plugin_shitbot/docs/help/help.md +17 -0
  23. nonebot_plugin_shitbot/docs/help/imgs/md2picex1.png +0 -0
  24. nonebot_plugin_shitbot/docs/help/imgs/md2picex2.png +0 -0
  25. nonebot_plugin_shitbot/docs/help/index.md +22 -0
  26. nonebot_plugin_shitbot/docs/help/md2pic.md +57 -0
  27. nonebot_plugin_shitbot/docs/help/perm.md +110 -0
  28. nonebot_plugin_shitbot/docs/help/pixiv.md +42 -0
  29. nonebot_plugin_shitbot/docs/help/randpic.md +34 -0
  30. nonebot_plugin_shitbot/docs/help/session.md +38 -0
  31. nonebot_plugin_shitbot/docs/help/shitpost.md +39 -0
  32. nonebot_plugin_shitbot/docs//346/235/203/351/231/220/347/263/273/347/273/237.md +220 -0
  33. nonebot_plugin_shitbot/handlers.py +150 -0
  34. nonebot_plugin_shitbot/msgutils.py +442 -0
  35. nonebot_plugin_shitbot/parser.py +284 -0
  36. nonebot_plugin_shitbot/permissions.py +198 -0
  37. nonebot_plugin_shitbot/scripts/p2png.sh +38 -0
  38. nonebot_plugin_shitbot/scripts/png2fr.sh +19 -0
  39. nonebot_plugin_shitbot/scripts/png2v.sh +24 -0
  40. nonebot_plugin_shitbot/session.py +76 -0
  41. nonebot_plugin_shitbot/tasks.py +25 -0
  42. nonebot_plugin_shitbot-0.1.0.dist-info/METADATA +17 -0
  43. nonebot_plugin_shitbot-0.1.0.dist-info/RECORD +46 -0
  44. nonebot_plugin_shitbot-0.1.0.dist-info/WHEEL +5 -0
  45. nonebot_plugin_shitbot-0.1.0.dist-info/licenses/LICENSE +661 -0
  46. nonebot_plugin_shitbot-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ from os import environ
2
+
3
+ # During unit tests, skip importing the heavy handler/command modules.
4
+ # These modules pull in the entire NoneBot stack and external plugins
5
+ # (e.g. nonebot_plugin_htmlrender → get_driver()). Pure-logic modules
6
+ # like parser.py should remain importable without a running NoneBot.
7
+ if not environ.get("PYTEST_RUNNING"):
8
+ from nonebot.plugin import PluginMetadata
9
+
10
+ from . import handlers # noqa: F401
11
+ from .config import ShitBotConfig
12
+
13
+ __plugin_meta__ = PluginMetadata(
14
+ name="shitbot",
15
+ description="个人自用自己手搓框架的多功能bot, 还在开发中",
16
+ usage="发送/help 查看帮助信息",
17
+ type="application",
18
+ homepage="https://github.com/Cmd-GZ/nonebot-plugin-shitbot",
19
+ config=ShitBotConfig,
20
+ supported_adapters={"~onebot.v11"},
21
+ )
@@ -0,0 +1,71 @@
1
+ import shutil
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ import httpx
6
+ from nonebot.log import logger
7
+
8
+ from .config import config
9
+
10
+
11
+ def validate_schema(data: Any, schema: Any) -> bool:
12
+ if schema is Any:
13
+ return True
14
+ if schema is None:
15
+ return False
16
+ if isinstance(schema, type):
17
+ return isinstance(data, schema)
18
+ if isinstance(schema, dict):
19
+ if not isinstance(data, dict):
20
+ return False
21
+ return all(
22
+ key in data and validate_schema(data[key], subschema)
23
+ for key, subschema in schema.items()
24
+ )
25
+ if isinstance(schema, list):
26
+ if not isinstance(data, list):
27
+ return False
28
+ length = len(schema)
29
+ if length < 1:
30
+ return False
31
+ return all(
32
+ validate_schema(item, schema[i if i < length else -1])
33
+ for i, item in enumerate(data)
34
+ )
35
+ return False
36
+
37
+
38
+ async def rm_path(path: Path):
39
+ if not path.exists():
40
+ return
41
+ if path.is_dir():
42
+ shutil.rmtree(path)
43
+ if path.is_file():
44
+ path.unlink()
45
+
46
+
47
+ async def rm_cache(group_id: str, user_id: str, pid: str):
48
+ cache_path = config.cache / group_id / user_id / pid
49
+ await rm_path(cache_path)
50
+ logger.info(f"已删除缓存: {cache_path}")
51
+
52
+
53
+ async def stuff_download(
54
+ client: httpx.AsyncClient,
55
+ url: str | httpx.URL,
56
+ output_path: Path,
57
+ *,
58
+ referer: str | None = None,
59
+ ):
60
+ try:
61
+ headers = {"Referer": referer} if referer is not None else {}
62
+ async with client.stream(
63
+ "GET", url, headers=headers, follow_redirects=True
64
+ ) as resp:
65
+ resp.raise_for_status()
66
+ with open(output_path, "wb") as f:
67
+ async for chunk in resp.aiter_bytes():
68
+ f.write(chunk)
69
+ except Exception as e:
70
+ logger.error(f"下载 {url} 失败: {e}")
71
+ raise
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent
6
+
7
+ from .msgutils import send_msg
8
+ from .parser import BotArgParser
9
+ from .permissions import permissions
10
+
11
+ if TYPE_CHECKING:
12
+ from .session import BotSession
13
+
14
+
15
+ # Base class of all the commands
16
+ class BotCommand:
17
+ # class fields:
18
+ # _name: command name
19
+ # _sentinel: used to prevent from calling __init__ directly
20
+ # fields:
21
+ # _bot: bot
22
+ # _session: the command's session
23
+ # _argv: current arguments of the command, which is also represented as the command's state
24
+ # _parser: arguments parser used to parse arguments
25
+ # _pid: command's id in its session
26
+ _argv: list[str] | None
27
+ _sentinel = object()
28
+ _name = "otherwise"
29
+
30
+ # Use make() instead of this
31
+ def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
32
+ if _internal is not self._sentinel:
33
+ raise TypeError("Please use BotCommand.make() instead of BotCommand()")
34
+ self._bot = bot
35
+ self._session = session
36
+ self._argv = None
37
+ self._parser = self._init_parser()
38
+ self._pid = _pid
39
+ session.commands[_pid] = self
40
+
41
+ @classmethod
42
+ def make(
43
+ cls, bot: Bot, session: BotSession, *, _pid: int | None = None
44
+ ) -> BotCommand | None:
45
+ if _pid is None:
46
+ _pid = session.curpid
47
+ if session.commands.get(_pid):
48
+ return None
49
+ return cls(bot, session, _pid=_pid, _internal=cls._sentinel)
50
+
51
+ @classmethod
52
+ def get_name(cls):
53
+ return cls._name
54
+
55
+ @property
56
+ def bot(self):
57
+ return self._bot
58
+
59
+ @property
60
+ def session(self):
61
+ return self._session
62
+
63
+ @property
64
+ def name(self):
65
+ return self._name
66
+
67
+ @property
68
+ def argv(self):
69
+ return self._argv
70
+
71
+ @property
72
+ def pid(self):
73
+ return self._pid
74
+
75
+ def _init_parser(self):
76
+ return BotArgParser()
77
+
78
+ async def _send_format_error(self):
79
+ tip = f"命令格式错误:\n{self._parser.err}\n"
80
+ tip += f"输入 /help {self.name} 查看使用方法."
81
+ await self.send_msg(tip)
82
+
83
+ # Judge if the arguments are legal based on the parser and send msg if it's illegal
84
+ async def _legal_case(self, argv: list[str]):
85
+ if self._parser.is_valid(argv):
86
+ return True
87
+ await self._send_format_error()
88
+ return False
89
+
90
+ async def _guard_state(self):
91
+ if self._argv is not None:
92
+ if (
93
+ self._session is None
94
+ or (command := self._session.commands.get(self._pid)) is None
95
+ ):
96
+ return False
97
+ tip = "错误:会话被占用\n"
98
+ tip += f"命令 {command.name} 正在运行,进行下一步前请先终止它或等待其完成。"
99
+ await self.send_msg(tip)
100
+ return False
101
+ return True
102
+
103
+ async def send_msg(
104
+ self,
105
+ msg: str | Message,
106
+ *,
107
+ group_id: str | None = None,
108
+ user_id: str | None = None,
109
+ ):
110
+ if not self.session:
111
+ return
112
+ gid, uid = group_id, user_id
113
+ if gid is None:
114
+ gid = self.session.group_id
115
+ if uid is None:
116
+ uid = self.session.user_id
117
+ if gid == "public":
118
+ return
119
+ if gid == "private":
120
+ gid = None
121
+ uid = int(uid)
122
+ else:
123
+ gid = int(gid)
124
+ uid = None
125
+ await send_msg(bot=self.bot, group_id=gid, user_id=uid, msg=msg)
126
+
127
+ def _check_perm(self, entry_name: str):
128
+ if not self.session:
129
+ return False
130
+ return permissions.check_permission(
131
+ entry_name, self.session.group_id, self.session.user_id
132
+ )
133
+
134
+ async def roger(self, event: MessageEvent):
135
+ pass
136
+
137
+ # Main function
138
+ async def run(self, args: Message):
139
+ if not self.session:
140
+ self.unlock()
141
+ return
142
+ if not self._check_perm("otherwise"):
143
+ self.unlock()
144
+ return
145
+ await self.send_msg("无效命令,请输入/help获取帮助。")
146
+ self.unlock()
147
+
148
+ # Disconnect with the session, you should call in run() before return
149
+ def unlock(self):
150
+ if self._session is None:
151
+ return
152
+ self._session.commands.pop(self._pid, None)
153
+ self._session.release()
154
+ self._session = None
@@ -0,0 +1,27 @@
1
+ from ..command import BotCommand
2
+ from .advrandpic_cmd import BotCommandAdvrandpic
3
+ from .autoreply_cmd import BotCommandAutoreply
4
+ from .autoreply_main_cmd import BotCommandAutoReplyMain
5
+ from .convert_cmd import BotCommandConvert
6
+ from .help_cmd import BotCommandHelp
7
+ from .md2pic_cmd import BotCommandMd2pic
8
+ from .perm_cmd import BotCommandPerm
9
+ from .pixiv_cmd import BotCommandPixiv
10
+ from .randpic_cmd import BotCommandRandpic
11
+ from .session_cmd import BotCommandSession
12
+ from .shitpost_cmd import BotCommandShitpost
13
+
14
+ __all__ = [
15
+ "BotCommand",
16
+ "BotCommandAdvrandpic",
17
+ "BotCommandAutoReplyMain",
18
+ "BotCommandAutoreply",
19
+ "BotCommandConvert",
20
+ "BotCommandHelp",
21
+ "BotCommandMd2pic",
22
+ "BotCommandPerm",
23
+ "BotCommandPixiv",
24
+ "BotCommandRandpic",
25
+ "BotCommandSession",
26
+ "BotCommandShitpost",
27
+ ]
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import httpx
6
+ from nonebot.adapters.onebot.v11 import Bot, Message, MessageSegment
7
+ from nonebot.log import logger
8
+
9
+ from ..command import BotCommand
10
+ from ..parser import BotArgParser
11
+
12
+ if TYPE_CHECKING:
13
+ from ..session import BotSession
14
+
15
+
16
+ class BotCommandAdvrandpic(BotCommand):
17
+ _name = "advrandpic"
18
+
19
+ def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
20
+ super().__init__(bot, session, _pid=_pid, _internal=_internal)
21
+ self._r18 = 0
22
+ self._num = 1
23
+ self._tag = None
24
+ self._size = "regular"
25
+
26
+ def _init_parser(self):
27
+ parser = BotArgParser()
28
+ parser.set_rule(max=0)
29
+ parser.add_opt(
30
+ "-r", required=True, choice=["off", "on", "only"], default=["off"]
31
+ )
32
+ parser.add_opt(
33
+ "-s", required=True, choice=["original", "regular"], default=["regular"]
34
+ )
35
+ parser.add_opt("-t", required=True)
36
+ parser.add_opt("-n", required=True, type=int, default=[1])
37
+ return parser
38
+
39
+ async def run(self, args: Message):
40
+ if not self.session:
41
+ return
42
+
43
+ new_argv = args.extract_plain_text().strip().split()
44
+ if not await self._legal_case(new_argv):
45
+ if self._argv is None:
46
+ self.unlock()
47
+ return
48
+
49
+ if not await self._guard_state():
50
+ return
51
+
52
+ self._argv = new_argv
53
+ self._parser.parse_argv(self._argv)
54
+
55
+ r18 = self._parser.opts_value["-r"][0]
56
+
57
+ if r18 == "off":
58
+ self._r18 = 0
59
+ if r18 == "on":
60
+ self._r18 = 2
61
+ if r18 == "only":
62
+ self._r18 = 1
63
+
64
+ tags = self._parser.opts_value["-t"]
65
+ if len(tags) > 0:
66
+ self._tag = tags[0].split("&")
67
+
68
+ self._num = self._parser.opts_value["-n"][0]
69
+ self._num = max(self._num, 1)
70
+ self._num = min(self._num, 10)
71
+ if not self._check_perm("multisetu"):
72
+ self._num = 1
73
+
74
+ self._size = self._parser.opts_value["-s"][0]
75
+
76
+ if not self._check_perm("advrandpic"):
77
+ await self.send_msg("权限不足")
78
+ self.unlock()
79
+ return
80
+
81
+ if self._r18 and not self._check_perm("nsfw"):
82
+ await self.send_msg("权限不足")
83
+ self.unlock()
84
+ return
85
+
86
+ api = "https://api.lolicon.app/setu/v2"
87
+ payload: dict[str, Any] = {
88
+ "r18": self._r18,
89
+ "num": self._num,
90
+ "size": self._size,
91
+ }
92
+ if self._tag is not None:
93
+ payload["tag"] = self._tag
94
+ headers = {"Content-Type": "application/json"}
95
+
96
+ async with httpx.AsyncClient() as client:
97
+ response = await client.post(api, headers=headers, json=payload)
98
+ if response.status_code != 200:
99
+ logger.error(f"api调用失败, 状态码{response.status_code}")
100
+ self.unlock()
101
+ return
102
+
103
+ data = response.json()
104
+
105
+ if len(data["data"]) < self._num:
106
+ await self.send_msg(f"未找到指定数量的图片,仅找到 {len(data['data'])} 张")
107
+
108
+ for pic in data["data"]:
109
+ pid = str(pic["pid"])
110
+ title = pic["title"]
111
+ author = pic["author"]
112
+ url = pic["urls"][self._size]
113
+ text = f"标题: {title}\n作者: {author}\nPID: {pid}"
114
+ try:
115
+ msg = Message(
116
+ [
117
+ MessageSegment("text", {"text": text}),
118
+ MessageSegment("image", {"url": url}),
119
+ ]
120
+ )
121
+ msg[0].data["summary"] = "我的新自拍喵[图片]"
122
+ await self.send_msg(msg)
123
+ logger.info("发送图片成功")
124
+ except Exception as e:
125
+ logger.error(f"发送图片失败: {e}")
126
+ text += f"\n图片发送失败, 大概率被河蟹了, 请尝试私聊使用该命令\n{e}"
127
+ await self.send_msg(text)
128
+
129
+ self.unlock()
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from nonebot.adapters.onebot.v11 import Bot, Message
4
+
5
+ from ..command import BotCommand
6
+ from ..parser import BotArgParser
7
+ from ..session import BotSession
8
+ from .autoreply_main_cmd import BotCommandAutoReplyMain
9
+
10
+
11
+ class BotCommandAutoreply(BotCommand):
12
+ def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
13
+ super().__init__(bot, session, _pid=_pid, _internal=_internal)
14
+
15
+ def _init_parser(self):
16
+ parser = BotArgParser()
17
+ parser.set_rule(max=0, need_subcmd=True)
18
+ start = parser.add_subparser("start")
19
+ stop = parser.add_subparser("stop")
20
+ start.set_rule(max=0)
21
+ stop.set_rule(max=0)
22
+ return parser
23
+
24
+ async def run(self, args: Message):
25
+ if not self.session:
26
+ return
27
+ new_argv = args.extract_plain_text().strip().split()
28
+
29
+ if not await self._legal_case(new_argv):
30
+ if self._argv is None:
31
+ self.unlock()
32
+ return
33
+
34
+ if not await self._guard_state():
35
+ return
36
+
37
+ self._argv = new_argv
38
+ self._parser.parse_argv(self._argv)
39
+ subcmd = self._parser.subcmd
40
+
41
+ if not self._check_perm("autoreplymanager"):
42
+ await self.send_msg("权限不足")
43
+ self.unlock()
44
+ return
45
+
46
+ if subcmd == "start":
47
+ autoreply_session = BotSession.make("public", "autoreply")
48
+ main_command = BotCommandAutoReplyMain.make(
49
+ self.bot, autoreply_session, _pid=autoreply_session.curpid
50
+ )
51
+ if main_command is None:
52
+ await self.send_msg("警告: 自动回复正在运行中")
53
+ await self.send_msg("自动回复已开启")
54
+ self.unlock()
55
+
56
+ elif subcmd == "stop":
57
+
58
+ async def _exe():
59
+ autoreply_session = BotSession.get_obj("public", "autoreply")
60
+ if autoreply_session is None:
61
+ await self.send_msg("警告: 自动回复未开启")
62
+ return
63
+ main_command = autoreply_session.commands.get(autoreply_session.curpid)
64
+ if main_command is None:
65
+ await self.send_msg("警告: 自动回复未开启")
66
+ return
67
+ main_command.unlock()
68
+
69
+ await _exe()
70
+ await self.send_msg("自动回复已关闭")
71
+ self.unlock()
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent, MessageSegment
6
+
7
+ from ..command import BotCommand
8
+ from ..config import config
9
+
10
+ if TYPE_CHECKING:
11
+ from ..session import BotSession
12
+
13
+
14
+ # Simple auto reply, just for fun :). May be further reconstructed in future.
15
+ class BotCommandAutoReplyMain(BotCommand):
16
+ _name = "autoreply_main"
17
+
18
+ def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
19
+ super().__init__(bot, session, _pid=_pid, _internal=_internal)
20
+
21
+ async def roger(self, event: MessageEvent):
22
+ group_id = str(getattr(event, "group_id", "private"))
23
+ if event.message_type == "private":
24
+ group_id = "private"
25
+ user_id = str(event.user_id)
26
+
27
+ for seg in event.get_message():
28
+ if seg.type != "text":
29
+ continue
30
+ text = seg.data.get("text", "")
31
+ cleaned_text = (
32
+ text.replace("!", "")
33
+ .replace(" ", "")
34
+ .replace("!", "")
35
+ .replace("w", "")
36
+ .replace("我", "")
37
+ )
38
+ if cleaned_text in [
39
+ "csn",
40
+ "草死你",
41
+ "操死你",
42
+ "🌿死你",
43
+ "艹死你",
44
+ "zjsncsn",
45
+ ]:
46
+ wcsn_path = config.client_base / "data" / "wcsn.jpg"
47
+ msg = Message(MessageSegment.image(f"file://{wcsn_path}"))
48
+ msg[0].data["sub_type"] = 1
49
+ msg[0].data["summary"] = "喵呜~"
50
+ await self.send_msg(msg, group_id=group_id, user_id=user_id)
51
+ return
52
+ cleaned = text.replace("?", "").replace(" ", "").replace("?", "")
53
+ if cleaned in ["这是你吗", "zsnm", "是你吗"]:
54
+ zsnm_path = config.client_base / "data" / "zsnm.jpg"
55
+ msg = Message(
56
+ [
57
+ MessageSegment.image(f"file://{zsnm_path}"),
58
+ MessageSegment.text("是我。"),
59
+ ]
60
+ )
61
+ msg[0].data["sub_type"] = 1
62
+ msg[0].data["summary"] = "喵呜~"
63
+ await self.send_msg(msg, group_id=group_id, user_id=user_id)
64
+ return
65
+
66
+ async def run(self, args: Message):
67
+ return