├── .gitattributes ├── .github └── workflows │ ├── main.yml │ └── rust.yml ├── .gitignore ├── COPYING.md ├── CREDITS.md ├── Cargo.lock ├── Cargo.toml ├── HISTORY.md ├── LOCAL_WINDOWS_10.md ├── README.md ├── RELEASE-NOTES-1.0.md ├── SECURITY.md ├── avatar.webp ├── bridge-cmd.drawio ├── bridge-design-old.drawio ├── bridge-design.drawio ├── config.simple.json ├── data └── .gitkeep ├── docs └── discord │ ├── images │ ├── 0.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png │ └── set_discord_channel_webhook.md ├── mirai_rs ├── Cargo.toml └── src │ ├── adapter │ ├── http_adapter.rs │ └── mod.rs │ ├── lib.rs │ ├── message.rs │ ├── mirai_http.rs │ ├── model │ ├── group.rs │ └── mod.rs │ └── response.rs ├── package-lock.json ├── package.json ├── rustfmt.toml ├── server.ts ├── src ├── bridge │ ├── bridge_message.rs │ ├── config.rs │ ├── manager │ │ ├── message_manager.rs │ │ ├── mod.rs │ │ └── user_manager.rs │ ├── mod.rs │ ├── pojo │ │ ├── form │ │ │ ├── bridge_message_ref_message_form.rs │ │ │ ├── bridge_message_save_form.rs │ │ │ ├── bridge_send_message_form.rs │ │ │ ├── bridge_user_save_form.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── po │ │ │ ├── bridge_message_po.rs │ │ │ ├── bridge_user_ref_po.rs │ │ │ └── mod.rs │ └── user.rs ├── bridge_cmd │ ├── bridge_client.rs │ ├── mod.rs │ └── process │ │ ├── bind_proc.rs │ │ └── mod.rs ├── bridge_data.rs ├── bridge_dc │ ├── bridge_client.rs │ ├── handler.rs │ └── mod.rs ├── bridge_log.rs ├── bridge_qq │ ├── group_message_id.rs │ ├── handler.rs │ └── mod.rs ├── bridge_tg │ └── mod.rs ├── cmd_adapter.rs ├── config.rs ├── logger.rs ├── main.rs ├── test_dc.rs ├── test_mirai.rs ├── test_regex.rs ├── test_reqwest.rs └── utils.rs └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js eol=lf 2 | *.json eol=lf 3 | *.md eol=lf 4 | *.rs eol=lf 5 | *.yml eol=lf 6 | *.webp binary 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Rust-static-build 2 | on: 3 | push: 4 | branches: [ _main ] 5 | pull_request: 6 | branches: [ _main ] 7 | env: 8 | CARGO_TERM_COLOR: always 9 | BUILD_TARGET: x86_64-unknown-linux-musl 10 | BINARY_NAME: message_bridge 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build-musl 17 | uses: jean-dfinity/rust-musl-action@master 18 | with: 19 | args: cargo +nightly build --target $BUILD_TARGET --release 20 | - uses: actions/upload-artifact@v2 21 | with: 22 | name: ${{ env.BINARY_NAME }} 23 | path: target/x86_64-unknown-linux-musl/release/${{ env.BINARY_NAME }}* 24 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions-rs/toolchain@v1.0.6 18 | with: 19 | toolchain: nightly 20 | override: true 21 | - run: cargo +nightly build --release --verbose 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | # compiled output 3 | /dist 4 | /node_modules 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | go-cqhttp 38 | 39 | /cache 40 | /config.json 41 | /config.dev.json 42 | /config.prod.json 43 | # 数据 44 | /data/*.json 45 | 46 | # qq数据 47 | device.json 48 | session.token 49 | qrcode.png 50 | 51 | # tg数据 52 | tg.pack.*/ 53 | telegram.session 54 | -------------------------------------------------------------------------------- /COPYING.md: -------------------------------------------------------------------------------- 1 | # License and copyright information 2 | 3 | ## License 4 | 5 | message_bridge_rs is licensed under the terms of the ISC License. Derivative 6 | works and later versions of the code must be attributed. 7 | 8 | For the full text of the license, see https://www.isc.org/licenses/ or 9 | **ISC License** below. 10 | 11 | ## Copyright owners 12 | 13 | message_bridge_rs contributors, including those listed in the CREDITS file, hold 14 | the copyright to this work. 15 | 16 | ## Additional license information 17 | 18 | Some components of message_bridge_rs imported from other projects may be under 19 | other Free and Open Source, or Free Culture, licenses. Specific details of their 20 | licensing information can be found in those components. 21 | 22 | ## ISC LICENSE 23 | 24 | ISC License 25 | 26 | Copyright (c) message_bridge_rs Project 27 | 28 | Permission to use, copy, modify, and/or distribute this software for any purpose 29 | with or without fee is hereby granted, provided that the above copyright notice 30 | and this permission notice appear in all copies. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 33 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 34 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 35 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 36 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 37 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 38 | THIS SOFTWARE. 39 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | message_bridge_rs is an open-source project. We would like to recognize the 4 | following names for their contribution to the project. 5 | 6 | ## Contributors 7 | 8 | * rabbitkiller-dev (rabbitkiller) 9 | * RogenDong (6uopdong) 10 | 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "message_bridge_rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | time = "0.3.14" 10 | color-eyre = "0.6.2" 11 | tracing = "0.1.36" 12 | tracing-error = "0.2.0" 13 | tracing-appender = "0.2.2" 14 | tracing-subscriber = { version = "0.3.16", features = [ 15 | "fmt", 16 | "env-filter", 17 | "local-time", 18 | "time", 19 | ] } 20 | lazy_static = "1.4.0" 21 | reqwest = { version = "0.11", features = ["json", "rustls-tls"] } 22 | tokio = { version = "1.14.0", features = ["full"] } 23 | serde_json = "1.0" 24 | serde = { version = "1.0", features = ["derive"] } 25 | chrono = "0.4.22" 26 | regex = "1.6.0" 27 | mime = "0.3.16" 28 | mime_guess = "2.0.4" 29 | md5 = "0.7.0" 30 | image-base64 = "0.1.0" 31 | anyhow = "1.0.69" 32 | proc_qq = { git = "https://github.com/niuhuan/rust_proc_qq.git", rev = "dda3d45" } 33 | teleser = { git = "https://github.com/niuhuan/teleser-rs.git", branch = "patched", features = ["proxy"] } 34 | 35 | [dependencies.serenity] 36 | default-features = false 37 | features = ["client", "gateway", "rustls_backend", "model"] 38 | version = "0.11.5" 39 | 40 | [dependencies.uuid] 41 | version = "1.1.2" 42 | features = [ 43 | "v4", # Lets you generate random UUIDs 44 | "fast-rng", # Use a faster (but still sufficiently random) RNG 45 | "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs 46 | ] 47 | 48 | [dependencies.url] 49 | version = "^2.1" 50 | features = ["serde"] 51 | 52 | [dependencies.clap] 53 | version = "^4.1" 54 | features = ["derive"] 55 | 56 | [dev-dependencies] 57 | tokio-test = "*" 58 | 59 | [workspace] 60 | members = [] 61 | 62 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Change notes from older releases. For current info, see RELEASE-NOTES-1.0. 2 | 3 | # message_bridge_rs 0.1 4 | 5 | ## message_bridge_rs 0.1.0 6 | 7 | This is the version of the initial commit of message_bridge_rs. 8 | 9 | This is also the last version of message_bridge_rs 0.1.x. Next version will be 10 | message_bridge_rs 1.0.x. 11 | 12 | ### Changes of the initial commit of message_bridge_rs 13 | 14 | * feat: Add Mirai verify, bind APIs 15 | -------------------------------------------------------------------------------- /LOCAL_WINDOWS_10.md: -------------------------------------------------------------------------------- 1 | # Windows10 本地运行 2 | 3 | ## 环境要求 4 | 1. 科学上网 5 | 2. [NodeJs (v14 以上)](https://nodejs.org/en) 6 | 3. Rust: `curl https://sh.rustup.rs -sSf | sh` 7 | 4. Git 8 | 9 | > 安装完成后接一下操作 10 | 11 | 1. 命令行运行 12 | ```shell 13 | # rust默认使用nightly 14 | rustup default nightly 15 | 16 | # 克隆本项目 17 | git clone 18 | ``` 19 | 20 | 2. 克隆本项目 21 | 22 | !TODO 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 通过注册Bot机器人, 实现不同平台用户之间的消息进行同步 3 | 4 | ((TODO: [补上示例图片])) 5 | 6 | ## 联系方式 7 | - Discord频道: https://discord.gg/9Xv4YsxPSA 8 | - QQ群:https://jq.qq.com/?_wv=1027&k=D8ymzW7M 9 | 10 | 欢迎使用和部署, 加入上方联系方式也可以进行体验 11 | 12 | ## 环境要求 13 | 1. 科学上网 14 | 2. [NodeJs (v14 以上)](https://nodejs.org/en) 15 | 3. Rust: `curl https://sh.rustup.rs -sSf | sh` 16 | 3. Git 17 | 18 | ## Windows10 本地运行 19 | 环境配置 20 | 21 | 1. 配置rust默认使用nightly运行 22 | > rustup default nightly 23 | 24 | 2. 配置config.json 25 | 26 | > 复制config.simple.json文件并重命令为config.json 27 | 28 | 3. 启动解释discord消息服务 29 | > npm install
30 | > npm start 31 | 32 | 4. 运行桥服务 33 | > cargo run 34 | 35 | ## CenterOS 36 | > 安装命令参考 37 | 38 | 1. [Git](https://git-scm.com/download/linux): 命令: `yum install git` 39 | 2. NodeJs (v14 以上): 命令自行baidu 40 | 3. 全局安装pm2: `npm install -g pm2` 41 | 4. [Rust + Cargo](https://forge.rust-lang.org/infra/other-installation-methods.html): 命令: `curl https://sh.rustup.rs -sSf | sh` 42 | 5. 配置文件: `cp config.simple.json config.json` 配置说明: CONFIG.md ((TODO: 说明config.json怎么配置)) 43 | 44 | ## CenterOS 部署方式 45 | 46 | > ps: 以上环境请务必自选解决 47 | 48 | ```shell 49 | > git clone https://github.com/rabbitkiller-dev/message_bridge_rs 50 | > npm install 51 | 52 | ## 启动 (pm2进程守护) 53 | > npm run build 54 | > pm2 start server.js --name bridge_js 55 | > pm2 start "cargo run" --name bridge_js 56 | ``` 57 | 58 | 59 | ## 功能情况 60 | 61 | ((TODO)) 62 | 63 | ### 指令方面 64 | - !help 65 | - !关联 66 | - !解除关联 67 | 68 | #### 1. 关联 69 | 1. 第一步: 发送指令, bot会返回验证码, 记住后在另一个平台回复 70 | > !关联 71 | 2. 第二步: 在另一个平台上, 使用自己的账号, 发送指令 72 | > !关联 xxxxxx 73 | 3. 第三步: 返回原来的平台, 进行关联确认 74 | > !确认关联 75 | 76 | ### 2.0 遗留项 77 | 1. qq群自动审批 78 | 2. 桥后台配置界面 79 | 2. bot命令搜图 80 | 2. bot命令关联qq与dc用户 81 | 82 | 83 | ``` 84 | !帮助 85 | !ping 86 | !确认绑定 87 | !解除绑定 88 | !查看绑定状态 89 | !来点[搜图] 90 | !废话生成器 91 | !猜数字游戏 92 | 93 | 管理员: 94 | !服务器状态 95 | !重启 96 | !查看所有成员绑定关系 97 | !绑定成员关联 [用户名] [用户名] 98 | !解除成员关联 [用户名] 99 | 100 | ``` 101 | 102 | 103 | -------------------------------------------------------------------------------- /RELEASE-NOTES-1.0.md: -------------------------------------------------------------------------------- 1 | # message_bridge_rs 1.0 2 | 3 | ## message_bridge_rs 1.0.0-alpha.4 4 | 5 | THIS IS NOT A RELEASE YET 6 | 7 | ### Changes since message_bridge_rs 1.0.0-alpha.3 8 | 9 | * (bug #17) docs: Update RELEASE-NOTES for 1.0.0-alpha.1, 1.0.0-alpha.2, 10 | 1.0.0-alpha.3 11 | 12 | ## message_bridge_rs 1.0.0-alpha.3 13 | 14 | message_bridge_rs 1.0.0-alpha.3 is an alpha-quality development branch. 15 | 16 | See bug #15, #16. 17 | 18 | ### Changes since message_bridge_rs 1.0.0-alpha.2 19 | 20 | * feat: Handle file extension issues - part 2 21 | * feat: Handle file extension issues - part 3 22 | 23 | ## message_bridge_rs 1.0.0-alpha.2 24 | 25 | message_bridge_rs 1.0.0-alpha.2 is an alpha-quality development branch. 26 | 27 | See bug #14. 28 | 29 | ### Changes since message_bridge_rs 1.0.0-alpha.1 30 | 31 | * (bug #7) fix: Fix CI 'act-build' 32 | * (bug #8, #12) docs: Update RELEASE-NOTES 33 | 34 | ## message_bridge_rs 1.0.0-alpha.1 35 | 36 | message_bridge_rs 1.0.0-alpha.1 is an alpha-quality development branch. 37 | 38 | See bug #13. 39 | 40 | ### Changes since message_bridge_rs 0.1.0 41 | 42 | * feat: Support forward Tencent QQ group messages to Discord 43 | * feat: Complete bridge encapsulation 44 | * feat: Implement forward bridge messages to Tencent QQ groups 45 | * feat: Add bridge user and log related stuffs 46 | * feat: Support send user avatar when forwarding message 47 | * (bug #2) feat: Add command 'channel' 48 | * feat: Support convert Tencent QQ group message images to bridge images and 49 | forward bridge images to Discord 50 | * feat: Support forward Discord image attachments to Tencent QQ group 51 | * feat: Implement read & write user binding data (sync) 52 | * perf: Extend bridge::User 53 | * fix: Fix cmd_adapter errors 54 | * feat: Add Discord and Tencent QQ group user query and tests 55 | * refactor: Adjust formats for map-type data 56 | * (bug #3) pref: Command enumeration 57 | * (bug #4) feat: Add command 'confirm bind' 58 | * pref: Split message handling 59 | * (bug #4) pref: Unify user ID type 60 | * feat: Parse Discord message and identify cross-platform user mentions by using 61 | JS library 62 | * (bug #4) feat: Update command 'bind' 63 | * (bug #4) pref: Improve data structures for map-type data 64 | * fix: Temporarily disable command responses 65 | * feat: Support mentioning Discord users from Tencent QQ grops 66 | * (bug #5) feat: Sync CMD command responses 67 | * ci: Create rust.yml 68 | * feat: Support context for Mirai events 69 | * (bug #4) fix: Check, create data directories before read/write binding data 70 | * (bug #4) fix: Return failure-type responses when failed to save binding data 71 | * (bug #4) pref: Simplify query parameters for user binding queries 72 | * ci: Create main.yml 73 | * ci: Update rust.yml - remove 'cargo test' 74 | * ci: Update rust.yml - add 'upload-artifact' 75 | * ci: Test builds 76 | * ci: Use releases mode for builds 77 | * ci: Upload build artifacts 78 | * feat: Use tracing to record logs 79 | * pref: Add, adjust tracing logs for bridge 80 | * feat: Add dependency 'lazy_static' 81 | * feat: Add bridge message history 82 | * feat: Record relationships between sent messages and forward messages 83 | * (bug #4) feat: Add command 'unbind' 84 | * feat: Implement BitOr (bitwise operation 'OR') for platform enumeration 85 | * feat: Change reqwest - use rustls 86 | * pref: Replace println to logs 87 | * (bug #8) docs: Add COPYING, CREDITS, HISTORY, RELEASE-NOTES, SECURITY 88 | * (bug #7) feat: Migrate QQ group functionalities from MCL to RICQ 89 | * (bug #7) feat: Complete Tencent QQ group bridge & support improved message 90 | images to bridge - part 1 91 | * (bug #7) feat: Complete Tencent QQ group bridge & support improved message 92 | images to bridge - part 2 93 | * (bug #7) feat: Add user_manager for bridge user management 94 | * (bug #4, #7) feat: Configure global service 'user_manager' & complete user 95 | mention handling in Discord and Tencent QQ groups 96 | * (bug #4, #7) feat: Complete user relation query feature & implement fetch 97 | cross-platform user mention for bonded accounts for Discord and Tencent QQ 98 | group 99 | * (bug #7) feat: Add binary expression macro 100 | * (bug #7) feat: Adjust variable name & update module structures - part 1 101 | * (bug #7) feat: Adjust variable name & update module structures - part 2 102 | * (bug #7) docs: Update README.md 103 | * (bug #7) feat: Remove feature 'message history' 104 | * (bug #7) fix: Log level too low when not being set in environment varibles 105 | * (bug #7) pref: Clean up modules 106 | * (bug #7) fix: Use nightly for CI 'act-build' for GitHub 107 | * feat: Handle file extension issues 108 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security information 2 | 3 | message_bridge_rs takes security very seriously. If you believe you have found a 4 | security issue, please report it at 5 | 6 | -------------------------------------------------------------------------------- /avatar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/avatar.webp -------------------------------------------------------------------------------- /bridge-cmd.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /bridge-design-old.drawio: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bridge-design.drawio: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/bridge-design.drawio -------------------------------------------------------------------------------- /config.simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "qqConfig": { 3 | "botId": # 机器人账号 #, 4 | "password": # 机器人登录密码 16位MD5 #, 5 | "version": # 登录协议 iPad MacOS QiDian AndroidPhone AndroidWatch #, 6 | "auth": # 无token时的认证方式 qr(二维码) pwd(账号+密码) # 7 | }, 8 | "discordConfig": { 9 | "botId": # 机器人的id(u64) #, 10 | "botToken": # 机器人的token # 11 | }, 12 | "telegramConfig": { 13 | "apiId": # apiId(i32) # , 14 | "apiHash": # apiHash # , 15 | "botToken": # botToken # 16 | }, 17 | "bridges": [ 18 | { 19 | "discord": { 20 | "id": # 频道的Webhook id(u64) #, 21 | "token": # 频道的token #, 22 | "channelId": # 频道的id(u64) # 23 | }, 24 | "tgGroup": # telegram群号 #, 25 | "qqGroup": # qq群号(u64) #, 26 | "enable": true 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/data/.gitkeep -------------------------------------------------------------------------------- /docs/discord/images/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/docs/discord/images/0.png -------------------------------------------------------------------------------- /docs/discord/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/docs/discord/images/1.png -------------------------------------------------------------------------------- /docs/discord/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/docs/discord/images/2.png -------------------------------------------------------------------------------- /docs/discord/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/docs/discord/images/3.png -------------------------------------------------------------------------------- /docs/discord/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/docs/discord/images/4.png -------------------------------------------------------------------------------- /docs/discord/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yefu24324/message_bridge_rs/699f4c7c1bc2f4ecb9b50a42ff13faebf8694d44/docs/discord/images/5.png -------------------------------------------------------------------------------- /docs/discord/set_discord_channel_webhook.md: -------------------------------------------------------------------------------- 1 | 1. 2 | ![0.png](images%2F0.png) 3 | 4 | ![1.png](images%2F1.png) 5 | 6 | ![2.png](images%2F2.png) 7 | 8 | ![3.png](images%2F3.png) 9 | 10 | ![4.png](images%2F4.png) 11 | 12 | https://discord.com/api/webhooks/1093074789809262662/n2WrC4sIHASKeklYFos6Usr3c2j3k7_v0O2USPQ8i0Ac27hk8TrWLi-z1NxR7TaFMMsY 13 | -------------------------------------------------------------------------------- /mirai_rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mirai_rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | reqwest = { version = "0.11", features = ["json"] } 10 | tokio = { version = "1.14.0", features = ["full"] } 11 | serde_json = "1.0" 12 | serde = { version = "1.0", features = ["derive"] } 13 | 14 | [dependencies.async-trait] 15 | version = "0.1.9" -------------------------------------------------------------------------------- /mirai_rs/src/adapter/http_adapter.rs: -------------------------------------------------------------------------------- 1 | pub struct HttpAdapter {} 2 | -------------------------------------------------------------------------------- /mirai_rs/src/adapter/mod.rs: -------------------------------------------------------------------------------- 1 | mod http_adapter; 2 | 3 | pub use http_adapter::HttpAdapter; 4 | -------------------------------------------------------------------------------- /mirai_rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod adapter; 2 | pub mod message; 3 | pub mod mirai_http; 4 | pub mod model; 5 | pub mod response; 6 | 7 | pub use async_trait::async_trait; 8 | use core::panic; 9 | use message::{EventPacket, MessageEvent}; 10 | use model::BaseResponse; 11 | use response::{AboutResponse, BindResponse, VerifyResponse}; 12 | use serde_json::{json, Value}; 13 | use std::collections::HashMap; 14 | use std::sync::Arc; 15 | 16 | pub type HttpResult = std::result::Result>; 17 | 18 | pub type Target = u64; 19 | 20 | pub struct Mirai { 21 | host: String, 22 | port: u32, 23 | verify_key: String, 24 | qq: u32, 25 | session_key: String, 26 | event_handler: Option>, 27 | } 28 | 29 | impl Mirai { 30 | pub fn builder(host: &str, port: u32, verify_key: &str) -> MiraiBuilder { 31 | MiraiBuilder { 32 | host: host.to_string(), 33 | port, 34 | verify_key: verify_key.to_string(), 35 | qq: 0, 36 | event_handler: None, 37 | } 38 | } 39 | 40 | /** 41 | * 认证 42 | * 发送verify_key获取session_key 43 | * https://github.com/project-mirai/mirai-api-http/blob/master/docs/adapter/HttpAdapter.md#%E8%AE%A4%E8%AF%81 44 | */ 45 | pub async fn verify(&mut self) -> HttpResult { 46 | let js = json!({"verifyKey": self.verify_key}); 47 | let client = reqwest::Client::new(); 48 | let mut data = HashMap::new(); 49 | data.insert("verifyKey", self.verify_key.as_str()); 50 | 51 | let resp: VerifyResponse = client 52 | .post(self.get_url("/verify")) 53 | .json(&data) 54 | .send() 55 | .await? 56 | .json() 57 | .await?; 58 | 59 | self.session_key = resp.session.clone(); 60 | 61 | Ok(resp) 62 | } 63 | 64 | pub async fn bind(&self) -> HttpResult { 65 | let js = json!({"verifyKey": self.verify_key}); 66 | let client = reqwest::Client::new(); 67 | let mut data: HashMap<&str, Value> = HashMap::new(); 68 | data.insert("sessionKey", json!(self.session_key)); 69 | data.insert("qq", json!(self.qq)); 70 | 71 | println!("{:?}", data); 72 | 73 | let resp: BindResponse = client 74 | .post(self.get_url("/bind")) 75 | .json(&data) 76 | .send() 77 | .await? 78 | .json() 79 | .await?; 80 | 81 | Ok(resp) 82 | } 83 | 84 | pub async fn about() -> HttpResult { 85 | let client = reqwest::Client::new(); 86 | let resp: AboutResponse = client 87 | .get("http://52.193.15.252:8080/about") 88 | .send() 89 | .await? 90 | .json() 91 | .await?; 92 | 93 | Ok(resp) 94 | } 95 | 96 | pub async fn fetch_message(&self, count: u32) -> HttpResult>> { 97 | let path = format!( 98 | "/fetchMessage?sessionKey={}&count={}", 99 | &self.session_key, count 100 | ); 101 | let client = reqwest::Client::new(); 102 | let resp: BaseResponse> = client 103 | .get(&self.get_url(path.as_str())) 104 | .send() 105 | .await? 106 | .json() 107 | .await?; 108 | 109 | Ok(resp) 110 | } 111 | 112 | pub async fn start(&mut self) { 113 | let event_handler = match &self.event_handler { 114 | Some(event_handler) => event_handler, 115 | None => { 116 | panic!(""); 117 | } 118 | }; 119 | loop { 120 | let result = self.fetch_message(1).await; 121 | let event_handler = event_handler.clone(); 122 | match result { 123 | Ok(res) => { 124 | for item in res.data { 125 | if let EventPacket::MessageEvent(message) = item { 126 | event_handler.message(&self, message).await; 127 | continue; 128 | } 129 | println!("接收到其它消息"); 130 | println!("{:?}", serde_json::to_string(&item).unwrap()); 131 | } 132 | } 133 | Err(err) => { 134 | println!("{:?}", err); 135 | println!("获取信息失败"); 136 | } 137 | } 138 | tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; 139 | // let result = self.http_adapter.fetch_message(10).await; 140 | // println!("接收到消息? {:?}", result); 141 | // for event in result.data { 142 | // match event { 143 | // EventPacket::MessageEvent(message_event) => { 144 | // self.event_handler.message(message_event.clone()).await; 145 | // match message_event { 146 | // MessageEvent::GroupMessage(message) => { 147 | // self.event_handler.group_message(message).await 148 | // } 149 | // _ => println!(), 150 | // } 151 | // } 152 | // event => { 153 | // println!("{:?}", event); 154 | // eprintln!("没有处理的事件"); 155 | // } 156 | // } 157 | // } 158 | } 159 | } 160 | 161 | pub async fn get_http(&self) -> mirai_http::MiraiHttp { 162 | mirai_http::MiraiHttp::new(self) 163 | } 164 | 165 | pub fn get_url(&self, uri: &str) -> String { 166 | return format!("http://{}:{}{}", self.host, self.port, uri); 167 | } 168 | } 169 | 170 | pub struct MiraiBuilder { 171 | host: String, 172 | port: u32, 173 | verify_key: String, 174 | qq: u32, 175 | 176 | event_handler: Option>, 177 | } 178 | 179 | impl MiraiBuilder { 180 | pub fn bind_qq(mut self, qq: u32) -> Self { 181 | self.qq = qq; 182 | self 183 | } 184 | /// Sets an event handler with multiple methods for each possible event. 185 | pub async fn event_handler(mut self, event_handler: H) -> Mirai { 186 | self.event_handler = Some(Arc::new(event_handler)); 187 | 188 | let mut mirai = Mirai { 189 | host: self.host, 190 | port: self.port, 191 | verify_key: self.verify_key, 192 | qq: self.qq, 193 | event_handler: self.event_handler, 194 | session_key: "".to_string(), 195 | }; 196 | 197 | println!("{},{}", &mirai.host, &mirai.port); 198 | 199 | match mirai.verify().await { 200 | Ok(res) => { 201 | mirai.session_key = res.session; 202 | } 203 | Err(err) => { 204 | eprintln!("{:?}", err); 205 | panic!("获取verify请求出错"); 206 | } 207 | } 208 | 209 | match mirai.bind().await { 210 | Ok(res) => {} 211 | Err(err) => { 212 | println!("绑定qq请求出错") 213 | } 214 | } 215 | 216 | mirai 217 | } 218 | } 219 | 220 | pub mod api { 221 | pub use super::message::EventPacket; 222 | pub use super::message::MessageEvent; 223 | } 224 | 225 | /// The core trait for handling events by serenity. 226 | #[async_trait] 227 | pub trait EventHandler: Send + Sync { 228 | async fn message(&self, ctx: &Mirai, msg: MessageEvent); 229 | } 230 | -------------------------------------------------------------------------------- /mirai_rs/src/message.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use serde_json::Value; 4 | 5 | use crate::model::group::{GroupMessage, GroupSender}; 6 | use crate::Target; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | #[serde(untagged)] 10 | pub enum EventPacket { 11 | MessageEvent(MessageEvent), 12 | // BotLoginEvent(), 13 | // BotMuteEvent(), 14 | // RecallEvent(), 15 | // GroupChangeEvent(), 16 | Unsupported(Value), 17 | } 18 | 19 | pub mod sender { 20 | use crate::Target; 21 | use serde::Deserialize; 22 | use serde::Serialize; 23 | 24 | #[derive(Debug, Clone, Serialize, Deserialize)] 25 | pub struct FriendSender { 26 | pub id: Target, 27 | pub nickname: String, 28 | pub remark: String, 29 | } 30 | 31 | #[derive(Debug, Clone, Serialize, Deserialize)] 32 | pub struct OtherClientSender { 33 | pub id: Target, 34 | pub platform: String, 35 | } 36 | } 37 | 38 | // #[serde(flatten)] 39 | // extra: std::collections::HashMap, 40 | #[derive(Debug, Clone, Serialize, Deserialize)] 41 | #[serde(tag = "type")] 42 | pub enum MessageEvent { 43 | GroupMessage(GroupMessage), 44 | TempMessage { 45 | #[serde(rename = "messageChain")] 46 | message_chain: MessageChain, 47 | sender: GroupSender, 48 | }, 49 | FriendMessage { 50 | #[serde(rename = "messageChain")] 51 | message_chain: MessageChain, 52 | sender: sender::FriendSender, 53 | }, 54 | StrangerMessage { 55 | #[serde(rename = "messageChain")] 56 | message_chain: MessageChain, 57 | sender: sender::FriendSender, 58 | }, 59 | OtherClientMessage { 60 | #[serde(rename = "messageChain")] 61 | message_chain: MessageChain, 62 | sender: sender::OtherClientSender, 63 | }, 64 | } 65 | 66 | pub type MessageChain = Vec; 67 | 68 | #[serde(tag = "type")] 69 | #[derive(Debug, Clone, Serialize, Deserialize)] 70 | pub enum MessageContent { 71 | Source { 72 | id: Target, 73 | time: u32, 74 | }, 75 | #[serde(rename_all = "camelCase")] 76 | Quote { 77 | id: u32, 78 | group_id: Target, 79 | sender_id: Target, 80 | target_id: Target, 81 | origin: MessageChain, 82 | }, 83 | At { 84 | target: Target, 85 | display: Option, 86 | }, 87 | AtAll {}, 88 | #[serde(rename_all = "camelCase")] 89 | Face { 90 | face_id: Option, 91 | name: Option, 92 | }, 93 | Plain { 94 | text: String, 95 | }, 96 | #[serde(rename_all = "camelCase")] 97 | Image { 98 | image_id: Option, 99 | url: Option, 100 | path: Option, 101 | base64: Option, 102 | }, 103 | #[serde(rename_all = "camelCase")] 104 | FlashImage { 105 | image_id: Option, 106 | url: Option, 107 | path: Option, 108 | base64: Option, 109 | }, 110 | #[serde(rename_all = "camelCase")] 111 | Voice { 112 | voice_id: Option, 113 | url: Option, 114 | path: Option, 115 | base64: Option, 116 | length: Option, 117 | }, 118 | Xml { 119 | xml: String, 120 | }, 121 | Json { 122 | json: String, 123 | }, 124 | App { 125 | content: String, 126 | }, 127 | Poke { 128 | name: Poke, 129 | }, 130 | Dice { 131 | value: u16, 132 | }, 133 | #[serde(rename_all = "camelCase")] 134 | MusicShare { 135 | kind: String, 136 | title: String, 137 | summary: String, 138 | jump_url: String, 139 | picture_url: String, 140 | music_url: String, 141 | brief: String, 142 | }, 143 | ForwardMessage { 144 | sender_id: Target, 145 | time: u32, 146 | sender_name: String, 147 | message_chain: MessageChain, 148 | message_id: u16, 149 | }, 150 | File { 151 | id: String, 152 | name: String, 153 | size: u32, 154 | }, 155 | MiraiCode { 156 | code: String, 157 | }, 158 | } 159 | 160 | #[derive(Debug, Clone, Serialize, Deserialize)] 161 | pub enum Poke { 162 | Poke, 163 | ShowLove, 164 | Like, 165 | Heartbroken, 166 | SixSixSix, 167 | FangDaZhao, 168 | } 169 | -------------------------------------------------------------------------------- /mirai_rs/src/mirai_http.rs: -------------------------------------------------------------------------------- 1 | use crate::message::EventPacket; 2 | use crate::message::MessageChain; 3 | use crate::model::group::Member; 4 | use crate::model::BaseResponse; 5 | use crate::model::SendGroupMessageResponse; 6 | use crate::{HttpResult, Mirai}; 7 | 8 | use serde_json::json; 9 | use std::collections::HashMap; 10 | 11 | pub struct MiraiHttp { 12 | host: String, 13 | port: u32, 14 | verify_key: String, 15 | qq: u32, 16 | session_key: String, 17 | req: reqwest::Client, 18 | } 19 | 20 | impl MiraiHttp { 21 | pub fn new(mirai: &Mirai) -> Self { 22 | MiraiHttp { 23 | host: mirai.host.clone(), 24 | port: mirai.port, 25 | verify_key: mirai.verify_key.clone(), 26 | qq: mirai.qq, 27 | session_key: mirai.session_key.clone(), 28 | req: reqwest::Client::new(), 29 | } 30 | } 31 | 32 | pub async fn fetch_message(&self, count: u32) -> HttpResult>> { 33 | let path = format!( 34 | "/fetchMessage?sessionKey={}&count={}", 35 | &self.session_key, count 36 | ); 37 | let client = reqwest::Client::new(); 38 | let resp: BaseResponse> = client 39 | .get(&self.get_url(path.as_str())) 40 | .send() 41 | .await? 42 | .json() 43 | .await?; 44 | 45 | Ok(resp) 46 | } 47 | 48 | pub async fn send_group_message( 49 | &self, 50 | message_chain: MessageChain, 51 | group: u64, 52 | ) -> HttpResult { 53 | let js = json!({ 54 | "sessionKey": self.session_key, 55 | "group": group, 56 | "messageChain": message_chain 57 | }); 58 | // let mut data = HashMap::new(); 59 | // data.insert("verifyKey", self.verify_key.as_str()); 60 | 61 | let response = match self 62 | .req 63 | .post(self.get_url("/sendGroupMessage")) 64 | .json(&js) 65 | .send() 66 | .await 67 | { 68 | Ok(resp) => resp, 69 | Err(err) => { 70 | println!("[mirai_http] send_group_message请求失败"); 71 | println!("[mirai_http] {:?}", err); 72 | Result::Err(err)? 73 | } 74 | }; 75 | println!("[mirai_http] send_group_message {}", response.status()); 76 | let resp = response.text().await.unwrap(); 77 | let resp: SendGroupMessageResponse = match serde_json::from_str(resp.as_str()) { 78 | Ok(resp) => resp, 79 | Err(err) => { 80 | println!("[mirai_http] send_group_message转换json失败"); 81 | println!("[mirai_http] {:?}", resp); 82 | println!("[mirai_http] {:?}", err); 83 | Result::Err(err)? 84 | } 85 | }; 86 | 87 | Ok(resp) 88 | } 89 | 90 | pub async fn member_list(&self, target: u64) -> HttpResult>> { 91 | let path = format!( 92 | "/memberList?sessionKey={}&target={}", 93 | &self.session_key, target 94 | ); 95 | 96 | let response = match self.req.get(self.get_url(path.as_str())).send().await { 97 | Ok(resp) => resp, 98 | Err(err) => { 99 | println!("[mirai_http] member_list请求失败"); 100 | println!("[mirai_http] {:?}", err); 101 | Result::Err(err)? 102 | } 103 | }; 104 | println!("[mirai_http] member_list {}", response.status()); 105 | let resp = response.text().await.unwrap(); 106 | let resp: BaseResponse> = match serde_json::from_str(resp.as_str()) { 107 | Ok(resp) => resp, 108 | Err(err) => { 109 | println!("[mirai_http] member_list转换json失败"); 110 | println!("[mirai_http] {:?}", resp); 111 | println!("[mirai_http] {:?}", err); 112 | Result::Err(err)? 113 | } 114 | }; 115 | 116 | Ok(resp) 117 | } 118 | 119 | pub async fn get_member_info(&self, target: u64, member_id: u64) -> HttpResult { 120 | let path = format!( 121 | "/memberInfo?sessionKey={}&target={}&memberId={}", 122 | &self.session_key, target, member_id 123 | ); 124 | 125 | let response = match self.req.get(self.get_url(path.as_str())).send().await { 126 | Ok(resp) => resp, 127 | Err(err) => { 128 | println!("[mirai_http] get_member_info请求失败"); 129 | println!("[mirai_http] {:?}", err); 130 | Result::Err(err)? 131 | } 132 | }; 133 | println!("[mirai_http] get_member_info {}", response.status()); 134 | let resp = response.text().await.unwrap(); 135 | let resp: Member = match serde_json::from_str(resp.as_str()) { 136 | Ok(resp) => resp, 137 | Err(err) => { 138 | println!("[mirai_http] get_member_info转换json失败"); 139 | println!("[mirai_http] {:?}", resp); 140 | println!("[mirai_http] {:?}", err); 141 | Result::Err(err)? 142 | } 143 | }; 144 | 145 | Ok(resp) 146 | } 147 | 148 | pub fn get_url(&self, uri: &str) -> String { 149 | return format!("http://{}:{}{}", self.host, self.port, uri); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /mirai_rs/src/model/group.rs: -------------------------------------------------------------------------------- 1 | use crate::message::MessageChain; 2 | use crate::Target; 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use serde_json::Value; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct GroupMessage { 9 | #[serde(rename = "messageChain")] 10 | pub message_chain: MessageChain, 11 | pub sender: GroupSender, 12 | } 13 | 14 | /** 15 | * 16 | */ 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub enum Permission { 19 | #[serde(rename = "ADMINISTRATOR")] 20 | Administrator, 21 | 22 | #[serde(rename = "OWNER")] 23 | Owner, 24 | 25 | #[serde(rename = "MEMBER")] 26 | Member, 27 | } 28 | 29 | #[derive(Debug, Clone, Serialize, Deserialize)] 30 | pub struct GroupSender { 31 | pub id: Target, 32 | 33 | #[serde(rename = "memberName")] 34 | pub member_name: String, 35 | 36 | #[serde(rename = "specialTitle")] 37 | pub special_title: String, 38 | 39 | pub permission: Permission, 40 | 41 | #[serde(rename = "joinTimestamp")] 42 | pub join_timestamp: u64, 43 | 44 | #[serde(rename = "lastSpeakTimestamp")] 45 | pub last_speak_timestamp: u64, 46 | 47 | #[serde(rename = "muteTimeRemaining")] 48 | pub mute_time_remaining: u64, 49 | 50 | pub group: Group, 51 | } 52 | 53 | #[derive(Debug, Clone, Serialize, Deserialize)] 54 | pub struct Group { 55 | pub id: Target, 56 | pub name: String, 57 | pub permission: Permission, 58 | } 59 | /** 60 | * 群成员信息类型 61 | */ 62 | #[derive(Debug, Clone, Serialize, Deserialize)] 63 | pub struct Member { 64 | /** 65 | * 群名片 66 | */ 67 | #[serde(rename = "memberName")] 68 | pub member_name: String, 69 | 70 | /** 71 | * 群权限 OWNER、ADMINISTRATOR 或 MEMBER 72 | */ 73 | permission: Permission, 74 | /** 75 | * 群头衔 76 | */ 77 | #[serde(rename = "specialTitle")] 78 | special_title: String, 79 | /** 80 | * 入群时间戳 81 | */ 82 | #[serde(rename = "joinTimestamp")] 83 | join_timestamp: i64, 84 | /** 85 | * 上一次发言时间戳 86 | */ 87 | #[serde(rename = "lastSpeakTimestamp")] 88 | last_speak_timestamp: i64, 89 | /** 90 | * 剩余禁言时间 91 | */ 92 | #[serde(rename = "muteTimeRemaining")] 93 | mute_time_remaining: u64, 94 | /** 95 | * 所在的群 96 | */ 97 | group: Group, 98 | } 99 | -------------------------------------------------------------------------------- /mirai_rs/src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod group; 2 | 3 | use serde::Deserialize; 4 | use serde::Serialize; 5 | use serde_json::Value; 6 | 7 | #[derive(Debug, Serialize, Deserialize)] 8 | pub struct SendGroupMessageResponse { 9 | pub code: u32, 10 | pub msg: String, 11 | pub messageId: u64, 12 | } 13 | 14 | /** 15 | * 基础响应格式 16 | */ 17 | #[derive(Debug, Serialize, Deserialize)] 18 | pub struct BaseResponse { 19 | pub code: u32, 20 | pub msg: String, 21 | pub data: T, 22 | } 23 | -------------------------------------------------------------------------------- /mirai_rs/src/response.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 5 | pub struct AboutResponse { 6 | pub code: u32, 7 | pub data: AboutData, 8 | } 9 | 10 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 11 | pub struct AboutData { 12 | pub version: String, 13 | } 14 | 15 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 16 | pub struct VerifyResponse { 17 | pub code: u32, 18 | pub session: String, 19 | } 20 | 21 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 22 | pub struct BindResponse { 23 | pub code: u32, 24 | pub msg: String, 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-server", 3 | "version": "3.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "tsc server.ts", 8 | "start": "ts-node --project tsconfig.json server.ts " 9 | }, 10 | "dependencies": { 11 | "discord-markdown": "^2.5.0" 12 | }, 13 | "devDependencies": { 14 | "@types/express": "^4.17.11", 15 | "@types/node": "^14.14.31", 16 | "ts-loader": "^8.0.17", 17 | "ts-node": "^10.9.1", 18 | "typescript": "^4.1.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 140 -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import { markdownEngine as markdown, htmlTag } from 'discord-markdown'; 3 | 4 | const server = http.createServer(async (req, res) => { 5 | console.log('ok'); 6 | 7 | const url = req.url?.split('?')[0]; 8 | const bodyStr = await new Promise((res, rej) => { 9 | let bodyStr = ''; 10 | req.on('data', (chunk) => { 11 | bodyStr += chunk 12 | }) 13 | req.on('end', () => { 14 | res(bodyStr); 15 | }) 16 | }) 17 | console.log(bodyStr); 18 | if (url === '/parse-discord-markdown') { 19 | const data = parseDiscrodMarkdown(bodyStr); 20 | console.log(data); 21 | res.setHeader('Content-Type', 'application/json') 22 | res.end(JSON.stringify(data)); 23 | return; 24 | } 25 | }) 26 | 27 | function parseDiscrodMarkdown(message: string): any { 28 | const ast = markdown.parserFor({ 29 | atDC: bridgeRule.atDC, 30 | atQQ: bridgeRule.atQQ, 31 | atKHL: bridgeRule.atKHL, 32 | DiscordAtUser: bridgeRule.DiscordAtUser, 33 | DiscordAtEveryone: bridgeRule.DiscordAtEveryone, 34 | DiscordAtHere: bridgeRule.DiscordAtHere, 35 | DiscordEmoji: bridgeRule.DiscordEmoji, 36 | Plain: bridgeRule.Plain, 37 | })(message) as Array<{ type: 'DiscordAtUser' | 'DiscordEmoji', [prop: string]: any }>; 38 | return ast; 39 | } 40 | 41 | export const bridgeRule = { 42 | atDC: { 43 | order: 0, 44 | match: source => /^@\[DC\] [^\n]+?#\d\d\d\d/.exec(source), 45 | parse: function (capture, parse, state) { 46 | console.log(capture); 47 | return { type: 'At', username: capture[0] }; 48 | }, 49 | html: function (node, output, state) { 50 | return '{{atDc}}'; 51 | }, 52 | }, 53 | atKHL: { 54 | order: 0, 55 | match: source => /^@\[KHL\] ([^\n#]+)#(\d\d\d\d)/.exec(source), 56 | parse: function (capture, parse, state) { 57 | console.log(capture); 58 | return { type: 'At', source: 'KHL', username: capture[1], discriminator: capture[2] }; 59 | }, 60 | html: function (node, output, state) { 61 | return '{{atDc}}'; 62 | }, 63 | }, 64 | atQQ: { 65 | order: 0, 66 | match: source => /^@\[QQ\] [^\n]+?\([0-9]+\)/.exec(source), 67 | parse: function (capture, parse, state) { 68 | console.log(capture); 69 | return { type: 'At', username: capture[0] }; 70 | }, 71 | html: function (node, output, state) { 72 | return '{{atDc}}'; 73 | }, 74 | }, 75 | Plain: Object.assign({}, markdown.defaultRules.text, { 76 | match: source => /^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff-]|\n\n|\n|\w+:\S|$)/.exec(source), 77 | parse: function (capture, parse, state) { 78 | return { type: 'Plain', text: capture[0] }; 79 | }, 80 | html: function (node, output, state) { 81 | if (state.escapeHTML) 82 | return markdown.sanitizeText(node.content); 83 | 84 | return node.content; 85 | }, 86 | }), 87 | DiscordAtUser: { 88 | order: markdown.defaultRules.strong.order, 89 | match: source => /^<@!?([0-9]*)>/.exec(source), 90 | parse: function (capture) { 91 | return { 92 | id: capture[1] 93 | }; 94 | }, 95 | html: function (node, output, state) { 96 | return htmlTag('span', state.discordCallback.user(node), { class: 'd-mention d-user' }, state); 97 | } 98 | }, 99 | DiscordAtEveryone: { 100 | order: markdown.defaultRules.strong.order, 101 | match: source => /^@everyone/.exec(source), 102 | parse: function () { 103 | return { }; 104 | }, 105 | html: function (node, output, state) { 106 | return htmlTag('span', state.discordCallback.everyone(node), { class: 'd-mention d-user' }, state); 107 | }, 108 | }, 109 | DiscordAtHere: { 110 | order: markdown.defaultRules.strong.order, 111 | match: source => /^@here/.exec(source), 112 | parse: function () { 113 | return { }; 114 | }, 115 | html: function (node, output, state) { 116 | return htmlTag('span', state.discordCallback.here(node), { class: 'd-mention d-user' }, state); 117 | } 118 | }, 119 | DiscordEmoji: { 120 | order: markdown.defaultRules.strong.order, 121 | match: source => /^<(a?):(\w+):(\d+)>/.exec(source), 122 | parse: function (capture) { 123 | return { 124 | animated: capture[1] === "a", 125 | name: capture[2], 126 | id: capture[3], 127 | }; 128 | }, 129 | html: function (node, output, state) { 130 | return htmlTag('img', '', { 131 | class: `d-emoji${node.animated ? ' d-emoji-animated' : ''}`, 132 | src: `https://cdn.discordapp.com/emojis/${node.id}.${node.animated ? 'gif' : 'png'}`, 133 | alt: `:${node.name}:` 134 | }, false, state); 135 | } 136 | }, 137 | khlEveryone: { 138 | order: markdown.defaultRules.strong.order, 139 | match: source => /\(met\)all\(met\)/.exec(source), 140 | parse: function () { 141 | return { type: 'AtAll' }; 142 | }, 143 | html: function (node, output, state) { 144 | return htmlTag('span', state.discordCallback.everyone(node), { class: 'd-mention d-user' }, state); 145 | }, 146 | }, 147 | }; 148 | 149 | parseDiscrodMarkdown(`@[DC] 6uopdong#4700 150 | !绑定 qq 1261972160 asd`); 151 | server.listen(3000) 152 | -------------------------------------------------------------------------------- /src/bridge/bridge_message.rs: -------------------------------------------------------------------------------- 1 | use crate::config::BridgeConfig; 2 | use serde::Deserialize; 3 | use serde::Serialize; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct BridgeMessage { 7 | pub id: String, 8 | // 桥用户 9 | pub sender_id: String, 10 | // 头像链接 11 | pub avatar_url: Option, 12 | pub bridge_config: BridgeConfig, 13 | pub message_chain: MessageChain, 14 | } 15 | 16 | pub type MessageChain = Vec; 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize)] 19 | #[serde(tag = "type")] 20 | pub enum MessageContent { 21 | /** 22 | * 回复 23 | */ 24 | Reply { 25 | /** 26 | * 想要回复的桥消息id 27 | */ 28 | id: Option, 29 | }, 30 | /** 31 | * 普通文本 32 | */ 33 | Plain { 34 | text: String, 35 | }, 36 | /** 37 | * 提及某人 38 | */ 39 | At { 40 | /** 41 | * 目标用户的桥用户id 42 | */ 43 | id: String, 44 | }, 45 | /** 46 | * 提及所有人 47 | */ 48 | AtAll, 49 | /** 50 | * 图片 51 | */ 52 | Image(Image), 53 | /** 54 | * 发生了一些错误 55 | */ 56 | Err { 57 | // 错误信息 58 | message: String, 59 | }, 60 | Othen, 61 | } 62 | 63 | #[derive(Debug, Clone, Serialize, Deserialize)] 64 | pub enum Image { 65 | Url(String), 66 | Path(String), 67 | Buff(Vec), 68 | } 69 | 70 | impl Image { 71 | pub(crate) async fn load_data(self) -> anyhow::Result> { 72 | match self { 73 | Image::Url(url) => Ok(reqwest::get(url).await?.bytes().await?.to_vec()), 74 | Image::Path(path) => Ok(tokio::fs::read(path).await?), 75 | Image::Buff(data) => Ok(data), 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/bridge/config.rs: -------------------------------------------------------------------------------- 1 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 2 | pub struct Config { 3 | /// 是否将二维码打印到终端 4 | #[serde(rename = "printQR")] 5 | pub print_qr: Option, 6 | #[serde(rename = "qqConfig")] 7 | pub qq_config: QQConfig, 8 | #[serde(rename = "discordConfig")] 9 | pub discord_config: DiscordConfig, 10 | #[serde(rename = "telegramConfig")] 11 | pub telegram_config: TelegramConfig, 12 | pub bridges: Vec, 13 | } 14 | 15 | impl Config { 16 | pub fn new() -> Self { 17 | let file = fs::read_to_string("./config.json").unwrap(); 18 | // println!("{file}"); 19 | let config: Config = serde_json::from_str(file.as_str()).unwrap(); 20 | 21 | config 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/bridge/manager/message_manager.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::fs; 3 | use tokio::sync::Mutex; 4 | 5 | use crate::bridge; 6 | use bridge::pojo::{BridgeMessagePO, BridgeMessageRefMessageForm}; 7 | pub struct BridgeMessageManager { 8 | messages: Vec, 9 | } 10 | 11 | impl BridgeMessageManager { 12 | pub fn new() -> BridgeMessageManager { 13 | let path = "./data/bridge_message.json"; 14 | if let Ok(true) = fs::try_exists(path) { 15 | let file = fs::read_to_string(path).unwrap(); 16 | return BridgeMessageManager { 17 | messages: serde_json::from_str(file.as_str()).unwrap(), 18 | }; 19 | } 20 | BridgeMessageManager { messages: vec![] } 21 | } 22 | 23 | /** 24 | * 查询指定消息 25 | */ 26 | pub async fn get(&self, id: &str) -> Option { 27 | for message in &self.messages { 28 | if id.eq(&message.id) { 29 | return Some(message.clone()); 30 | } 31 | } 32 | None 33 | } 34 | /** 35 | * 保存消息 36 | */ 37 | pub async fn save(&mut self, form: bridge::pojo::BridgeSendMessageForm) -> String { 38 | let id = uuid::Uuid::new_v4().to_string(); 39 | let mut bridge_message = bridge::pojo::BridgeMessagePO { 40 | id: id.clone(), 41 | refs: vec![], 42 | sender_id: form.sender_id, 43 | avatar_url: form.avatar_url, 44 | message_chain: form.message_chain, 45 | }; 46 | bridge_message.refs.push(form.origin_message); 47 | self.messages.push(bridge_message); 48 | self.serialize(); 49 | id 50 | } 51 | 52 | /** 53 | * 关联消息桥消息 54 | */ 55 | pub async fn ref_bridge_message(&mut self, form: BridgeMessageRefMessageForm) -> bool { 56 | let message = self 57 | .messages 58 | .iter_mut() 59 | .find(|message| form.bridge_message_id.eq(&message.id)); 60 | match message { 61 | Some(message) => { 62 | message.refs.push(bridge::pojo::BridgeMessageRefPO { 63 | origin_id: form.origin_id, 64 | platform: form.platform, 65 | }); 66 | self.serialize(); 67 | true 68 | } 69 | None => false, 70 | } 71 | // for user in &self.bridge_users { 72 | // if origin_id.eq(&user.origin_id) && platform.eq(&user.platform) { 73 | // return Some(user.clone()); 74 | // } 75 | // } 76 | // let mut bridge_message = bridge::pojo::BridgeMessagePO { 77 | // id: id.clone(), 78 | // refs: vec![], 79 | // }; 80 | // bridge_message.refs.push(bridge::pojo::BridgeMessageRefPO { 81 | // platform: form.platform, 82 | // origin_id: form.origin_id, 83 | // }); 84 | // self.messages.push(bridge_message); 85 | // self.serialize(); 86 | // id 87 | } 88 | 89 | /** 90 | * 根据关联id和平台查询桥消息 91 | */ 92 | pub async fn find_by_ref_and_platform( 93 | &self, 94 | origin_id: &str, 95 | platform: &str, 96 | ) -> Result, String> { 97 | let refs: Vec<&BridgeMessagePO> = self 98 | .messages 99 | .iter() 100 | .filter(|message| { 101 | message 102 | .refs 103 | .iter() 104 | .find(|refs| refs.origin_id.eq(origin_id) && refs.platform.eq(platform)) 105 | .is_some() 106 | }) 107 | .collect(); 108 | if refs.len() > 1 { 109 | return Err("关联的消息查询到了多条".to_string()); 110 | } 111 | if refs.len() == 1 { 112 | let po = refs.get(0).unwrap().clone().clone(); 113 | return Ok(Some(po)); 114 | } 115 | Ok(None) 116 | } 117 | 118 | fn serialize(&self) { 119 | let content = serde_json::to_string(&self.messages).unwrap(); 120 | fs::write("./data/bridge_message.json", content).unwrap(); 121 | } 122 | } 123 | 124 | lazy_static! { 125 | pub static ref BRIDGE_MESSAGE_MANAGER: Mutex = 126 | Mutex::new(BridgeMessageManager::new()); 127 | } 128 | -------------------------------------------------------------------------------- /src/bridge/manager/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod message_manager; 2 | pub mod user_manager; 3 | 4 | pub use message_manager::*; 5 | pub use user_manager::*; 6 | -------------------------------------------------------------------------------- /src/bridge/manager/user_manager.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use lazy_static::lazy_static; 4 | use std::fs; 5 | use tokio::sync::Mutex; 6 | 7 | use crate::bridge::pojo::BridgeUserSaveForm; 8 | use crate::bridge::user::BridgeUser; 9 | use crate::elo; 10 | 11 | pub struct BridgeUserManager { 12 | bridge_users: Vec, 13 | } 14 | 15 | impl BridgeUserManager { 16 | pub fn new() -> BridgeUserManager { 17 | let path = "./data/bridge_user.json"; 18 | if let Ok(true) = fs::try_exists(path) { 19 | let file = fs::read_to_string(path).unwrap(); 20 | let bridge_users: Vec = serde_json::from_str(file.as_str()).unwrap(); 21 | return BridgeUserManager { bridge_users }; 22 | } 23 | BridgeUserManager { bridge_users: vec![] } 24 | } 25 | 26 | /// 根据id查询指定用户 27 | pub async fn get(&self, id: &str) -> Option { 28 | for user in &self.bridge_users { 29 | if id.eq(&user.id) { 30 | return Some(user.clone()); 31 | } 32 | } 33 | None 34 | } 35 | 36 | /// 模糊查询用户 (源id和平台) 37 | pub async fn like(&self, origin_id: &str, platform: &str) -> Option { 38 | for user in &self.bridge_users { 39 | if origin_id.eq(&user.origin_id) && platform.eq(&user.platform) { 40 | return Some(user.clone()); 41 | } 42 | } 43 | None 44 | } 45 | 46 | pub async fn likeAndSave(&mut self, form: BridgeUserSaveForm) -> Result { 47 | match self.like(&form.origin_id, &form.platform).await { 48 | Some(user) => Ok(user), 49 | None => self.save(form).await, 50 | } 51 | } 52 | 53 | /// 通过关联id和平台查询绑定的另一个账号 54 | pub async fn findByRefAndPlatform(&self, ref_id: &str, platform: &str) -> Option { 55 | for user in &self.bridge_users { 56 | if let None = user.ref_id { 57 | continue; 58 | } 59 | if ref_id.eq(user.ref_id.as_ref().unwrap()) && platform.eq(&user.platform) { 60 | return Some(user.clone()); 61 | } 62 | } 63 | None 64 | } 65 | 66 | /// 保存一条新的用户 67 | pub async fn save(&mut self, form: BridgeUserSaveForm) -> Result { 68 | if self.like(&form.origin_id, &form.platform).await.is_some() { 69 | let help = format!("该平台{:}已存在用户id为{:}的用户", &form.platform, &form.origin_id); 70 | tracing::info!("{help}"); 71 | return Err(help); 72 | } 73 | let user = BridgeUser { 74 | id: uuid::Uuid::new_v4().to_string(), 75 | origin_id: form.origin_id, 76 | platform: form.platform, 77 | display_text: form.display_text, 78 | ref_id: None, 79 | }; 80 | self.bridge_users.push(user.clone()); 81 | self.serialize(); 82 | Ok(user) 83 | } 84 | 85 | /// # 批量保存的用户 86 | /// ### Argument 87 | /// `queue` 待更新队列 88 | /// ### Returns 89 | /// - `Ok(..)` 更新行数 90 | /// - `Err(..)` 失败描述 91 | pub async fn batch_update(&mut self, queue: &[BridgeUser]) -> Result { 92 | let mut count = 0; 93 | for item in queue { 94 | let old = elo! { 95 | self.bridge_users.iter_mut().find(|t| t.id == item.id) 96 | ;;// else 97 | continue 98 | }; 99 | old.display_text = item.display_text.clone(); 100 | old.origin_id = item.origin_id.clone(); 101 | old.platform = item.platform.clone(); 102 | old.ref_id = item.ref_id.clone(); 103 | count += 1; 104 | } 105 | self.serialize(); 106 | Ok(count) 107 | } 108 | 109 | fn serialize(&self) { 110 | let content = serde_json::to_string(&self.bridge_users).unwrap(); 111 | fs::write("./data/bridge_user.json", content).unwrap(); 112 | } 113 | } 114 | 115 | lazy_static! { 116 | // static ref VEC:Vec = vec![0x18u8, 0x11u8]; 117 | // static ref MAP: HashMap = { 118 | // let mut map = HashMap::new(); 119 | // map.insert(18, "hury".to_owned());s 120 | // map 121 | // }; 122 | // static ref PAGE:u32 = mulit(18); 123 | 124 | pub static ref BRIDGE_USER_MANAGER: Mutex = Mutex::new(BridgeUserManager::new()); 125 | } 126 | -------------------------------------------------------------------------------- /src/bridge/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | use std::ops::BitOr; 3 | use std::str::FromStr; 4 | use std::sync::Arc; 5 | use tokio::sync::Mutex; 6 | 7 | use serde::Deserialize; 8 | use serde::Serialize; 9 | use tokio::sync::broadcast; 10 | 11 | use crate::bridge; 12 | use crate::bridge::BridgeClientPlatform::*; 13 | 14 | pub use bridge_message::{BridgeMessage, Image, MessageChain, MessageContent}; 15 | 16 | pub mod bridge_message; 17 | pub mod manager; 18 | pub mod pojo; 19 | pub mod user; 20 | 21 | /// 解析枚举文本错误 22 | #[derive(Debug)] 23 | pub struct ParseEnumErr(String); 24 | impl Display for ParseEnumErr { 25 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 26 | write!(f, "{}", self.0) 27 | } 28 | } 29 | 30 | /// 客户端所属平台 31 | /// # implement 32 | /// ## [`FromStr`] 33 | /// 凭借该特征可以将 str 解析为枚举 34 | /// ``` 35 | /// println!("{:?}", "qq".parse::()); 36 | /// ``` 37 | /// ## [`BitOr`] 38 | /// 借此特征简化枚举的“位标识”操作 39 | #[derive(PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] 40 | #[repr(u64)] 41 | pub enum BridgeClientPlatform { 42 | Discord = 1, 43 | QQ = 1 << 1, 44 | Cmd = 1 << 2, 45 | Telegram = 1 << 3, 46 | } 47 | impl BitOr for BridgeClientPlatform { 48 | type Output = u64; 49 | fn bitor(self, rhs: Self) -> Self::Output { 50 | self as u64 | rhs as u64 51 | } 52 | } 53 | impl Display for BridgeClientPlatform { 54 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 55 | let name = match self { 56 | Discord => "DC", 57 | QQ => "QQ", 58 | Cmd => "CMD", 59 | Telegram => "TG", 60 | }; 61 | write!(f, "{}", name) 62 | } 63 | } 64 | impl FromStr for BridgeClientPlatform { 65 | type Err = ParseEnumErr; 66 | fn from_str(s: &str) -> Result { 67 | Ok(match &*s.to_lowercase() { 68 | "dc" => Discord, 69 | "qq" => QQ, 70 | "cmd" => Cmd, 71 | "tg" => Telegram, 72 | _ => return Err(ParseEnumErr(format!("平台'{}'未定义", s))), 73 | }) 74 | } 75 | } 76 | impl BridgeClientPlatform { 77 | /// 数值转枚举 78 | pub fn by(val: u64) -> Option { 79 | Some(match val { 80 | 1 => Discord, 81 | 2 => QQ, 82 | 4 => Cmd, 83 | 8 => Telegram, 84 | _ => return None, 85 | }) 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod ts_bridge_client_platform { 91 | use BCP::*; 92 | 93 | use crate::bridge::BridgeClientPlatform as BCP; 94 | 95 | #[test] 96 | fn ts_display() { 97 | println!("dc:{}, qq:{}", Discord, QQ) 98 | } 99 | 100 | #[test] 101 | fn ts_parse() { 102 | println!("parse 'qQ' to enum: {}", "qQ".parse::().unwrap()); 103 | println!("parse 'Dc' to enum: {}", "Dc".parse::().unwrap()); 104 | } 105 | } 106 | 107 | pub struct BridgeService { 108 | pub clients: Vec>, 109 | } 110 | 111 | impl BridgeService { 112 | pub fn new() -> Self { 113 | BridgeService { clients: vec![] } 114 | } 115 | 116 | pub async fn create_client(name: &str, service: Arc>) -> Arc { 117 | let clients = &mut service.lock().await.clients; 118 | if clients.iter().any(|client| client.name == name) { 119 | panic!("存在同一个桥名: {}", name); 120 | } 121 | let client = Arc::new(BridgeClient::new(name, service.clone())); 122 | clients.push(client.clone()); 123 | client 124 | } 125 | } 126 | 127 | pub struct BridgeClient { 128 | pub name: String, 129 | pub bridge: Arc>, 130 | pub sender: broadcast::Sender, 131 | pub receiver: broadcast::Receiver, 132 | } 133 | 134 | impl BridgeClient { 135 | pub fn new(name: &str, bridge: Arc>) -> Self { 136 | let (sender, receiver) = broadcast::channel(32); 137 | BridgeClient { 138 | bridge, 139 | name: name.to_string(), 140 | sender, 141 | receiver, 142 | } 143 | } 144 | 145 | /** 146 | * 向其它桥发送消息 147 | */ 148 | pub async fn send_message(&self, message: bridge::pojo::BridgeSendMessageForm) { 149 | let bridge = self.bridge.lock().await; 150 | 151 | // let client = bridge 152 | // .clients 153 | // .iter() 154 | // .filter(|client| &client.name != &self.name); 155 | 156 | let bridge_message = bridge::BridgeMessage { 157 | id: bridge::manager::BRIDGE_MESSAGE_MANAGER.lock().await.save(message.clone()).await, 158 | sender_id: message.sender_id, 159 | avatar_url: message.avatar_url, 160 | bridge_config: message.bridge_config, 161 | message_chain: message.message_chain, 162 | }; 163 | 164 | for client in bridge.clients.iter() { 165 | if &client.name != &self.name { 166 | if let Err(e) = client.sender.send(bridge_message.clone()) { 167 | tracing::error!("消息中转异常:{:#?}", e); 168 | } 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/bridge/pojo/form/bridge_message_ref_message_form.rs: -------------------------------------------------------------------------------- 1 | pub struct BridgeMessageRefMessageForm { 2 | /** 3 | * 桥消息id 4 | */ 5 | pub bridge_message_id: String, 6 | /** 7 | * 平台: Discord = DC, QQ = QQ 8 | */ 9 | pub platform: String, 10 | /** 11 | * 来源id 12 | */ 13 | pub origin_id: String, 14 | } 15 | -------------------------------------------------------------------------------- /src/bridge/pojo/form/bridge_message_save_form.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge; 2 | pub struct BridgeMessageSaveAndRefForm { 3 | /** 4 | * 消息体 5 | */ 6 | pub message_chain: bridge::MessageChain, 7 | } 8 | -------------------------------------------------------------------------------- /src/bridge/pojo/form/bridge_send_message_form.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::{self, MessageContent}; 2 | use crate::BridgeConfig; 3 | use serde::{Deserialize, Serialize}; 4 | /** 5 | * 向桥发送消息的表单 6 | */ 7 | #[derive(Deserialize, Serialize, Debug, Clone)] 8 | pub struct BridgeSendMessageForm { 9 | // 桥用户 10 | pub sender_id: String, 11 | // 头像链接 12 | pub avatar_url: Option, 13 | // 消息配置(TODO: 抽象配置) 14 | pub bridge_config: BridgeConfig, 15 | // 消息体 16 | pub message_chain: Vec, 17 | // 消息来源 18 | pub origin_message: bridge::pojo::BridgeMessageRefPO, 19 | } 20 | -------------------------------------------------------------------------------- /src/bridge/pojo/form/bridge_user_save_form.rs: -------------------------------------------------------------------------------- 1 | pub struct BridgeUserSaveForm { 2 | /** 3 | * 平台: Discord, QQ 4 | */ 5 | pub platform: String, 6 | /** 7 | * 来源id 8 | */ 9 | pub origin_id: String, 10 | /** 11 | * 用户显示文本 12 | */ 13 | pub display_text: String, 14 | } 15 | -------------------------------------------------------------------------------- /src/bridge/pojo/form/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bridge_message_ref_message_form; 2 | pub mod bridge_send_message_form; 3 | pub mod bridge_user_save_form; 4 | 5 | pub use bridge_message_ref_message_form::*; 6 | pub use bridge_send_message_form::*; 7 | pub use bridge_user_save_form::*; 8 | -------------------------------------------------------------------------------- /src/bridge/pojo/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod form; 2 | pub mod po; 3 | 4 | pub use form::*; 5 | pub use po::*; 6 | -------------------------------------------------------------------------------- /src/bridge/pojo/po/bridge_message_po.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::MessageChain; 2 | use serde::Deserialize; 3 | use serde::Serialize; 4 | 5 | #[derive(Deserialize, Serialize, Debug, Clone)] 6 | pub struct BridgeMessagePO { 7 | /** 8 | * id 9 | */ 10 | pub id: String, 11 | /** 12 | * 桥用户 13 | */ 14 | pub sender_id: String, 15 | /** 16 | * 头像链接 17 | */ 18 | pub avatar_url: Option, 19 | /** 20 | * 关联桥消息的列表 21 | */ 22 | pub refs: Vec, 23 | /** 24 | * 消息内容 25 | */ 26 | pub message_chain: MessageChain, 27 | } 28 | 29 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 30 | pub struct BridgeMessageRefPO { 31 | /** 32 | * 平台: Discord = DC, QQ = QQ 33 | */ 34 | pub platform: String, 35 | /** 36 | * 来源id 37 | */ 38 | pub origin_id: String, 39 | } 40 | -------------------------------------------------------------------------------- /src/bridge/pojo/po/bridge_user_ref_po.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | 4 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 5 | pub struct BridgeUserRefPO { 6 | /** 7 | * 关联id 8 | */ 9 | pub id: String, 10 | } 11 | -------------------------------------------------------------------------------- /src/bridge/pojo/po/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bridge_user_ref_po; 2 | pub mod bridge_message_po; 3 | 4 | pub use bridge_user_ref_po::*; 5 | pub use bridge_message_po::*; 6 | -------------------------------------------------------------------------------- /src/bridge/user.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use std::fmt::{Display, Formatter, Result as FmtResult}; 4 | 5 | use crate::bridge; 6 | 7 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] 8 | pub struct BridgeUser { 9 | /** 10 | * id 11 | */ 12 | pub id: String, 13 | /** 14 | * 平台: Discord = DC, QQ = QQ 15 | */ 16 | pub platform: String, 17 | /** 18 | * 来源id 19 | */ 20 | pub origin_id: String, 21 | /** 22 | * 平台: Discord, QQ 23 | */ 24 | pub display_text: String, 25 | /** 26 | * 关联表id 27 | */ 28 | pub ref_id: Option, 29 | } 30 | 31 | impl BridgeUser { 32 | /** 33 | * 查询该用户指定平台关联的用户 34 | */ 35 | pub async fn find_by_platform(&self, platform: &str) -> Option { 36 | return if let Some(ref_id) = &self.ref_id { 37 | bridge::manager::BRIDGE_USER_MANAGER 38 | .lock() 39 | .await 40 | .findByRefAndPlatform(ref_id, platform) 41 | .await 42 | } else { 43 | None 44 | }; 45 | } 46 | } 47 | 48 | impl Display for BridgeUser { 49 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 50 | write!(f, "[{}] {}", self.platform, self.display_text) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/bridge_cmd/bridge_client.rs: -------------------------------------------------------------------------------- 1 | // TODO 交互式操作的上下文 2 | 3 | use clap::{FromArgMatches, Subcommand}; 4 | use std::sync::Arc; 5 | 6 | use crate::{ 7 | bridge::{ 8 | manager::BRIDGE_USER_MANAGER, 9 | pojo::{BridgeMessageRefPO, BridgeSendMessageForm}, 10 | BridgeClient, BridgeMessage, MessageContent, 11 | }, 12 | elr, 13 | }; 14 | 15 | use super::{BridgeCommand, CommandCentext, CommandMessageParser}; 16 | 17 | /// 识别解析以 BridgeMessage 为载体的指令 18 | impl CommandMessageParser for BridgeMessage { 19 | #[tracing::instrument(skip_all)] 20 | fn try_parse(&self, from_client: &str) -> Result, &'static str> { 21 | let ctx = &self.message_chain; 22 | let Some(MessageContent::Plain { text }) = ctx.first() else { 23 | return Err("获取不到文本!"); 24 | }; 25 | let text = text.trim(); 26 | if text.is_empty() || !text.starts_with('!') { 27 | return Err("空消息;或前缀错误!"); 28 | } 29 | let args = text.split_whitespace(); 30 | let patter = BridgeCommand::augment_subcommands(clap::Command::new("cc").no_binary_name(true)); 31 | // let mat = elr!(patter.clone().try_get_matches_from(args) ;; return None); 32 | // let cmd = elr!(BridgeCommand::from_arg_matches(&mat) ;; return None); 33 | let Ok(mat) = patter.clone().try_get_matches_from(args) else { 34 | return Err("此消息不符合指令格式!"); 35 | }; 36 | let Ok(cmd) = BridgeCommand::from_arg_matches(&mat) else { 37 | return Err("未匹配相关指令!"); 38 | }; 39 | Ok(CommandCentext { 40 | client: from_client.to_string(), 41 | src_msg: self.clone(), 42 | ctx: patter, 43 | token: cmd, 44 | }) 45 | } 46 | } 47 | 48 | /// 接收桥内消息,尝试处理 49 | #[tracing::instrument(skip_all)] 50 | pub async fn listen(bridge: Arc) { 51 | let mut subs = bridge.sender.subscribe(); 52 | loop { 53 | let message = elr!(subs.recv().await ;; continue); 54 | // 匹配消息是否是命令 55 | let cmd = match message.try_parse(&bridge.name) { 56 | Ok(cmd) => cmd, 57 | Err(e) => { 58 | tracing::debug!("{e}"); 59 | continue; 60 | } 61 | }; 62 | tracing::info!("[指令] {:?}", cmd.token); 63 | // 指令反馈 64 | let feedback = match cmd.process_command().await { 65 | Ok(fb) => fb, 66 | Err(e) => { 67 | tracing::error!("{e}"); 68 | continue; 69 | } 70 | }; 71 | let Some(user) = BRIDGE_USER_MANAGER.lock().await.like("00000001", "CMD").await else { 72 | tracing::warn!("无法获取CMD用户!"); 73 | continue; 74 | }; 75 | let bridge_msg = BridgeSendMessageForm { 76 | origin_message: BridgeMessageRefPO { 77 | origin_id: uuid::Uuid::new_v4().to_string(), 78 | platform: "CMD".to_string(), 79 | }, 80 | avatar_url: Some(format!("https://q1.qlogo.cn/g?b=qq&nk=3245538509&s=100")), 81 | bridge_config: message.bridge_config.clone(), 82 | message_chain: feedback, 83 | sender_id: user.id, 84 | }; 85 | bridge.send_message(bridge_msg).await 86 | } // loop 87 | } 88 | -------------------------------------------------------------------------------- /src/bridge_cmd/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{bridge, Config}; 2 | use clap::Parser; 3 | use std::sync::Arc; 4 | 5 | pub mod bridge_client; 6 | pub mod process; 7 | 8 | pub const CMD_TIP: &str = "!help"; 9 | pub const CMD_BIND: &str = "!关联"; 10 | pub const CMD_UNBIND: &str = "!解除关联"; 11 | pub const CMD_CONFIRM_BIND: &str = "!确认关联"; 12 | 13 | #[derive(Parser, Debug)] 14 | pub enum BridgeCommand { 15 | #[command(name = CMD_BIND)] 16 | Bind { 17 | token: Option, 18 | }, 19 | #[command(name = CMD_UNBIND)] 20 | Unbind { 21 | platform: String, 22 | }, 23 | #[command(name = CMD_CONFIRM_BIND)] 24 | ConfirmBind, 25 | #[command(name = CMD_TIP)] 26 | Tips { 27 | command: Option, 28 | }, 29 | } 30 | 31 | /// 指令内容 32 | pub struct CommandCentext { 33 | /// 基础内容 34 | pub token: BridgeCommand, 35 | /// 详细内容 36 | pub ctx: clap::Command, 37 | /// 客户端 38 | pub client: String, 39 | /// 源消息 40 | pub src_msg: M, 41 | } 42 | 43 | /// 指令消息解析 44 | pub trait CommandMessageParser { 45 | /// # 检查是否指令 46 | /// ### Argument 47 | /// - `&self` 待解析消息的载体 48 | /// - `client` 消息源客户端 49 | /// ### Return 50 | /// - 指令内容 51 | fn try_parse(&self, client: &str) -> Result, &'static str>; 52 | } 53 | 54 | pub async fn start(_config: Arc, bridge: Arc) { 55 | tracing::info!("[CMD] 初始化指令处理器"); 56 | apply_bridge_user().await; 57 | bridge_client::listen(bridge.clone()).await; 58 | } 59 | 60 | /** 61 | * 申请桥用户 62 | */ 63 | pub async fn apply_bridge_user() -> bridge::user::BridgeUser { 64 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER 65 | .lock() 66 | .await 67 | .likeAndSave(bridge::pojo::BridgeUserSaveForm { 68 | origin_id: "00000001".to_string(), 69 | platform: "CMD".to_string(), 70 | display_text: "桥命令Bot".to_string(), 71 | }) 72 | .await; 73 | bridge_user.unwrap() 74 | } 75 | -------------------------------------------------------------------------------- /src/bridge_cmd/process/bind_proc.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use lazy_static::lazy_static; 4 | use tokio::sync::Mutex; 5 | use tracing::instrument; 6 | 7 | use crate::{ 8 | bridge::{manager::BRIDGE_USER_MANAGER, user::BridgeUser}, 9 | elo, 10 | }; 11 | 12 | /// 关联组 13 | #[derive(Debug)] 14 | struct Mapping { 15 | /// 申请者id 16 | req: String, 17 | /// 响应者id 18 | resp: Option, 19 | } 20 | 21 | lazy_static! { 22 | /// # 缓存绑定申请者 23 | /// - `key` 口令 24 | /// - `value` 关联组 25 | static ref CACHE_REQ: Mutex> = Mutex::new(HashMap::with_capacity(32)); 26 | } 27 | 28 | /// 根据输入的一对信息元查询桥用户信息 29 | async fn get_pair(a: &str, b: &str) -> (Option, Option) { 30 | let tab_user = BRIDGE_USER_MANAGER.lock().await; 31 | (tab_user.get(a).await, tab_user.get(b).await) 32 | } 33 | 34 | /// 检查是否已经绑定 35 | async fn is_bound(a: &str, b: &str) -> bool { 36 | let (a, b) = get_pair(a, b).await; 37 | if a.is_none() || b.is_none() { 38 | return false; 39 | } 40 | let (a, b) = (a.unwrap(), b.unwrap()); 41 | if a.ref_id.is_some() && b.ref_id.is_some() && a.ref_id == b.ref_id { 42 | return true; 43 | } 44 | false 45 | } 46 | 47 | /// # 添加申请 48 | /// ### Argument 49 | /// `req_user_id` 申请者id 50 | /// ### Return 51 | /// `Ok(..)` 回应口令 52 | #[instrument(skip_all)] 53 | pub async fn add_req(req_user_id: &str) -> Result { 54 | let cache = &mut CACHE_REQ.lock().await; 55 | let token = loop { 56 | let tmp = &uuid::Uuid::new_v4().to_string()[..6]; 57 | if !cache.contains_key(tmp) { 58 | break tmp.to_string(); 59 | } 60 | }; 61 | // 移除旧数据 62 | cache.retain(|_, m| &req_user_id != &m.req); 63 | cache.insert( 64 | token.clone(), 65 | Mapping { 66 | req: req_user_id.to_string(), 67 | resp: None, 68 | }, 69 | ); 70 | tracing::debug!("缓存中的绑定申请数量: {}", cache.len()); 71 | Ok(token) 72 | } 73 | 74 | /// # 缓存回应 75 | /// ### Arguments 76 | /// - `token` 口令 77 | /// - `resp_user_id` 回应者id 78 | /// ### Return 79 | /// `Err(..)` 失败描述 80 | #[instrument(skip_all)] 81 | pub async fn update_resp(token: String, resp_user_id: &str) -> Result<(), &'static str> { 82 | let mut cache = CACHE_REQ.lock().await; 83 | let mapping = elo!(cache.get_mut(&token) ;; return Err("无效的口令!")); 84 | if &mapping.req == resp_user_id { 85 | return Err("不要自引用"); 86 | } else if let Some(old_resp) = &mapping.resp { 87 | // 查重 88 | if old_resp == resp_user_id { 89 | return Ok(()); 90 | } else if is_bound(&mapping.req, resp_user_id).await { 91 | return Err("您与该账户已经存在关联。如有疑问请联系管理员。"); 92 | } 93 | } 94 | mapping.resp = Some(resp_user_id.to_string()); 95 | { 96 | // trace mapping update 97 | let upd_mapping = cache.get(&token).unwrap(); 98 | tracing::trace!(?upd_mapping); 99 | } 100 | tracing::debug!("缓存中的绑定申请数量: {}", cache.len()); 101 | Ok(()) 102 | } 103 | 104 | /// # 确认建立关联 105 | /// ### Argument 106 | /// `req_user_id` 申请者信息 107 | /// ### Returnt 108 | /// `Err(..)` 失败描述 109 | #[instrument(skip_all)] 110 | pub async fn confirm_bind(req_user_id: &str) -> Result<(), &'static str> { 111 | let resp_user_id = { 112 | let cache = &mut CACHE_REQ.lock().await; 113 | let mapping = cache.iter().find(|(_, m)| &m.req == req_user_id); 114 | let Some((token, m)) = mapping else { 115 | return Err("您未申请绑定,或申请已被重置。"); 116 | }; 117 | if m.resp.is_none() { 118 | return Err("您的关联申请暂未收获回应!"); 119 | } 120 | // don't use immut-borrow 121 | let key = token.clone(); 122 | // take data 123 | let mapping = cache.remove(&key).unwrap(); 124 | tracing::debug!("缓存中的绑定申请数量: {}", cache.len()); 125 | mapping.resp.unwrap() 126 | }; 127 | tracing::debug!(a = req_user_id, b = resp_user_id); 128 | 129 | // get bridge user 130 | let (mut user_a, mut user_b) = { 131 | let (a, b) = get_pair(req_user_id, &resp_user_id).await; 132 | if a.is_none() || b.is_none() { 133 | tracing::warn!("桥用户信息缺失!"); 134 | tracing::warn!("【{req_user_id}】{a:?}"); 135 | tracing::warn!("【{resp_user_id}】{b:?}"); 136 | return Err("关联用户不存在!"); 137 | } 138 | (a.unwrap(), b.unwrap()) 139 | }; 140 | let tab_user = &mut BRIDGE_USER_MANAGER.lock().await; 141 | // copy or create ref_id 142 | let ref_id = if user_a.ref_id.is_some() { 143 | user_a.ref_id 144 | } else if user_b.ref_id.is_some() { 145 | user_b.ref_id 146 | } else { 147 | Some(uuid::Uuid::new_v4().to_string()) 148 | }; 149 | user_a.ref_id = ref_id.clone(); 150 | user_b.ref_id = ref_id.clone(); 151 | 152 | tracing::trace!(?user_a, ?user_b); 153 | match tab_user.batch_update(&[user_a, user_b]).await { 154 | Ok(c) => { 155 | tracing::info!("{c}行修改成功"); 156 | Ok(()) 157 | } 158 | Err(e) => { 159 | tracing::error!("用户关联保存失败!{e}"); 160 | Err("保存失败!") 161 | } 162 | } 163 | } 164 | 165 | /// # 解除关联 166 | /// ### Arguments 167 | /// - `user_id` 申请者信息 168 | /// - `platform` 指定平台 169 | /// ### Returnt 170 | /// `Err(..)` 失败描述 171 | #[instrument(skip_all)] 172 | pub async fn unbind(user_id: &str, platform: &str) -> Result<(), &'static str> { 173 | let tab_user = &mut BRIDGE_USER_MANAGER.lock().await; 174 | let ref_id = { 175 | let Some(user) = &tab_user.get(user_id).await else { 176 | tracing::warn!("找不到 id 为【{user_id}】的桥用户!"); 177 | return Err("获取用户信息失败"); 178 | }; 179 | elo!(user.ref_id.clone() ;; return Ok(())) 180 | }; 181 | 182 | let Some(mut target) = tab_user.findByRefAndPlatform(&ref_id, platform).await else { 183 | return Ok(()); 184 | }; 185 | target.ref_id = None; 186 | match tab_user.batch_update(&[target]).await { 187 | Ok(c) => { 188 | tracing::info!("{c}行修改成功"); 189 | Ok(()) 190 | } 191 | Err(e) => { 192 | tracing::error!("保存解除关联失败!{e}"); 193 | Err("操作失败") 194 | } 195 | } // match 196 | } 197 | -------------------------------------------------------------------------------- /src/bridge_cmd/process/mod.rs: -------------------------------------------------------------------------------- 1 | //! 指令处理 2 | //! TODO 枚举所有错误 3 | 4 | mod bind_proc; 5 | 6 | use tracing::instrument; 7 | 8 | use crate::bridge::{BridgeMessage, MessageContent}; 9 | 10 | use super::{BridgeCommand, CommandCentext, CMD_BIND, CMD_CONFIRM_BIND, CMD_UNBIND}; 11 | 12 | type Feedback = Result, String>; 13 | 14 | #[inline] 15 | fn simple_feedback(msg: &str) -> Feedback { 16 | Ok(vec![MessageContent::Plain { text: msg.to_string() }]) 17 | } 18 | #[inline] 19 | fn simple_fail(msg: &str) -> Feedback { 20 | Err(msg.to_string()) 21 | } 22 | 23 | impl CommandCentext { 24 | /// # 申请关联 25 | /// ### Return 26 | /// 验证口令 27 | #[instrument(skip_all)] 28 | async fn req_bind(&self) -> Feedback { 29 | let Ok(token) = bind_proc::add_req(&self.src_msg.sender_id).await else { 30 | return simple_fail("申请失败,请联系管理员处理。"); 31 | }; 32 | Ok(vec![MessageContent::Plain { 33 | text: format!("申请成功。请切换客户端,使用验证码回应请求: {token}"), 34 | }]) 35 | } 36 | 37 | /// 回应申请 38 | /// ### Argument 39 | /// `token` 验证口令 40 | #[instrument(skip_all)] 41 | async fn resp_bind(&self, token: String) -> Feedback { 42 | if let Err(e) = bind_proc::update_resp(token, &self.src_msg.sender_id).await { 43 | tracing::error!("{e}"); 44 | return simple_fail("提交失败,请联系管理员处理。"); 45 | } 46 | simple_feedback("OK,请回到原客户端进行确认。") 47 | } 48 | 49 | /// 申请/回应用户关联 50 | async fn bind(&self) -> Feedback { 51 | if let BridgeCommand::Bind { token } = &self.token { 52 | if let Some(t) = token { 53 | return self.resp_bind(t.clone()).await; 54 | } 55 | } 56 | self.req_bind().await 57 | } 58 | 59 | /// 接收关联 60 | #[instrument(skip_all)] 61 | async fn confirm_bind(&self) -> Feedback { 62 | if let Err(e) = bind_proc::confirm_bind(&self.src_msg.sender_id).await { 63 | tracing::error!("{e}"); 64 | return simple_fail("关联失败,请联系管理员处理。"); 65 | } 66 | simple_feedback("完成关联。") 67 | } 68 | 69 | /// 取消关联 70 | #[instrument(skip_all)] 71 | async fn unbind(&self) -> Feedback { 72 | if let BridgeCommand::Unbind { platform } = &self.token { 73 | if platform == &self.client { 74 | return simple_fail("不要做自引用操作"); 75 | } 76 | if let Err(e) = bind_proc::unbind(&self.src_msg.sender_id, platform).await { 77 | tracing::error!("{e}"); 78 | return simple_fail("操作失败,请联系管理员处理。"); 79 | } 80 | }; 81 | simple_feedback("已取消关联。") 82 | } 83 | 84 | /// 获取指令帮助 85 | fn get_help(&self) -> Feedback { 86 | let mut sub = "".to_string(); 87 | if let BridgeCommand::Tips { command } = &self.token { 88 | if let Some(tmp) = command { 89 | if tmp.starts_with('!') { 90 | sub = tmp.to_owned(); 91 | } else { 92 | sub = format!("!{tmp}"); 93 | } 94 | } 95 | } 96 | // TODO 文本内容通过 toml 文件读写 97 | let text = match &*sub { 98 | CMD_BIND => format!( 99 | "申请关联,获取验证码;或者用验证码回应申请 100 | 用法:{CMD_BIND} [口令] 101 | 口令\t\t选填。无口令时申请;有口令时回应申请 102 | 【申请关联】{CMD_BIND} 103 | 【回应申请】{CMD_BIND} 1a2b3c" 104 | ), 105 | CMD_CONFIRM_BIND => format!("确定保存关联。无参\n用法: {CMD_CONFIRM_BIND}"), 106 | CMD_UNBIND => format!( 107 | "【解除桥用户关联】解除指定平台的关联 108 | 用法:{CMD_UNBIND} <平台> 109 | 平台\t\t必填,单选。选项:QQ、DC=Discord、TG=Telegram 110 | 【用例】{CMD_UNBIND} DC" 111 | ), 112 | _ => format!( 113 | "桥的可用指令: 114 | 【申请/回应关联桥用户】{CMD_BIND} [口令] 115 | 【确认关联】{CMD_CONFIRM_BIND} 116 | 【解除桥用户关联】{CMD_UNBIND} <平台>" 117 | ), 118 | }; 119 | Ok(vec![MessageContent::Plain { text }]) 120 | } 121 | 122 | /// # 指令处理 123 | /// ### Return 124 | /// - `Some(feedback)` 反馈指令处理结果 125 | /// - `Err(..)` 失败描述 126 | pub async fn process_command(&self) -> Feedback { 127 | use super::BridgeCommand::*; 128 | match self.token { 129 | Bind { .. } => self.bind().await, 130 | ConfirmBind => self.confirm_bind().await, 131 | Unbind { .. } => self.unbind().await, 132 | Tips { .. } => self.get_help(), 133 | // _ => Err("TODO".to_string()), 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/bridge_data.rs: -------------------------------------------------------------------------------- 1 | //! 定义桥的数据结构,读写方法 2 | 3 | use crate::bridge::{BridgeClientPlatform as BCP, User}; 4 | use std::fs::{create_dir_all, OpenOptions}; 5 | use std::io::{Read, Write}; 6 | use std::path::Path; 7 | use tracing::error; 8 | 9 | ///! 定义绑定映射 10 | pub mod bind_map { 11 | use super::*; 12 | 13 | /// 平台枚举, unique_id, display_id 14 | type BindKey = (u64, u64, u64); 15 | type BindData = Vec<(BindKey, BindKey)>; 16 | 17 | const BIND_MAP_PATH: &str = "./data/BindMap.json"; 18 | 19 | /// 尝试获取映射 20 | /// # 参数 21 | /// - `from(platform, unique/display_id)` 目标用户(客户端平台,系统/显示id) 22 | /// - `to_p` 指向绑定的平台 23 | /// # 返回 24 | /// 含部分有效字段的 User: platform, unique_id, display_id 25 | pub fn get_bind(from: (BCP, u64), to_p: BCP) -> Option { 26 | // 有必要自绑定吗? 27 | if to_p == from.0 { 28 | return None; 29 | } 30 | let pp = from.0 | to_p; 31 | let data = load(); 32 | 33 | for (a @ (p1, u1, d1), b @ (p2, u2, d2)) in data.iter() { 34 | if (p1 | p2) == pp { 35 | let f: Option = 36 | if *u1 == from.1 || *d1 == from.1 { 37 | Some(*b) 38 | } else if *u2 == from.1 || *d2 == from.1 { 39 | Some(*a) 40 | } else { 41 | None 42 | }; 43 | if let Some((p, u, d)) = f { 44 | return Some(User { 45 | name: "".to_string(), 46 | avatar_url: None, 47 | platform_id: 0, 48 | platform: BCP::by(p).unwrap(), 49 | display_id: d, 50 | unique_id: u, 51 | }); 52 | } 53 | } 54 | } 55 | None 56 | } 57 | 58 | /// 添加映射 59 | /// - `user1`, `user2` 一对映射 60 | pub fn add_bind(user1: &User, user2: &User) { 61 | let p = (user1.platform as u64, user2.platform as u64); 62 | let mut data = load(); 63 | let mut add = true; 64 | let pp = p.0 | p.1; 65 | 66 | for ((p1, u1, d1), (p2, u2, d2)) in data.iter() { 67 | if (p1 | p2) == pp { 68 | if (*u1 == user1.unique_id && *u2 == user2.unique_id) || 69 | (*u2 == user1.unique_id && *u1 == user2.unique_id) { 70 | add = false; 71 | break; 72 | } 73 | if (*d1 == user1.display_id && *d2 == user2.display_id) || 74 | (*d2 == user1.display_id && *d1 == user2.display_id) { 75 | add = false; 76 | break; 77 | } 78 | } 79 | } 80 | 81 | if add { 82 | data.push(((p.0, user1.unique_id, user1.display_id), (p.1, user2.unique_id, user2.display_id))); 83 | save(&data); 84 | } 85 | } 86 | 87 | pub fn rm_bind(from: (BCP, u64), to_p: BCP) -> bool { 88 | let mut data = load(); 89 | let len = data.len(); 90 | let pp = from.0 | to_p; 91 | 92 | data.retain(|((p1, u1, d1), (p2, u2, d2))| { 93 | if (p1 | p2) == pp { 94 | !(*u1 == from.1 || *u2 == from.1 || *d1 == from.1 || *d2 == from.1) 95 | } else { 96 | true 97 | } 98 | }); 99 | if len > data.len() { 100 | save(&data); 101 | true 102 | } else { 103 | false 104 | } 105 | } 106 | 107 | /// 指定一对用户删除映射 108 | /// - `user1`, `user2` 一对映射;移除映射需成对操作 109 | #[allow(unused)] 110 | pub fn rm_bind_pair(user1: &User, user2: &User) -> bool { 111 | let mut data = load(); 112 | let len = data.len(); 113 | let pp = user1.platform | user2.platform; 114 | 115 | data.retain(|((p1, u1, d1), (p2, u2, d2))| { 116 | if (p1 | p2) == pp { 117 | !((*u1 == user1.unique_id && *u2 == user2.unique_id) || 118 | (*u1 == user2.unique_id && *u2 == user1.unique_id) || 119 | (*d1 == user1.display_id && *d2 == user2.display_id) || 120 | (*d1 == user2.display_id && *d2 == user1.display_id)) 121 | } else { 122 | true 123 | } 124 | }); 125 | if len > data.len() { 126 | save(&data); 127 | true 128 | } else { 129 | false 130 | } 131 | } 132 | 133 | /// 指定用户删除其所有关联映射 134 | /// - `user` 目标用户 135 | #[allow(unused)] 136 | pub fn rm_user_all_bind(user: &User) { 137 | let mut data = load(); 138 | let len = data.len(); 139 | let p = user.platform as u64; 140 | 141 | data.retain(|((p1, u1, d1), (p2, u2, d2))| 142 | if (p1 | p2) & p > 0 { 143 | !(*u1 == user.unique_id || *u2 == user.unique_id || 144 | *d1 == user.display_id || *d2 == user.display_id) 145 | } else { 146 | true 147 | }); 148 | if len > data.len() { 149 | save(&data); 150 | } 151 | } 152 | 153 | /// 初始化数据文件目录 154 | /// # return 155 | /// 检查与创建是否成功 156 | fn init_dir() -> bool { 157 | let dat_dir = Path::new(BIND_MAP_PATH).parent().unwrap(); 158 | if dat_dir.as_os_str().is_empty() || dat_dir.exists() { 159 | return true; 160 | } 161 | if let Err(e) = create_dir_all(dat_dir) { 162 | error!("目录'{}'创建失败!{:#?}", dat_dir.to_str().unwrap(), e); 163 | return false; 164 | } 165 | true 166 | } 167 | 168 | /// 读取,加载本地数据 169 | fn load() -> BindData { 170 | if !init_dir() { 171 | return BindData::new(); 172 | } 173 | let mut json = String::new(); 174 | let file = OpenOptions::new() 175 | .read(true) 176 | .write(true) 177 | .create(true) 178 | .open(BIND_MAP_PATH); 179 | match file { 180 | Ok(mut f) => { 181 | if let Err(e) = f.read_to_string(&mut json) { 182 | error!("Can not read file({}); {:#?}", BIND_MAP_PATH, e); 183 | } 184 | } 185 | Err(e) => error!("Can not open/create data file({}); {:#?}", BIND_MAP_PATH, e), 186 | }; 187 | 188 | if !json.is_empty() { 189 | match serde_json::from_str::(&json.as_str()) { 190 | Ok(mut data) => { 191 | // 删除无平台映射 192 | data.retain(|((p1, ..), (p2, ..))| *p1 > 0 && *p2 > 0); 193 | return data; 194 | } 195 | Err(e) => error!("BindMap load fail, data can not be parsed; {:#?}", e), 196 | } 197 | } 198 | BindData::new() 199 | } 200 | 201 | /// 数据写入本地 202 | /// TODO 异步读写 203 | fn save(data: &BindData) -> bool { 204 | if !init_dir() { 205 | return false; 206 | } 207 | let raw: String; 208 | match serde_json::to_string(data) { 209 | Ok(json) => { 210 | raw = json; 211 | } 212 | Err(e) => { 213 | error!("Fail to parse BindMap to JSON; {:#?}", e); 214 | return false; 215 | } 216 | } 217 | 218 | let file = OpenOptions::new() 219 | .truncate(true) 220 | .write(true) 221 | .create(true) 222 | .open(BIND_MAP_PATH); 223 | match file { 224 | Ok(mut f) => { 225 | if let Err(e) = f.write_all(raw.as_bytes()) { 226 | error!("Can not write to file({}); {:#?}", BIND_MAP_PATH, e); 227 | return false; 228 | } 229 | } 230 | Err(e) => { 231 | error!("Can not open/create data file({}); {:#?}", BIND_MAP_PATH, e); 232 | return false; 233 | } 234 | }; 235 | true 236 | } 237 | 238 | #[cfg(test)] 239 | mod ts_bind_map { 240 | use { 241 | chrono::Local, 242 | crate::{ 243 | bridge::{ 244 | BridgeClientPlatform::*, 245 | User, 246 | }, 247 | bridge_data::bind_map::*, 248 | }, 249 | }; 250 | 251 | #[test] 252 | fn add() { 253 | let uls = get_users(); 254 | let st = Local::now().timestamp_millis(); 255 | add_bind(&uls[0], &uls[1]); 256 | add_bind(&uls[2], &uls[3]); 257 | add_bind(&uls[4], &uls[1]); 258 | add_bind(&uls[5], &uls[2]); 259 | let et = Local::now().timestamp_millis(); 260 | println!("add 4 mapping: {}ms", et - st); 261 | } 262 | 263 | #[test] 264 | fn get() { 265 | let u1 = User { 266 | name: "".to_string(), 267 | avatar_url: None, 268 | unique_id: 111_111, 269 | display_id: 111, 270 | platform_id: 111, 271 | platform: Discord, 272 | }; 273 | let u2 = User { 274 | name: "".to_string(), 275 | avatar_url: None, 276 | unique_id: 0, 277 | display_id: 0, 278 | platform_id: 0, 279 | platform: QQ, 280 | }; 281 | let st = Local::now().timestamp_millis(); 282 | match get_bind((u1.platform, u1.unique_id), QQ) { 283 | None => println!("{} no mapping", u1.unique_id), 284 | Some(ou) => println!("{} map to {}", u1.unique_id, ou.unique_id), 285 | } 286 | match get_bind((u2.platform, u2.unique_id), Discord) { 287 | None => println!("{} no mapping user", u2.unique_id), 288 | Some(ou) => println!("{} map to {}", u2.unique_id, ou.unique_id), 289 | } 290 | let et = Local::now().timestamp_millis(); 291 | println!("get 2 mapping: {}ms", et - st); 292 | } 293 | 294 | #[test] 295 | fn rm() { 296 | let uls = get_users(); 297 | rm_bind_pair(&uls[0], &uls[1]); 298 | rm_bind_pair(&uls[2], &uls[3]); 299 | } 300 | 301 | #[test] 302 | fn rm_all() { 303 | let uls = get_users(); 304 | rm_user_all_bind(&uls[1]); 305 | } 306 | 307 | fn get_users() -> Vec { 308 | fn emp() -> String { 309 | "".to_string() 310 | } 311 | vec![ 312 | User { 313 | name: emp(), 314 | avatar_url: None, 315 | unique_id: 111_111, 316 | display_id: 111, 317 | platform_id: 111, 318 | platform: Discord, 319 | }, 320 | User { 321 | name: emp(), 322 | avatar_url: None, 323 | unique_id: 222_222, 324 | display_id: 222, 325 | platform_id: 222, 326 | platform: QQ, 327 | }, 328 | User { 329 | name: emp(), 330 | avatar_url: None, 331 | unique_id: 333_333, 332 | display_id: 333, 333 | platform_id: 333, 334 | platform: Discord, 335 | }, 336 | User { 337 | name: emp(), 338 | avatar_url: None, 339 | unique_id: 444_444, 340 | display_id: 444, 341 | platform_id: 444, 342 | platform: QQ, 343 | }, 344 | User { 345 | name: emp(), 346 | avatar_url: None, 347 | unique_id: 555_555, 348 | display_id: 555, 349 | platform_id: 555, 350 | platform: Discord, 351 | }, 352 | User { 353 | name: emp(), 354 | avatar_url: None, 355 | unique_id: 666_666, 356 | display_id: 666, 357 | platform_id: 666, 358 | platform: Discord, 359 | }, 360 | ] 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/bridge_dc/bridge_client.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::sync::Arc; 3 | 4 | use proc_qq::re_exports::image; 5 | use serenity::builder::CreateButton; 6 | use serenity::http::Http; 7 | use serenity::model::application::component::ButtonStyle; 8 | use serenity::model::channel::AttachmentType; 9 | use serenity::model::webhook::Webhook; 10 | 11 | use crate::bridge; 12 | 13 | use super::{find_member_by_name, parse_text_mention_rule, to_reply_content, MentionText}; 14 | 15 | #[tracing::instrument(name = "bridge_dc_sync", skip_all)] 16 | pub async fn listen(bridge: Arc, http: Arc) { 17 | let mut subs = bridge.sender.subscribe(); 18 | loop { 19 | let message = &subs.recv().await.unwrap(); 20 | tracing::info!("收到桥的消息, 同步到discord上"); 21 | let webhook = Webhook::from_id_with_token( 22 | &http, 23 | message.bridge_config.discord.id, 24 | message.bridge_config.discord.token.as_str(), 25 | ) 26 | .await 27 | .unwrap(); 28 | tracing::debug!("discord info: {:#?}", webhook); 29 | let guild_id = webhook.guild_id.unwrap(); 30 | 31 | // 组装dc消息 32 | let mut content: Vec = Vec::new(); 33 | let mut reply_content: Vec = Vec::new(); 34 | let mut reply_message_id = "".to_string(); 35 | let mut fils: Vec = Vec::new(); 36 | for chain in &message.message_chain { 37 | match chain { 38 | bridge::MessageContent::Plain { text } => { 39 | let mention_text_list = parse_text_mention_rule(text.to_string()); 40 | for mention_text in mention_text_list { 41 | match mention_text { 42 | MentionText::Text(text) => content.push(text), 43 | MentionText::MentionText { name, discriminator } => { 44 | let member = find_member_by_name(&http, guild_id.0, &name, &discriminator).await; 45 | if let Some(member) = member { 46 | content.push(format!("<@{}>", member.user.id.0)); 47 | } else { 48 | content.push(format!("@[DC] {}#{}", name, discriminator)); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | bridge::MessageContent::Image(image) => match image { 55 | bridge::Image::Url(url) => fils.push(AttachmentType::Image(url::Url::parse(url).unwrap())), 56 | bridge::Image::Path(path) => fils.push(AttachmentType::Path(Path::new(path))), 57 | bridge::Image::Buff(data) => { 58 | match image::guess_format(data) { 59 | Ok(format) => fils.push(AttachmentType::Bytes { 60 | data: data.into(), 61 | filename: format!("file.{}", format.extensions_str()[0]), 62 | }), 63 | Err(_) => {} 64 | }; 65 | } 66 | }, 67 | bridge::MessageContent::Reply { id } => { 68 | if let Some(id) = id { 69 | let reply_message = bridge::manager::BRIDGE_MESSAGE_MANAGER.lock().await.get(id).await; 70 | if let Some(reply_message) = reply_message { 71 | let refs = reply_message.refs.iter().find(|refs| refs.platform.eq("DC")); 72 | if let Some(refs) = refs { 73 | reply_message_id = refs.origin_id.clone(); 74 | } 75 | reply_content = to_reply_content(reply_message).await; 76 | } else { 77 | content.push("> {回复消息}\n".to_string()); 78 | } 79 | } 80 | } 81 | bridge::MessageContent::At { id } => { 82 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER.lock().await.get(id).await; 83 | if let None = bridge_user { 84 | content.push(format!("@[UN] {}", id)); 85 | continue; 86 | } 87 | let bridge_user = bridge_user.unwrap(); 88 | // 查看桥关联的本平台用户id 89 | if let Some(ref_user) = bridge_user.find_by_platform("DC").await { 90 | content.push(format!("<@{}>", ref_user.origin_id)); 91 | continue; 92 | } 93 | // 没有关联账号用标准格式发送消息 94 | content.push(format!("@{}", bridge_user.to_string())); 95 | } 96 | _ => tracing::warn!(unit = ?chain, "无法识别的MessageChain"), 97 | }; 98 | } 99 | tracing::debug!(?content, ?fils, "桥内消息链组装完成"); 100 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER 101 | .lock() 102 | .await 103 | .get(&message.sender_id) 104 | .await 105 | .unwrap(); 106 | let resp = webhook 107 | .execute(&http, true, |w| { 108 | // 配置发送者头像 109 | if let Some(url) = &message.avatar_url { 110 | w.avatar_url(url.as_str()); 111 | } 112 | tracing::debug!("消息头像url:{:?}", message.avatar_url); 113 | // 配置发送者用户名 114 | w.username(bridge_user.to_string()); 115 | if content.len() == 0 && fils.len() == 0 { 116 | content.push("{本次发送的消息没有内容}".to_string()); 117 | } 118 | // w.components(|c| c.add_action_row()); 119 | w.add_files(fils); 120 | reply_content.append(&mut content); 121 | w.content(reply_content.join("")); 122 | if reply_content.len() > 0 { 123 | w.components(|c| { 124 | c.create_action_row(|row| { 125 | let mut button = CreateButton::default(); 126 | button.style(ButtonStyle::Link); 127 | button.url(format!( 128 | "https://discord.com/channels/{}/{}/{}", 129 | guild_id, message.bridge_config.discord.channelId, reply_message_id 130 | )); 131 | button.label("跳转回复"); 132 | row.add_button(button) 133 | }) 134 | }); 135 | } 136 | println!("add_button: {:?}", w); 137 | // w.content(content.join("")); 138 | // .content(content.join("")).components(f).content(content.join("")) 139 | w 140 | }) 141 | .await; 142 | 143 | match resp { 144 | Ok(result) => { 145 | if let Some(msg) = result { 146 | // 发送成功后, 将平台消息和桥消息进行关联, 为以后进行回复功能 147 | bridge::manager::BRIDGE_MESSAGE_MANAGER 148 | .lock() 149 | .await 150 | .ref_bridge_message(bridge::pojo::BridgeMessageRefMessageForm { 151 | bridge_message_id: message.id.clone(), 152 | platform: "DC".to_string(), 153 | origin_id: msg.id.0.to_string(), 154 | }) 155 | .await; 156 | } else { 157 | tracing::error!("同步的消息没有返回消息id") 158 | } 159 | tracing::info!("已同步消息") 160 | } 161 | Err(err) => { 162 | tracing::error!(?err, "消息同步失败!") 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/bridge_dc/handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use serenity::async_trait; 4 | use serenity::model::channel::Message; 5 | use serenity::model::channel::MessageReference; 6 | use serenity::model::gateway::Ready; 7 | use serenity::model::Timestamp; 8 | use serenity::prelude::*; 9 | use tracing::{debug, error, info, instrument, trace}; 10 | 11 | use crate::bridge::Image; 12 | use crate::bridge_dc::apply_bridge_user; 13 | use crate::{bridge, Config}; 14 | 15 | pub struct Handler { 16 | pub config: Arc, 17 | pub bridge: Arc, 18 | } 19 | 20 | #[async_trait] 21 | impl EventHandler for Handler { 22 | #[instrument(skip_all, name = "bridge_dc_recv")] 23 | async fn message(&self, ctx: Context, msg: Message) { 24 | if msg.author.id == self.config.discord_config.botId { 25 | // 收到自己bot的消息, 不要继续以免消息循环 26 | return; 27 | } 28 | if filter_message(&msg) { 29 | return; 30 | } 31 | 32 | // 收到桥配置的webhook消息, 不要继续以免消息循环 33 | if self.config.bridges.iter().any(|bridge| msg.author.id == bridge.discord.id) { 34 | return; 35 | } 36 | let bridge_config = match self 37 | .config 38 | .bridges 39 | .iter() 40 | .find(|bridge| msg.channel_id == bridge.discord.channelId && bridge.enable) 41 | { 42 | Some(c) => c, 43 | // 该消息的频道没有配置桥, 忽略这个消息 44 | None => return, 45 | }; 46 | let bridge_user = apply_bridge_user(msg.author.id.0, msg.author.name.as_str(), msg.author.discriminator).await; 47 | let mut bridge_message = bridge::pojo::BridgeSendMessageForm { 48 | sender_id: bridge_user.id, 49 | avatar_url: None, 50 | bridge_config: bridge_config.clone(), 51 | message_chain: Vec::new(), 52 | origin_message: bridge::pojo::BridgeMessageRefPO { 53 | origin_id: msg.id.0.to_string(), 54 | platform: "DC".to_string(), 55 | }, 56 | }; 57 | if let Some(url) = msg.author.avatar_url() { 58 | bridge_message.avatar_url = Some(url.replace(".webp?size=1024", ".png?size=40").to_string()); 59 | } 60 | if let Some(reply) = msg.message_reference { 61 | bridge_message.message_chain.push(to_reply_bridge_message(reply).await); 62 | } 63 | let result = crate::utils::parser_message(&msg.content).await; 64 | for ast in result { 65 | match ast { 66 | crate::utils::MarkdownAst::Plain { text } => { 67 | bridge_message.message_chain.push(bridge::MessageContent::Plain { text }); 68 | } 69 | crate::utils::MarkdownAst::At { username } => { 70 | trace!("用户'{}'收到@", username); 71 | bridge_message.message_chain.push(bridge::MessageContent::Plain { text: username }); 72 | // bridge_message 73 | // .message_chain 74 | // .push(bridge::MessageContent::At { 75 | // bridge_user_id: None, 76 | // username, 77 | // }); 78 | } 79 | crate::utils::MarkdownAst::DiscordAtUser { id } => { 80 | let id: u64 = id.parse::().unwrap(); 81 | let member = ctx.http.get_member(msg.guild_id.unwrap().0, id).await.unwrap(); 82 | let bridge_user = apply_bridge_user(id, member.user.name.as_str(), member.user.discriminator).await; 83 | // let member_name = 84 | // format!("[DC] {}#{}", member.user.name, member.user.discriminator); 85 | // trace!("用户'{}'收到@", member_name); 86 | bridge_message.message_chain.push(bridge::MessageContent::At { id: bridge_user.id }); 87 | } 88 | crate::utils::MarkdownAst::DiscordAtEveryone {} => { 89 | bridge_message.message_chain.push(bridge::MessageContent::AtAll); 90 | } 91 | crate::utils::MarkdownAst::DiscordAtHere {} => { 92 | bridge_message.message_chain.push(bridge::MessageContent::AtAll); 93 | } 94 | crate::utils::MarkdownAst::DiscordEmoji { id, animated, .. } => { 95 | let suffix = if animated { "gif" } else { "png" }; 96 | bridge_message.message_chain.push(bridge::MessageContent::Image(Image::Url(format!( 97 | "https://cdn.discordapp.com/emojis/{}.{}", 98 | id, suffix 99 | )))); 100 | } 101 | } 102 | } 103 | // 将附件一股脑的放进图片里面 TODO: 以后在区分非图片的附件 104 | for attachment in msg.attachments { 105 | trace!(attachment.url); 106 | bridge_message 107 | .message_chain 108 | .push(bridge::MessageContent::Image(Image::Url(attachment.url))); 109 | } 110 | debug!("dc 桥的消息链:{:#?}", bridge_message.message_chain); 111 | 112 | self.bridge.send_message(bridge_message).await; 113 | if msg.content == "!hello" { 114 | // The create message builder allows you to easily create embeds and messages 115 | // using a builder syntax. 116 | // This example will create a message that says "Hello, World!", with an embed that has 117 | // a title, description, an image, three fields, and a footer. 118 | let msg = msg 119 | .channel_id 120 | .send_message(&ctx.http, |m| { 121 | m.content("Hello, World!") 122 | .embed(|e| { 123 | e.title("This is a title") 124 | .description("This is a description") 125 | .image("attachment://ferris_eyes.png") 126 | .fields(vec![ 127 | ("This is the first field", "This is a field body", true), 128 | ("This is the second field", "Both fields are inline", true), 129 | ]) 130 | .field("This is the third field", "This is not an inline field", false) 131 | .footer(|f| f.text("This is a footer")) 132 | // Add a timestamp for the current time 133 | // This also accepts a rfc3339 Timestamp 134 | .timestamp(Timestamp::now()) 135 | }) 136 | .add_file("./ferris_eyes.png") 137 | }) 138 | .await; 139 | 140 | if let Err(why) = msg { 141 | error!("消息发送失败!{:#?}", why); 142 | } 143 | } 144 | } 145 | 146 | #[instrument(skip_all, target = "bridge_dc")] 147 | async fn ready(&self, ctx: Context, ready: Ready) { 148 | tracing::warn!("(Guild={:?})准备连接Discord伺服器", ready.guilds); 149 | for bridge_config in self.config.bridges.iter() { 150 | match ctx.http.get_channel(bridge_config.discord.channelId).await { 151 | Ok(channel) => { 152 | let msg = "Message Bridge正在运行中..."; 153 | let resp = channel 154 | .id() 155 | .send_message(&ctx.http, |m| { 156 | m.content(msg); 157 | m 158 | }) 159 | .await; 160 | if let Err(e) = resp { 161 | error!(msg, err = ?e, "消息发送失败!") 162 | } else { 163 | info!("已连接到 discord 频道 {}", bridge_config.discord.channelId); 164 | } 165 | } 166 | Err(e) => error!( 167 | channel = bridge_config.discord.channelId, 168 | err = ?e, 169 | "获取 discord 频道失败!", 170 | ), 171 | } 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * DC的回复消息处理成桥的回复消息 178 | */ 179 | async fn to_reply_bridge_message(reply: MessageReference) -> bridge::MessageContent { 180 | use bridge::MessageContent; 181 | if let None = reply.message_id { 182 | return MessageContent::Err { 183 | message: "回复一条DC消息, 但是DC没有提供消息id, 同步回复消息失败".to_string(), 184 | }; 185 | } 186 | let message_id = reply.message_id.unwrap().0.to_string(); 187 | let result = bridge::manager::BRIDGE_MESSAGE_MANAGER 188 | .lock() 189 | .await 190 | .find_by_ref_and_platform(&message_id, "DC") 191 | .await; 192 | let result = result.unwrap(); 193 | if let Some(reply) = result { 194 | return MessageContent::Reply { 195 | id: Some(reply.id.clone()), 196 | }; 197 | } 198 | MessageContent::Err { 199 | message: "回复一条DC消息, 但是同步回复消息失败".to_string(), 200 | } 201 | } 202 | 203 | /** 204 | * 过滤不同步的消息 205 | * @return true: 不同步 206 | * TODO: 从配置文件读取 207 | */ 208 | fn filter_message(msg: &Message) -> bool { 209 | let mut regex_config: Vec = vec![]; 210 | regex_config.push("Issue labeled by .*".to_string()); 211 | for regex_str in regex_config { 212 | let regex = regex::Regex::new(regex_str.as_str()).unwrap(); 213 | if regex.is_match(msg.content.as_str()) { 214 | return true; 215 | } 216 | } 217 | false 218 | } 219 | -------------------------------------------------------------------------------- /src/bridge_dc/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use serenity::http::Http; 4 | use serenity::model::guild::Member; 5 | use serenity::prelude::*; 6 | use tracing::{instrument, warn}; 7 | 8 | use crate::bridge::pojo::BridgeMessagePO; 9 | use crate::bridge::user::BridgeUser; 10 | 11 | // use crate::bridge_message_history::{BridgeMessageHistory, Platform}; 12 | use crate::{bridge, bridge_dc, Config}; 13 | 14 | pub mod bridge_client; 15 | pub mod handler; 16 | 17 | pub use handler::*; 18 | 19 | pub async fn start(config: Arc, bridge: Arc) { 20 | tracing::info!("[DC] 初始化DC桥"); 21 | let token = &config.discord_config.botToken; 22 | let intents = 23 | GatewayIntents::GUILD_MESSAGES | GatewayIntents::GUILD_MEMBERS | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; 24 | 25 | let mut client = Client::builder(&token, intents) 26 | .event_handler(bridge_dc::Handler { 27 | config: config.clone(), 28 | bridge: bridge.clone(), 29 | }) 30 | .await 31 | .expect("Err creating client"); 32 | // 创建机器人的webhook, 能够发送交互组件消息 33 | // let webhook = http.create_webhook(channel_id, &map, None).await?; 34 | // let map = serde_json::json!({"name": "test"}); 35 | // let webhook = client 36 | // .cache_and_http 37 | // .http 38 | // .create_webhook(781347109676384297, &map, None) 39 | // .await 40 | // .unwrap(); 41 | // let webhook = client 42 | // .cache_and_http 43 | // .http 44 | // .get_webhook(1084186702567981077) 45 | // .await 46 | // .unwrap(); 47 | // println!("webhook_id: {}", webhook.id); 48 | // webhook 49 | // .execute(&client.cache_and_http.http, false, |w| { 50 | // w.username("Webhook test").components(|c| { 51 | // c.create_action_row(|row| { 52 | // row.create_button(|b| b.style(ButtonStyle::Link).url("https://discord.com/channels/724829522230378536/781347109676384297/1082716108953497681").label("跳转回复")) 53 | // }) 54 | // }) 55 | // }) 56 | // .await 57 | // .expect("Could not execute webhook."); 58 | let cache = client.cache_and_http.clone(); 59 | 60 | tokio::select! { 61 | _ = client.start() => { 62 | tracing::warn!("[DC] Discord客户端退出"); 63 | }, 64 | _ = bridge_client::listen(bridge.clone(), cache.http.clone()) => { 65 | tracing::warn!("[DC] Discord桥关闭"); 66 | }, 67 | } 68 | } 69 | 70 | /** 71 | * 申请桥用户 72 | */ 73 | pub async fn apply_bridge_user(id: u64, name: &str, discriminator: u16) -> BridgeUser { 74 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER 75 | .lock() 76 | .await 77 | .likeAndSave(bridge::pojo::BridgeUserSaveForm { 78 | origin_id: id.to_string(), 79 | platform: "DC".to_string(), 80 | display_text: format!("{}#{}", name, discriminator), 81 | }) 82 | .await; 83 | bridge_user.unwrap() 84 | } 85 | 86 | /** 87 | * 通过名称和discriminator查询成员 88 | */ 89 | #[instrument(level = "debug", skip(http), ret)] 90 | pub async fn find_member_by_name(http: &Http, guild_id: u64, nickname: &str, discriminator: &str) -> Option { 91 | let members = http.get_guild_members(guild_id, None, None).await.unwrap(); 92 | let member = members 93 | .into_iter() 94 | .find(|member| member.user.name == nickname && member.user.discriminator.to_string() == discriminator); 95 | member 96 | } 97 | 98 | /** 99 | * 将桥消息转化成回复dc的消息 100 | */ 101 | pub async fn to_reply_content(reply_message: BridgeMessagePO) -> Vec { 102 | let user = match bridge::manager::BRIDGE_USER_MANAGER 103 | .lock() 104 | .await 105 | .get(&reply_message.sender_id) 106 | .await 107 | { 108 | Some(user) => user.display_text, 109 | None => { 110 | format!("[UN] {}", reply_message.sender_id) 111 | } 112 | }; 113 | 114 | let mut content: String = String::new(); 115 | content.push_str(format!("回复 @{} 的消息\n", user).as_str()); 116 | for chain in reply_message.message_chain { 117 | match chain { 118 | bridge::MessageContent::Plain { text } => content.push_str(&text), 119 | bridge::MessageContent::Image(..) => content.push_str("[图片]"), 120 | bridge::MessageContent::Reply { .. } => content.push_str("[回复消息]"), 121 | bridge::MessageContent::At { id } => { 122 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER.lock().await.get(&id).await; 123 | if let None = bridge_user { 124 | content.push_str(format!("@[UN] {}", id).as_str()); 125 | continue; 126 | } 127 | let bridge_user = bridge_user.unwrap(); 128 | // 查看桥关联的本平台用户id 129 | if let Some(ref_user) = bridge_user.find_by_platform("DC").await { 130 | content.push_str(format!("@{}", ref_user.to_string()).as_str()); 131 | continue; 132 | } 133 | // 没有关联账号用标准格式发送消息 134 | content.push_str(format!("@{}", bridge_user.to_string()).as_str()); 135 | } 136 | _ => warn!(unit = ?chain, "无法识别的MessageChain"), 137 | }; 138 | } 139 | let mut result: Vec = vec![]; 140 | let splis: Vec<&str> = content.split("\n").collect(); 141 | for sp in splis { 142 | result.push(format!("> {}\n", sp)); 143 | } 144 | // result.push(value) 145 | result 146 | } 147 | 148 | /** 149 | * 解析文本规则取出提及@[DC]用户的文本 150 | */ 151 | #[derive(Debug)] 152 | pub enum MentionText { 153 | Text(String), 154 | MentionText { name: String, discriminator: String }, 155 | } 156 | pub fn parse_text_mention_rule(text: String) -> Vec { 157 | let mut text = text; 158 | let mut chain: Vec = vec![]; 159 | let split_const = "#|x-x|#".to_string(); 160 | let reg_at_user = regex::Regex::new(r"@\[DC\] ([^\n^#^@]+)?#(\d\d\d\d)").unwrap(); 161 | // let caps = reg_at_user.captures(text); 162 | while let Some(caps) = reg_at_user.captures(text.as_str()) { 163 | println!("{:?}", caps); 164 | let from = caps.get(0).unwrap().as_str(); 165 | let name = caps.get(1).unwrap().as_str().to_string(); 166 | let discriminator = caps.get(2).unwrap().as_str().to_string(); 167 | 168 | let result = text.replace(from, &split_const); 169 | let splits: Vec<&str> = result.split(&split_const).collect(); 170 | let prefix = splits.get(0).unwrap(); 171 | chain.push(MentionText::Text(prefix.to_string())); 172 | chain.push(MentionText::MentionText { name, discriminator }); 173 | if let Some(fix) = splits.get(1) { 174 | text = fix.to_string(); 175 | } 176 | } 177 | if text.len() > 0 { 178 | chain.push(MentionText::Text(text.to_string())); 179 | } 180 | println!("parse_text_mention_rule: {:?}", chain); 181 | chain 182 | } 183 | -------------------------------------------------------------------------------- /src/bridge_log.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::fs; 4 | use chrono::Local; 5 | 6 | pub struct BridgeLog { 7 | 8 | } 9 | impl BridgeLog { 10 | pub fn write_log(content: &str) { 11 | let mut log_content = fs::read_to_string("./bridge_log.log").unwrap(); 12 | let utc = Local::now().format("%Y-%m-%d %H:%M:%S"); 13 | let content = format!(r#"{log_content} 14 | ===Start {utc}=== 15 | {content} 16 | ===End {utc}==="#); 17 | fs::write("./bridge_log.log", content).unwrap(); 18 | } 19 | } 20 | #[cfg(test)] 21 | #[allow(non_snake_case)] 22 | mod test { 23 | use super::*; 24 | 25 | #[test] 26 | fn writeLog() { 27 | BridgeLog::write_log("test"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/bridge_qq/group_message_id.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | /** 3 | * 由于没有唯一值, 只能由qq群号+seqs组成唯一值 4 | */ 5 | pub struct GroupMessageId { 6 | pub group_id: u64, 7 | pub seqs: i32, 8 | } 9 | 10 | impl GroupMessageId { 11 | pub fn new(group_id: u64, seqs: i32) -> GroupMessageId { 12 | GroupMessageId { group_id, seqs } 13 | } 14 | 15 | pub fn from_bridge_message_id(bridge_message_id: &str) -> GroupMessageId { 16 | let splits: Vec<&str> = bridge_message_id.split('|').collect(); 17 | let group_id: u64 = splits[1].parse::().unwrap(); 18 | let seqs: i32 = splits[2].parse::().unwrap(); 19 | GroupMessageId { group_id, seqs } 20 | } 21 | } 22 | 23 | impl Display for GroupMessageId { 24 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 25 | write!(f, "|{}|{}|", self.group_id, self.seqs) 26 | } 27 | } 28 | 29 | #[test] 30 | fn test() { 31 | // reply_seq: 6539, 32 | // sender: 243249439, 33 | let i1 = GroupMessageId::new(243249439, 6539); 34 | assert_eq!(i1.to_string(), format!("|{}|{}|", 243249439, 6539)); 35 | println!("{}", i1.to_string()); 36 | 37 | let i2 = GroupMessageId::from_bridge_message_id(i1.to_string().as_str()); 38 | println!("{}", i2.to_string()); 39 | 40 | assert_eq!(i1.to_string(), i2.to_string(),); 41 | } 42 | -------------------------------------------------------------------------------- /src/bridge_qq/handler.rs: -------------------------------------------------------------------------------- 1 | //! 负责处理 qq 消息 2 | 3 | use std::sync::Arc; 4 | 5 | use proc_qq::re_exports::async_trait::async_trait; 6 | use proc_qq::re_exports::ricq_core; 7 | use proc_qq::re_exports::ricq_core::msg::elem; 8 | use proc_qq::{ 9 | FriendMessageEvent, GroupMessageEvent, GroupTempMessageEvent, LoginEventProcess, 10 | MessageChainPointTrait, MessageEvent, MessageEventProcess, 11 | }; 12 | use tracing::{debug, error, info}; 13 | 14 | use crate::bridge::{BridgeClient, Image, MessageContent}; 15 | use crate::config::BridgeConfig; 16 | use crate::{bridge, elo, utils, Config}; 17 | 18 | use super::group_message_id::GroupMessageId; 19 | use super::{apply_bridge_user, RqClient}; 20 | 21 | const OKK: anyhow::Result = Ok(true); 22 | 23 | async fn recv_group_msg( 24 | event: &GroupMessageEvent, 25 | config: &BridgeConfig, 26 | bridge: Arc, 27 | ) -> anyhow::Result { 28 | let mut _pass = true; 29 | let msg = &event.inner; 30 | let group_id = msg.group_code as u64; 31 | let sender_id = msg.from_uin as u64; 32 | let sender_nickname = msg.group_card.clone(); 33 | info!( 34 | "[{}]{group_id}-[{sender_nickname}]{sender_id} '{}'", 35 | msg.group_name, msg.elements 36 | ); 37 | // 为发送者申请桥用户 38 | let bridge_user = apply_bridge_user(sender_id, sender_nickname.as_str()).await; 39 | // 并接该群消息的id 40 | let qq_message_id = GroupMessageId::new(group_id, msg.seqs.get(0).unwrap().clone()); 41 | // 组装向桥发送的消息体表单 42 | let mut bridge_message = bridge::pojo::BridgeSendMessageForm { 43 | sender_id: bridge_user.id, 44 | avatar_url: Some(format!("https://q1.qlogo.cn/g?b=qq&nk={sender_id}&s=100")), 45 | bridge_config: config.clone(), 46 | message_chain: Vec::new(), 47 | origin_message: bridge::pojo::BridgeMessageRefPO { 48 | origin_id: qq_message_id.to_string(), 49 | platform: "QQ".to_string(), 50 | }, 51 | }; 52 | 53 | for chain1 in &event.message_chain().0 { 54 | let chain = elem::RQElem::from(chain1.clone()); 55 | match chain { 56 | elem::RQElem::At(at) => { 57 | debug!("RQElem::At: {:?}", at); 58 | let bridge_user = apply_bridge_user( 59 | at.target as u64, 60 | elo!(at.display.strip_prefix("@") ;; continue), 61 | ) 62 | .await; 63 | bridge_message 64 | .message_chain 65 | .push(MessageContent::At { id: bridge_user.id }); 66 | } 67 | elem::RQElem::Text(text) => { 68 | debug!("RQElem::Text: {:?}", text); 69 | bridge_message 70 | .message_chain 71 | .push(MessageContent::Plain { text: text.content }); 72 | } 73 | elem::RQElem::GroupImage(group_image) => { 74 | debug!("group_image: {:?}", group_image); 75 | debug!("group_image2: {:?}", group_image.url()); 76 | let file_path = match utils::download_and_cache(group_image.url().as_str()).await { 77 | Ok(path) => Some(path), 78 | Err(_) => { 79 | tracing::error!("下载图片失败: {:?}", group_image.url()); 80 | None 81 | } 82 | }; 83 | bridge_message.message_chain.push(MessageContent::Image( 84 | if let Some(path) = file_path { 85 | Image::Path(path) 86 | } else { 87 | Image::Url(group_image.url()) 88 | }, 89 | )); 90 | } 91 | elem::RQElem::Other(o) => { 92 | if let ricq_core::pb::msg::elem::Elem::SrcMsg(source_msg) = *o { 93 | debug!("疑似回复消息 id: {:?}", source_msg); 94 | let seqs = source_msg.orig_seqs.first().unwrap().clone(); 95 | let group_message_id = GroupMessageId::new(source_msg.to_uin() as u64, seqs); 96 | let result = bridge::manager::BRIDGE_MESSAGE_MANAGER 97 | .lock() 98 | .await 99 | .find_by_ref_and_platform(group_message_id.to_string().as_str(), "QQ") 100 | .await; 101 | if let Err(err) = result { 102 | bridge_message 103 | .message_chain 104 | .push(MessageContent::Err { message: err }); 105 | continue; 106 | } 107 | let result = result.unwrap(); 108 | if let Some(reply) = result { 109 | // 这条是一个笨逻辑, qq的回复会自动at, 这里把他去掉 110 | bridge_message.message_chain.pop(); 111 | bridge_message.message_chain.pop(); 112 | // 填入回复的消息 113 | bridge_message.message_chain.push(MessageContent::Reply { 114 | id: Some(reply.id.clone()), 115 | }); 116 | continue; 117 | } 118 | bridge_message.message_chain.push(MessageContent::Err { 119 | message: "回复一条QQ消息, 但是同步回复消息失败".to_string(), 120 | }); 121 | } else { 122 | debug!("未解读 elem: {:?}", o); 123 | } 124 | } 125 | o => { 126 | debug!("未处理 elem: {:?}", o); 127 | bridge_message.message_chain.push(MessageContent::Plain { 128 | text: "[未处理]".to_string(), 129 | }); 130 | } 131 | } 132 | } 133 | bridge.send_message(bridge_message).await; 134 | OKK 135 | } 136 | 137 | /// 陌生人、群成员临时会话 138 | async fn recv_tmp_msg(event: &GroupTempMessageEvent) -> anyhow::Result { 139 | debug!("tmp session msg: {:?}", event.inner); 140 | // TODO proc tmp session msg 141 | OKK 142 | } 143 | 144 | /// 好友消息 145 | async fn recv_friend_msg(event: &FriendMessageEvent) -> anyhow::Result { 146 | debug!("friend msg: {:?}", event.inner); 147 | // TODO proc friend msg 148 | OKK 149 | } 150 | 151 | pub struct DefaultHandler { 152 | pub config: Arc, 153 | pub bridge: Arc, 154 | pub origin_client: Option>, 155 | } 156 | impl DefaultHandler { 157 | fn find_cfg_by_group(&self, group_id: u64) -> Option<&BridgeConfig> { 158 | let bridge_config = self 159 | .config 160 | .bridges 161 | .iter() 162 | .find(|b| group_id == b.qqGroup && b.enable); 163 | Some(bridge_config?) 164 | } 165 | } 166 | #[async_trait] 167 | impl MessageEventProcess for DefaultHandler { 168 | async fn handle(&self, event: &MessageEvent) -> anyhow::Result { 169 | let res = match event { 170 | MessageEvent::FriendMessage(e) => recv_friend_msg(e).await, 171 | MessageEvent::GroupTempMessage(e) => recv_tmp_msg(e).await, 172 | MessageEvent::GroupMessage(group_msg_event) => { 173 | let gid = group_msg_event.inner.group_code as u64; 174 | debug!("收到群消息({gid})"); 175 | // 如果频道没有配置桥, 则忽略消息 176 | let Some(bridge_cfg) = self.find_cfg_by_group(gid) else { 177 | info!("群({gid})未启用消息同步"); 178 | return OKK; 179 | }; 180 | recv_group_msg(group_msg_event, bridge_cfg, self.bridge.clone()).await 181 | } 182 | }; 183 | match res { 184 | Ok(flag) => Ok(flag), 185 | Err(err) => { 186 | let bot_id = event.client().uin().await; 187 | error!(?err, "[{bot_id}] 消息处理时异常"); 188 | Ok(false) 189 | } 190 | } 191 | // return 192 | } 193 | } 194 | #[async_trait] 195 | impl LoginEventProcess for DefaultHandler { 196 | async fn handle(&self, _: &proc_qq::LoginEvent) -> anyhow::Result { 197 | tracing::info!("[QQ] 登录到qq客户端"); 198 | OKK 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/bridge_qq/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use proc_qq::re_exports::ricq::msg::MessageChain; 4 | use proc_qq::re_exports::ricq_core::msg::elem; 5 | use proc_qq::FileSessionStore; 6 | use proc_qq::{Authentication, ClientBuilder, DeviceSource, ModuleEventHandler, ModuleEventProcess, ShowQR}; 7 | use tracing::{debug, error}; 8 | 9 | use crate::bridge_qq::handler::DefaultHandler; 10 | use crate::{bridge, Config}; 11 | use bridge::pojo::BridgeMessagePO; 12 | 13 | mod group_message_id; 14 | mod handler; 15 | 16 | use group_message_id::GroupMessageId; 17 | 18 | type RqClient = proc_qq::re_exports::ricq::Client; 19 | 20 | pub async fn upload_group_image(group_id: u64, url: &str, rq_client: Arc) -> anyhow::Result { 21 | let client = reqwest::Client::new(); 22 | let stream = client.get(url).send().await?; 23 | let img_bytes = stream.bytes().await.unwrap(); 24 | let group_image = rq_client.upload_group_image(group_id as i64, img_bytes.as_ref()).await?; 25 | Ok(group_image) 26 | } 27 | 28 | /// # 处理 at 消息 29 | /// ## Argument 30 | /// - `target` 被 at 用户 31 | /// - `send_content` 同步消息链 32 | async fn proc_at(target: &str, send_content: &mut MessageChain) { 33 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER.lock().await.get(target).await; 34 | if let None = bridge_user { 35 | send_content.push(elem::Text::new(format!("@[UN] {}", target))); 36 | return; 37 | } 38 | let bridge_user = bridge_user.unwrap(); 39 | // 查看桥关联的本平台用户id 40 | if let Some(ref_user) = bridge_user.find_by_platform("QQ").await { 41 | if let Ok(origin_id) = ref_user.origin_id.parse::() { 42 | send_content.push(elem::At::new(origin_id)); 43 | return; 44 | } 45 | } 46 | // 没有关联账号用标准格式发送消息 47 | send_content.push(elem::Text::new(format!("@{}", bridge_user.to_string()))); 48 | } 49 | 50 | /** 51 | * 同步消息方法 52 | */ 53 | pub async fn sync_message(bridge: Arc, rq_client: Arc) { 54 | let mut subs = bridge.sender.subscribe(); 55 | let bot_id = rq_client.uin().await; 56 | loop { 57 | let message = match subs.recv().await { 58 | Ok(m) => m, 59 | Err(err) => { 60 | error!(?err, "[{bot_id}] 消息同步失败"); 61 | continue; 62 | } 63 | }; 64 | 65 | let mut send_content = MessageChain::default(); 66 | 67 | // 配置发送者头像 68 | if let Some(avatar_url) = &message.avatar_url { 69 | debug!("用户头像: {:?}", message.avatar_url); 70 | let image = upload_group_image(message.bridge_config.qqGroup, avatar_url, rq_client.clone()).await; 71 | if let Result::Ok(image) = image { 72 | send_content.push(image); 73 | } 74 | } 75 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER.lock().await.get(&message.sender_id).await; 76 | // 配置发送者用户名 77 | send_content.push(elem::Text::new(format!("{}\n", bridge_user.unwrap().to_string()))); 78 | 79 | for chain in &message.message_chain { 80 | match chain { 81 | bridge::MessageContent::Reply { id } => { 82 | if let Some(id) = id { 83 | let reply_message = bridge::manager::BRIDGE_MESSAGE_MANAGER.lock().await.get(id).await; 84 | if let Some(reply_message) = reply_message { 85 | to_reply_content(&mut send_content, reply_message, bot_id).await 86 | } else { 87 | send_content.push(elem::Text::new("> {回复消息}\n".to_string())); 88 | } 89 | } 90 | } 91 | // 桥文本 转 qq文本 92 | bridge::MessageContent::Plain { text } => { 93 | let mention_text_list = parse_text_mention_rule(text.to_string()); 94 | for mention_text in mention_text_list { 95 | match mention_text { 96 | MentionText::Text(text) => send_content.push(elem::Text::new(text)), 97 | MentionText::MentionText { id, .. } => send_content.push(elem::At::new(id)), 98 | } 99 | } 100 | } 101 | // @桥用户 转 @qq用户 或 @文本 102 | bridge::MessageContent::At { id } => proc_at(id, &mut send_content).await, 103 | // 桥图片 转 qq图片 104 | bridge::MessageContent::Image(image) => { 105 | debug!("桥消息-图片: {:?}", image); 106 | match image.clone().load_data().await { 107 | Ok(data) => match rq_client 108 | .upload_group_image(message.bridge_config.qqGroup as i64, data.as_slice()) 109 | .await 110 | { 111 | Ok(image) => { 112 | send_content.push(image); 113 | } 114 | Err(_) => {} 115 | }, 116 | Err(_) => {} 117 | } 118 | } 119 | _ => send_content.push(elem::Text::new("{未处理的桥信息}".to_string())), 120 | } 121 | } 122 | debug!("[QQ] 同步消息"); 123 | debug!("{:?}", send_content); 124 | debug!("{:?}", message.bridge_config.qqGroup as i64); 125 | 126 | // seqs: [6539], rands: [1442369605], time: 1678267174 127 | // rq_client.send_message(routing_head, message_chain, ptt); 128 | 129 | let result = rq_client 130 | .send_group_message(message.bridge_config.qqGroup as i64, send_content) 131 | .await 132 | .ok(); 133 | if let Some(receipt) = result { 134 | // 发送成功后, 将平台消息和桥消息进行关联, 为以后进行回复功能 135 | let seqs = receipt.seqs.first().unwrap().clone(); 136 | let group_message_id = GroupMessageId { 137 | group_id: message.bridge_config.qqGroup, 138 | seqs, 139 | }; 140 | bridge::manager::BRIDGE_MESSAGE_MANAGER 141 | .lock() 142 | .await 143 | .ref_bridge_message(bridge::pojo::BridgeMessageRefMessageForm { 144 | bridge_message_id: message.id, 145 | platform: "QQ".to_string(), 146 | origin_id: group_message_id.to_string(), 147 | }) 148 | .await; 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * 消息桥构建入口 155 | */ 156 | pub async fn start(config: Arc, bridge: Arc) { 157 | tracing::info!("[QQ] 初始化QQ桥"); 158 | // 确认配置无误 159 | let auth = match config.qq_config.get_auth() { 160 | Ok(a) => a, 161 | Err(e) => { 162 | tracing::error!(?e); 163 | return; 164 | } 165 | }; 166 | let version = match config.qq_config.get_version() { 167 | Ok(a) => a, 168 | Err(e) => { 169 | tracing::error!(?e); 170 | return; 171 | } 172 | }; 173 | 174 | let handler = DefaultHandler { 175 | config: config.clone(), 176 | bridge: bridge.clone(), 177 | origin_client: None, 178 | }; 179 | let handler = Box::new(handler); 180 | let on_message = ModuleEventHandler { 181 | name: "OnMessage".to_owned(), 182 | process: ModuleEventProcess::Message(handler), 183 | }; 184 | 185 | // let modules = module!("qq_bridge", "qq桥模块", handler); 186 | let module = proc_qq::Module { 187 | id: "qq_bridge".to_string(), 188 | name: "qq桥模块".to_string(), 189 | handles: vec![on_message], 190 | }; 191 | 192 | let mut show_qr = ShowQR::OpenBySystem; 193 | if let Authentication::QRCode = auth { 194 | if config.print_qr.unwrap_or_else(|| false) { 195 | show_qr = ShowQR::PrintToConsole; 196 | } 197 | } 198 | 199 | let client = ClientBuilder::new() 200 | .session_store(FileSessionStore::boxed("session.token")) 201 | .authentication(auth) 202 | .show_rq(show_qr) 203 | .device(DeviceSource::JsonFile("device.json".to_owned())) 204 | .version(version) 205 | .modules(vec![module]) 206 | .build() 207 | .await 208 | .unwrap(); 209 | let arc = Arc::new(client); 210 | tokio::select! { 211 | Err(e) = proc_qq::run_client(arc.clone()) => { 212 | tracing::error!(err = ?e, "[QQ] QQ客户端退出"); 213 | }, 214 | _ = sync_message(bridge.clone(), arc.rq_client.clone()) => { 215 | tracing::warn!("[QQ] QQ桥关闭"); 216 | }, 217 | } 218 | } 219 | 220 | /** 221 | * 申请桥用户 222 | */ 223 | async fn apply_bridge_user(id: u64, name: &str) -> bridge::user::BridgeUser { 224 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER 225 | .lock() 226 | .await 227 | .likeAndSave(bridge::pojo::BridgeUserSaveForm { 228 | origin_id: id.to_string(), 229 | platform: "QQ".to_string(), 230 | display_text: format!("{}({})", name, id), 231 | }) 232 | .await; 233 | bridge_user.unwrap() 234 | } 235 | 236 | /** 237 | * 处理回复 238 | */ 239 | async fn to_reply_content(message_chain: &mut MessageChain, reply_message: BridgeMessagePO, uni: i64) { 240 | let refs = reply_message.refs.iter().find(|refs| refs.platform.eq("QQ")); 241 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER 242 | .lock() 243 | .await 244 | .get(&reply_message.sender_id) 245 | .await 246 | .unwrap(); 247 | if let Some(refs) = refs { 248 | let group_message_id = GroupMessageId::from_bridge_message_id(&refs.origin_id); 249 | let mut reply_content = MessageChain::default(); 250 | let sender: i64 = if bridge_user.platform == "QQ" { 251 | bridge_user.origin_id.parse::().unwrap() 252 | } else { 253 | uni 254 | }; 255 | for chain in reply_message.message_chain { 256 | match chain { 257 | bridge::MessageContent::Reply { .. } => { 258 | reply_content.push(elem::Text::new("[回复消息]".to_string())); 259 | } 260 | bridge::MessageContent::Plain { text } => reply_content.push(elem::Text::new(text.to_string())), 261 | bridge::MessageContent::At { id } => proc_at(&id, &mut reply_content).await, 262 | bridge::MessageContent::Image(..) => reply_content.push(elem::Text::new("[图片]".to_string())), 263 | _ => {} 264 | } 265 | } 266 | let reply = elem::Reply { 267 | reply_seq: group_message_id.seqs, 268 | sender, 269 | time: 0, 270 | elements: reply_content, 271 | }; 272 | message_chain.with_reply(reply); 273 | } else { 274 | message_chain.push(elem::Text::new("{回复消息}".to_string())); 275 | } 276 | // let mut reply_content = MessageChain::default(); 277 | // reply_content.push(elem::Text::new("test custom reply3".to_string())); 278 | // let reply = elem::Reply { 279 | // reply_seq: 6539, 280 | // sender: 243249439, 281 | // time: 1678267174, 282 | // elements: reply_content, 283 | // }; 284 | // send_content.with_reply(reply); 285 | // send_content.0.push(ricq_core::pb::msg::elem::Elem::SrcMsg( 286 | // ricq_core::pb::msg::SourceMsg { 287 | // orig_seqs: vec![6539], 288 | // sender_uin: Some(243249439), 289 | // time: Some(1678267174), 290 | // flag: Some(1), 291 | // elems: reply_content.into(), 292 | // rich_msg: Some(vec![]), 293 | // pb_reserve: Some(vec![]), 294 | // src_msg: Some(vec![]), 295 | // troop_name: Some(vec![]), 296 | // ..Default::default() 297 | // }, 298 | // )); 299 | } 300 | 301 | /** 302 | * 解析文本规则取出提及@[DC]用户的文本 303 | */ 304 | #[derive(Debug)] 305 | pub enum MentionText { 306 | Text(String), 307 | MentionText { name: String, id: i64 }, 308 | } 309 | pub fn parse_text_mention_rule(text: String) -> Vec { 310 | let mut text = text; 311 | let mut chain: Vec = vec![]; 312 | let split_const = "#|x-x|#".to_string(); 313 | let reg_at_user = regex::Regex::new(r"@\[QQ\] ([^\n^@]+)\(([0-9]+)\)").unwrap(); 314 | while let Some(caps) = reg_at_user.captures(text.as_str()) { 315 | println!("{:?}", caps); 316 | let from = caps.get(0).unwrap().as_str(); 317 | let name = caps.get(1).unwrap().as_str().to_string(); 318 | let id = caps.get(2).unwrap().as_str().parse::().unwrap(); 319 | 320 | let result = text.replace(from, &split_const); 321 | let splits: Vec<&str> = result.split(&split_const).collect(); 322 | let prefix = splits.get(0).unwrap(); 323 | chain.push(MentionText::Text(prefix.to_string())); 324 | chain.push(MentionText::MentionText { name, id }); 325 | if let Some(fix) = splits.get(1) { 326 | text = fix.to_string(); 327 | } 328 | } 329 | if text.len() > 0 { 330 | chain.push(MentionText::Text(text.to_string())); 331 | } 332 | println!("parse_text_mention_rule: {:?}", chain); 333 | chain 334 | } 335 | -------------------------------------------------------------------------------- /src/bridge_tg/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::Path; 3 | use std::sync::Arc; 4 | 5 | use anyhow::Result; 6 | use lazy_static::lazy_static; 7 | use proc_qq::re_exports::image; 8 | use teleser::re_exports::async_trait::async_trait; 9 | use teleser::re_exports::grammers_client::types::{Chat, Media, Message}; 10 | use teleser::re_exports::grammers_client::{Client, InitParams, InputMessage}; 11 | use teleser::re_exports::grammers_session::PackedChat; 12 | use teleser::re_exports::grammers_tl_types::enums::MessageEntity; 13 | use teleser::{Auth, ClientBuilder, FileSessionStore, NewMessageProcess, Process, StaticBotToken}; 14 | use tokio::io::AsyncWriteExt; 15 | use tokio::sync::Mutex; 16 | use tracing::{debug, error, warn}; 17 | 18 | use crate::bridge; 19 | use crate::bridge::MessageContent::Plain; 20 | use crate::bridge::{BridgeClient, Image, MessageContent}; 21 | use crate::config::{BridgeConfig, Config}; 22 | 23 | pub async fn start(config: Arc, bridge: Arc) { 24 | // 还原pack 25 | let folder = format!( 26 | "tg.pack.{}", 27 | config.telegram_config.botToken.split(":").next().unwrap() 28 | ); 29 | if !Path::new(folder.as_str()).exists() { 30 | tokio::fs::create_dir(folder.as_str()).await.unwrap(); 31 | } 32 | let mut lock = PACK_MAP.lock().await; 33 | let mut rd = tokio::fs::read_dir(folder.as_str()).await.unwrap(); 34 | while let Some(file) = rd.next_entry().await.unwrap() { 35 | let id = file.file_name().to_str().unwrap().parse::().unwrap(); 36 | let data = tokio::fs::read(file.path()).await.unwrap(); 37 | match PackedChat::from_bytes(&data) { 38 | Ok(chat) => { 39 | lock.insert(id, chat); 40 | } 41 | Err(_) => {} 42 | } 43 | } 44 | drop(lock); 45 | // 初始化 46 | tracing::info!("[TG] 初始化TG桥"); 47 | let module = teleser::Module { 48 | id: "tg_new_message".to_owned(), 49 | name: "tg_new_message".to_owned(), 50 | handlers: vec![teleser::Handler { 51 | id: "tg_new_message".to_owned(), 52 | process: Process::NewMessageProcess(Box::new(TgNewMessage { 53 | config: config.clone(), 54 | bridge: bridge.clone(), 55 | pack_folder: folder.clone(), 56 | })), 57 | }], 58 | }; 59 | let client = ClientBuilder::new() 60 | .with_api_id(config.telegram_config.apiId.clone()) 61 | .with_api_hash(config.telegram_config.apiHash.clone()) 62 | .with_session_store(Box::new(FileSessionStore { 63 | path: "telegram.session".to_string(), 64 | })) 65 | .with_auth(Auth::AuthWithBotToken(Box::new(StaticBotToken { 66 | token: config.telegram_config.botToken.clone(), 67 | }))) 68 | .with_init_params(Some({ 69 | let mut params = InitParams::default(); 70 | params.device_model = "message_bridge_rs".to_owned(); 71 | if let Ok(url) = std::env::var("BRIDGE_TG_PROXY") { 72 | params.proxy_url = Some(url); 73 | } 74 | params 75 | })) 76 | .with_modules(Arc::new(vec![module])) 77 | .build() 78 | .unwrap(); 79 | let arc = Arc::new(client); 80 | tokio::select! { 81 | _ = teleser::run_client_and_reconnect(arc.clone()) => { 82 | tracing::warn!("[TG] TG客户端退出"); 83 | }, 84 | _ = sync_message(bridge.clone(), arc) => { 85 | tracing::warn!("[TG] TG桥关闭"); 86 | }, 87 | } 88 | } 89 | 90 | pub struct TgNewMessage { 91 | pub config: Arc, 92 | pub bridge: Arc, 93 | pub pack_folder: String, 94 | } 95 | 96 | impl TgNewMessage { 97 | fn find_cfg_by_group(&self, group_id: i64) -> Option<&BridgeConfig> { 98 | let bridge_config = self 99 | .config 100 | .bridges 101 | .iter() 102 | .find(|b| group_id == b.tgGroup && b.enable); 103 | Some(bridge_config?) 104 | } 105 | async fn pack_chat(&self, event: &Message) { 106 | let chat = event.chat(); 107 | let mut lock = PACK_MAP.lock().await; 108 | if !lock.contains_key(&chat.id()) { 109 | let pack = chat.pack(); 110 | let _ = tokio::fs::write( 111 | Path::new(self.pack_folder.as_str()).join(format!("{}", chat.id())), 112 | pack.to_bytes(), 113 | ) 114 | .await; 115 | lock.insert(chat.id(), pack); 116 | } 117 | } 118 | } 119 | 120 | #[async_trait] 121 | impl NewMessageProcess for TgNewMessage { 122 | async fn handle(&self, client: &mut Client, event: &Message) -> Result { 123 | self.pack_chat(event).await; 124 | if !event.outgoing() { 125 | if let Chat::Group(group) = event.chat() { 126 | if let Some(config) = self.find_cfg_by_group(group.id()) { 127 | if let Some(Chat::User(user)) = event.sender() { 128 | // 为发送者申请桥用户 129 | let bridge_user = apply_bridge_user(user.id(), user.full_name()).await; 130 | // 组装向桥发送的消息体表单 131 | let mut bridge_message = bridge::pojo::BridgeSendMessageForm { 132 | sender_id: bridge_user.id, 133 | avatar_url: None, 134 | bridge_config: config.clone(), 135 | message_chain: Vec::new(), 136 | origin_message: bridge::pojo::BridgeMessageRefPO { 137 | origin_id: "".to_string(), 138 | platform: "TG".to_string(), 139 | }, 140 | }; 141 | // 下载图片 142 | let media = event.media(); 143 | if let Some(Media::Photo(_)) = &media { 144 | // download media 存在一定时间以后不能使用的BUG, 已经使用临时仓库解决 145 | // see: https://github.com/Lonami/grammers/issues/166 146 | match download_media(client, &media.unwrap()).await { 147 | Ok(data) => bridge_message 148 | .message_chain 149 | .push(MessageContent::Image(Image::Buff(data))), 150 | Err(err) => { 151 | error!("下载TG图片失败 : {:?}", err) 152 | } 153 | } 154 | } 155 | if !event.text().is_empty() { 156 | if let Some(entities) = event.fmt_entities() { 157 | let text = event.text(); 158 | let mut offset: usize = 0; 159 | for x in entities { 160 | match x { 161 | MessageEntity::Mention(m) => { 162 | // todo 用数据库和更新用户名和id的对应关系, 因为机器人api不允许使用用户名转id的功能 163 | bridge_message.message_chain.push(Plain { 164 | text: text[offset..(m.offset) as usize].to_string(), 165 | }); 166 | offset = (m.offset + m.length) as usize; 167 | // todo @ 168 | // bridge_message.message_chain.push(At { 169 | // }); 170 | bridge_message.message_chain.push(Plain { 171 | text: text[(m.offset as usize) 172 | ..((m.offset + m.length) as usize)] 173 | .to_string(), 174 | }); 175 | } 176 | MessageEntity::MentionName(m) => { 177 | if offset < m.offset as usize { 178 | bridge_message.message_chain.push(Plain { 179 | text: text[offset..(m.offset as usize)] 180 | .to_string(), 181 | }); 182 | offset = (m.offset + m.length) as usize; 183 | // todo @ 184 | // bridge_message.message_chain.push(At { 185 | // }); 186 | bridge_message.message_chain.push(Plain { 187 | text: format!( 188 | "@{}", 189 | text[(m.offset as usize) 190 | ..((m.offset + m.length) as usize)] 191 | .to_string() 192 | ), 193 | }); 194 | } 195 | } 196 | _ => {} 197 | } 198 | } 199 | } else { 200 | bridge_message.message_chain.push(Plain { 201 | text: event.text().to_owned(), 202 | }); 203 | } 204 | } 205 | if !bridge_message.message_chain.is_empty() { 206 | self.bridge.send_message(bridge_message).await; 207 | } 208 | } 209 | } 210 | } 211 | } 212 | Ok(false) 213 | } 214 | } 215 | 216 | lazy_static! { 217 | static ref PACK_MAP: Mutex> = Mutex::new(HashMap::new()); 218 | } 219 | 220 | pub async fn sync_message(bridge: Arc, teleser_client: Arc) { 221 | let mut subs = bridge.sender.subscribe(); 222 | loop { 223 | let message = match subs.recv().await { 224 | Ok(m) => m, 225 | Err(err) => { 226 | error!(?err, "[tg] 消息同步失败"); 227 | continue; 228 | } 229 | }; 230 | // 配置发送者头像 231 | if let Some(avatar_url) = &message.avatar_url { 232 | debug!("用户头像: {:?}", avatar_url); 233 | } 234 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER 235 | .lock() 236 | .await 237 | .get(&message.sender_id) 238 | .await 239 | .unwrap(); 240 | // telegram 每条消息只能带一个附件或一个图片 241 | // 同时可以发一组图片消息,但是只有第一个图片消息可以带文字,文字会显示到一组消息的最下方 242 | // todo 发送图片消息和 @ 243 | let mut builder = vec![]; 244 | let mut images = vec![]; 245 | for x in &message.message_chain { 246 | match x { 247 | MessageContent::Reply { .. } => {} 248 | MessageContent::Plain { text } => builder.push(text.as_str()), 249 | MessageContent::At { .. } => { 250 | // todo 251 | } 252 | MessageContent::AtAll => {} 253 | MessageContent::Image(image) => images.push(image), 254 | MessageContent::Err { .. } => {} 255 | MessageContent::Othen => {} 256 | } 257 | } 258 | // 获取PACK 259 | let map_lock = PACK_MAP.lock().await; 260 | let chat = match map_lock.get(&message.bridge_config.tgGroup) { 261 | Some(chat) => Some(chat.clone()), 262 | None => { 263 | warn!("PACK 未找到 : {}", message.bridge_config.tgGroup); 264 | None 265 | } 266 | }; 267 | drop(map_lock); 268 | // 269 | if let Some(chat) = chat { 270 | let lock = teleser_client.inner_client.lock().await; 271 | let inner_client = lock.clone(); 272 | drop(lock); 273 | if let Some(inner_client) = inner_client { 274 | // send message 275 | if !images.is_empty() { 276 | for x in images { 277 | match x.clone().load_data().await { 278 | Ok(data) => { 279 | match image::guess_format(&data) { 280 | Ok(format) => { 281 | let len = data.len(); 282 | let mut reader = std::io::Cursor::new(data); 283 | let upload = inner_client 284 | .upload_stream( 285 | &mut reader, 286 | len, 287 | format!("file.{}", format.extensions_str()[0]), 288 | ) 289 | .await; 290 | match upload { 291 | Ok(img) => { 292 | let _ = inner_client 293 | .send_message( 294 | chat.clone(), 295 | InputMessage::text(format!( 296 | "{}:", 297 | bridge_user.to_string(), 298 | )) 299 | .photo(img), 300 | ) 301 | .await; 302 | } 303 | Err(_) => {} 304 | } 305 | } 306 | Err(_) => {} 307 | }; 308 | } 309 | Err(_) => {} 310 | } 311 | } 312 | } 313 | if !builder.is_empty() { 314 | let send = builder.join(""); 315 | if !send.is_empty() { 316 | let _ = inner_client 317 | .send_message( 318 | chat.clone(), 319 | format!("{} : {}", bridge_user.to_string(), send), 320 | ) 321 | .await; 322 | } 323 | } 324 | } 325 | } 326 | } 327 | } 328 | 329 | async fn download_media(c: &mut teleser::InnerClient, media: &Media) -> Result> { 330 | let mut data = Vec::::new(); 331 | let mut download = c.iter_download(&media); 332 | while let Some(chunk) = download.next().await? { 333 | data.write(chunk.as_slice()).await?; 334 | } 335 | Ok(data) 336 | } 337 | 338 | /** 339 | * 申请桥用户 340 | */ 341 | async fn apply_bridge_user(id: i64, name: String) -> bridge::user::BridgeUser { 342 | let bridge_user = bridge::manager::BRIDGE_USER_MANAGER 343 | .lock() 344 | .await 345 | .likeAndSave(bridge::pojo::BridgeUserSaveForm { 346 | origin_id: id.to_string(), 347 | platform: "TG".to_string(), 348 | display_text: format!("{}({})", name, id), 349 | }) 350 | .await; 351 | bridge_user.unwrap() 352 | } 353 | -------------------------------------------------------------------------------- /src/cmd_adapter.rs: -------------------------------------------------------------------------------- 1 | //! 接收桥收到的用户指令,加以识别和响应 2 | 3 | use std::sync::Arc; 4 | 5 | use chrono::Local; 6 | use tracing::{debug, info, trace, warn}; 7 | 8 | use crate::bridge::{BridgeClient, BridgeClientPlatform, BridgeMessage, MessageContent::Plain, User}; 9 | use crate::bridge_cmd; 10 | use crate::bridge_cmd::{bind_meta::BindMeta, Cmd::*}; 11 | use crate::bridge_data::bind_map::{add_bind, get_bind, rm_bind}; 12 | use crate::Config; 13 | 14 | type CacheBind = Vec<(i64, BindMeta)>; 15 | 16 | /// 缓存超时(毫秒) 17 | const CACHE_TIMEOUT: i64 = 30_000; 18 | 19 | /// 持续接收指令消息 20 | pub async fn listen(conf: Arc, bridge: Arc) { 21 | // cache token - bind cmd 22 | let mut cache_bind: CacheBind = Vec::with_capacity(1024); 23 | let mut rx = bridge.sender.subscribe(); 24 | 25 | loop { 26 | let msg = rx.recv().await.unwrap(); 27 | // match cmd 28 | if let Some((cmd, args)) = bridge_cmd::kind(&msg.message_chain) { 29 | let result = match cmd { 30 | Help => get_help(&msg.user), 31 | Bind => try_cache_bind(&msg.user, &mut cache_bind, args).to_string(), 32 | ConfirmBind => try_bind(&msg.user, &mut cache_bind).to_string(), 33 | Unbind => try_unbind(&msg.user, args), 34 | }; 35 | if result.is_empty() { 36 | continue; 37 | } 38 | let bc = conf.bridges.iter().find(|b| b.enable); 39 | if let Some(bc) = bc { 40 | let mut feedback = BridgeMessage { 41 | id: uuid::Uuid::new_v4().to_string(), 42 | bridge_config: bc.clone(), 43 | message_chain: Vec::new(), 44 | user: msg.user.clone(), 45 | }; 46 | feedback.user.platform = BridgeClientPlatform::Cmd; 47 | feedback.message_chain.push(Plain { text: result }); 48 | debug!("bot feedback: {:#?}", feedback); 49 | bridge.send(feedback); 50 | } 51 | } 52 | } // loop 53 | } 54 | 55 | /// 开启频道 56 | pub async fn start(config: Arc, bridge: Arc) { 57 | tokio::select! { 58 | _ = listen(config.clone(), bridge.clone()) => {}, 59 | } 60 | } 61 | 62 | /// 获取指令帮助 63 | /// - `user` 调用者 64 | fn get_help(user: &User) -> String { 65 | // !来点[搜图] 66 | // !废话生成器 67 | // !猜数字游戏 68 | 69 | // 管理员: 70 | // !服务器状态 71 | // !重启 72 | // !查看所有成员绑定关系 73 | // !绑定成员关联 [用户名] [用户名] 74 | // !解除成员关联 [用户名] 75 | let bind_str = match user.platform { 76 | BridgeClientPlatform::Discord => "!绑定 qq [qq号]".to_string(), 77 | BridgeClientPlatform::QQ => "!绑定 dc [#9617(仅填写数字)]".to_string(), 78 | _ => "".to_string(), 79 | }; 80 | format!( 81 | "!帮助\n\ 82 | !ping\n\ 83 | {}\n\ 84 | !确认绑定\n\ 85 | !解除绑定\n\ 86 | !查看绑定状态", 87 | bind_str 88 | ) 89 | } 90 | 91 | /// 解析绑定指令的参数 92 | /// - `args` 指令参数 93 | fn parse_bind_args(args: &Vec) -> Option<(BridgeClientPlatform, u64)> { 94 | let p: BridgeClientPlatform = match args[0].parse() { 95 | Err(e) => { 96 | warn!(?e, "无法绑定未定义平台的账户。"); 97 | return None; 98 | } 99 | Ok(p) => p, 100 | }; 101 | let u: u64 = match args[1].parse() { 102 | Err(_) => { 103 | warn!("目前只支持纯数字id。"); 104 | return None; 105 | } 106 | Ok(p) => p, 107 | }; 108 | Some((p, u)) 109 | } 110 | 111 | /// 查询映射 112 | /// - `from` 请求者信息 113 | /// - `to` 指令参数:绑定的平台和用户id 114 | fn is_mapping(from: (BridgeClientPlatform, u64), to: (BridgeClientPlatform, u64)) -> bool { 115 | if let Some(mapping) = get_bind(from, to.0) { 116 | if mapping.unique_id == to.1 || mapping.display_id == to.1 { 117 | info!( 118 | "'{} {}' 已映射至 '{} {}'", 119 | from.0, from.1, mapping.platform, mapping.unique_id 120 | ); 121 | return true; 122 | } 123 | } 124 | false 125 | } 126 | 127 | /// 检查绑定指令,尝试缓存 128 | /// - `sign` 指令内容 129 | /// - `cache` 缓存集合 130 | fn try_cache_bind(user: &User, caches: &mut CacheBind, args: Option>) -> String { 131 | // TODO 防过量请求,避免缓存爆炸 132 | // TODO 检查权限 133 | if let Some(bind_to) = parse_bind_args(&args.unwrap()) { 134 | // 查询映射 135 | if is_mapping((user.platform, user.unique_id), bind_to) { 136 | return "此用户已绑定".to_string(); 137 | } 138 | 139 | let new_meta = BindMeta::new((user.platform, user.unique_id), bind_to); 140 | let now = Local::now().timestamp_millis(); 141 | caches.retain(|(t, old_meta)| { 142 | if now - *t > CACHE_TIMEOUT { 143 | trace!("缓存已过期 [dl:{} > ch:{}] {:?}", now - CACHE_TIMEOUT, *t, old_meta); 144 | return false; 145 | } 146 | if new_meta == *old_meta { 147 | trace!("刷新缓存 [{} -> {}]", *t, now); 148 | return false; 149 | } 150 | true 151 | }); 152 | debug!("缓存绑定请求: {:?}", new_meta); 153 | caches.push((now, new_meta)); 154 | info!("缓存请求数: {}", caches.len()); 155 | return format!("已记录,{}秒后失效。", CACHE_TIMEOUT / 1000); 156 | } 157 | warn!("绑定指令参数解析失败!"); 158 | get_help(user) 159 | } 160 | 161 | /// 尝试建立映射 162 | /// - `user` 接受绑定的用户 163 | /// - `cache` 缓存集合 164 | fn try_bind(user: &User, caches: &mut CacheBind) -> &'static str { 165 | let deadline = Local::now().timestamp_millis() - CACHE_TIMEOUT; 166 | let mut opt: Option = None; 167 | caches.retain(|(t, m)| { 168 | if *t < deadline { 169 | trace!("缓存已过期 [dl:{} > ch:{}] {:?}", deadline, *t, m); 170 | return false; 171 | } 172 | if m.to.platform == user.platform 173 | && (m.to.user == user.unique_id || m.to.user == user.display_id) 174 | { 175 | if opt == None { 176 | opt = Some(*m); 177 | } 178 | return false; 179 | } 180 | true 181 | }); 182 | if let Some(m) = opt { 183 | let from = m.from.to_user(); 184 | add_bind(&from, user); 185 | info!("{}({}) bind to {}", from.platform, from.unique_id, user.name); 186 | return "绑定完成" 187 | } 188 | "" 189 | } 190 | 191 | /// 尝试解绑 192 | /// - `user` 发送解绑指令的用户 193 | /// - `args` 指令参数 194 | fn try_unbind(user: &User, args: Option>) -> String { 195 | match (args.unwrap())[0].parse::() { 196 | Ok(p) => { 197 | let msg = if p == user.platform { 198 | "原地TP?" 199 | } else { 200 | if rm_bind((user.platform, user.unique_id), p) { 201 | "已解除绑定" 202 | } else { 203 | "未向此平台绑定用户" 204 | } 205 | }; 206 | return msg.to_string(); 207 | } 208 | e => warn!(?e, "无法绑定未定义平台的账户。"), 209 | } 210 | get_help(user) 211 | } 212 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | use proc_qq::re_exports::ricq::version; 4 | use proc_qq::Authentication; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | use std::fs; 8 | 9 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 10 | pub struct Config { 11 | /// 是否将二维码打印到终端 12 | #[serde(rename = "printQR")] 13 | pub print_qr: Option, 14 | #[serde(rename = "qqConfig")] 15 | pub qq_config: QQConfig, 16 | #[serde(rename = "discordConfig")] 17 | pub discord_config: DiscordConfig, 18 | #[serde(rename = "telegramConfig")] 19 | pub telegram_config: TelegramConfig, 20 | pub bridges: Vec, 21 | } 22 | 23 | impl Config { 24 | pub fn new() -> Self { 25 | let file = fs::read_to_string("./config.json").unwrap(); 26 | // println!("{file}"); 27 | let config: Config = serde_json::from_str(file.as_str()).unwrap(); 28 | 29 | config 30 | } 31 | } 32 | 33 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 34 | pub struct QQConfig { 35 | /// bot账号 36 | pub botId: Option, 37 | /// bot登录密码(可选) 38 | pub password: Option, 39 | version: String, 40 | /// 登录认证方式(无token时) 41 | auth: String, 42 | } 43 | impl QQConfig { 44 | /// 获取认证方式 45 | pub fn get_auth(&self) -> anyhow::Result { 46 | use Authentication::*; 47 | match &*self.auth.to_lowercase() { 48 | "pwd" => { 49 | if self.botId.is_none() || self.password.is_none() { 50 | return Err(anyhow::anyhow!("[QQ] 需配置账号(botId)密码(password)!")); 51 | } 52 | let pwd = self.password.as_ref().unwrap(); 53 | if pwd.len() != 16 { 54 | return Err(anyhow::anyhow!("[QQ] 密码请使用16位MD5加密")); 55 | } 56 | let mut buf = [0; 16]; 57 | let mut x = 0; 58 | for b in pwd.bytes() { 59 | if x > 15 { 60 | break; 61 | } 62 | buf[x] = b; 63 | x += 1; 64 | } 65 | Ok(UinPasswordMd5(self.botId.unwrap(), buf)) 66 | } 67 | "qr" => Ok(QRCode), 68 | _ => Err(anyhow::anyhow!( 69 | "[QQ] 登录方式目前仅支持:二维码(qr)、账号密码(pwd)" 70 | )), 71 | } // match 72 | } 73 | /// 获取客户端协议 74 | pub fn get_version(&self) -> anyhow::Result<&'static version::Version> { 75 | use proc_qq::re_exports::ricq::version::*; 76 | match &*self.version.to_lowercase() { 77 | "ipad" => Ok(&IPAD), 78 | "macos" => Ok(&MACOS), 79 | "qidian" => Ok(&QIDIAN), 80 | "androidphone" => Ok(&ANDROID_PHONE), 81 | "androidwatch" => Ok(&ANDROID_WATCH), 82 | v => Err(anyhow::anyhow!("[QQ] 暂不支持[{v}]协议,请更换!")), 83 | } // match 84 | } 85 | } 86 | 87 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 88 | pub struct DiscordConfig { 89 | pub botId: u64, 90 | pub botToken: String, 91 | } 92 | 93 | #[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] 94 | pub struct TelegramConfig { 95 | pub apiId: i32, 96 | pub apiHash: String, 97 | pub botToken: String, 98 | } 99 | 100 | #[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] 101 | pub struct BridgeConfig { 102 | pub discord: DiscordBridgeConfig, 103 | pub qqGroup: u64, 104 | pub tgGroup: i64, 105 | pub enable: bool, 106 | } 107 | 108 | #[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] 109 | pub struct DiscordBridgeConfig { 110 | pub id: u64, 111 | pub token: String, 112 | pub channelId: u64, 113 | } 114 | 115 | #[derive(Clone, Deserialize, Serialize, Debug, Eq, PartialEq)] 116 | pub struct BridgeUser { 117 | id: String, 118 | qq: u64, 119 | discordId: u64, 120 | } 121 | 122 | #[cfg(test)] 123 | #[allow(non_snake_case)] 124 | mod test { 125 | use super::*; 126 | 127 | #[test] 128 | fn getConfig() { 129 | let config = Config::new(); 130 | println!("config:"); 131 | println!("{:?}", config); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | //! 配置日志追踪 2 | 3 | use time::format_description::FormatItem; 4 | use time::UtcOffset; 5 | use tracing::{debug, error, info, trace, warn, Level}; 6 | use tracing_appender::non_blocking::WorkerGuard; 7 | use tracing_appender::rolling; 8 | use tracing_subscriber::fmt::time::OffsetTime; 9 | use tracing_subscriber::fmt::writer::MakeWriterExt; 10 | use tracing_subscriber::layer::SubscriberExt; 11 | use tracing_subscriber::util::SubscriberInitExt; 12 | use tracing_subscriber::{fmt, EnvFilter}; 13 | 14 | const LOG_DIR: &str = "./logs"; 15 | const F_PFX_NOR: &str = "bridge_log.log"; 16 | const F_PFX_ERR: &str = "bridge_err.log"; 17 | const ENV_NAME: &str = "MSG_BRIDGE"; 18 | const ENV_DEF_VAL: &str = "info,message_bridge_rs=debug"; 19 | 20 | /// 配置时区和时间格式 21 | fn get_timer(t_fmt: Vec) -> OffsetTime> { 22 | match UtcOffset::from_hms(8, 0, 0) { 23 | Ok(ofs) => OffsetTime::new(ofs, t_fmt), 24 | Err(e) => { 25 | eprintln!("配置时区异常!{:#?}", e); 26 | panic!("配置时区异常!"); 27 | } 28 | } 29 | } 30 | 31 | /// 获取日志环境变量 32 | /// - 预期值不存在时使用默认值 33 | fn get_env_filter() -> EnvFilter { 34 | match EnvFilter::try_from_env(ENV_NAME) { 35 | Ok(e) => e, 36 | _ => { 37 | println!("使用环境变量默认值:{ENV_NAME}={ENV_DEF_VAL}"); 38 | EnvFilter::builder().parse_lossy(ENV_DEF_VAL) 39 | } 40 | } 41 | } 42 | 43 | /// 初始化日志 44 | pub fn init_logger() -> (WorkerGuard, WorkerGuard) { 45 | println!("init logger..."); 46 | let t_fmt1 = time::format_description::parse( 47 | "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]", 48 | ) 49 | .unwrap(); 50 | let t_fmt2 = 51 | time::format_description::parse("[hour]:[minute]:[second].[subsecond digits:3]").unwrap(); 52 | 53 | // 日志文件。日志文件不上色(with_ansi(false)) 54 | // normal.log: INFO < 等级 < WARN 55 | let (ff, nl_guard) = tracing_appender::non_blocking(rolling::never(LOG_DIR, F_PFX_NOR)); 56 | let f_normal = fmt::layer() 57 | .with_ansi(false) 58 | .with_writer(ff.with_min_level(Level::WARN).with_max_level(Level::INFO)); 59 | let (ff, el_guard) = tracing_appender::non_blocking(rolling::never(LOG_DIR, F_PFX_ERR)); 60 | // error.log 61 | let f_error = fmt::layer() 62 | .with_ansi(false) 63 | .with_writer(ff.with_max_level(Level::ERROR)); 64 | let (f_normal, f_error) = { 65 | let timer = get_timer(t_fmt1); 66 | ( 67 | f_normal.with_timer(timer.clone()), 68 | f_error.with_timer(timer), 69 | ) 70 | }; 71 | 72 | // 标准输出 73 | let timer = get_timer(t_fmt2); 74 | let std_out = fmt::layer() 75 | .compact() 76 | .with_timer(timer) 77 | // 终端输出上色 78 | .with_ansi(true) 79 | .with_writer(std::io::stdout); 80 | // 注册 81 | tracing_subscriber::registry() 82 | // 从环境变量读取日志等级 83 | .with(get_env_filter()) 84 | .with(std_out) 85 | .with(f_normal) 86 | .with(f_error) 87 | .init(); 88 | // color_eyre 处理 panic 89 | if let Err(e) = color_eyre::install() { 90 | error!("color_eyre 配置异常!{:#?}", e); 91 | } 92 | 93 | trace!("logger ready."); 94 | debug!("logger ready."); 95 | info!("logger ready."); 96 | warn!("logger ready."); 97 | error!("logger ready."); 98 | 99 | (nl_guard, el_guard) 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | use tracing_subscriber::fmt; 106 | #[test] 107 | fn ts_env() { 108 | let std_out = fmt::layer() 109 | .compact() 110 | .with_ansi(true) 111 | .with_writer(std::io::stdout); 112 | tracing_subscriber::registry() 113 | .with(get_env_filter()) 114 | .with(std_out) 115 | .init(); 116 | if let Err(e) = color_eyre::install() { 117 | error!("color_eyre 配置异常!{:#?}", e); 118 | } 119 | trace!("logger ready."); 120 | debug!("logger ready."); 121 | info!("logger ready."); 122 | warn!("logger ready."); 123 | error!("logger ready."); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(fs_try_exists)] 2 | 3 | use config::*; 4 | use std::sync::Arc; 5 | use tokio::sync::Mutex; 6 | use tracing::{info, Level}; 7 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 8 | 9 | mod bridge; 10 | mod bridge_cmd; 11 | mod bridge_dc; 12 | mod bridge_log; 13 | mod bridge_qq; 14 | mod bridge_tg; 15 | mod config; 16 | mod logger; 17 | mod utils; 18 | 19 | pub type HttpResult = std::result::Result>; 20 | 21 | #[tokio::main] 22 | async fn main() -> Result<(), Box> { 23 | let _log_guard = logger::init_logger(); 24 | let config = Arc::new(Config::new()); 25 | info!("config: {:#?}", config); 26 | info!("config loaded"); 27 | let bridge_service = bridge::BridgeService::new(); 28 | let bridge_service = Arc::new(Mutex::new(bridge_service)); 29 | let bridge_dc_client = bridge::BridgeService::create_client("bridge_dc_client", bridge_service.clone()).await; 30 | let bridge_qq_client = bridge::BridgeService::create_client("bridge_qq_client", bridge_service.clone()).await; 31 | let _bridge_tg_client = bridge::BridgeService::create_client("bridge_tg_client", bridge_service.clone()).await; 32 | let bridge_cmd_client = bridge::BridgeService::create_client("bridge_cmd_client", bridge_service.clone()).await; 33 | 34 | tokio::select! { 35 | _ = bridge_dc::start(config.clone(), bridge_dc_client) => {}, 36 | _ = bridge_qq::start(config.clone(), bridge_qq_client) => {}, 37 | // _ = bridge_tg::start(config.clone(), bridge_tg_client) => {}, 38 | _ = bridge_cmd::start(config.clone(), bridge_cmd_client) => {}, 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | fn _init_tracing_subscriber() { 45 | tracing_subscriber::registry() 46 | .with(tracing_subscriber::fmt::layer().with_target(true).without_time()) 47 | .with( 48 | tracing_subscriber::filter::Targets::new() 49 | .with_target("ricq", Level::DEBUG) 50 | .with_target("proc_qq", Level::DEBUG) 51 | // 这里改成自己的crate名称 52 | .with_target("message_bridge_rs", Level::DEBUG), 53 | ) 54 | .init(); 55 | } 56 | 57 | /// # 2元表达式宏 - Result 58 | /// ## Example 59 | /// ``` 60 | /// assert_eq!(elr!(Ok::<_, ()>(1) ;; 2), 1); 61 | /// assert_eq!(elr!(Err(0) ;; 42), 42); 62 | /// ``` 63 | #[macro_export] 64 | macro_rules! elr { 65 | ($opt:expr ;; $ret:expr) => { 66 | if let Ok(v) = $opt { 67 | v 68 | } else { 69 | $ret 70 | } 71 | }; 72 | } 73 | /// # 2元表达式宏 - Option 74 | /// ## Example 75 | /// ``` 76 | /// assert_eq!(elo!(Some(1) ;; 2), 1); 77 | /// assert_eq!(elo!(None ;; 42), 42); 78 | /// ``` 79 | #[macro_export] 80 | macro_rules! elo { 81 | ($opt:expr ;; $ret:expr) => { 82 | if let Some(v) = $opt { 83 | v 84 | } else { 85 | $ret 86 | } 87 | }; 88 | } 89 | 90 | #[cfg(test)] 91 | #[allow(unused)] 92 | mod test { 93 | use super::*; 94 | 95 | macro_rules! aw { 96 | ($e:expr) => { 97 | tokio_test::block_on($e) 98 | }; 99 | } 100 | 101 | #[test] 102 | fn ts_el() { 103 | assert_eq!(elr!(Ok::<_, ()>(1) ;; 2), 1); 104 | assert_eq!(elr!(Err(0) ;; 42), 42); 105 | assert_eq!(elo!(Some(1) ;; 2), 1); 106 | assert_eq!(elo!(None ;; 42), 42); 107 | } 108 | 109 | #[test] 110 | fn test() -> Result<(), Box> { 111 | Ok(()) 112 | } 113 | 114 | #[test] 115 | fn get_config() { 116 | let config = Config::new(); 117 | println!("config:"); 118 | println!("{:?}", config); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test_dc.rs: -------------------------------------------------------------------------------- 1 | use serenity::async_trait; 2 | use serenity::http::Http; 3 | use serenity::model::channel::AttachmentType; 4 | use serenity::model::channel::Message; 5 | use serenity::model::gateway::{GatewayIntents, Presence, Ready}; 6 | use serenity::model::prelude::Guild; 7 | use serenity::model::webhook::Webhook; 8 | use serenity::prelude::*; 9 | use serenity::utils::MessageBuilder; 10 | 11 | use std::env; 12 | 13 | use crate::config::Config; 14 | 15 | #[test] 16 | fn use_webhook_send_dc_message() { 17 | tokio::runtime::Builder::new_current_thread() 18 | .enable_all() 19 | .build() 20 | .unwrap() 21 | .block_on(async { 22 | let config = Config::new(); 23 | let bridgeConfig = config.bridges.get(0).unwrap(); 24 | let http = Http::new(""); 25 | let webhook = Webhook::from_id_with_token( 26 | &http, 27 | bridgeConfig.discord.id, 28 | &bridgeConfig.discord.token, 29 | ) 30 | .await 31 | .unwrap(); 32 | // webhook.kind 33 | webhook 34 | .execute(&http, false, |w| { 35 | w.username("Webhook test").components(|c| { 36 | c.create_action_row(|row| { 37 | row.create_button(|b| b.custom_id("btn").label("测试")) 38 | }) 39 | }) 40 | }) 41 | .await 42 | .expect("Could not execute webhook."); 43 | }) 44 | } 45 | 46 | #[test] 47 | fn test_tokio_select() { 48 | tokio::runtime::Builder::new_current_thread() 49 | .enable_all() 50 | .build() 51 | .unwrap() 52 | .block_on(async { 53 | tokio::select! { 54 | _ = async { 55 | loop { 56 | tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; 57 | println!("第一个异步运行中..."); 58 | } 59 | } => { 60 | println!("第一个异步结束"); 61 | }, 62 | val = async { 63 | loop { 64 | tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; 65 | println!("第一个异步运行中..."); 66 | } 67 | "hello" 68 | } => { 69 | println!("第二个异步结束"); 70 | } 71 | } 72 | println!("结束"); 73 | }) 74 | } 75 | 76 | /** 77 | * 获取伺服所有用户 78 | */ 79 | #[test] 80 | fn use_webhook_get_guild_user() { 81 | tokio::runtime::Builder::new_current_thread() 82 | .enable_all() 83 | .build() 84 | .unwrap() 85 | .block_on(async { 86 | let config = Config::new(); 87 | 88 | let token = &config.discord_config.botToken; 89 | 90 | let http = Http::new(&token); 91 | let member = http 92 | .get_member(724829522230378536, 724827488588660837) 93 | .await 94 | .unwrap(); 95 | println!("member: {:?}", member); 96 | }) 97 | } 98 | 99 | /** 100 | * 获取伺服所有用户 101 | */ 102 | #[test] 103 | fn use_webhook_get_guild_all_user() { 104 | let config = Config::new(); 105 | 106 | let token = &config.discord_config.botToken; 107 | 108 | tokio::runtime::Builder::new_current_thread() 109 | .enable_all() 110 | .build() 111 | .unwrap() 112 | .block_on(async { 113 | let http = Http::new(&token); 114 | let member = http 115 | .get_guild_members(724829522230378536, None, None) 116 | .await 117 | .unwrap(); 118 | println!("members: {:?}", member); 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /src/test_mirai.rs: -------------------------------------------------------------------------------- 1 | use crate::Config; 2 | use mirai_rs::api::MessageEvent; 3 | use mirai_rs::message::{MessageChain, MessageContent}; 4 | use mirai_rs::EventHandler; 5 | use mirai_rs::Mirai; 6 | 7 | use std::sync::Arc; 8 | 9 | pub struct MiraiBridgeHandler; 10 | #[mirai_rs::async_trait] 11 | impl EventHandler for MiraiBridgeHandler { 12 | async fn message(&self, ctx: &Mirai, msg: MessageEvent) {} 13 | } 14 | 15 | #[cfg(test)] 16 | #[allow(non_snake_case)] 17 | fn test() {} 18 | 19 | #[test] 20 | fn test_mirai_send_group_message() { 21 | tokio::runtime::Builder::new_current_thread() 22 | .enable_all() 23 | .build() 24 | .unwrap() 25 | .block_on(async { 26 | let config = Arc::new(Config::new()); 27 | let mut mirai = Mirai::builder( 28 | &config.miraiConfig.host, 29 | config.miraiConfig.port, 30 | &config.miraiConfig.verifyKey, 31 | ) 32 | .bind_qq(3245538509) 33 | .event_handler(MiraiBridgeHandler) 34 | .await; 35 | let http = mirai.get_http().await; 36 | let mut message_chian: MessageChain = vec![]; 37 | message_chian.push(MessageContent::Plain { 38 | text: "测试发送消息".to_string(), 39 | }); 40 | let result = http 41 | .send_group_message(message_chian, 518986671) 42 | .await 43 | .unwrap(); 44 | println!("请求成功"); 45 | println!("{:?}", result); 46 | }) 47 | } 48 | 49 | #[test] 50 | fn test_mirai_get_group_user() { 51 | tokio::runtime::Builder::new_current_thread() 52 | .enable_all() 53 | .build() 54 | .unwrap() 55 | .block_on(async { 56 | let config = Arc::new(Config::new()); 57 | let mut mirai = Mirai::builder( 58 | &config.miraiConfig.host, 59 | config.miraiConfig.port, 60 | &config.miraiConfig.verifyKey, 61 | ) 62 | .bind_qq(3245538509) 63 | .event_handler(MiraiBridgeHandler) 64 | .await; 65 | let http = mirai.get_http().await; 66 | let result = http.get_member_info(518986671, 243249439).await.unwrap(); 67 | println!("请求成功"); 68 | println!("{:?}", result); 69 | }) 70 | } 71 | 72 | #[test] 73 | fn test_mirai_get_group_all_user() { 74 | tokio::runtime::Builder::new_current_thread() 75 | .enable_all() 76 | .build() 77 | .unwrap() 78 | .block_on(async { 79 | let config = Arc::new(Config::new()); 80 | let mut mirai = Mirai::builder( 81 | &config.miraiConfig.host, 82 | config.miraiConfig.port, 83 | &config.miraiConfig.verifyKey, 84 | ) 85 | .bind_qq(3245538509) 86 | .event_handler(MiraiBridgeHandler) 87 | .await; 88 | let http = mirai.get_http().await; 89 | let result = http.member_list(518986671).await.unwrap(); 90 | println!("请求成功"); 91 | println!("{:?}", result); 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /src/test_regex.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use std::io::Cursor; 3 | use std::{fs::File, io::Write}; 4 | 5 | #[test] 6 | fn test_mirai_send_group_message() { 7 | let url = "https://cdn.discordapp.com/avatars/724827488588660837/71919445a77c9076e3915da81028a305.webp?size=1024"; 8 | url.replace(".webp?size=1024", ".png?size=30"); 9 | } 10 | /** 11 | * 测试正则裁剪和替换文本内容 12 | */ 13 | #[test] 14 | fn test() { 15 | let mut chain: Vec = vec![]; 16 | let splitTo = "#|x-x|#".to_string(); 17 | let reg_at_user = regex::Regex::new(r"@\[DC\] ([^\n^#^@]+)?#(\d\d\d\d)").unwrap(); 18 | let mut text = r#"test qq 1 @[DC] 6uopdong#4700你看看@[DC] rabbitkiller#7372"#.to_string(); 19 | // let caps = reg_at_user.captures(text); 20 | while let Some(caps) = reg_at_user.captures(text.as_str()) { 21 | println!("{:?}", caps); 22 | let from = caps.get(0).unwrap().as_str(); 23 | let name = caps.get(1).unwrap().as_str(); 24 | let disc = caps.get(2).unwrap().as_str(); 25 | 26 | let result = text.replace(from, &splitTo); 27 | let splits: Vec<&str> = result.split(&splitTo).collect(); 28 | let prefix = splits.get(0).unwrap(); 29 | chain.push(prefix.to_string()); 30 | if let Some(fix) = splits.get(1) { 31 | text = fix.to_string(); 32 | } 33 | } 34 | chain.push(text.to_string()); 35 | println!("{:?}", chain); 36 | } 37 | /** 38 | * 测试正则裁剪和替换文本内容 qq 39 | */ 40 | #[test] 41 | fn test_qq() { 42 | let mut chain: Vec = vec![]; 43 | let splitTo = "#|x-x|#".to_string(); 44 | let reg_at_user = regex::Regex::new(r"@\[QQ\] ([^\n^@]+)\(([0-9]+)\)").unwrap(); 45 | let mut text = r#"test qq 1 @[QQ] sanda(243249439)你看看@[QQ] sanda(243249439)"#.to_string(); 46 | // let caps = reg_at_user.captures(text); 47 | while let Some(caps) = reg_at_user.captures(text.as_str()) { 48 | println!("{:?}", caps); 49 | let from = caps.get(0).unwrap().as_str(); 50 | let name = caps.get(1).unwrap().as_str(); 51 | let disc = caps.get(2).unwrap().as_str(); 52 | 53 | let result = text.replace(from, &splitTo); 54 | let splits: Vec<&str> = result.split(&splitTo).collect(); 55 | let prefix = splits.get(0).unwrap(); 56 | chain.push(prefix.to_string()); 57 | if let Some(fix) = splits.get(1) { 58 | text = fix.to_string(); 59 | } 60 | } 61 | chain.push(text.to_string()); 62 | println!("{:?}", chain); 63 | } 64 | 65 | /** 66 | * dc 67 | */ 68 | #[test] 69 | fn test2() { 70 | let text = r#"test qq 1 @[DC] 6uopdong#4700你看看@[DC] rabbitkiller#7372"#.to_string(); 71 | let splits: Vec<&str> = text.split(" ").collect(); 72 | let mut reply_content: Vec = vec![]; 73 | for sp in splits { 74 | reply_content.push(format!("> {}\n", sp)); 75 | } 76 | let mut content = vec![]; 77 | content.push("测试看看".to_string()); 78 | // result.push(value) 79 | reply_content.append(&mut content); 80 | println!("{:?}", reply_content); 81 | } 82 | -------------------------------------------------------------------------------- /src/test_reqwest.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use std::io::Cursor; 3 | use std::{fs::File, io::Write}; 4 | 5 | #[test] 6 | fn test_mirai_send_group_message() { 7 | tokio::runtime::Builder::new_current_thread() 8 | .enable_all() 9 | .build() 10 | .unwrap() 11 | .block_on(async { 12 | let mut f = File::create("71919445a77c9076e3915da81028a305.webp").unwrap(); 13 | let client = reqwest::Client::new(); 14 | let mut stream = client.get("https://cdn.discordapp.com/avatars/724827488588660837/71919445a77c9076e3915da81028a305.webp?size=1024") 15 | .send().await.unwrap(); 16 | let mut a = Cursor::new(stream.bytes().await.unwrap()); 17 | std::io::copy(&mut a, &mut f); 18 | }) 19 | } 20 | 21 | #[test] 22 | fn test_reqwest_download_menitype() { 23 | tokio::runtime::Builder::new_current_thread() 24 | .enable_all() 25 | .build() 26 | .unwrap() 27 | .block_on(async { 28 | // let mut f = File::create("71919445a77c9076e3915da81028a305.webp").unwrap(); 29 | let client = reqwest::Client::new(); 30 | let url = "http://gchat.qpic.cn/gchatpic_new/1261972160/518986671-3123968978-4B7951A1D35B974B288EAC20C09033B4/0?term=2"; 31 | let stream = client.get(url) 32 | .send().await.unwrap(); 33 | let content_type = stream.headers().get(reqwest::header::CONTENT_TYPE); 34 | println!("{:?}", content_type); 35 | if let Some(value) = content_type { 36 | let mine = value.to_str().unwrap().parse::().unwrap(); 37 | let ext = match mime_guess::get_mime_extensions(&mine) { 38 | Some(exts) => { 39 | println!("exts: {:?}", exts); 40 | exts.get(0).unwrap().to_string() 41 | }, 42 | None => { 43 | mine.subtype().to_string() 44 | } 45 | }; 46 | println!(".ext {:?}", ext); 47 | let file_name = format!("{:?}.{}", md5::compute(url.as_bytes()), ext); 48 | let mut f = File::create(file_name).unwrap(); 49 | let mut a = Cursor::new(stream.bytes().await.unwrap()); 50 | std::io::copy(&mut a, &mut f).unwrap(); 51 | } 52 | }) 53 | } 54 | 55 | #[test] 56 | fn test_path() { 57 | use std::path::{self, Path}; 58 | let name = "23403b7883ae191a770a022e5d30b221"; 59 | let ext = ".jpe"; 60 | println!("{}{}", name, ext); 61 | let a = Path::new("cache").join("config.json"); 62 | // let a = path::absolute(a).unwrap(); 63 | println!("{:?}", a); 64 | } 65 | 66 | #[test] 67 | fn test_path2() { 68 | use std::path::Path; 69 | let path = Path::new("cache").join("xxx.jpe"); 70 | println!("1: {:?}", path); 71 | let path = path.to_str().unwrap().to_string(); 72 | println!("2: {:?}", path); 73 | let path = Path::new(&path); 74 | println!("3: {:?}", path); 75 | 76 | let path = "cache\\831b2596d4466add31064ea593811ccc.jpe"; 77 | let path = Path::new(path); 78 | println!("{:?}", path); 79 | let path = &"cache\\831b2596d4466add31064ea593811ccc.jpe".to_string(); 80 | let path = Path::new(path); 81 | println!("{:?}", path); 82 | } 83 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use std::io::Cursor; 4 | use std::path; 5 | use tokio::fs::File; 6 | 7 | pub async fn download_and_cache(url: &str) -> Result { 8 | init().await; 9 | let client = reqwest::Client::new(); 10 | let stream = client.get(url).send().await?; 11 | let url_md5 = format!("{:x}", md5::compute(url.as_bytes())); 12 | 13 | let content_type = stream.headers().get(reqwest::header::CONTENT_TYPE); 14 | 15 | let ext = match content_type { 16 | Some(value) => get_mine_type_ext(value.to_str().unwrap()), 17 | None => String::new(), 18 | }; 19 | let file_name = format!("{}{}", url_md5, ext); 20 | let file_name = path::Path::new("cache").join(file_name); 21 | let mut f = File::create(file_name.clone()).await.unwrap(); 22 | let mut a = Cursor::new(stream.bytes().await.unwrap()); 23 | tokio::io::copy(&mut a, &mut f).await.unwrap(); 24 | 25 | Ok(file_name.to_str().unwrap().to_string()) 26 | } 27 | 28 | pub fn get_mine_type_ext(mime_type: &str) -> String { 29 | if "image/jpeg".eq(mime_type) { 30 | return ".jpg".to_string(); 31 | } 32 | let mine = mime_type.parse::().unwrap(); 33 | let ext = match mime_guess::get_mime_extensions(&mine) { 34 | Some(exts) => exts.first().unwrap().to_string(), 35 | None => mine.subtype().to_string(), 36 | }; 37 | format!(".{}", ext) 38 | } 39 | 40 | pub async fn init() { 41 | if !std::path::Path::new("cache").exists() { 42 | if let Err(err) = tokio::fs::create_dir("cache").await { 43 | println!("初始化cache目录失败"); 44 | println!("{:?}", err); 45 | } 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | #[serde(tag = "type")] 51 | pub enum MarkdownAst { 52 | // Common: 文本 53 | Plain { 54 | text: String, 55 | }, 56 | // Common: @成员 57 | At { 58 | username: String, 59 | }, 60 | // DC: @成员 61 | DiscordAtUser { 62 | id: String, 63 | }, 64 | // DC: @所有人 65 | DiscordAtEveryone {}, 66 | // DC: @频道所有人 67 | DiscordAtHere {}, 68 | // DC: emoji图片 69 | DiscordEmoji { 70 | id: String, 71 | name: String, 72 | animated: bool, 73 | }, 74 | } 75 | 76 | /** 77 | * 将dc和qq消息进行解析 78 | */ 79 | pub async fn parser_message(content: &str) -> Vec { 80 | let client = reqwest::Client::new(); 81 | let mut result: Vec = client 82 | .post("http://localhost:3000/parse-discord-markdown") 83 | .body(content.to_string()) 84 | .timeout(std::time::Duration::from_secs(2)) 85 | .send() 86 | .await 87 | .expect("请求解析discord消息服务失败") 88 | .json() 89 | .await 90 | .expect("解析discord消息回传解析json失败"); 91 | 92 | if let Some(ast) = result.last() { 93 | if let MarkdownAst::Plain { text } = ast { 94 | if text.eq("\n") { 95 | result.remove(result.len() - 1); 96 | } 97 | } 98 | } 99 | if let Some(ast) = result.last() { 100 | if let MarkdownAst::Plain { text } = ast { 101 | if text.eq("\n") { 102 | result.remove(result.len() - 1); 103 | } 104 | } 105 | } 106 | 107 | result 108 | } 109 | 110 | #[test] 111 | fn test_send_post_parse_discord_message() { 112 | let message = r#"@[DC] 6uopdong#4700 113 | !绑定 qq 1261972160"#; 114 | tokio::runtime::Builder::new_current_thread() 115 | .enable_all() 116 | .build() 117 | .unwrap() 118 | .block_on(async { 119 | println!("发送"); 120 | let client = reqwest::Client::new(); 121 | let resp: Vec = client 122 | .post("http://localhost:3000/parse-discord-markdown") 123 | .body(message) 124 | .send() 125 | .await 126 | .unwrap() 127 | .json() 128 | .await 129 | .unwrap(); 130 | 131 | println!("{:?}", resp); 132 | }) 133 | } 134 | 135 | #[test] 136 | fn test2() { 137 | println!("{:?}", "zhangsan"); 138 | println!("{}", "zhangsan"); 139 | } 140 | #[test] 141 | fn test3() { 142 | let r = "@rabbitBot2".strip_prefix("@").unwrap(); 143 | println!("{:?}", r); 144 | } 145 | 146 | #[test] 147 | fn test4() { 148 | let mine = "image/jpeg".parse::().unwrap(); 149 | println!("{:?}", mine); 150 | match mime_guess::get_mime_extensions(&mine) { 151 | Some(exts) => { 152 | println!("Some: {:?}", exts); 153 | println!("Some: {}", exts.first().unwrap().to_string()); 154 | } 155 | None => { 156 | println!("None: {}", mine.subtype().to_string()); 157 | } 158 | }; 159 | } 160 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "noEmitOnError": true, 15 | "noImplicitAny": false 16 | }, 17 | "exclude": [ 18 | "node_modules" 19 | ], 20 | // 此配置生效范围 21 | "include": [ 22 | "server.ts" 23 | ] 24 | } --------------------------------------------------------------------------------