├── .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 | sponsor 38 | sponsor 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 | --------------------------------------------------------------------------------