nonebot-plugin-savepic 0.1.3__tar.gz → 0.2.0__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.
@@ -1,20 +1,24 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-savepic
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: 保存表情包(语录)与随机出图
5
5
  Home-page: https://github.com/Yan-Zero/nonebot-plugin-savepic
6
6
  License: MIT
7
7
  Author: Yan
8
8
  Author-email: 1964649083@qq.com
9
- Requires-Python: >=3.10,<4.0
9
+ Requires-Python: >=3.10,<3.13
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
+ Requires-Dist: arclet-alconna (>=1.7.14,<2.0.0)
14
15
  Requires-Dist: asyncpg (>=0.28.0,<0.29.0)
15
16
  Requires-Dist: nonebot-adapter-onebot (>=2.2.3,<3.0.0)
16
17
  Requires-Dist: nonebot-plugin-datastore (>=1.1.0,<2.0.0)
17
18
  Requires-Dist: nonebot2 (>=2.0.1,<3.0.0)
19
+ Requires-Dist: numpy (>=1.25.2,<2.0.0)
20
+ Requires-Dist: pillow (>=10.0.0,<11.0.0)
21
+ Requires-Dist: scipy (>=1.11.1,<2.0.0)
18
22
  Description-Content-Type: text/markdown
19
23
 
20
24
  <div align="center">
