.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # nonebot backup group files
4 |
5 | ✨ 一个基于 [NoneBot2](https://github.com/nonebot/nonebot2) 的插件,用于备份 QQ 群文件 ✨
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ## 术语介绍
30 |
31 | `临时文件`: 不在文件夹内的文件(因为随手传个视频 大图也会进群文件,并且不在文件夹里的)
32 |
33 | ## 用途
34 |
35 | 备份群文件
36 |
37 | 会检索是否已备份, 所以请放心使用
38 |
39 | 会备份到 robot 安装目录/qqgroup/qq 群号
40 |
41 | ## 用法
42 |
43 | 配置文件`.env.*`中添加:
44 |
45 | ```python
46 | backup_group=[""] # 启用插件的群, 默认为[],代表所有群
47 | backup_command="备份群文件" # 设置插件触发命令
48 | backup_maxsize=300 # 超过多少M的文件不备份, 会在后面提醒哪些没备份
49 | backup_minsize=0.01 # 低于多少M的文件不备份, 默认10kb
50 |
51 | backup_temp_files = True # 是否备份`临时文件`,默认备份
52 | backup_temp_file_ignore = [".gif", ".png", ".jpg", ".mp4"] # 忽略`临时文件`哪些文件后缀
53 |
54 | recovery_command = "恢复群文件"
55 | ```
56 |
57 | ### 使用方法
58 |
59 | 直接打 `备份群文件` 或者自定义的触发命令即可
60 |
61 | ## 恢复群文件
62 |
63 | 请把备份的旧群文件夹名改为新群id, qqgroup/112334567 -> qqgroup/77655432
64 |
65 | 如何使用: 对应群输入`恢复群文件`即可
66 |
67 | 前提1: 机器人需要有管理员权限(否则无法创建文件夹)
68 |
69 | 前提2: 小bug?新群需要手动创建一下文件夹(任意一个),然后删除,不然第一次机器人好像无法创建文件夹
70 |
71 | 前提3: 小bug?上传空文件(0b)会失败 不过正经人也不会上传空文件, 就没写判断
72 |
73 | ## TODO
74 |
75 | 备份群精华: 不知道为啥12月7日之前的消息获取不到
76 |
77 | ## 缺点
78 |
79 | 不会同步删除群文件
80 |
81 | ## 参考来源
82 |
83 | [DirectLinker](https://github.com/ninthseason/nonebot-plugin-directlinker)
84 |
85 | 因为我想备份群文件, 防止炸群, 然后发现只有这一个类似的~ 无奈只能魔改下
86 |
--------------------------------------------------------------------------------
/nonebot_plugin_backup/__init__.py:
--------------------------------------------------------------------------------
1 | from nonebot import get_driver, logger, on_shell_command
2 | from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent
3 | from nonebot.typing import T_State
4 | from nonebot.rule import ArgumentParser
5 | from collections import deque
6 |
7 | import requests
8 | import os
9 | import time
10 | from pathlib import Path
11 |
12 |
13 | backup_group = get_driver().config.dict().get('backup_group', [])
14 | backup_command = get_driver().config.dict().get('backup_command', "备份群文件")
15 | backup_maxsize = get_driver().config.dict().get('backup_maxsize', 300)
16 | backup_minsize = get_driver().config.dict().get('backup_minsize', 0.01)
17 | backup_temp_files = get_driver().config.dict().get('backup_temp_files', True)
18 | backup_temp_file_ignore = get_driver().config.dict().get(
19 | 'backup_temp_file_ignore', [".gif", ".png", ".jpg", ".mp4"])
20 |
21 |
22 | linker_parser = ArgumentParser(add_help=False)
23 | linker = on_shell_command(backup_command, parser=linker_parser, priority=1)
24 |
25 | recovery_command = get_driver().config.dict().get('recovery_command', "恢复群文件")
26 | recovery_parser = ArgumentParser(add_help=False)
27 | recovery = on_shell_command(
28 | recovery_command, parser=recovery_parser, priority=1)
29 |
30 |
31 | essence_command = get_driver().config.dict().get('essence_command', "备份群精华")
32 | essence_parser = ArgumentParser(add_help=False)
33 | essence = on_shell_command(
34 | essence_command, parser=essence_parser, priority=1)
35 |
36 |
37 | class EventInfo:
38 | fdindex = -1
39 | fsuccess = fjump = fsizes = 0
40 | fdtoolarge = []
41 | fbroken = []
42 | fdnames = []
43 |
44 | def __init__(self) -> None:
45 | ...
46 |
47 | def init(self) -> None:
48 | self.fdindex = -1
49 | self.fsuccess = self.fjump = self.fsizes = 0
50 | self.fdtoolarge = []
51 | self.fbroken = []
52 | self.fdnames = []
53 |
54 |
55 | async def SaveToDisk(bot, ff, fdpath, EIF, gid):
56 | fname = ff["file_name"]
57 | fid = ff["file_id"]
58 | fbusid = ff["busid"]
59 | fsize = ff["file_size"]
60 | fpath = Path(fdpath, fname)
61 |
62 | if fsize/1024/1024 < backup_minsize:
63 | return
64 |
65 | if fsize/1024/1024 > backup_maxsize:
66 | EIF.fdtoolarge.append(
67 | EIF.fdnames[EI.fdindex] + "/" + fname)
68 | return
69 |
70 | if not Path(fpath).exists():
71 | try:
72 |
73 | finfo = await bot.get_group_file_url(group_id=gid, file_id=str(fid), bus_id=int(fbusid))
74 | url = finfo['url']
75 | req = requests.get(url)
76 |
77 | if not Path(fdpath).exists():
78 | os.makedirs(fdpath)
79 | with open(fpath, 'wb') as mfile:
80 | mfile.write(req.content)
81 | EIF.fsizes += fsize
82 | EIF.fsuccess += 1
83 | except Exception as e:
84 | EIF.fbroken.append(fdpath + "/" + fname)
85 | print(e)
86 | logger.debug("文件获取不到/已损坏:" + fdpath + "/" + fname)
87 | else:
88 | EIF.fsizes += Path(fpath).stat().st_size
89 | EIF.fjump += 1
90 |
91 |
92 | async def createFolder(bot, root_dir, gid):
93 | root = await bot.get_group_root_files(group_id=gid)
94 | folders = root.get("folders")
95 | fdnames = []
96 | fdnames.extend([i["folder_name"] for i in folders])
97 |
98 | for parent, dirs, files in os.walk(root_dir):
99 | if dirs:
100 | for fd_name in dirs:
101 | if fd_name not in fdnames:
102 | print(fd_name)
103 | await bot.create_group_file_folder(
104 | group_id=gid, name=fd_name, parent_i="/")
105 |
106 |
107 | async def upload_files(bot, gid, folder_id, root_dir):
108 | group_root = await bot.get_group_files_by_folder(group_id=gid, folder_id=folder_id)
109 | files = group_root.get("files")
110 | filenames = []
111 | if files:
112 | filenames = [ff["file_name"] for ff in files]
113 | if os.path.exists(root_dir):
114 | for entry in os.scandir(root_dir):
115 | if entry.is_file() and entry.name not in filenames:
116 | absolute_path = Path(root_dir).resolve().joinpath(entry.name)
117 |
118 | await bot.upload_group_file(
119 | group_id=gid, file=str(absolute_path), name=entry.name, folder=folder_id)
120 |
121 | EI = EventInfo()
122 |
123 |
124 | @essence.handle()
125 | async def essen(bot: Bot, event: GroupMessageEvent, state: T_State):
126 | gid = event.group_id
127 |
128 | args = vars(state.get("_args"))
129 | logger.debug(args)
130 |
131 | messages = []
132 | essen_list = await bot.get_essence_msg_list(group_id=gid)
133 | for es in essen_list:
134 | print("------------")
135 | print(es["sender_nick"])
136 | print(es["message_id"])
137 | print(es["sender_time"])
138 | message_id = es["message_id"]
139 | # try:
140 | msg = await bot.get_msg(message_id=message_id)
141 | print(msg["message"])
142 | node = {
143 | "type": "node",
144 | "data": {
145 | "name": "精华收集者",
146 | "uin": "10086",
147 | "content": msg["message"]
148 | }}
149 | messages.append(node)
150 | # except Exception as e:
151 | # print(e)
152 | await bot.send_group_forward_msg(group_id=491207941, messages=messages)
153 |
154 | await essence.finish("备份完成")
155 |
156 |
157 | @recovery.handle()
158 | async def recover(bot: Bot, event: GroupMessageEvent, state: T_State):
159 | EI.init()
160 | gid = event.group_id
161 | if str(gid) in backup_group or backup_group == []:
162 | args = vars(state.get("_args"))
163 | logger.debug(args)
164 |
165 | await bot.send(event, "恢复中,请稍后(需要管理员权限)")
166 | # tstart = time.time()
167 | root_dir = "./qqgroup/" + str(gid)
168 |
169 | # 创建群文件夹
170 | await createFolder(bot, root_dir, gid)
171 |
172 | # 上传文件
173 |
174 | root = await bot.get_group_root_files(group_id=gid)
175 | folders = root.get("folders")
176 | # fdpath = "./qqgroup/" + str(event.group_id)
177 |
178 | await upload_files(bot, gid, "/", root_dir)
179 |
180 | # 广度优先搜索
181 | dq = deque()
182 |
183 | if folders:
184 | dq.extend([i["folder_id"] for i in folders])
185 | EI.fdnames.extend([i["folder_name"] for i in folders])
186 |
187 | while dq:
188 | EI.fdindex += 1
189 | _ = dq.popleft()
190 | logger.debug("下一个搜索的文件夹:" + _)
191 | root = await bot.get_group_files_by_folder(group_id=gid, folder_id=_)
192 |
193 | await upload_files(bot, gid, _, root_dir + "/" + EI.fdnames[EI.fdindex])
194 |
195 | await recovery.finish("恢复完成")
196 |
197 |
198 | @linker.handle()
199 | async def link(bot: Bot, event: GroupMessageEvent, state: T_State):
200 | EI.init()
201 | gid = event.group_id
202 | if str(gid) in backup_group or backup_group == []:
203 | args = vars(state.get("_args"))
204 | logger.debug(args)
205 |
206 | await bot.send(event, "备份中,请稍后…(不会备份根目录文件,请把重要文件放文件夹里)")
207 | tstart = time.time()
208 | root = await bot.get_group_root_files(group_id=gid)
209 | folders = root.get("folders")
210 | if backup_temp_files:
211 | files = root.get("files")
212 | fdpath = "./qqgroup/" + str(event.group_id)
213 | if files:
214 | for ff in files:
215 | suf = Path(ff["file_name"]).suffix
216 | if suf in backup_temp_file_ignore:
217 | continue
218 |
219 | await SaveToDisk(bot, ff, fdpath, EI, gid)
220 |
221 | # 广度优先搜索
222 | dq = deque()
223 |
224 | if folders:
225 | dq.extend([i["folder_id"] for i in folders])
226 | EI.fdnames.extend([i["folder_name"] for i in folders])
227 |
228 | while dq:
229 | EI.fdindex += 1
230 | _ = dq.popleft()
231 | logger.debug("下一个搜索的文件夹:" + _)
232 | root = await bot.get_group_files_by_folder(group_id=gid, folder_id=_)
233 |
234 | fdpath = "./qqgroup/" + \
235 | str(gid) + "/" + EI.fdnames[EI.fdindex]
236 |
237 | file = root.get("files")
238 |
239 | if file:
240 | for ff in file:
241 | await SaveToDisk(bot, ff, fdpath, EI, gid)
242 |
243 | if len(EI.fdtoolarge) == 0:
244 | EI.fdtoolarge = "无"
245 | else:
246 | EI.fdtoolarge = "\n".join(EI.fdtoolarge)
247 |
248 | if len(EI.fbroken) == 0:
249 | EI.fbroken = ""
250 | else:
251 | EI.fbroken = "检测到损坏文件:" + '\n'.join(EI.fbroken)
252 |
253 | EI.fsizes = round(EI.fsizes/1024/1024, 2)
254 | tsum = round(time.time()-tstart, 2)
255 |
256 | await linker.finish("此次备份耗时%2d秒; 共备份%d个文件,跳过已备份%d个文件, 累计备份大小%.2f M,\n未备份大文件列表(>%dm):\n%s\n%s" % (tsum, EI.fsuccess, EI.fjump, EI.fsizes, backup_maxsize, EI.fdtoolarge, EI.fbroken))
257 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | with open("README.md", "r", encoding="utf-8") as fh:
4 | long_description = fh.read()
5 |
6 | setup(
7 | name="nonebot-plugin-backup",
8 | version="1.0.5",
9 | author="Yueli",
10 | author_email="yuelioi1210@gmail.com",
11 | description="A plugin based on NoneBot2 to backup files in qq group.",
12 | long_description=long_description,
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/Yuelioi/nonebot-plugin-backup",
15 | project_urls={
16 | "Bug Tracker": "https://github.com/Yuelioi/nonebot-plugin-backup/issues",
17 | },
18 | classifiers=[
19 | "Programming Language :: Python :: 3",
20 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
21 | "Operating System :: OS Independent",
22 | ],
23 | packages=["nonebot_plugin_backup"],
24 | python_requires=">=3.7",
25 | install_requires=[
26 | "nonebot2 >= 2.0.0b2",
27 | "nonebot-adapter-onebot >= 2.0.0b1",
28 | 'requests>=2.28.1'
29 | ],
30 | )
31 |
--------------------------------------------------------------------------------
/test/1.py:
--------------------------------------------------------------------------------
1 | a = [".py", ".ass"]
2 |
--------------------------------------------------------------------------------