├── .env
├── .env.dev
├── .env.prod
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── config.yaml
├── gocq-template.yml
├── pyproject.toml
└── src
├── common
├── config.py
└── define.py
└── plugins
├── init
└── __init__.py
├── recv
├── __init__.py
└── utils.py
└── send
├── __init__.py
└── utils.py
/.env:
--------------------------------------------------------------------------------
1 | ENVIRONMENT=prod
2 | DRIVER=~fastapi
3 | HOST=0.0.0.0
4 | PORT=8090
5 | GOCQ_CONFIG_TEMPLATE_PATH="gocq-template.yml"
--------------------------------------------------------------------------------
/.env.dev:
--------------------------------------------------------------------------------
1 | LOG_LEVEL=DEBUG
2 |
--------------------------------------------------------------------------------
/.env.prod:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MistEO/FileQQ/e9f6444a2025b46909ba278d653d212ec3b54f81/.env.prod
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/python
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python
4 |
5 | ### Python ###
6 | # Byte-compiled / optimized / DLL files
7 | __pycache__/
8 | *.py[cod]
9 | *$py.class
10 |
11 | # C extensions
12 | *.so
13 |
14 | # Distribution / packaging
15 | .Python
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | pip-wheel-metadata/
29 | share/python-wheels/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 | MANIFEST
34 |
35 | # PyInstaller
36 | # Usually these files are written by a python script from a template
37 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
38 | *.manifest
39 | *.spec
40 |
41 | # Installer logs
42 | pip-log.txt
43 | pip-delete-this-directory.txt
44 |
45 | # Unit test / coverage reports
46 | htmlcov/
47 | .tox/
48 | .nox/
49 | .coverage
50 | .coverage.*
51 | .cache
52 | nosetests.xml
53 | coverage.xml
54 | *.cover
55 | *.py,cover
56 | .hypothesis/
57 | .pytest_cache/
58 | pytestdebug.log
59 |
60 | # Translations
61 | *.mo
62 | *.pot
63 |
64 | # Django stuff:
65 | *.log
66 | local_settings.py
67 | db.sqlite3
68 | db.sqlite3-journal
69 |
70 | # Flask stuff:
71 | instance/
72 | .webassets-cache
73 |
74 | # Scrapy stuff:
75 | .scrapy
76 |
77 | # Sphinx documentation
78 | docs/_build/
79 | doc/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # IPython
88 | profile_default/
89 | ipython_config.py
90 |
91 | # pyenv
92 | .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
102 | __pypackages__/
103 |
104 | # Celery stuff
105 | celerybeat-schedule
106 | celerybeat.pid
107 |
108 | # SageMath parsed files
109 | *.sage.py
110 |
111 | # Environments
112 | .env
113 | .venv
114 | env/
115 | venv/
116 | ENV/
117 | env.bak/
118 | venv.bak/
119 |
120 | # Spyder project settings
121 | .spyderproject
122 | .spyproject
123 |
124 | # Rope project settings
125 | .ropeproject
126 |
127 | # mkdocs documentation
128 | /site
129 |
130 | # mypy
131 | .mypy_cache/
132 | .dmypy.json
133 | dmypy.json
134 |
135 | # Pyre type checker
136 | .pyre/
137 |
138 | # pytype static type analyzer
139 | .pytype/
140 |
141 | # End of https://www.toptal.com/developers/gitignore/api/python
142 |
143 | accounts
144 | cache
145 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.formatting.provider": "black"
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 MistEO
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FileQQ | 文件QQ
2 |
3 | 通过文件来聊 QQ
4 |
5 | 这是一个很奇怪的项目,为什么有人会需要通过文件聊 QQ 呢?
6 |
7 | 我只能说,迫不得已,懂的都懂了_(:з」∠)_
8 |
9 | ## 使用说明
10 |
11 | 1. 环境配置
12 |
13 | ```bash
14 | pip install nb-cli
15 | nb plugin install nonebot_plugin_apscheduler
16 | nb plugin install nonebot_plugin_gocqhttp
17 | nb driver install websockets
18 | nb driver install fastapi
19 | ```
20 |
21 | 2. `nb run` 运行
22 | 3. 打开 登录你的账号
23 | 4. 在 `cache/recv` 文件夹下查看聊天记录
24 | 5. 在对应的 `cache/send` 文件夹下回复消息,以 两个回车 或 `#` 结尾
25 |
26 | ## Tips
27 |
28 | - 推荐使用 code-server(http 方式)。当然 ssh 也可以,不过有 ssh 为什么不用 vscode qq 呢?
29 | - `config.yaml` 里有一些选项可以看着改改
30 | - 可以把经常水的群创建个快捷方式(软链接)出来
31 |
32 | ## 打赏
33 |
34 | 请作者喝杯咖啡吧~ (请备注 FileQQ 项目,感谢你的资瓷 ✿✿ヽ(°▽°)ノ✿)
35 |
36 |
37 |