@@ -0,0 +1,190 @@
1
+ from nonebot import require
2
+ from nonebot import get_driver
3
+ from nonebot import on_command
4
+ from nonebot.params import CommandArg, Arg
5
+ from nonebot.adapters.onebot.v11.message import Message as V11Msg
6
+ from nonebot.adapters.onebot.v11.message import MessageSegment as V11Seg
7
+ from nonebot.internal.adapter.bot import Bot
8
+ from nonebot.adapters.onebot.v11.event import GroupMessageEvent
9
+ from nonebot.matcher import Matcher
10
+ from nonebot.typing import T_State
11
+ from nonebot.plugin import PluginMetadata
12
+ from asyncpg.exceptions import InvalidRegularExpressionError
13
+ from arclet.alconna import Alconna, Option, Args, CommandMeta, append
14
+ import os
15
+
16
+ require("nonebot_plugin_datastore")
17
+
18
+ from .config import Config
19
+ from .pic_sql import savepic, rename, delete, write_pic, randpic, load_pic, select_pic
20
+ from .rule import PIC_AMDIN
21
+ from .ext_listener import pic_listen
22
+ from .picture import p_hash
23
+
24
+
25
+ __plugin_meta__ = PluginMetadata(
26
+ name="Savepic",
27
+ description="表情包保存",
28
+ usage="""用法:
29
+ /savepic <文件名> <图片>
30
+ /savepic -g <文件名> <全局图片>
31
+ /savepic -d <文件名> 删除图图片
32
+ /savepic -m <原文件名> <新文件名> 重命名图片
33
+ /randpic <关键词> 随机图片
34
+ <文件名> 发送图片""",
35
+ config=Config,
36
+ homepage="https://github.com/Yan-Zero/nonebot-plugin-savepic",
37
+ type="application",
38
+ supported_adapters=["~onebot.v11"],
39
+ )
40
+
41
+ INVALID_FILENAME_CHARACTERS = r'\/:*?"<>|'
42
+
43
+ p_config = Config.parse_obj(get_driver().config)
44
+
45
+ rpic = on_command("randpic", aliases={"随机图"}, priority=5)
46
+
47
+
48
+ @rpic.handle()
49
+ async def _(bot: Bot, event, args: V11Msg = CommandArg()):
50
+ reg = args.extract_plain_text().strip()
51
+ group_id = (
52
+ "globe"
53
+ if not isinstance(event, GroupMessageEvent)
54
+ else f"qq_group:{event.group_id}"
55
+ )
56
+ try:
57
+ pic = await randpic(reg, group_id)
58
+ if pic:
59
+ file_ = await load_pic(pic.url)
60
+ await bot.send(event, V11Msg([pic.name + "\n", V11Seg.image(file=file_)]))
61
+ except InvalidRegularExpressionError:
62
+ await rpic.finish("正则表达式错误")
63
+ except Exception as ex:
64
+ await rpic.finish(str(ex))
65
+
66
+
67
+ spic = on_command("savepic", aliases={"存图"}, priority=5)
68
+ a_spic = Alconna(
69
+ "/savepic",
70
+ Option("-d", help_text="删除图片"),
71
+ Option("-g", help_text="全局"),
72
+ Option("-ac", help_text="允许相似碰撞"),
73
+ Args.filename[str],
74
+ meta=CommandMeta(description="保存图片,默认保存到本群"),
75
+ )
76
+
77
+
78
+ @spic.handle()
79
+ async def _(
80
+ bot: Bot,
81
+ matcher: Matcher,
82
+ event: GroupMessageEvent,
83
+ state: T_State,
84
+ ):
85
+ command = a_spic.parse(event.message.extract_plain_text())
86
+ if not command.matched:
87
+ await spic.finish(str(command.error_info) + "\n\n" + a_spic.get_help())
88
+
89
+ state["savepiv_group"] = "globe" if command.g else f"qq_group:{event.group_id}"
90
+ filename = command.filename
91
+ for c in INVALID_FILENAME_CHARACTERS:
92
+ filename = filename.replace(c, "-")
93
+ if not filename.endswith((".jpg", ".png", ".gif")):
94
+ filename += ".jpg"
95
+ state["savepiv_filename"] = filename
96
+ state["savepiv_ac"] = command.ac is not None
97
+
98
+ if command.d:
99
+ if not await PIC_AMDIN(bot, event):
100
+ await spic.finish("不支持选项 -d")
101
+ try:
102
+ await delete(filename, state["savepiv_group"])
103
+ except Exception as ex:
104
+ await spic.finish(str(ex))
105
+ await spic.finish("图片已删除")
106
+
107
+ pic = await select_pic(filename, state["savepiv_group"])
108
+ if pic and pic.group == state["savepiv_group"]:
109
+ spic.finish("文件名已存在。")
110
+
111
+ picture = event.message.get("image")
112
+ if not picture and event.reply:
113
+ picture = event.reply.message.get("image")
114
+ if picture:
115
+ matcher.set_arg("picture", picture)
116
+
117
+
118
+ @spic.got("picture", "图呢?")
119
+ async def _(state: T_State, picture: V11Msg = Arg()):
120
+ picture = picture.get("image")
121
+ if not picture:
122
+ await spic.finish("6,这也不是图啊")
123
+
124
+ try:
125
+ dir = await write_pic(picture[0].data["url"], p_config.savepic_dir)
126
+ except Exception as ex:
127
+ await spic.finish("存图失败。" + "\n" + str(ex))
128
+ try:
129
+ await savepic(
130
+ state["savepiv_filename"],
131
+ dir,
132
+ p_hash(await load_pic(dir)),
133
+ state["savepiv_group"],
134
+ state["savepiv_ac"],
135
+ )
136
+ await spic.send("保存成功")
137
+ except Exception as ex:
138
+ os.remove(dir)
139
+ await spic.finish(str(ex))
140
+
141
+
142
+ s_mvpic = on_command("mvpic", priority=5)
143
+ a_mvpic = Alconna(
144
+ "/mvpic",
145
+ Option("-l", args=Args.filename[str], help_text="本地图片", action=append),
146
+ Option("-g", args=Args.filename[str], help_text="全局图片", action=append),
147
+ meta=CommandMeta(description="重命名图片,按照参数先后判断"),
148
+ )
149
+
150
+
151
+ @s_mvpic.handle()
152
+ async def _(
153
+ bot: Bot,
154
+ event: GroupMessageEvent,
155
+ ):
156
+ if not await PIC_AMDIN(bot, event):
157
+ await spic.finish("不支持选项 -m")
158
+
159
+ cmd = a_mvpic.parse(event.message.extract_plain_text())
160
+ if not cmd.matched:
161
+ await s_mvpic.finish(str(cmd.error_info) + "\n\n" + a_mvpic.get_help())
162
+ options = cmd.options
163
+ if not options:
164
+ await s_mvpic.finish("文件名呢?" + "\n\n" + a_mvpic.get_help())
165
+
166
+ sg = "globe" if list(options.keys())[0] == "g" else f"qq_group:{event.group_id}"
167
+ if len(options["g" if sg == "globe" else "l"].args.get("filename", [])) >= 2:
168
+ dg = sg
169
+ else:
170
+ dg = "globe" if sg != "globe" else f"qq_group:{event.group_id}"
171
+ if ("g" if dg == "globe" else "l") not in options:
172
+ await s_mvpic.finish("至多只有一个文件名哦?" + "\n\n" + a_mvpic.get_help())
173
+
174
+ sname = options["g" if sg == "globe" else "l"].args["filename"][0]
175
+ dname = options["g" if dg == "globe" else "l"].args["filename"][
176
+ 1 if sg == dg else 0
177
+ ]
178
+ for c in INVALID_FILENAME_CHARACTERS:
179
+ sname = sname.replace(c, "-")
180
+ dname = dname.replace(c, "-")
181
+ if not sname.endswith((".jpg", ".png", ".gif")):
182
+ sname += ".jpg"
183
+ if not dname.endswith((".jpg", ".png", ".gif")):
184
+ dname += ".jpg"
185
+
186
+ try:
187
+ await rename(sname, dname, sg, dg)
188
+ except Exception as ex:
189
+ await spic.finish(str(ex))
190
+ await spic.finish("图片已重命名")
@@ -3,5 +3,7 @@ from pydantic import BaseModel, Extra
3
3
 
4
4
  class Config(BaseModel, extra=Extra.ignore):
5
5
  """Plugin Config Here"""
6
+
6
7
  savepic_admin: list[str] = []
7
8
  savepic_dir: str = "savepic"
9
+ savepic_sqlurl: str
@@ -0,0 +1,34 @@
1
+ from nonebot import on_message
2
+ from nonebot.adapters.onebot.v11.event import MessageEvent
3
+ from nonebot.internal.adapter.bot import Bot
4
+ from nonebot.adapters.onebot.v11.message import MessageSegment as V11Seg
5
+ from nonebot.adapters.onebot.v11.event import GroupMessageEvent
6
+
7
+ from .pic_sql import load_pic, select_pic
8
+
9
+
10
+ async def endswith_pic(event: MessageEvent):
11
+ text = event.message.extract_plain_text().strip()
12
+ if " " in text:
13
+ return False
14
+ return text.endswith((".jpg", ".png", ".gif"))
15
+
16
+
17
+ pic_listen = on_message(rule=endswith_pic)
18
+
19
+
20
+ @pic_listen.handle()
21
+ async def _(bot: Bot, event: MessageEvent):
22
+ name = event.message.extract_plain_text().strip()
23
+ group_id = (
24
+ "globe"
25
+ if not isinstance(event, GroupMessageEvent)
26
+ else f"qq_group:{event.group_id}"
27
+ )
28
+ try:
29
+ pic = await select_pic(name, group_id)
30
+ if pic:
31
+ file_ = await load_pic(pic.url)
32
+ await bot.send(event, V11Seg.image(file=file_))
33
+ except Exception as ex:
34
+ await pic_listen.finish(str(ex))
@@ -1,14 +1,18 @@
1
1
  from nonebot_plugin_datastore import get_plugin_data
2
2
  from sqlalchemy import TEXT
3
+ from sqlalchemy.dialects.postgresql import BIT
3
4
  from sqlalchemy.orm import Mapped, mapped_column
4
5
 
5
6
  plugin_data = get_plugin_data()
6
7
  plugin_data.use_global_registry()
7
8
  Model = plugin_data.Model
8
9
 
10
+
9
11
  class PicData(Model):
10
12
  """消息记录"""
11
13
 
14
+ __tablename__ = "picdata"
15
+
12
16
  __table_args__ = {"extend_existing": True}
13
17
 
14
18
  id: Mapped[int] = mapped_column(primary_key=True)
@@ -17,4 +21,6 @@ class PicData(Model):
17
21
  group: Mapped[str] = mapped_column(TEXT)
18
22
  """ 所属群组 id """
19
23
  url: Mapped[str] = mapped_column(TEXT)