38 |

39 |
40 |
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | recv:
2 | avatar: true # 是否显示头像
3 | image: true # 是否显示聊天图片
4 |
5 |
6 | focus:
7 | group: # 关注的群
8 | mode: whitelist
9 | list: []
10 |
11 | user: # 关注的人
12 | mode: blacklist
13 | list: []
14 |
15 |
16 | debug: false # 调试模式
17 |
18 | message_suffix: '' #消息小尾巴
--------------------------------------------------------------------------------
/gocq-template.yml:
--------------------------------------------------------------------------------
1 | # Go-CQHTTP的默认配置文件, 没必要最好不要动
2 | # 以三个`{}`包裹的为模板项, 请不要修改
3 |
4 | account: # 账号相关
5 | password: "{{{account.password}}}" # 密码为空时使用扫码登录
6 | encrypt: false # 是否开启密码加密
7 | status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态
8 |
9 | relogin: # 重连设置
10 | delay: 3 # 首次重连延迟, 单位秒
11 | interval: 3 # 重连间隔
12 | max-times: 0 # 最大重连次数, 0为无限制
13 |
14 | # 是否使用服务器下发的新地址进行重连
15 | # 注意, 此设置可能导致在海外服务器上连接情况更差
16 | use-sso-address: true
17 |
18 | # prettier-ignore
19 | uin: {{{account.uin}}} # QQ账号
20 |
21 | heartbeat:
22 | # 心跳频率, 单位秒
23 | # -1 为关闭心跳
24 | interval: 5
25 |
26 | message:
27 | # 上报数据类型
28 | # 可选: string,array
29 | post-format: array
30 |
31 | # 是否忽略无效的CQ码, 如果为假将原样发送
32 | ignore-invalid-cqcode: false
33 |
34 | # 是否强制分片发送消息
35 | # 分片发送将会带来更快的速度
36 | # 但是兼容性会有些问题
37 | force-fragment: false
38 |
39 | # 是否将url分片发送
40 | fix-url: false
41 |
42 | # 下载图片等请求的网络代理
43 | proxy-rewrite: ""
44 |
45 | # 是否上报自身消息
46 | report-self-message: true
47 |
48 | # 移除服务端的Reply附带的At
49 | remove-reply-at: true
50 |
51 | # 为Reply附加更多信息
52 | extra-reply-data: true
53 |
54 | # 跳过 Mime 扫描, 忽略错误数据
55 | skip-mime-scan: false
56 |
57 | output:
58 | # 日志等级: trace,debug,info,warn,error
59 | log-level: warn
60 |
61 | # 日志时效 单位天. 超过这个时间之前的日志将会被自动删除. 设置为 0 表示永久保留.
62 | log-aging: 15
63 |
64 | # 是否在每次启动时强制创建全新的文件储存日志. 为 false 的情况下将会在上次启动时创建的日志文件续写
65 | log-force-new: true
66 |
67 | # 是否启用 DEBUG
68 | debug: false # 开启调试模式
69 |
70 | # 输出日志颜色设置, 会影响前台日志输出, 请勿修改
71 | log-colorful: false
72 |
73 | # 默认中间件锚点
74 | default-middlewares: &default # 访问密钥, 强烈推荐在公网的服务器设置
75 | access-token: ""
76 |
77 | # 事件过滤器文件目录
78 | filter: ""
79 |
80 | # API限速设置
81 | # 该设置为全局生效
82 | rate-limit:
83 | enabled: false # 是否启用限速
84 | frequency: 1 # 令牌回复频率, 单位秒
85 | bucket: 1 # 令牌桶大小
86 |
87 | database: # 数据库相关设置
88 | leveldb:
89 | # 是否启用内置leveldb数据库
90 | # 启用将会增加10-20MB的内存占用和一定的磁盘空间
91 | # 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
92 | enable: true
93 |
94 | # 媒体文件缓存, 删除此项则使用缓存文件(旧版行为)
95 | # 若需要将所有账号的缓存设置为同一份,可尝试将路径设置为同一个。但可能会带来写冲突问题,请谨慎设置
96 | cache:
97 | image: data/image.db
98 | video: data/video.db
99 |
100 | servers:
101 | - ws-reverse:
102 | universal: "{{{server_address}}}"
103 | reconnect-interval: 3000
104 | middlewares:
105 | <<: *default
106 | access-token: "{{{access_token}}}"
107 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "FileQQ"
3 | version = "0.1.0"
4 | description = "FileQQ"
5 | readme = "README.md"
6 | requires-python = ">=3.8, <4.0"
7 |
8 | [tool.nonebot]
9 | adapters = [
10 | { name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" }
11 | ]
12 | plugins = ["nonebot_plugin_gocqhttp", "nonebot_plugin_apscheduler"]
13 | plugin_dirs = ["src/plugins"]
14 |
--------------------------------------------------------------------------------
/src/common/config.py:
--------------------------------------------------------------------------------
1 | import yaml
2 |
3 | with open("config.yaml", "r", encoding="utf-8") as f:
4 | GLOBAL_CONFIG = yaml.safe_load(f)
5 |
6 | print("GLOBAL_CONFIG:", GLOBAL_CONFIG)
7 |
8 | RECV_AVATAR_ENABLED = GLOBAL_CONFIG["recv"]["avatar"]
9 | RECV_IMAGE_ENABLED = GLOBAL_CONFIG["recv"]["image"]
10 |
11 | FOCUS_GROUP = (
12 | GLOBAL_CONFIG["focus"]["group"]["mode"] != "blacklist",
13 | GLOBAL_CONFIG["focus"]["group"]["list"],
14 | )
15 | FOCUS_USER = (
16 | GLOBAL_CONFIG["focus"]["user"]["mode"] != "blacklist",
17 | GLOBAL_CONFIG["focus"]["user"]["list"],
18 | )
19 |
20 | DEBUG_MODE = GLOBAL_CONFIG["debug"]
21 |
22 | MESSAGE_SUFFIX = GLOBAL_CONFIG["message_suffix"]
23 |
--------------------------------------------------------------------------------
/src/common/define.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | ROOT_PATH = Path("cache")
4 |
5 | RECV_PATH = ROOT_PATH / "recv"
6 | SEND_PATH = ROOT_PATH / "send"
7 | RECV_FILE_FORMAT = "md"
8 | SEND_FILE_FORMAT = "txt"
9 |
10 | RECV_AVATAR_PATH = RECV_PATH / "avatars"
11 | RECV_IMAGE_PATH = RECV_PATH / "images"
12 |
13 | RECV_GROUP_PATH = RECV_PATH / "group"
14 | RECV_GROUP_ID_PATH = RECV_GROUP_PATH / "by_id"
15 | RECV_GROUP_NAME_PATH = RECV_GROUP_PATH / "by_name"
16 | RECV_GROUP_MEMO_PATH = RECV_GROUP_PATH / "by_memo"
17 |
18 | RECV_USER_PATH = RECV_PATH / "user"
19 | RECV_USER_ID_PATH = RECV_USER_PATH / "by_id"
20 | RECV_USER_NAME_PATH = RECV_USER_PATH / "by_name"
21 | RECV_USER_MEMO_PATH = RECV_USER_PATH / "by_memo"
22 |
23 | RECV_FOCUS_PATH = RECV_PATH / "focus"
24 | RECV_FOCUS_ID_PATH = RECV_FOCUS_PATH / "by_id"
25 | RECV_FOCUS_NAME_PATH = RECV_FOCUS_PATH / "by_name"
26 | RECV_FOCUS_MEMO_PATH = RECV_FOCUS_PATH / "by_memo"
27 |
28 |
29 | SEND_GROUP_PATH = SEND_PATH / "group"
30 | SEND_GROUP_ID_PATH = SEND_GROUP_PATH / "by_id"
31 | SEND_GROUP_NAME_PATH = SEND_GROUP_PATH / "by_name"
32 | SEND_GROUP_MEMO_PATH = SEND_GROUP_PATH / "by_memo"
33 |
34 | SEND_USER_PATH = SEND_PATH / "user"
35 | SEND_USER_ID_PATH = SEND_USER_PATH / "by_id"
36 | SEND_USER_NAME_PATH = SEND_USER_PATH / "by_name"
37 | SEND_USER_MEMO_PATH = SEND_USER_PATH / "by_memo"
38 |
39 | SEND_FOCUS_PATH = SEND_PATH / "focus"
40 | SEND_FOCUS_ID_PATH = SEND_FOCUS_PATH / "by_id"
41 | SEND_FOCUS_NAME_PATH = SEND_FOCUS_PATH / "by_name"
42 | SEND_FOCUS_MEMO_PATH = SEND_FOCUS_PATH / "by_memo"
43 |
--------------------------------------------------------------------------------
/src/plugins/init/__init__.py:
--------------------------------------------------------------------------------
1 | from nonebot import get_bot, on_message
2 | from nonebot.rule import Rule
3 |
4 | import os
5 | import shutil
6 | import atexit
7 |
8 | import src.common.define as define
9 | from src.common.config import DEBUG_MODE
10 |
11 |
12 | @atexit.register
13 | def clear_cache():
14 | shutil.rmtree(define.RECV_AVATAR_PATH, ignore_errors=True)
15 | shutil.rmtree(define.RECV_IMAGE_PATH, ignore_errors=True)
16 |
17 | shutil.rmtree(define.RECV_GROUP_ID_PATH, ignore_errors=True)
18 | shutil.rmtree(define.RECV_GROUP_NAME_PATH, ignore_errors=True)
19 | shutil.rmtree(define.RECV_GROUP_MEMO_PATH, ignore_errors=True)
20 | shutil.rmtree(define.RECV_USER_ID_PATH, ignore_errors=True)
21 | shutil.rmtree(define.RECV_USER_NAME_PATH, ignore_errors=True)
22 | shutil.rmtree(define.RECV_USER_MEMO_PATH, ignore_errors=True)
23 |
24 | shutil.rmtree(define.SEND_GROUP_ID_PATH, ignore_errors=True)
25 | shutil.rmtree(define.SEND_GROUP_NAME_PATH, ignore_errors=True)
26 | shutil.rmtree(define.SEND_GROUP_MEMO_PATH, ignore_errors=True)
27 | shutil.rmtree(define.SEND_USER_ID_PATH, ignore_errors=True)
28 | shutil.rmtree(define.SEND_USER_NAME_PATH, ignore_errors=True)
29 | shutil.rmtree(define.SEND_USER_MEMO_PATH, ignore_errors=True)
30 |
31 |
32 | def make_dirs():
33 | os.makedirs(define.RECV_AVATAR_PATH, exist_ok=True)
34 | os.makedirs(define.RECV_IMAGE_PATH, exist_ok=True)
35 |
36 | os.makedirs(define.RECV_GROUP_ID_PATH, exist_ok=True)
37 | os.makedirs(define.RECV_GROUP_NAME_PATH, exist_ok=True)
38 | os.makedirs(define.RECV_GROUP_MEMO_PATH, exist_ok=True)
39 |
40 | os.makedirs(define.RECV_USER_ID_PATH, exist_ok=True)
41 | os.makedirs(define.RECV_USER_NAME_PATH, exist_ok=True)
42 | os.makedirs(define.RECV_USER_MEMO_PATH, exist_ok=True)
43 |
44 | os.makedirs(define.SEND_GROUP_ID_PATH, exist_ok=True)
45 | os.makedirs(define.SEND_GROUP_NAME_PATH, exist_ok=True)
46 | os.makedirs(define.SEND_GROUP_MEMO_PATH, exist_ok=True)
47 |
48 | os.makedirs(define.SEND_USER_ID_PATH, exist_ok=True)
49 | os.makedirs(define.SEND_USER_NAME_PATH, exist_ok=True)
50 | os.makedirs(define.SEND_USER_MEMO_PATH, exist_ok=True)
51 |
52 |
53 | async def sync_groups():
54 | group_infos = await get_bot().call_api("get_group_list", **{})
55 | for group_info in group_infos:
56 | group_id = group_info["group_id"]
57 | group_name = group_info["group_name"].replace("/", "_")
58 | group_memo = (
59 | group_info["group_memo"].replace("/", "_")
60 | if "group_memo" in group_info
61 | else group_name
62 | )
63 |
64 | send_id_path = (
65 | define.SEND_GROUP_ID_PATH / f"{group_id}.{define.SEND_FILE_FORMAT}"
66 | )
67 | # send_id_path.touch(exist_ok=True) # 要用的时候再创建
68 | send_name_path = (
69 | define.SEND_GROUP_NAME_PATH
70 | / f"{group_name}_{group_id}.{define.SEND_FILE_FORMAT}"
71 | )
72 | send_memo_path = (
73 | define.SEND_GROUP_MEMO_PATH
74 | / f"{group_memo}_{group_id}.{define.SEND_FILE_FORMAT}"
75 | )
76 |
77 | os.symlink(send_id_path.absolute(), send_name_path.absolute())
78 | os.symlink(send_id_path.absolute(), send_memo_path.absolute())
79 |
80 | recv_id_path = (
81 | define.RECV_GROUP_ID_PATH / f"{group_id}.{define.RECV_FILE_FORMAT}"
82 | )
83 | send_id_relpath = os.path.relpath(send_id_path, recv_id_path.parent)
84 | send_name_relpath = os.path.relpath(send_name_path, recv_id_path.parent)
85 | send_memo_relpath = os.path.relpath(send_memo_path, recv_id_path.parent)
86 |
87 | with open(recv_id_path, "w", encoding='utf-8') as f:
88 | f.write(
89 | f"""
90 | ## {group_memo} ({group_name}) {group_id}
91 |
92 | [REPLY_BY_ID]({send_id_relpath})
93 | [REPLY_BY_NAME](<{send_name_relpath}>)
94 | [REPLY_BY_MEMO](<{send_memo_relpath}>)
95 |
96 | """
97 | )
98 | os.symlink(
99 | recv_id_path.absolute(),
100 | define.RECV_GROUP_NAME_PATH
101 | / f"{group_name}_{group_id}.{define.RECV_FILE_FORMAT}",
102 | )
103 | os.symlink(
104 | recv_id_path.absolute(),
105 | define.RECV_GROUP_MEMO_PATH
106 | / f"{group_memo}_{group_id}.{define.RECV_FILE_FORMAT}",
107 | )
108 |
109 |
110 | async def sync_friends():
111 | friend_infos = await get_bot().call_api("get_friend_list", **{})
112 | for friend_info in friend_infos:
113 | user_id = friend_info["user_id"]
114 | nickname = friend_info["nickname"].replace("/", "_")
115 | remark = (
116 | friend_info["remark"].replace("/", "_")
117 | if "remark" in friend_info
118 | else nickname
119 | )
120 |
121 | send_id_path = define.SEND_USER_ID_PATH / f"{user_id}.{define.SEND_FILE_FORMAT}"
122 | # send_id_path.touch(exist_ok=True) # 要用的时候再创建
123 | send_name_path = (
124 | define.SEND_USER_NAME_PATH
125 | / f"{nickname}_{user_id}.{define.SEND_FILE_FORMAT}"
126 | )
127 | send_memo_path = (
128 | define.SEND_USER_MEMO_PATH / f"{remark}_{user_id}.{define.SEND_FILE_FORMAT}"
129 | )
130 |
131 | os.symlink(send_id_path.absolute(), send_name_path.absolute())
132 | os.symlink(send_id_path.absolute(), send_memo_path.absolute())
133 |
134 | recv_id_path = define.RECV_USER_ID_PATH / f"{user_id}.{define.RECV_FILE_FORMAT}"
135 | send_id_relpath = os.path.relpath(send_id_path, recv_id_path.parent)
136 | send_name_relpath = os.path.relpath(send_name_path, recv_id_path.parent)
137 | send_memo_relpath = os.path.relpath(send_memo_path, recv_id_path.parent)
138 |
139 | with open(recv_id_path, "w", encoding="utf-8") as f:
140 | f.write(
141 | f"""
142 | ## {remark} ({nickname}) {user_id}
143 |
144 | [REPLY_BY_ID]({send_id_relpath})
145 | [REPLY_BY_NAME](<{send_name_relpath}>)
146 | [REPLY_BY_MEMO](<{send_memo_relpath}>)
147 |
148 | """
149 | )
150 | os.symlink(
151 | recv_id_path.absolute(),
152 | define.RECV_USER_NAME_PATH
153 | / f"{nickname}_{user_id}.{define.RECV_FILE_FORMAT}",
154 | )
155 | os.symlink(
156 | recv_id_path.absolute(),
157 | define.RECV_USER_MEMO_PATH
158 | / f"{remark}_{user_id}.{define.RECV_FILE_FORMAT}",
159 | )
160 |
161 |
162 | inited = False
163 |
164 |
165 | async def to_init(bot, event, state) -> bool:
166 | return not inited
167 |
168 |
169 | any_msg = on_message(priority=0, block=False, rule=Rule(to_init))
170 |
171 |
172 | @any_msg.handle()
173 | async def _(bot, event, state):
174 | global inited
175 | if inited:
176 | return
177 |
178 | inited = True
179 | clear_cache()
180 | make_dirs()
181 | await sync_groups()
182 | await sync_friends()
183 |
--------------------------------------------------------------------------------
/src/plugins/recv/__init__.py:
--------------------------------------------------------------------------------
1 | from nonebot import get_bot, on_message, on
2 | from nonebot.typing import T_State
3 | from nonebot.adapters import Bot
4 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, PrivateMessageEvent, Event
5 |
6 | from datetime import datetime
7 |
8 | import src.common.define as define
9 | from .utils import nbevent_2_mdmsg, avatar_html, get_nickname_in_group, get_friends_names
10 |
11 |
12 | any_msg = on_message(priority=100, block=False)
13 |
14 |
15 | @any_msg.handle()
16 | async def _(bot: Bot, event: GroupMessageEvent, state: T_State):
17 | await handle_group_message(bot, event, state)
18 |
19 |
20 | async def handle_group_message(
21 | bot: Bot, event: Event, state: T_State, is_self: bool = False
22 | ):
23 | send_id_path = (
24 | define.SEND_GROUP_ID_PATH / f"{event.group_id}.{define.SEND_FILE_FORMAT}"
25 | )
26 | if not send_id_path.exists():
27 | send_id_path.touch()
28 |
29 | user_id = event.user_id
30 | avatar = avatar_html(user_id)
31 | card, nickname = await get_nickname_in_group(user_id, event.group_id)
32 | time = datetime.now().strftime("%H:%M:%S")
33 | message = await nbevent_2_mdmsg(event)
34 |
35 | text = f"""
36 | **{card}** ({nickname}) {user_id} : _{time}_
37 | {avatar}
38 | {message}
39 |
40 | """
41 |
42 | with open(
43 | define.RECV_GROUP_ID_PATH / f"{event.group_id}.{define.RECV_FILE_FORMAT}", "a", encoding="utf-8"
44 | ) as f:
45 | f.write(text)
46 |
47 |
48 | @any_msg.handle()
49 | async def _(bot: Bot, event: PrivateMessageEvent, state: T_State):
50 | await handle_private_message(bot, event, state)
51 |
52 |
53 | async def handle_private_message(
54 | bot: Bot, event: Event, state: T_State, is_self: bool = False
55 | ):
56 | user_id = event.user_id if not is_self else event.target_id
57 | send_id_path = define.SEND_USER_ID_PATH / f"{user_id}.{define.SEND_FILE_FORMAT}"
58 | if not send_id_path.exists():
59 | send_id_path.touch()
60 |
61 | avatar = avatar_html(event.user_id)
62 | card = "我" if is_self else await get_friends_names(event.user_id)
63 | time = datetime.now().strftime("%H:%M:%S")
64 | message = await nbevent_2_mdmsg(event)
65 |
66 | text = f"""
67 | **{card}** _{time}_
68 | {avatar}
69 | {message}
70 |
71 | """
72 | with open(
73 | define.RECV_USER_ID_PATH / f"{user_id}.{define.RECV_FILE_FORMAT}", "a", encoding="utf-8"
74 | ) as f:
75 | f.write(text)
76 |
77 |
78 | my_msg = on("message_sent", priority=100, block=False)
79 |
80 |
81 | @my_msg.handle()
82 | async def _(bot: Bot, event: Event, state: T_State):
83 | if event.message_type == "group":
84 | await handle_group_message(bot, event, state, True)
85 | elif event.message_type == "private":
86 | await handle_private_message(bot, event, state, True)
87 |
--------------------------------------------------------------------------------
/src/plugins/recv/utils.py:
--------------------------------------------------------------------------------
1 | from nonebot import get_bot
2 | from nonebot.adapters.onebot.v11 import Event, MessageSegment
3 |
4 | import httpx
5 | from pathlib import Path
6 | from typing import Tuple
7 |
8 | import src.common.define as define
9 | from src.common.config import (
10 | RECV_IMAGE_ENABLED,
11 | RECV_AVATAR_ENABLED,
12 | FOCUS_GROUP,
13 | FOCUS_USER,
14 | )
15 |
16 |
17 | def avatar_html(user_id: int, size: int = 32) -> str:
18 | if RECV_AVATAR_ENABLED:
19 | return f'
'
20 | else:
21 | return ""
22 |
23 |
24 | def image_html(path: Path, scale: int = 100) -> str:
25 | return f'
'
26 |
27 |
28 | async def get_nickname_in_group(user_id: int, group_id: int) -> Tuple[str, str]:
29 | user_info = await get_bot().call_api(
30 | "get_group_member_info",
31 | **{
32 | "group_id": group_id,
33 | "user_id": user_id,
34 | },
35 | )
36 |
37 | def name_replace(name: str) -> str:
38 | # 简单弄下防注入
39 | return name.replace("/", "").replace("\\", "").replace("$$", "")
40 |
41 | nickname = name_replace(user_info["nickname"])
42 | card = name_replace(user_info["card"])
43 | card = card if card else nickname
44 |
45 | return card, nickname
46 |
47 |
48 | async def nbevent_2_mdmsg(event: Event) -> str:
49 | is_group = event.message_type == "group"
50 | focus_mode, focus_list = FOCUS_GROUP if is_group else FOCUS_USER
51 | if focus_mode:
52 | focus = (
53 | event.group_id in focus_list if is_group else event.user_id in focus_list
54 | )
55 | else:
56 | focus = (
57 | event.group_id not in focus_list
58 | if is_group
59 | else event.user_id not in focus_list
60 | )
61 |
62 | result = ""
63 | for seg in event.message:
64 | if isinstance(seg, dict):
65 | # for message_sent
66 | seg = MessageSegment(**seg)
67 |
68 | if seg.type == "image":
69 | url = seg.data["url"]
70 | filename = seg.data["file"]
71 | path = (define.RECV_IMAGE_PATH / filename).with_suffix(".png")
72 | if not path.exists() and focus:
73 | # 下载图片
74 | async with httpx.AsyncClient() as client:
75 | r = await client.get(url)
76 | with open(path, "wb") as f:
77 | f.write(r.content)
78 | # 生成图片链接
79 | if RECV_IMAGE_ENABLED:
80 | scale = 15 if seg.data["subType"] == "1" else 100
81 | result += image_html(path, scale)
82 | else:
83 | result += f"[IMAGE](/{path})"
84 | result += f"\n\n"
85 |
86 | elif seg.type == "at":
87 | at_qq = seg.data["qq"]
88 | card, _ = await get_nickname_in_group(at_qq, event.group_id)
89 | result += f"**`@{card}`** {avatar_html(at_qq)} "
90 |
91 | elif seg.type == "reply":
92 | reply_text = seg.data["text"]
93 | result += f"> {reply_text}\n\n"
94 |
95 | elif str(seg).strip():
96 | result += f"`{seg}` "
97 |
98 | return result
99 |
100 |
101 | friend_names = {}
102 |
103 |
104 | async def get_friends_names(user_id):
105 | global friend_names
106 | if len(friend_names) > 0:
107 | if user_id in friend_names:
108 | return friend_names[user_id]
109 | else:
110 | return f"**未知好友** {user_id}"
111 |
112 | friend_list = await get_bot().call_api("get_friend_list", **{})
113 | for friend in friend_list:
114 | user_id = friend["user_id"]
115 | nickname = friend["nickname"]
116 | remark = friend["remark"] if "remark" in friend else nickname
117 |
118 | friend_names[user_id] = f"**{remark}** ({nickname}) {user_id}"
119 |
120 | return friend_names[user_id]
121 |
--------------------------------------------------------------------------------
/src/plugins/send/__init__.py:
--------------------------------------------------------------------------------
1 | from nonebot import get_bot, logger, require
2 |
3 | import src.common.define as define
4 | from .utils import walk_sender, text_2_msg, get_message_suffix
5 |
6 |
7 | async def sync_group():
8 | message = walk_sender(define.SEND_GROUP_ID_PATH)
9 | if not message:
10 | return
11 |
12 | path, context = message
13 | group_id = path.stem
14 | message_suffix = get_message_suffix()
15 |
16 | # 先清空文件,避免重复发送
17 | with open(path, "w", encoding="utf-8") as f:
18 | pass
19 |
20 | context = text_2_msg(context + message_suffix)
21 | logger.info(f"发送群消息: {group_id}: {context}")
22 | await get_bot().call_api(
23 | "send_group_msg", **{"message": context, "group_id": group_id}
24 | )
25 |
26 |
27 | async def sync_friend():
28 | message = walk_sender(define.SEND_USER_ID_PATH)
29 | if not message:
30 | return
31 |
32 | path, context = message
33 | user_id = path.stem
34 | message_suffix = get_message_suffix()
35 |
36 | # 先清空文件,避免重复发送
37 | with open(path, "w", encoding="utf-8") as f:
38 | pass
39 |
40 | context = text_2_msg(context + message_suffix)
41 | logger.info(f"发送私聊消息: {user_id}: {context}")
42 | await get_bot().call_api(
43 | "send_private_msg", **{"message": context, "user_id": user_id}
44 | )
45 |
46 |
47 | send_sched = require("nonebot_plugin_apscheduler").scheduler
48 |
49 |
50 | @send_sched.scheduled_job("interval", seconds=2)
51 | async def send_message():
52 | await sync_group()
53 | await sync_friend()
54 |
--------------------------------------------------------------------------------
/src/plugins/send/utils.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import re
3 | from src.common.config import MESSAGE_SUFFIX
4 |
5 | def walk_sender(dir_path: Path):
6 | if not dir_path.exists():
7 | return None
8 |
9 | for path in dir_path.iterdir():
10 | if not path.stat().st_size or not path.stem.isdigit():
11 | continue
12 |
13 | with open(path, "r", encoding="utf-8") as f:
14 | context = f.read()
15 | if not context:
16 | continue
17 |
18 | if context.endswith("\n\n"):
19 | return path, context[:-2]
20 | elif context.endswith("#"):
21 | return path, context[:-1]
22 | # else 正在输入中
23 |
24 | return None
25 |
26 |
27 | def text_2_msg(text: str) -> str:
28 | text = text.strip()
29 | if "@" not in text:
30 | return text
31 |
32 | def replace_at(match):
33 | return f"[CQ:at,qq={match.group(1)}]"
34 |
35 | return re.sub(r"@(\d+)", replace_at, text)
36 |
37 | def get_message_suffix() -> str:
38 | return MESSAGE_SUFFIX
39 |
--------------------------------------------------------------------------------