20
- """ 图片目录 """
24
+ """ 图片目录 """
25
+ phash: Mapped[bytes] = mapped_column(BIT(256))
26
+ """ 图片的 phash 值 """
@@ -0,0 +1,219 @@
1
+ import sqlalchemy as sa
2
+ from sqlalchemy.ext.asyncio.session import AsyncSession
3
+ from sqlalchemy.ext.asyncio import create_async_engine
4
+ from sqlalchemy import select
5
+ from nonebot import get_driver
6
+ import os
7
+ import random
8
+ import csv
9
+ import hashlib
10
+ from sqlalchemy import cast
11
+
12
+ from .model import PicData, BIT
13
+ from .picture import load_pic, p_hash
14
+ from .config import Config
15
+
16
+ gdriver = get_driver()
17
+ p_config = Config.parse_obj(gdriver.config)
18
+ _async_database = None
19
+
20
+
21
+ def AsyncDatabase():
22
+ if not _async_database:
23
+ raise RuntimeError("Database is not initialized")
24
+ return _async_database
25
+
26
+
27
+ def del_pic(url: str):
28
+ if os.path.exists(url):
29
+ os.remove(url)
30
+
31
+
32
+ async def write_pic(url: str, des_dir: str = None) -> str:
33
+ if not des_dir:
34
+ des_dir = "savepic"
35
+ if not os.path.exists(des_dir):
36
+ os.makedirs(des_dir)
37
+
38
+ byte = await load_pic(url)
39
+ filename = hashlib.sha256(byte).hexdigest()
40
+ while os.path.exists(os.path.join(des_dir, filename)):
41
+ filename += "_"
42
+ with open(os.path.join(des_dir, filename), "wb") as f:
43
+ f.write(byte)
44
+ return os.path.join(des_dir, filename)
45
+
46
+
47
+ async def select_pic(filename: str, group: str):
48
+ async with AsyncSession(_async_database) as db_session:
49
+ pic = await db_session.scalar(
50
+ select(PicData)
51
+ .where(PicData.name == filename)
52
+ .where(PicData.group == group)
53
+ )
54
+ if pic:
55
+ return pic
56
+
57
+ pic = await db_session.scalar(
58
+ select(PicData)
59
+ .where(PicData.name == filename)
60
+ .where(PicData.group == "globe")
61
+ )
62
+ if pic:
63
+ return pic
64
+
65
+
66
+ async def savepic(
67
+ filename: str,
68
+ url: str,
69
+ hash: bytes,
70
+ group_id: str = "globe",
71
+ collision_allow: bool = False,
72
+ ):
73
+ pic = PicData(
74
+ group=group_id,
75
+ name=filename,
76
+ url=url,
77
+ phash=hash,
78
+ )
79
+ async with AsyncSession(_async_database) as db_session:
80
+ # select the name == ""
81
+ despic = await select_pic(filename, group_id)
82
+ if despic and despic.group == group_id:
83
+ raise Exception("文件名已存在。")
84
+ despic = await db_session.scalar(
85
+ select(PicData)
86
+ .where(PicData.group == group_id)
87
+ .where(
88
+ sa.func.bit_count(PicData.phash.bitwise_xor(cast(hash, BIT(256)))) <= 7
89
+ )
90
+ )
91
+ if despic and not collision_allow:
92
+ raise Exception("相似图片已存在。" + "\n" + despic.name)
93
+
94
+ if random.randint(1, 10) == 1:
95
+ empty = await db_session.scalar(select(PicData).where(PicData.name == ""))
96
+ else:
97
+ empty = None
98
+
99
+ if empty:
100
+ empty.group = group_id
101
+ empty.url = url
102
+ empty.name = filename
103
+ empty.phash = pic.phash
104
+ await db_session.merge(empty)
105
+ else:
106
+ db_session.add(pic)
107
+ await db_session.commit()
108
+
109
+
110
+ async def init_db():
111
+ # check if the table exists
112
+ global _async_database
113
+ _async_database = create_async_engine(
114
+ p_config.savepic_sqlurl, pool_pre_ping=True, pool_size=25, max_overflow=20
115
+ )
116
+
117
+ try:
118
+ metadata = sa.MetaData()
119
+ table = sa.Table(
120
+ "picdata",
121
+ metadata,
122
+ sa.Column("id", sa.Integer, primary_key=True),
123
+ sa.Column("name", sa.TEXT, nullable=False),
124
+ sa.Column("group", sa.TEXT, nullable=False),
125
+ sa.Column("url", sa.TEXT, nullable=False),
126
+ sa.Column("phash", BIT(256), nullable=False),
127
+ )
128
+ async with _async_database.begin() as conn:
129
+ await conn.run_sync(metadata.create_all)
130
+ except Exception:
131
+ pass
132
+
133
+
134
+ @gdriver.on_startup
135
+ async def _():
136
+ if not p_config.savepic_sqlurl:
137
+ raise Exception("请配置 savepic_sqlurl")
138
+
139
+ await init_db()
140
+ if os.path.exists("savepic_picdata.csv"):
141
+ # 加载旧版数据
142
+ print("加载旧版数据")
143
+ failed = []
144
+ with open("savepic_picdata.csv", "r") as f:
145
+ for i in csv.reader(f):
146
+ _, name, group, url = i
147
+ if not name or url == "url":
148
+ continue
149
+ t = await select_pic(name, group)
150
+ if t and t.group == group:
151
+ continue
152
+ p_hash_ = p_hash(await load_pic(url))
153
+ print(i)
154
+ try:
155
+ await savepic(name, url, p_hash_, group, True)
156
+ except Exception as ex:
157
+ print(ex)
158
+ failed.append(i)
159
+ os.remove("savepic_picdata.csv")
160
+ with open("savepic.csv", "w+") as f:
161
+ writer = csv.writer(f)
162
+ writer.writerow(["id", "name", "group", "url"])
163
+ for i in failed:
164
+ writer.writerow(i)
165
+
166
+
167
+ async def rename(ori: str, des: str, s_group: str, d_group: str):
168
+ async with AsyncSession(_async_database) as db_session:
169
+ pic = await db_session.scalar(
170
+ select(PicData)
171
+ .where(PicData.name == ori)
172
+ .where(sa.or_(PicData.group == s_group, PicData.group == "globe"))
173
+ )
174
+ if not pic:
175
+ raise Exception("重命名失败,没有找到图片")
176
+ despic = await db_session.scalar(
177
+ select(PicData).where(
178
+ sa.and_(PicData.name == des, PicData.group == d_group)
179
+ )
180
+ )
181
+ if despic:
182
+ raise Exception("重命名失败,目标文件名已存在")
183
+ pic.name = des
184
+ pic.group = d_group
185
+
186
+ await db_session.merge(pic)
187
+ await db_session.commit()
188
+
189
+
190
+ async def delete(filename: str, group: str):
191
+ async with AsyncSession(_async_database) as db_session:
192
+ pic = await db_session.scalar(
193
+ select(PicData).where(
194
+ sa.and_(PicData.name == filename, PicData.group == group)
195
+ )
196
+ )
197
+ if pic:
198
+ del_pic(pic.url)
199
+ pic.name = ""
200
+ await db_session.merge(pic)
201
+ await db_session.commit()
202
+ else:
203
+ raise Exception("删除失败,没有找到图片")
204
+
205
+
206
+ async def randpic(reg: str, group: str = "globe") -> PicData:
207
+ reg = reg.strip()
208
+ if not reg:
209
+ reg = ".*"
210
+ async with AsyncSession(_async_database) as db_session:
211
+ pic = await db_session.scalar(
212
+ select(PicData)
213
+ .where(sa.or_(PicData.group == group, PicData.group == "globe"))
214
+ .where(PicData.name != "")
215
+ .order_by(sa.func.random())
216
+ .where(PicData.name.regexp_match(reg))
217
+ )
218
+ if pic:
219
+ return pic
@@ -0,0 +1,48 @@
1
+ import scipy.fftpack as fft
2
+ import numpy as np
3
+ import httpx
4
+ import os
5
+ from PIL import Image
6
+ from io import BytesIO
7
+
8
+ _httpx_async = None
9
+
10
+
11
+ def remove_alpha(im: Image) -> Image:
12
+ # Only process if image has transparency (http://stackoverflow.com/a/1963146)
13
+ if im.mode in ("RGBA", "LA") or (im.mode == "P" and "transparency" in im.info):
14
+ # Need to convert to RGBA if LA format due to a bug in PIL (http://stackoverflow.com/a/1963146)
15
+ alpha = im.convert("RGBA").split()[-1]
16
+
17
+ # Create a new background image of our matt color.
18
+ # Must be RGBA because paste requires both images have the same format
19
+ # (http://stackoverflow.com/a/8720632 and http://stackoverflow.com/a/9459208)
20
+ bg = Image.new("RGBA", im.size, (255, 255, 255) + (255,))
21
+ bg.paste(im, mask=alpha)
22
+ return bg
23
+
24
+ else:
25
+ return im
26
+
27
+
28
+ async def load_pic(url: str) -> bytes:
29
+ global _httpx_async
30
+ if url.startswith("http"):
31
+ if not _httpx_async:
32
+ _httpx_async = httpx.AsyncClient()
33
+ resp = await _httpx_async.get(url)
34
+ resp.raise_for_status()
35
+ return resp.content
36
+ if os.path.exists(url):
37
+ with open(url, "rb") as f:
38
+ return f.read()
39
+ raise Exception("不支持的 URL")
40
+
41
+
42
+ def p_hash(img: bytes) -> bytes:
43
+ pic = remove_alpha(
44
+ Image.open(BytesIO(img)).resize((128, 128), Image.Resampling.LANCZOS)
45
+ ).convert("L")
46
+ dct = fft.dct(np.array(pic))
47
+ average = np.median(dct)
48
+ return np.packbits(dct[:16, :16] > average).tobytes()
@@ -4,8 +4,8 @@ This module is used to define the permission of the bot.
4
4
 
5
5
  import nonebot
6
6
  from nonebot.adapters import Bot, Event
7
- from nonebot_plugin_datastore import get_plugin_data
8
7
  from nonebot.internal.permission import Permission
8
+
9
9
  from .config import Config
10
10
 
11
11
  global_config = nonebot.get_driver().config
@@ -32,4 +32,4 @@ class Savepic_Admin(Permission):
32
32
  )
33
33
 
34
34
 
35
- PIC_AMDIN = Permission(Savepic_Admin())
35
+ PIC_AMDIN = Permission(Savepic_Admin())
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "nonebot-plugin-savepic"
3
- version = "0.1.3"
3
+ version = "0.2.0"
4
4
  description = "保存表情包(语录)与随机出图"
5
5
  authors = ["Yan <1964649083@qq.com>"]
6
6
  license = "MIT"
@@ -9,11 +9,15 @@ packages = [{include = "nonebot_plugin_savepic"}]
9
9
  homepage = "https://github.com/Yan-Zero/nonebot-plugin-savepic"
10
10
 
11
11
  [tool.poetry.dependencies]
12
- python = "^3.10"
12
+ python = "^3.10, <3.13"
13
13
  nonebot2 = "^2.0.1"
14
14
  nonebot-plugin-datastore = "^1.1.0"
15
15
  asyncpg = "^0.28.0"
16
16
  nonebot-adapter-onebot = "^2.2.3"
17
+ arclet-alconna = "^1.7.14"
18
+ numpy = "^1.25.2"
19
+ scipy = "^1.11.1"
20
+ pillow = "^10.0.0"
17
21
 
18
22
 
19
23
  [build-system]
@@ -1,157 +0,0 @@
1
- from nonebot import require
2
- from nonebot import get_driver
3
-
4
- require("nonebot_plugin_datastore")
5
-
6
- from nonebot import on_command, on_message
7
- from nonebot.params import CommandArg, Arg
8
- from nonebot.adapters.onebot.v11.message import Message as V11Msg
9
- from nonebot.adapters.onebot.v11.message import MessageSegment as V11Seg
10
- from nonebot.adapters.onebot.v11 import Bot
11
- from nonebot.adapters.onebot.v11.event import MessageEvent
12
- from nonebot.adapters.onebot.v11.event import GroupMessageEvent
13
- from nonebot.matcher import Matcher
14
- from nonebot.typing import T_State
15
-
16
- from asyncpg.exceptions import InvalidRegularExpressionError
17
-
18
- from .config import Config
19
- from .pic_sql import savepic, rename, delete, write_pic, randpic, load_pic, select_pic
20
- from .rule import PIC_AMDIN
21
-
22
- from nonebot.plugin import PluginMetadata
23
-
24
- __plugin_meta__ = PluginMetadata(
25
- name="Savepic",
26
- description="表情包保存",
27
- usage="""用法:
28
- /savepic <文件名> <图片>
29
- /savepic -g <文件名> <全局图片>
30
- /savepic -d <文件名> 删除图图片
31
- /savepic -m <原文件名> <新文件名> 重命名图片
32
- /randpic <关键词> 随机图片
33
- <文件名> 发送图片""",
34
- config=Config,
35
- homepage="https://github.com/Yan-Zero/nonebot-plugin-savepic",
36
- type="application",
37
- supported_adapters=[
38
- '~onebot.v11'
39
- ]
40
- )
41
-
42
- INVALID_FILENAME_CHARACTERS = r'\/:*?"<>|'
43
-
44
- global_config = get_driver().config
45
- p_config = Config.parse_obj(global_config)
46
-
47
- rpic = on_command("randpic", aliases={"随机图"}, priority=5)
48
- @rpic.handle()
49
- async def _(bot: Bot, event, args: V11Msg = CommandArg()):
50
- reg = args.extract_plain_text().strip()
51
- group_id = 'globe' if not isinstance(event, GroupMessageEvent) else f"qq_group:{event.group_id}"
52
- try:
53
- pic = await randpic(reg, group_id)
54
- if pic:
55
- file_ = await load_pic(pic.url)
56
- await bot.send(event, V11Msg([pic.name + "\n", V11Seg.image(file=file_)]))
57
- except InvalidRegularExpressionError:
58
- await rpic.finish('正则表达式错误')
59
- except Exception as ex:
60
- await rpic.finish(str(ex))
61
-
62
- spic = on_command("savepic", aliases={"存图"}, priority=5)
63
- @spic.handle()
64
- async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, state: T_State, args: V11Msg = CommandArg()):
65
- if not args:
66
- await spic.finish('''用法:
67
- /savepic <文件名> <图片>
68
- /savepic -g <文件名> <全局图片>
69
- /savepic -d <文件名> 删除图图片
70
- /savepic -m <原文件名> <新文件名> 重命名图片''')
71
- params = args.extract_plain_text().strip().split()
72
- state['savepiv_group'] = 'globe' if '-g' in params and await PIC_AMDIN(bot, event) else f"qq_group:{event.group_id}"
73
- _d = '-d' in params
74
- _m = '-m' in params
75
- if _d:
76
- params.remove('-d')
77
- if _m:
78
- params.remove('-m')
79
- if '-g' in params:
80
- params.remove('-g')
81
-
82
- if not params:
83
- await spic.finish("文件名?")
84
- filename = params[0].strip()
85
- for c in INVALID_FILENAME_CHARACTERS:
86
- filename = filename.replace(c, "-")
87
- if not filename.endswith(('.jpg', '.png', '.gif')):
88
- filename += '.jpg'
89
-
90
- if _d:
91
- if not await PIC_AMDIN(bot, event):
92
- await spic.finish('不支持选项 -d')
93
- try:
94
- await delete(filename, state['savepiv_group'])
95
- except Exception as ex:
96
- await spic.finish(str(ex))
97
- await spic.finish('图片已删除')
98
-
99
- if _m:
100
- if not await PIC_AMDIN(bot, event):
101
- await spic.finish('不支持选项 -m')
102
- if len(params) <= 1:
103
- await spic.finish("目标文件名?")
104
- for c in INVALID_FILENAME_CHARACTERS:
105
- params[1] = params[1].replace(c, "-")
106
- if not params[1].endswith(('.jpg', '.png', '.gif')):
107
- params[1] += '.jpg'
108
-
109
- try:
110
- await rename(filename, params[1], f"qq_group:{event.group_id}", state['savepiv_group'])
111
- except Exception as ex:
112
- await spic.finish(str(ex))
113
- await spic.finish('图片已重命名')
114
-
115
- pic = await select_pic(filename, state['savepiv_group'])
116
- if pic and pic.group == state['savepiv_group']:
117
- spic.finish("图片已存在。")
118
- picture = args.get('image')
119
- state['savepiv_filename'] = filename
120
- if not picture and event.reply:
121
- picture = event.reply.message.get('image')
122
- if picture:
123
- matcher.set_arg('picture', picture)
124
-
125
- @spic.got('picture', "图呢?")
126
- async def _(bot: Bot, event: GroupMessageEvent, state: T_State, picture: V11Msg = Arg()):
127
- picture = picture.get('image')
128
- if not picture:
129
- await spic.finish('6,这也不是图啊')
130
- try:
131
- dir = await write_pic(picture[0].data['url'], p_config.savepic_dir)
132
- except Exception as ex:
133
- await spic.finish("存图失败。" + '\n' + str(ex))
134
- try:
135
- await savepic(state['savepiv_filename'], dir, state['savepiv_group'])
136
- await spic.send("保存成功")
137
- except Exception as ex:
138
- await spic.finish(str(ex))
139
-
140
- async def endswith_pic(event: MessageEvent):
141
- text = event.message.extract_plain_text().strip()
142
- if ' ' in text:
143
- return False
144
- return text.endswith(('.jpg', '.png', '.gif'))
145
-
146
- pic_listen = on_message(rule=endswith_pic)
147
- @pic_listen.handle()
148
- async def _(bot: Bot, event: MessageEvent):
149
- name = event.message.extract_plain_text().strip()
150
- group_id = 'globe' if not isinstance(event, GroupMessageEvent) else f"qq_group:{event.group_id}"
151
- try:
152
- pic = await select_pic(name, group_id)
153
- if pic:
154
- file_ = await load_pic(pic.url)
155
- await bot.send(event, V11Seg.image(file=file_))
156
- except Exception as ex:
157
- await rpic.finish(str(ex))
@@ -1,150 +0,0 @@
1
- from nonebot_plugin_datastore import create_session
2
- import sqlalchemy as sa
3
- from sqlalchemy import select, update
4
- import hashlib
5
- import httpx
6
- import os
7
- import random
8
- from sqlalchemy.sql import func
9
- from nonebot_plugin_datastore.db import post_db_init
10
-
11
- from .model import PicData
12
-
13
- httpx_async = None
14
-
15
- async def load_pic(url: str)-> bytes:
16
- global httpx_async
17
- if url.startswith("http"):
18
- if not httpx_async:
19
- httpx_async = httpx.AsyncClient()
20
- resp = await httpx_async.get(url)
21
- resp.raise_for_status()
22
- return resp.content
23
- if os.path.exists(url):
24
- with open(url, 'rb') as f:
25
- return f.read()
26
- raise Exception("不支持的统一资源定位器")
27
-
28
- def del_pic(url: str):
29
- if os.path.exists(url):
30
- os.remove(url)
31
-
32
- async def write_pic(url: str, des_dir: str = None) -> str:
33
- if not des_dir:
34
- des_dir = 'savepic'
35
- if not os.path.exists(des_dir):
36
- os.makedirs(des_dir)
37
-
38
- byte = await load_pic(url)
39
- filename = hashlib.sha256(byte).hexdigest()
40
- while os.path.exists(os.path.join(des_dir, filename)):
41
- filename = hashlib.sha256((filename + str(random.randint(0, 100000))).encode()).hexdigest()
42
- with open(os.path.join(des_dir, filename), 'wb+') as f:
43
- f.write(byte)
44
- return os.path.join(des_dir, filename)
45
-
46
- async def select_pic(filename: str, group: str):
47
- async with create_session() as db_session:
48
- pic = await db_session.scalar(select(PicData)
49
- .where(PicData.name == filename)
50
- .where(PicData.group == group))
51
- if pic:
52
- return pic
53
-
54
- pic = await db_session.scalar(select(PicData)
55
- .where(PicData.name == filename)
56
- .where(PicData.group == 'globe'))
57
- if pic:
58
- return pic
59
-
60
- async def savepic(filename: str, dir: str, group_id: str):
61
- pic = PicData(
62
- group = group_id,
63
- name = filename,
64
- url = dir,
65
- )
66
- async with create_session() as db_session:
67
- # select the name == ""
68
- despic = await select_pic(filename, group_id)
69
- if despic and despic.group == group_id:
70
- raise Exception("文件名已存在。")
71
-
72
- if random.randint(1, 10) == 1:
73
- empty = await db_session.scalar(select(PicData).where(PicData.name == ""))
74
- else:
75
- empty = None
76
-
77
- if empty:
78
- empty.group = group_id
79
- empty.url = dir
80
- empty.name = filename
81
- await db_session.merge(empty)
82
- else:
83
- db_session.add(pic)
84
- await db_session.commit()
85
-
86
- @post_db_init
87
- async def init():
88
- if not os.path.exists('history_savepic'):
89
- return
90
- for root, dirs, files in os.walk('history_savepic'):
91
- group = root.split('/')[-1]
92
- for file in files:
93
- dir = os.path.join("savepic", file.replace(' ', ''))
94
- if os.path.exists(dir):
95
- dir = await write_pic(os.path.join(root, file))
96
- os.remove(os.path.join(root, file))
97
- else:
98
- os.rename(os.path.join(root, file), dir)
99
-
100
- try:
101
- await savepic(file, dir, group)
102
- print(f"save {file} to {group}")
103
- except Exception as ex:
104
- print(ex)
105
- os.remove(dir)
106
-
107
- async def rename(ori: str, des: str, s_group: str, d_group: str):
108
- async with create_session() as db_session:
109
- pic = await db_session.scalar(select(PicData).where(
110
- PicData.name == ori
111
- ).where(sa.or_(PicData.group == s_group, PicData.group == "globe")))
112
- if not pic:
113
- raise Exception("重命名失败,没有找到图片")
114
- despic = await db_session.scalar(select(PicData).where(
115
- sa.and_(PicData.name == des, PicData.group == d_group)
116
- ))
117
- if despic:
118
- raise Exception("重命名失败,目标文件名已存在")
119
- pic.name = des
120
- pic.group = d_group
121
-
122
- await db_session.merge(pic)
123
- await db_session.commit()
124
-
125
- async def delete(filename: str, group: str):
126
- async with create_session() as db_session:
127
- pic = await db_session.scalar(select(PicData).where(
128
- sa.and_(PicData.name == filename, PicData.group == group)
129
- ))
130
- if pic:
131
- del_pic(pic.url)
132
- pic.name = ""
133
- await db_session.merge(pic)
134
- await db_session.commit()
135
- else:
136
- raise Exception("删除失败,没有找到图片")
137
-
138
- async def randpic(reg: str, group: str = "globe") -> PicData:
139
- reg = reg.strip()
140
- if not reg:
141
- reg = ".*"
142
- async with create_session() as db_session:
143
- pic = await db_session.scalar(select(PicData)
144
- .where(sa.or_(PicData.group == group, PicData.group == "globe"))
145
- .where(PicData.name != "")
146
- .order_by(func.random())
147
- .where(PicData.name.regexp_match(reg))
148
- )
149
- if pic:
150
- return pic