├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── plugin_support.md └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── epl-test ├── sdk.e ├── test.e └── test.json ├── gen-header.bat ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── native ├── cq_apis.h ├── cq_events.h ├── mirai_apis.h ├── native.cpp ├── native.h ├── native.rc ├── native.sln ├── native.vcxproj ├── native.vcxproj.filters ├── native.vcxproj.user ├── org_itxtech_mirainative_Bridge.h └── resource.h ├── settings.gradle └── src └── main ├── kotlin └── org │ └── itxtech │ └── mirainative │ ├── Bridge.kt │ ├── MiraiNative.kt │ ├── bridge │ ├── ForwardMessageDecoder.kt │ ├── MiraiBridge.kt │ ├── MiraiImpl.kt │ └── NativeBridge.kt │ ├── manager │ ├── CacheManager.kt │ ├── EventManager.kt │ ├── LibraryManager.kt │ └── PluginManager.kt │ ├── message │ ├── ChainCodeConverter.kt │ └── RichMessageHelper.kt │ ├── plugin │ ├── NativePlugin.kt │ └── PluginInfo.kt │ ├── ui │ ├── FloatingWindow.kt │ └── Tray.kt │ └── util │ ├── Configuration.kt │ ├── Music.kt │ └── NpmHelper.kt └── resources ├── CQP.dll ├── META-INF └── services │ └── net.mamoe.mirai.console.plugin.jvm.JvmPlugin └── icon.jpg /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告Bug 3 | about: 创建一个Bug报告 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 警告:请认真填写以下栏目,如果不符合要求,将直接被关闭。编辑完请删除此行。 11 | 12 | **描述这个Bug** 13 | 清晰准确地表述这个Bug。 14 | 15 | **复现步骤** 16 | 17 | **期望的行为** 18 | 清晰准确地描述您期望的行为。 19 | 20 | **截图和日志** 21 | 您必须提交相应的截图和 mirai console 日志 22 | 23 | **运行环境** 24 | - 操作系统:[例子:Windows 10 2004] 25 | - `mirai core` 版本:[例子:1.2.1] 26 | - `mirai console` 版本:[例子:1.0] 27 | - `mirai native` 版本:[例子:1.9.0] 28 | - `Java` 运行时版本:[例子:11.0.7 x86] 29 | 30 | **更多信息** 31 | 如果有其他相关的信息可以填写在此处。 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 特性请求 3 | about: 创建一个特性请求 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 警告:请认真填写以下栏目,如果不符合要求,将直接被关闭。编辑完请删除此行。 11 | 12 | **特性描述** 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/plugin_support.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 插件支持 3 | about: 创建一个插件支持请求 4 | title: '' 5 | labels: 'plugin support' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 警告:请认真填写以下栏目,如果不符合要求,将直接被关闭。编辑完请删除此行。 11 | 12 | **插件信息** 13 | - 名称: 14 | - 版本: 15 | 16 | **请求描述** 17 | 如果酷Q插件运行于`mirai native`发生错误,请确定该错误发生由`mirai native`造成。 18 | 判断依据可以查看`Java`生成的错误报告,最后一行跟踪显示包含`[CQP.dll +0xxxxx]`或`j org.itxtech.mirainative.xxxxxxx`即为`mirai native`发生错误。 19 | 20 | 如果酷Q插件未能按预期运行于`mirai native`,请在此准确描述情况。 21 | 22 | **截图和日志** 23 | 您必须提交相应的截图,`mirai console` 日志或崩溃日志 24 | 25 | **运行环境** 26 | - 操作系统:[例子:Windows 10 2004] 27 | - `mirai core` 版本:[例子:1.2.1] 28 | - `mirai console` 版本:[例子:1.0] 29 | - `mirai native` 版本:[例子:1.9.0] 30 | - `Java` 运行时版本:[例子:11.0.7 x86] 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Native Build 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | check: 6 | runs-on: windows-latest 7 | steps: 8 | 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup JDK 11 13 | uses: actions/setup-java@v3 14 | with: 15 | distribution: 'adopt' 16 | java-version: '11' 17 | 18 | - name: Add msbuild to PATH 19 | uses: microsoft/setup-msbuild@v1.1 20 | with: 21 | msbuild-architecture: x64 22 | 23 | - name: Native Build 24 | working-directory: native 25 | run: > 26 | msbuild native.sln 27 | -t:rebuild 28 | -p:Configuration=Release 29 | -p:Platform=x86 30 | -p:AssemblyName=CQP 31 | 32 | - name: Native Upload 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: native-build 36 | path: native/Release 37 | 38 | - name: CQP Move 39 | run: copy src\main\resources\CQP.dll native\Release\CQP.dll 40 | 41 | - name: chmod -R 777 * 42 | run: chmod -R 777 * 43 | 44 | - name: Init gradle project 45 | run: ./gradlew assemble --scan 46 | 47 | - name: Mirai Plugin Build 48 | run: ./gradlew buildPlugin --scan 49 | 50 | - name: Mirai Plugin SHA1SUM 51 | shell: bash 52 | working-directory: build/mirai 53 | run: find . -name "*.jar" -type f -exec sh -c "sha1sum {} | cut -b-40 > {}.sha1" \; 54 | 55 | - name: Mirai Plugin Upload 56 | uses: actions/upload-artifact@v3 57 | with: 58 | name: mirai-plugin-build 59 | path: build/mirai 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | native/.vs 4 | native/Debug 5 | native/Release 6 | native/x64 7 | libs 8 | *.aps 9 | build 10 | out 11 | epl-test/*.dll 12 | epl-test/*.bak 13 | epl-test/*.ec 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mirai Native 2 | 3 | __强大的 `mirai` 原生插件加载器__ 4 | 5 | Mirai Native 支持所有`stdcall`方式导出方法的`DLL`与 [mirai](https://github.com/mamoe/mirai) 交互。 6 | 7 | 与**大部分**`酷Q`插件兼容,**不支持**`CPK`和解包的`DLL`,需获取`DLL`和`JSON`原文件,`JSON`文件**不支持**注释。 8 | 9 | ## `Mirai Native` 仅支持 `Windows 32位 Java` 10 | 11 | 1. 可前往 [Temurin](https://adoptium.net/temurin/releases/) 下载 12 | 2. 选择 `Windows` -> `x86` 下载 `Windows 32位 Java` 13 | 3. 支持 `Java 11` 之后的版本 14 | 15 | ## [欢迎参与建设`Mirai Native`插件中心](https://github.com/iTXTech/mirai-native/discussions/121) 16 | 17 | ## [Wiki - 开发者和用户必读](https://github.com/iTXTech/mirai-native/wiki) 18 | 19 | ## [下载 `Mirai Native`](https://github.com/iTXTech/mirai-native/releases) 20 | 21 | ## 使用 [Mirai Console Loader](https://github.com/iTXTech/mirai-console-loader) 安装`Mirai Native` 22 | 23 | * `MCL` 支持自动更新插件,支持设置插件更新频道等功能 24 | 25 | `.\mcl --update-package org.itxtech:mirai-native --channel stable --type plugin` 26 | 27 | ## `Mirai Native Tray` 28 | 29 | * 右键`流泪猫猫头`打开 `Mirai Native` 托盘菜单。 30 | * 左键`流泪猫猫头`显示悬浮窗。 31 | 32 | ## `mirai Native Plugin Manager` 33 | 34 | ``` 35 | > npm 36 | Mirai Native 插件管理器 37 | 38 | /disable <插件Id> 停用指定 Mirai Native 插件 39 | /enable <插件Id> 启用指定 Mirai Native 插件 40 | /info <插件Id> 查看指定 Mirai Native 插件的详细信息 41 | /list 列出所有 Mirai Native 插件 42 | /load 加载指定DLL文件 43 | /menu <插件Id> <方法名> 调用指定 Mirai Native 插件的菜单方法 44 | /reload <插件Id> 重新载入指定 Mirai Native 插件 45 | /unload <插件Id> 卸载指定 Mirai Native 插件 46 | ``` 47 | 48 | 49 | ## 开源许可证 50 | 51 | iTXTech Mirai Native 52 | Copyright (C) 2020-2022 iTX Technologies 53 | 54 | This program is free software: you can redistribute it and/or modify 55 | it under the terms of the GNU Affero General Public License as 56 | published by the Free Software Foundation, either version 3 of the 57 | License, or (at your option) any later version. 58 | 59 | This program is distributed in the hope that it will be useful, 60 | but WITHOUT ANY WARRANTY; without even the implied warranty of 61 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 62 | GNU Affero General Public License for more details. 63 | 64 | You should have received a copy of the GNU Affero General Public License 65 | along with this program. If not, see . 66 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "1.7.20" 3 | kotlin("plugin.serialization") version "1.7.20" 4 | 5 | id("me.him188.maven-central-publish") version "1.0.0-dev-3" 6 | id("net.mamoe.mirai-console") version "2.13.0-RC2" 7 | } 8 | 9 | group = "org.itxtech" 10 | version = "2.0.1" 11 | description = "强大的 mirai 原生插件加载器。" 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | compileOnly("org.jetbrains.kotlinx:atomicfu:0.18.4") 19 | implementation("io.ktor:ktor-client-okhttp:2.1.3") { 20 | exclude(group = "org.jetbrains.kotlin") 21 | exclude(group = "org.jetbrains.kotlinx") 22 | exclude(group = "org.slf4j") 23 | } 24 | implementation("com.squareup.okhttp3:okhttp:4.10.0") { 25 | exclude(group = "org.jetbrains.kotlin") 26 | exclude(group = "org.jetbrains.kotlinx") 27 | exclude(group = "org.slf4j") 28 | } 29 | } 30 | 31 | mavenCentralPublish { 32 | singleDevGithubProject("iTXTech", "mirai-native") 33 | licenseAGplV3() 34 | useCentralS01() 35 | } 36 | 37 | tasks.named("jar") { 38 | manifest { 39 | attributes["Name"] = "iTXTech MiraiNative" 40 | attributes["Revision"] = Runtime.getRuntime().exec("git rev-parse --short HEAD") 41 | .inputStream.bufferedReader().readText().trim() 42 | } 43 | } 44 | 45 | mirai { 46 | jvmTarget = JavaVersion.VERSION_11 47 | } 48 | -------------------------------------------------------------------------------- /epl-test/sdk.e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTXTech/mirai-native/980b5e64086ac734544528536274b696fe3afe0e/epl-test/sdk.e -------------------------------------------------------------------------------- /epl-test/test.e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTXTech/mirai-native/980b5e64086ac734544528536274b696fe3afe0e/epl-test/test.e -------------------------------------------------------------------------------- /epl-test/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "ret": 1, 3 | "apiver": 9, 4 | "name": "Mirai Native API Tester", 5 | "version": "1.0.0", 6 | "version_id": 1, 7 | "author": "PeratX@iTXTech.org", 8 | "description": "API测试工具", 9 | "event": [ 10 | { 11 | "id": 1, 12 | "type": 21, 13 | "name": "私聊消息处理", 14 | "function": "_eventPrivateMsg", 15 | "priority": 30000 16 | }, 17 | { 18 | "id": 2, 19 | "type": 2, 20 | "name": "群消息处理", 21 | "function": "_eventGroupMsg", 22 | "priority": 30000 23 | }, 24 | { 25 | "id": 3, 26 | "type": 4, 27 | "name": "讨论组消息处理", 28 | "function": "_eventDiscussMsg", 29 | "priority": 30000 30 | }, 31 | { 32 | "id": 4, 33 | "type": 11, 34 | "name": "群文件上传事件处理", 35 | "function": "_eventGroupUpload", 36 | "priority": 30000 37 | }, 38 | { 39 | "id": 5, 40 | "type": 101, 41 | "name": "群管理变动事件处理", 42 | "function": "_eventSystem_GroupAdmin", 43 | "priority": 30000 44 | }, 45 | { 46 | "id": 6, 47 | "type": 102, 48 | "name": "群成员减少事件处理", 49 | "function": "_eventSystem_GroupMemberDecrease", 50 | "priority": 30000 51 | }, 52 | { 53 | "id": 7, 54 | "type": 103, 55 | "name": "群成员增加事件处理", 56 | "function": "_eventSystem_GroupMemberIncrease", 57 | "priority": 30000 58 | }, 59 | { 60 | "id": 8, 61 | "type": 104, 62 | "name": "群禁言事件处理", 63 | "function": "_eventSystem_GroupBan", 64 | "priority": 30000 65 | }, 66 | { 67 | "id": 10, 68 | "type": 201, 69 | "name": "好友已添加事件处理", 70 | "function": "_eventFriend_Add", 71 | "priority": 30000 72 | }, 73 | { 74 | "id": 11, 75 | "type": 301, 76 | "name": "好友添加请求处理", 77 | "function": "_eventRequest_AddFriend", 78 | "priority": 30000 79 | }, 80 | { 81 | "id": 12, 82 | "type": 302, 83 | "name": "群添加请求处理", 84 | "function": "_eventRequest_AddGroup", 85 | "priority": 30000 86 | }, 87 | { 88 | "id": 1001, 89 | "type": 1001, 90 | "name": "酷Q启动事件", 91 | "priority": 30000, 92 | "function": "_eventStartup" 93 | }, 94 | { 95 | "id": 1002, 96 | "type": 1002, 97 | "name": "酷Q关闭事件", 98 | "priority": 30000, 99 | "function": "_eventExit" 100 | }, 101 | { 102 | "id": 1003, 103 | "type": 1003, 104 | "name": "应用已被启用", 105 | "priority": 30000, 106 | "function": "_eventEnable" 107 | }, 108 | { 109 | "id": 1004, 110 | "type": 1004, 111 | "name": "应用将被停用", 112 | "priority": 30000, 113 | "function": "_eventDisable" 114 | } 115 | ], 116 | "status": [ 117 | { 118 | "id": 1, 119 | "name": "随机数", 120 | "title": "RAND", 121 | "function": "_status", 122 | "period": "1000" 123 | } 124 | ], 125 | "menu": [ 126 | { 127 | "name": "loadWin", 128 | "function": "_menuA" 129 | } 130 | ], 131 | "auth": [] 132 | } 133 | -------------------------------------------------------------------------------- /gen-header.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd build\classes 3 | "C:\Program Files\Java\jdk1.8.0_241\bin\javah" -classpath ".\kotlin\main" org.itxtech.mirainative.Bridge 4 | copy /y org_itxtech_mirainative_Bridge.h ..\..\native 5 | cd ..\.. 6 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTXTech/mirai-native/980b5e64086ac734544528536274b696fe3afe0e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /native/cq_apis.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "native.h" 4 | 5 | CQAPI(int32_t, CQ_addLog, 16)(int32_t plugin_id, int32_t priority, const char* type, const char* content) 6 | { 7 | auto env = attach_java(); 8 | auto t = CharsToByteArray(env, type); 9 | auto c = CharsToByteArray(env, content); 10 | auto method = env->GetStaticMethodID(bclz, "addLog", "(II[B[B)V"); 11 | env->CallStaticVoidMethod(bclz, method, plugin_id, priority, t, c); 12 | env->DeleteLocalRef(t); 13 | env->DeleteLocalRef(c); 14 | detach_java(); 15 | return 0; 16 | } 17 | 18 | CQAPI(int32_t, CQ_canSendImage, 4)(int32_t) 19 | { 20 | return 1; 21 | } 22 | 23 | CQAPI(int32_t, CQ_canSendRecord, 4)(int32_t) 24 | { 25 | return 1; 26 | } 27 | 28 | int32_t sendMsg(int32_t plugin_id, int64_t acc, const char* msg, const char* m) 29 | { 30 | auto env = attach_java(); 31 | auto jstr = CharsToByteArray(env, msg); 32 | auto method = env->GetStaticMethodID(bclz, m, "(IJ[B)I"); 33 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, acc, jstr); 34 | env->DeleteLocalRef(jstr); 35 | detach_java(); 36 | return result; 37 | } 38 | 39 | CQAPI(int32_t, CQ_sendPrivateMsg, 16)(int32_t plugin_id, int64_t account, const char* msg) 40 | { 41 | return sendMsg(plugin_id, account, msg, "sendPrivateMessage"); 42 | } 43 | 44 | CQAPI(int32_t, CQ_sendGroupMsg, 16)(int32_t plugin_id, int64_t group, const char* msg) 45 | { 46 | return sendMsg(plugin_id, group, msg, "sendGroupMessage"); 47 | } 48 | 49 | CQAPI(int32_t, CQ_setFatal, 8)(int32_t plugin_id, const char* info) 50 | { 51 | CQ_addLog(plugin_id, 22, "", info); 52 | return 0; 53 | } 54 | 55 | CQAPI(const char*, CQ_getAppDirectory, 4)(int32_t plugin_id) 56 | { 57 | auto env = attach_java(); 58 | auto method = env->GetStaticMethodID(bclz, "getPluginDataDir", "(I)[B"); 59 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id)); 60 | auto r = ByteArrayToChars(env, result); 61 | env->DeleteLocalRef(result); 62 | detach_java(); 63 | return delay_mem_free(r); 64 | } 65 | 66 | CQAPI(int64_t, CQ_getLoginQQ, 4)(int32_t plugin_id) 67 | { 68 | auto env = attach_java(); 69 | auto method = env->GetStaticMethodID(bclz, "getLoginQQ", "(I)J"); 70 | auto result = env->CallStaticLongMethod(bclz, method, plugin_id); 71 | detach_java(); 72 | return result; 73 | } 74 | 75 | CQAPI(const char*, CQ_getLoginNick, 4)(int32_t plugin_id) 76 | { 77 | auto env = attach_java(); 78 | auto method = env->GetStaticMethodID(bclz, "getLoginNick", "(I)[B"); 79 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id)); 80 | auto r = ByteArrayToChars(env, result); 81 | env->DeleteLocalRef(result); 82 | detach_java(); 83 | return delay_mem_free(r); 84 | } 85 | 86 | CQAPI(int32_t, CQ_setGroupAnonymous, 16)(int32_t plugin_id, int64_t group, BOOL enable) 87 | { 88 | auto env = attach_java(); 89 | auto method = env->GetStaticMethodID(bclz, "setGroupAnonymous", "(IJZ)I"); 90 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, enable != FALSE); 91 | detach_java(); 92 | return result; 93 | } 94 | 95 | CQAPI(int32_t, CQ_setGroupBan, 28)(int32_t plugin_id, int64_t group, int64_t member, int64_t duration) 96 | { 97 | auto env = attach_java(); 98 | auto method = env->GetStaticMethodID(bclz, "setGroupBan", "(IJJJ)I"); 99 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, member, duration); 100 | detach_java(); 101 | return result; 102 | } 103 | 104 | CQAPI(int32_t, CQ_setGroupCard, 24)(int32_t plugin_id, int64_t group, int64_t member, const char* card) 105 | { 106 | auto env = attach_java(); 107 | auto method = env->GetStaticMethodID(bclz, "setGroupCard", "(IJJ[B)I"); 108 | auto jstr = CharsToByteArray(env, card); 109 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, member, jstr); 110 | env->DeleteLocalRef(jstr); 111 | detach_java(); 112 | return result; 113 | } 114 | 115 | CQAPI(int32_t, CQ_setGroupLeave, 16)(int32_t plugin_id, int64_t group, BOOL dismiss) 116 | { 117 | auto env = attach_java(); 118 | auto method = env->GetStaticMethodID(bclz, "setGroupLeave", "(IJZ)I"); 119 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, dismiss != FALSE); 120 | detach_java(); 121 | return result; 122 | } 123 | 124 | CQAPI(int32_t, CQ_setGroupSpecialTitle, 32)(int32_t plugin_id, int64_t group, int64_t member, 125 | const char* title, int64_t duration) 126 | { 127 | auto env = attach_java(); 128 | auto method = env->GetStaticMethodID(bclz, "setGroupSpecialTitle", "(IJJ[BJ)I"); 129 | auto jstr = CharsToByteArray(env, title); 130 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, member, jstr, duration); 131 | env->DeleteLocalRef(jstr); 132 | detach_java(); 133 | return result; 134 | } 135 | 136 | CQAPI(int32_t, CQ_setGroupWholeBan, 16)(int32_t plugin_id, int64_t group, BOOL enable) 137 | { 138 | auto env = attach_java(); 139 | auto method = env->GetStaticMethodID(bclz, "setGroupWholeBan", "(IJZ)I"); 140 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, enable != FALSE); 141 | detach_java(); 142 | return result; 143 | } 144 | 145 | CQAPI(int32_t, CQ_deleteMsg, 12)(int32_t plugin_id, int64_t msg_id) 146 | { 147 | auto env = attach_java(); 148 | auto method = env->GetStaticMethodID(bclz, "recallMsg", "(IJ)I"); 149 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, msg_id); 150 | detach_java(); 151 | return result; 152 | } 153 | 154 | CQAPI(const char*, CQ_getFriendList, 8)(int32_t plugin_id, BOOL reserved) 155 | { 156 | auto env = attach_java(); 157 | auto method = env->GetStaticMethodID(bclz, "getFriendList", "(IZ)[B"); 158 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, reserved != FALSE)); 159 | auto r = ByteArrayToChars(env, result); 160 | env->DeleteLocalRef(result); 161 | detach_java(); 162 | return delay_mem_free(r); 163 | } 164 | 165 | CQAPI(const char*, CQ_getGroupInfo, 16)(int32_t plugin_id, int64_t group, BOOL cache) 166 | { 167 | auto env = attach_java(); 168 | auto method = env->GetStaticMethodID(bclz, "getGroupInfo", "(IJZ)[B"); 169 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, group, cache != FALSE)); 170 | auto r = ByteArrayToChars(env, result); 171 | env->DeleteLocalRef(result); 172 | detach_java(); 173 | return delay_mem_free(r); 174 | } 175 | 176 | CQAPI(const char*, CQ_getGroupList, 4)(int32_t plugin_id) 177 | { 178 | auto env = attach_java(); 179 | auto method = env->GetStaticMethodID(bclz, "getGroupList", "(I)[B"); 180 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id)); 181 | auto r = ByteArrayToChars(env, result); 182 | env->DeleteLocalRef(result); 183 | detach_java(); 184 | return delay_mem_free(r); 185 | } 186 | 187 | CQAPI(const char*, CQ_getGroupMemberInfoV2, 24)(int32_t plugin_id, int64_t group, int64_t account, BOOL cache) 188 | { 189 | auto env = attach_java(); 190 | auto method = env->GetStaticMethodID(bclz, "getGroupMemberInfo", "(IJJZ)[B"); 191 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, group, account, cache != FALSE)); 192 | auto r = ByteArrayToChars(env, result); 193 | env->DeleteLocalRef(result); 194 | detach_java(); 195 | return delay_mem_free(r); 196 | } 197 | 198 | CQAPI(const char*, CQ_getGroupMemberList, 12)(int32_t plugin_id, int64_t group) 199 | { 200 | auto env = attach_java(); 201 | auto method = env->GetStaticMethodID(bclz, "getGroupMemberList", "(IJ)[B"); 202 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, group)); 203 | auto r = ByteArrayToChars(env, result); 204 | env->DeleteLocalRef(result); 205 | detach_java(); 206 | return delay_mem_free(r); 207 | } 208 | 209 | CQAPI(const char*, CQ_getCookiesV2, 8)(int32_t plugin_id, const char* domain) 210 | { 211 | auto env = attach_java(); 212 | auto method = env->GetStaticMethodID(bclz, "getCookies", "(I[B)[B"); 213 | auto jstr = CharsToByteArray(env, domain); 214 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, jstr)); 215 | auto r = ByteArrayToChars(env, result); 216 | env->DeleteLocalRef(result); 217 | env->DeleteLocalRef(jstr); 218 | detach_java(); 219 | return delay_mem_free(r); 220 | } 221 | 222 | CQAPI(const char*, CQ_getCsrfToken, 4)(int32_t plugin_id) 223 | { 224 | auto env = attach_java(); 225 | auto method = env->GetStaticMethodID(bclz, "getCsrfToken", "(I)[B"); 226 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id)); 227 | auto r = ByteArrayToChars(env, result); 228 | env->DeleteLocalRef(result); 229 | detach_java(); 230 | return delay_mem_free(r); 231 | } 232 | 233 | CQAPI(const char*, CQ_getImage, 8)(int32_t plugin_id, const char* image) 234 | { 235 | auto env = attach_java(); 236 | auto method = env->GetStaticMethodID(bclz, "getImage", "(I[B)[B"); 237 | auto jstr = CharsToByteArray(env, image); 238 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, jstr)); 239 | auto r = ByteArrayToChars(env, result); 240 | env->DeleteLocalRef(result); 241 | env->DeleteLocalRef(jstr); 242 | detach_java(); 243 | return delay_mem_free(r); 244 | } 245 | 246 | CQAPI(const char*, CQ_getRecordV2, 12)(int32_t plugin_id, const char* file, const char* format) 247 | { 248 | auto env = attach_java(); 249 | auto method = env->GetStaticMethodID(bclz, "getRecord", "(I[B[B)[B"); 250 | auto f = CharsToByteArray(env, file); 251 | auto fmt = CharsToByteArray(env, format); 252 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, f, fmt)); 253 | auto r = ByteArrayToChars(env, result); 254 | env->DeleteLocalRef(f); 255 | env->DeleteLocalRef(fmt); 256 | env->DeleteLocalRef(result); 257 | detach_java(); 258 | return delay_mem_free(r); 259 | } 260 | 261 | CQAPI(const char*, CQ_getStrangerInfo, 16)(int32_t plugin_id, int64_t account, BOOL cache) 262 | { 263 | auto env = attach_java(); 264 | auto method = env->GetStaticMethodID(bclz, "getStrangerInfo", "(IJZ)[B"); 265 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, account, cache != FALSE)); 266 | auto r = ByteArrayToChars(env, result); 267 | env->DeleteLocalRef(result); 268 | detach_java(); 269 | return delay_mem_free(r); 270 | } 271 | 272 | CQAPI(int32_t, CQ_sendDiscussMsg, 16)(int32_t plugin_id, int64_t group, const char* msg) 273 | { 274 | return sendMsg(plugin_id, group, msg, "sendDiscussMessage"); 275 | } 276 | 277 | CQAPI(int32_t, CQ_sendLikeV2, 16)(int32_t plugin_id, int64_t account, int32_t times) 278 | { 279 | auto env = attach_java(); 280 | auto method = env->GetStaticMethodID(bclz, "sendLike", "(IJI)I"); 281 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, account, times); 282 | detach_java(); 283 | return result; 284 | } 285 | 286 | CQAPI(int32_t, CQ_setDiscussLeave, 12)(int32_t plugin_id, int64_t group) 287 | { 288 | auto env = attach_java(); 289 | auto method = env->GetStaticMethodID(bclz, "setDiscussLeave", "(IJ)I"); 290 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group); 291 | detach_java(); 292 | return result; 293 | } 294 | 295 | CQAPI(int32_t, CQ_setFriendAddRequest, 16)(int32_t plugin_id, const char* id, int32_t type, const char* remark) 296 | { 297 | auto env = attach_java(); 298 | auto method = env->GetStaticMethodID(bclz, "setFriendAddRequest", "(I[BI[B)I"); 299 | auto i = CharsToByteArray(env, id); 300 | auto r = CharsToByteArray(env, remark); 301 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, i, type, r); 302 | env->DeleteLocalRef(i); 303 | env->DeleteLocalRef(r); 304 | detach_java(); 305 | return result; 306 | } 307 | 308 | CQAPI(int32_t, CQ_setGroupAddRequestV2, 20)(int32_t plugin_id, const char* id, int32_t req_type, int32_t fb_type, 309 | const char* reason) 310 | { 311 | auto env = attach_java(); 312 | auto method = env->GetStaticMethodID(bclz, "setGroupAddRequest", "(I[BII[B)I"); 313 | auto i = CharsToByteArray(env, id); 314 | auto r = CharsToByteArray(env, reason); 315 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, i, req_type, fb_type, r); 316 | env->DeleteLocalRef(i); 317 | env->DeleteLocalRef(r); 318 | detach_java(); 319 | return result; 320 | } 321 | 322 | CQAPI(int32_t, CQ_setGroupAdmin, 24)(int32_t plugin_id, int64_t group, int64_t account, BOOL admin) 323 | { 324 | auto env = attach_java(); 325 | auto method = env->GetStaticMethodID(bclz, "setGroupAdmin", "(IJJZ)I"); 326 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, account, admin != FALSE); 327 | detach_java(); 328 | return result; 329 | } 330 | 331 | CQAPI(int32_t, CQ_setGroupAnonymousBan, 24)(int32_t plugin_id, int64_t group, const char* id, int64_t duration) 332 | { 333 | auto env = attach_java(); 334 | auto method = env->GetStaticMethodID(bclz, "setGroupAnonymousBan", "(IJ[BJ)I"); 335 | auto i = CharsToByteArray(env, id); 336 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, i, duration); 337 | env->DeleteLocalRef(i); 338 | detach_java(); 339 | return result; 340 | } 341 | 342 | // Legacy 343 | 344 | CQAPI(const char*, CQ_getCookies, 4)(int32_t plugin_id) 345 | { 346 | return CQ_getCookiesV2(plugin_id, ""); 347 | } 348 | 349 | CQAPI(int32_t, CQ_setGroupAddRequest, 16)(int32_t plugin_id, const char* id, int32_t req_type, int32_t fb_type) 350 | { 351 | return CQ_setGroupAddRequestV2(plugin_id, id, req_type, fb_type, ""); 352 | } 353 | 354 | CQAPI(int32_t, CQ_sendLike, 12)(int32_t plugin_id, int64_t account) 355 | { 356 | return CQ_sendLikeV2(plugin_id, account, 1); 357 | } 358 | 359 | CQAPI(int32_t, CQ_setFunctionMark, 8)(int32_t plugin_id, const char* name) 360 | { 361 | return 0; 362 | } 363 | 364 | CQAPI(const char*, CQ_getRecord, 12)(int32_t plugin_id, const char* file, const char* format) 365 | { 366 | return CQ_getRecordV2(plugin_id, file, format); 367 | } 368 | -------------------------------------------------------------------------------- /native/cq_events.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "native.h" 4 | 5 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvPrivateMessage( 6 | JNIEnv* env, jclass clz, jint id, jbyteArray method, jint type, jint msg_id, jlong acct, jbyteArray msg, jint font) 7 | { 8 | const auto m = EvPriMsg(GetMethod(env, id, method)); 9 | if (m) 10 | { 11 | auto result = m(type, msg_id, acct, ByteArrayToString(env, msg).c_str(), font); 12 | return result; 13 | } 14 | return 0; 15 | } 16 | 17 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvGroupMessage( 18 | JNIEnv* env, jclass clz, jint id, jbyteArray method, jint type, jint msg_id, jlong grp, 19 | jlong acct, jbyteArray anon, jbyteArray msg, jint font) 20 | { 21 | const auto m = EvGroupMsg(GetMethod(env, id, method)); 22 | if (m) 23 | { 24 | auto result = m(type, msg_id, grp, acct, ByteArrayToString(env, anon).c_str(), 25 | ByteArrayToString(env, msg).c_str(), font); 26 | return result; 27 | } 28 | return 0; 29 | } 30 | 31 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvGroupAdmin( 32 | JNIEnv* env, jclass clz, jint id, jbyteArray method, jint type, jint time, jlong grp, jlong acct) 33 | { 34 | const auto m = EvGroupAdmin(GetMethod(env, id, method)); 35 | if (m) 36 | { 37 | return m(type, time, grp, acct); 38 | } 39 | return 0; 40 | } 41 | 42 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvGroupMember( 43 | JNIEnv* env, jclass clz, jint id, jbyteArray method, jint type, jint time, jlong grp, jlong acct, jlong mbr) 44 | { 45 | const auto m = EvGroupMember(GetMethod(env, id, method)); 46 | if (m) 47 | { 48 | return m(type, time, grp, acct, mbr); 49 | } 50 | return 0; 51 | } 52 | 53 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvGroupBan( 54 | JNIEnv* env, jclass clz, jint id, jbyteArray method, jint type, jint time, jlong grp, 55 | jlong acct, jlong mbr, jlong dur) 56 | { 57 | const auto m = EvGroupBan(GetMethod(env, id, method)); 58 | if (m) 59 | { 60 | return m(type, time, grp, acct, mbr, dur); 61 | } 62 | return 0; 63 | } 64 | 65 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvRequestAddGroup( 66 | JNIEnv* env, jclass clz, jint id, jbyteArray method, jint type, jint time, 67 | jlong grp, jlong acct, jbyteArray msg, jbyteArray flag) 68 | { 69 | const auto m = EvRequestAddGroup(GetMethod(env, id, method)); 70 | if (m) 71 | { 72 | auto result = m(type, time, grp, acct, ByteArrayToString(env, msg).c_str(), 73 | ByteArrayToString(env, flag).c_str()); 74 | return result; 75 | } 76 | return 0; 77 | } 78 | 79 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvRequestAddFriend( 80 | JNIEnv* env, jclass clz, jint id, jbyteArray method, jint type, jint time, 81 | jlong acct, jbyteArray msg, jbyteArray flag) 82 | { 83 | const auto m = EvRequestAddFriend(GetMethod(env, id, method)); 84 | if (m) 85 | { 86 | auto result = m(type, time, acct, ByteArrayToString(env, msg).c_str(), ByteArrayToString(env, flag).c_str()); 87 | return result; 88 | } 89 | return 0; 90 | } 91 | 92 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvFriendAdd( 93 | JNIEnv* env, jclass clz, jint id, jbyteArray method, jint type, jint time, jlong acct) 94 | { 95 | const auto m = EvFriendAdd(GetMethod(env, id, method)); 96 | if (m) 97 | { 98 | return m(type, time, acct); 99 | } 100 | return 0; 101 | } 102 | -------------------------------------------------------------------------------- /native/mirai_apis.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "native.h" 4 | 5 | CQAPI(int32_t, isMiraiNative, 0)() 6 | { 7 | return 1; 8 | } 9 | 10 | CQAPI(int32_t, mQuoteMessage, 12)(int32_t plugin_id, int32_t msg_id, const char* msg) 11 | { 12 | auto env = attach_java(); 13 | auto m = CharsToByteArray(env, msg); 14 | auto method = env->GetStaticMethodID(bclz, "quoteMessage", "(II[B)I"); 15 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, msg_id, m); 16 | env->DeleteLocalRef(m); 17 | detach_java(); 18 | return result; 19 | } 20 | 21 | CQAPI(int32_t, mForwardMessage, 24)(int32_t plugin_id, int32_t type, int64_t id, const char* strategy, const char* msg) 22 | { 23 | auto env = attach_java(); 24 | auto s = CharsToByteArray(env, strategy); 25 | auto m = CharsToByteArray(env, msg); 26 | auto method = env->GetStaticMethodID(bclz, "forwardMessage", "(IIJ[B[B)I"); 27 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, type, id, s, m); 28 | env->DeleteLocalRef(s); 29 | env->DeleteLocalRef(m); 30 | detach_java(); 31 | return result; 32 | } 33 | 34 | CQAPI(int32_t, mSetGroupKick, 28)(int32_t plugin_id, int64_t group, int64_t member, BOOL reject, const char* msg) 35 | { 36 | auto env = attach_java(); 37 | auto m = CharsToByteArray(env, msg); 38 | auto method = env->GetStaticMethodID(bclz, "setGroupKick", "(IJJZ[B)I"); 39 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, member, reject != FALSE, m); 40 | detach_java(); 41 | env->DeleteLocalRef(m); 42 | return result; 43 | } 44 | 45 | CQAPI(int32_t, CQ_setGroupKick, 24)(int32_t plugin_id, int64_t group, int64_t member, BOOL reject) 46 | { 47 | return mSetGroupKick(plugin_id, group, member, reject, "\0"); 48 | } 49 | 50 | CQAPI(const char*, mGetGroupEntranceAnnouncement, 12)(int32_t plugin_id, int64_t group) 51 | { 52 | auto env = attach_java(); 53 | auto method = env->GetStaticMethodID(bclz, "getGroupEntranceAnnouncement", "(IJ)[B"); 54 | auto result = jbyteArray(env->CallStaticObjectMethod(bclz, method, plugin_id, group)); 55 | auto r = ByteArrayToChars(env, result); 56 | env->DeleteLocalRef(result); 57 | detach_java(); 58 | return delay_mem_free(r); 59 | } 60 | 61 | CQAPI(int32_t, mSetGroupEntranceAnnouncement, 16)(int32_t plugin_id, int64_t group, const char* a) 62 | { 63 | auto env = attach_java(); 64 | auto an = CharsToByteArray(env, a); 65 | auto method = env->GetStaticMethodID(bclz, "setGroupEntranceAnnouncement", "(IJ[B)I"); 66 | auto result = env->CallStaticIntMethod(bclz, method, plugin_id, group, an); 67 | detach_java(); 68 | env->DeleteLocalRef(an); 69 | return result; 70 | } 71 | -------------------------------------------------------------------------------- /native/native.cpp: -------------------------------------------------------------------------------- 1 | #include "native.h" 2 | 3 | #include "cq_events.h" 4 | #include "cq_apis.h" 5 | #include "mirai_apis.h" 6 | 7 | // Load 8 | 9 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) 10 | { 11 | JNIEnv* env; 12 | if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_8) != JNI_OK) 13 | { 14 | return -1; 15 | } 16 | jvm = vm; 17 | auto b = env->FindClass("org/itxtech/mirainative/Bridge"); 18 | bclz = jclass(env->NewGlobalRef(b)); 19 | 20 | return JNI_VERSION_1_8; 21 | } 22 | 23 | // Utilities 24 | 25 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_shutdown(JNIEnv* env, jclass clz) 26 | { 27 | env->DeleteGlobalRef(bclz); 28 | running = false; 29 | mem_thread.join(); 30 | return 0; 31 | } 32 | 33 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_setCurrentDirectory(JNIEnv* env, jclass clz, jbyteArray dir) 34 | { 35 | SetCurrentDirectoryA(ByteArrayToString(env, dir).c_str()); 36 | return 0; 37 | } 38 | 39 | // Plugin 40 | 41 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_loadNativePlugin( 42 | JNIEnv* env, jclass clz, jbyteArray file, jint id) 43 | { 44 | native_plugin plugin = {id, const_cast(ByteArrayToChars(env, file))}; 45 | const auto dll = LoadLibraryA(plugin.file); 46 | 47 | if (dll != nullptr) 48 | { 49 | plugin.dll = dll; 50 | plugins[id] = plugin; 51 | 52 | const auto init = FuncInitialize(GetProcAddress(dll, "Initialize")); 53 | if (init) 54 | { 55 | init(plugin.id); 56 | } 57 | 58 | return 0; 59 | } 60 | return GetLastError(); 61 | } 62 | 63 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_freeNativePlugin( 64 | JNIEnv* env, jclass clz, jint id) 65 | { 66 | auto r = FreeLibrary(plugins[id].dll); 67 | free((void*)plugins[id].file); 68 | if (r != FALSE) 69 | { 70 | return 0; 71 | } 72 | return GetLastError(); 73 | } 74 | 75 | JNIEXPORT void JNICALL Java_org_itxtech_mirainative_Bridge_processMessage(JNIEnv* env, jclass clz) 76 | { 77 | MSG msg; 78 | while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE) > 0) 79 | { 80 | TranslateMessage(&msg); 81 | DispatchMessage(&msg); 82 | } 83 | } 84 | 85 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_callIntMethod( 86 | JNIEnv* env, jclass clz, jint id, jbyteArray method) 87 | { 88 | const auto m = IntMethod(GetMethod(env, id, method)); 89 | if (m) 90 | { 91 | return m(); 92 | } 93 | return 0; 94 | } 95 | 96 | JNIEXPORT jbyteArray JNICALL Java_org_itxtech_mirainative_Bridge_callStringMethod( 97 | JNIEnv* env, jclass clz, jint id, jbyteArray method) 98 | { 99 | const char* rtn = ""; 100 | const auto m = StringMethod(GetMethod(env, id, method)); 101 | if (m) 102 | { 103 | rtn = m(); 104 | } 105 | return CharsToByteArray(env, rtn); 106 | } 107 | -------------------------------------------------------------------------------- /native/native.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "org_itxtech_mirainative_Bridge.h" 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace std; 13 | 14 | #define CQAPI(ReturnType, Name, Size) __pragma(comment(linker, "/EXPORT:" #Name "=_" #Name "@" #Size))\ 15 | extern "C" __declspec(dllexport) ReturnType __stdcall Name 16 | 17 | typedef int32_t (__stdcall* IntMethod)(); 18 | typedef const char* (__stdcall* StringMethod)(); 19 | typedef int32_t (__stdcall* FuncInitialize)(int32_t); 20 | 21 | typedef int32_t (__stdcall* EvPriMsg)(int32_t, int32_t, int64_t, const char*, int32_t); 22 | typedef int32_t (__stdcall* EvGroupMsg)(int32_t, int32_t, int64_t, int64_t, const char*, const char*, int32_t); 23 | typedef int32_t (__stdcall* EvGroupAdmin)(int32_t, int32_t, int64_t, int64_t); 24 | typedef int32_t (__stdcall* EvGroupMember)(int32_t, int32_t, int64_t, int64_t, int64_t); 25 | typedef int32_t (__stdcall* EvGroupBan)(int32_t, int32_t, int64_t, int64_t, int64_t, int64_t); 26 | typedef int32_t (__stdcall* EvRequestAddGroup)(int32_t, int32_t, int64_t, int64_t, const char*, const char*); 27 | typedef int32_t (__stdcall* EvRequestAddFriend)(int32_t, int32_t, int64_t, const char*, const char*); 28 | typedef int32_t (__stdcall* EvFriendAdd)(int32_t, int32_t, int64_t); 29 | 30 | struct native_plugin 31 | { 32 | int id; 33 | const char* file; 34 | HMODULE dll; 35 | bool enabled; 36 | 37 | native_plugin() 38 | { 39 | id = -1; 40 | file = ""; 41 | dll = nullptr; 42 | enabled = false; 43 | } 44 | 45 | native_plugin(int i, char* f) 46 | { 47 | id = i; 48 | file = f; 49 | dll = nullptr; 50 | enabled = true; 51 | } 52 | }; 53 | 54 | // Global var 55 | 56 | map plugins; 57 | priority_queue> mem_queue; 58 | mutex mem_mutex; 59 | bool running = true; 60 | 61 | JavaVM* jvm = nullptr; 62 | jclass bclz = nullptr; 63 | 64 | 65 | thread mem_thread([] 66 | { 67 | while (running) 68 | { 69 | { 70 | unique_lock lock(mem_mutex); 71 | while (!mem_queue.empty() && time(nullptr) - mem_queue.top().first > 300) 72 | { 73 | free((void*)mem_queue.top().second); 74 | mem_queue.pop(); 75 | } 76 | } 77 | this_thread::sleep_for(500ms); 78 | } 79 | }); 80 | 81 | const char* delay_mem_free(const char* str) 82 | { 83 | unique_lock lock(mem_mutex); 84 | mem_queue.push({time(nullptr), str}); 85 | return str; 86 | } 87 | 88 | // Strings 89 | 90 | // Memory returned from this function need to be freed using free() 91 | const char* ByteArrayToChars(JNIEnv* env, jbyteArray arr) 92 | { 93 | jsize len = env->GetArrayLength(arr); 94 | char* buffer = (char*)malloc(len + 1); 95 | jbyte* elements = env->GetByteArrayElements(arr, nullptr); 96 | memcpy(buffer, elements, len); 97 | buffer[len] = '\0'; 98 | env->ReleaseByteArrayElements(arr, elements, JNI_ABORT); 99 | return buffer; 100 | } 101 | 102 | string ByteArrayToString(JNIEnv* env, jbyteArray arr) 103 | { 104 | const auto* buf = ByteArrayToChars(env, arr); 105 | string s(buf); 106 | free((void*)buf); 107 | return s; 108 | } 109 | 110 | jbyteArray CharsToByteArray(JNIEnv* env, const char* str) 111 | { 112 | if (str == nullptr) 113 | { 114 | str = "\0"; 115 | } 116 | auto len = strlen(str); 117 | auto arr = env->NewByteArray(len); 118 | env->SetByteArrayRegion(arr, 0, len, reinterpret_cast(str)); 119 | return arr; 120 | } 121 | 122 | FARPROC GetMethod(JNIEnv* env, jint id, jbyteArray method) 123 | { 124 | return GetProcAddress(plugins[id].dll, ByteArrayToString(env, method).c_str()); 125 | } 126 | 127 | JNIEnv* attach_java() 128 | { 129 | JNIEnv* env = nullptr; 130 | if (jvm) 131 | { 132 | const int stat = jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_8); 133 | if (stat == JNI_EDETACHED) 134 | { 135 | JavaVMAttachArgs args = {JNI_VERSION_1_8, nullptr, nullptr}; 136 | jvm->AttachCurrentThread(reinterpret_cast(&env), &args); 137 | } 138 | } 139 | return env; 140 | } 141 | 142 | void detach_java() 143 | { 144 | jvm->DetachCurrentThread(); 145 | } 146 | -------------------------------------------------------------------------------- /native/native.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTXTech/mirai-native/980b5e64086ac734544528536274b696fe3afe0e/native/native.rc -------------------------------------------------------------------------------- /native/native.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29728.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "native", "native.vcxproj", "{747CA574-363A-4A4A-A84E-613F8DEB35A3}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {747CA574-363A-4A4A-A84E-613F8DEB35A3}.Debug|x64.ActiveCfg = Debug|x64 17 | {747CA574-363A-4A4A-A84E-613F8DEB35A3}.Debug|x64.Build.0 = Debug|x64 18 | {747CA574-363A-4A4A-A84E-613F8DEB35A3}.Debug|x86.ActiveCfg = Debug|Win32 19 | {747CA574-363A-4A4A-A84E-613F8DEB35A3}.Debug|x86.Build.0 = Debug|Win32 20 | {747CA574-363A-4A4A-A84E-613F8DEB35A3}.Release|x64.ActiveCfg = Release|x64 21 | {747CA574-363A-4A4A-A84E-613F8DEB35A3}.Release|x64.Build.0 = Release|x64 22 | {747CA574-363A-4A4A-A84E-613F8DEB35A3}.Release|x86.ActiveCfg = Release|Win32 23 | {747CA574-363A-4A4A-A84E-613F8DEB35A3}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {8A955186-D2CF-4650-A562-490076201282} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /native/native.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 16.0 23 | {747CA574-363A-4A4A-A84E-613F8DEB35A3} 24 | native 25 | 10.0 26 | 27 | 28 | 29 | DynamicLibrary 30 | true 31 | v143 32 | Unicode 33 | 34 | 35 | DynamicLibrary 36 | false 37 | v143 38 | true 39 | Unicode 40 | 41 | 42 | DynamicLibrary 43 | true 44 | v143 45 | Unicode 46 | 47 | 48 | DynamicLibrary 49 | false 50 | v143 51 | true 52 | Unicode 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | true 74 | $(JAVA_HOME)\include\win32;$(JAVA_HOME)\include;$(IncludePath) 75 | $(JAVA_HOME)\include\win32;$(JAVA_HOME)\include;$(ReferencePath) 76 | 77 | 78 | true 79 | $(JAVA_HOME)\include;$(JAVA_HOME)\include\win32;$(IncludePath) 80 | $(JAVA_HOME)\include\win32;$(JAVA_HOME)\include;$(ReferencePath) 81 | 82 | 83 | false 84 | $(JAVA_HOME)\include\win32;$(JAVA_HOME)\include;$(IncludePath) 85 | $(JAVA_HOME)\include\win32;$(JAVA_HOME)\include;$(ReferencePath) 86 | 87 | 88 | false 89 | $(JAVA_HOME)\include;$(JAVA_HOME)\include\win32;$(IncludePath) 90 | $(JAVA_HOME)\include\win32;$(JAVA_HOME)\include;$(ReferencePath) 91 | 92 | 93 | 94 | Level3 95 | true 96 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 97 | true 98 | 99 | 100 | Console 101 | true 102 | 103 | 104 | 105 | 106 | 107 | 108 | Level3 109 | true 110 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 111 | true 112 | 113 | 114 | Console 115 | true 116 | 117 | 118 | 119 | 120 | 121 | 122 | Level3 123 | true 124 | true 125 | true 126 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 127 | true 128 | MaxSpeed 129 | MultiThreaded 130 | 131 | 132 | Console 133 | true 134 | true 135 | true 136 | 137 | 138 | 139 | 140 | 141 | 142 | Level3 143 | true 144 | true 145 | true 146 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 147 | true 148 | MaxSpeed 149 | 150 | 151 | Console 152 | true 153 | true 154 | true 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /native/native.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 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 | -------------------------------------------------------------------------------- /native/native.vcxproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /native/org_itxtech_mirainative_Bridge.h: -------------------------------------------------------------------------------- 1 | /* DO NOT EDIT THIS FILE - it is machine generated */ 2 | #include 3 | /* Header for class org_itxtech_mirainative_Bridge */ 4 | 5 | #ifndef _Included_org_itxtech_mirainative_Bridge 6 | #define _Included_org_itxtech_mirainative_Bridge 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | #undef org_itxtech_mirainative_Bridge_PRI_MSG_SUBTYPE_FRIEND 11 | #define org_itxtech_mirainative_Bridge_PRI_MSG_SUBTYPE_FRIEND 11L 12 | #undef org_itxtech_mirainative_Bridge_PRI_MSG_SUBTYPE_ONLINE_STATE 13 | #define org_itxtech_mirainative_Bridge_PRI_MSG_SUBTYPE_ONLINE_STATE 1L 14 | #undef org_itxtech_mirainative_Bridge_PRI_MSG_SUBTYPE_GROUP 15 | #define org_itxtech_mirainative_Bridge_PRI_MSG_SUBTYPE_GROUP 2L 16 | #undef org_itxtech_mirainative_Bridge_PRI_MSG_SUBTYPE_DISCUSS 17 | #define org_itxtech_mirainative_Bridge_PRI_MSG_SUBTYPE_DISCUSS 3L 18 | #undef org_itxtech_mirainative_Bridge_PERM_SUBTYPE_CANCEL_ADMIN 19 | #define org_itxtech_mirainative_Bridge_PERM_SUBTYPE_CANCEL_ADMIN 1L 20 | #undef org_itxtech_mirainative_Bridge_PERM_SUBTYPE_SET_ADMIN 21 | #define org_itxtech_mirainative_Bridge_PERM_SUBTYPE_SET_ADMIN 2L 22 | #undef org_itxtech_mirainative_Bridge_MEMBER_LEAVE_QUIT 23 | #define org_itxtech_mirainative_Bridge_MEMBER_LEAVE_QUIT 1L 24 | #undef org_itxtech_mirainative_Bridge_MEMBER_LEAVE_KICK 25 | #define org_itxtech_mirainative_Bridge_MEMBER_LEAVE_KICK 2L 26 | #undef org_itxtech_mirainative_Bridge_MEMBER_JOIN_PERMITTED 27 | #define org_itxtech_mirainative_Bridge_MEMBER_JOIN_PERMITTED 1L 28 | #undef org_itxtech_mirainative_Bridge_MEMBER_JOIN_INVITED_BY_ADMIN 29 | #define org_itxtech_mirainative_Bridge_MEMBER_JOIN_INVITED_BY_ADMIN 2L 30 | #undef org_itxtech_mirainative_Bridge_REQUEST_GROUP_APPLY 31 | #define org_itxtech_mirainative_Bridge_REQUEST_GROUP_APPLY 1L 32 | #undef org_itxtech_mirainative_Bridge_REQUEST_GROUP_INVITED 33 | #define org_itxtech_mirainative_Bridge_REQUEST_GROUP_INVITED 2L 34 | #undef org_itxtech_mirainative_Bridge_GROUP_UNMUTE 35 | #define org_itxtech_mirainative_Bridge_GROUP_UNMUTE 1L 36 | #undef org_itxtech_mirainative_Bridge_GROUP_MUTE 37 | #define org_itxtech_mirainative_Bridge_GROUP_MUTE 2L 38 | /* 39 | * Class: org_itxtech_mirainative_Bridge 40 | * Method: shutdown 41 | * Signature: ()I 42 | */ 43 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_shutdown 44 | (JNIEnv *, jclass); 45 | 46 | /* 47 | * Class: org_itxtech_mirainative_Bridge 48 | * Method: setCurrentDirectory 49 | * Signature: ([B)I 50 | */ 51 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_setCurrentDirectory 52 | (JNIEnv *, jclass, jbyteArray); 53 | 54 | /* 55 | * Class: org_itxtech_mirainative_Bridge 56 | * Method: loadNativePlugin 57 | * Signature: ([BI)I 58 | */ 59 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_loadNativePlugin 60 | (JNIEnv *, jclass, jbyteArray, jint); 61 | 62 | /* 63 | * Class: org_itxtech_mirainative_Bridge 64 | * Method: freeNativePlugin 65 | * Signature: (I)I 66 | */ 67 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_freeNativePlugin 68 | (JNIEnv *, jclass, jint); 69 | 70 | /* 71 | * Class: org_itxtech_mirainative_Bridge 72 | * Method: pEvPrivateMessage 73 | * Signature: (I[BIIJ[BI)I 74 | */ 75 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvPrivateMessage 76 | (JNIEnv *, jclass, jint, jbyteArray, jint, jint, jlong, jbyteArray, jint); 77 | 78 | /* 79 | * Class: org_itxtech_mirainative_Bridge 80 | * Method: pEvGroupMessage 81 | * Signature: (I[BIIJJ[B[BI)I 82 | */ 83 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvGroupMessage 84 | (JNIEnv *, jclass, jint, jbyteArray, jint, jint, jlong, jlong, jbyteArray, jbyteArray, jint); 85 | 86 | /* 87 | * Class: org_itxtech_mirainative_Bridge 88 | * Method: pEvGroupAdmin 89 | * Signature: (I[BIIJJ)I 90 | */ 91 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvGroupAdmin 92 | (JNIEnv *, jclass, jint, jbyteArray, jint, jint, jlong, jlong); 93 | 94 | /* 95 | * Class: org_itxtech_mirainative_Bridge 96 | * Method: pEvGroupMember 97 | * Signature: (I[BIIJJJ)I 98 | */ 99 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvGroupMember 100 | (JNIEnv *, jclass, jint, jbyteArray, jint, jint, jlong, jlong, jlong); 101 | 102 | /* 103 | * Class: org_itxtech_mirainative_Bridge 104 | * Method: pEvGroupBan 105 | * Signature: (I[BIIJJJJ)I 106 | */ 107 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvGroupBan 108 | (JNIEnv *, jclass, jint, jbyteArray, jint, jint, jlong, jlong, jlong, jlong); 109 | 110 | /* 111 | * Class: org_itxtech_mirainative_Bridge 112 | * Method: pEvRequestAddGroup 113 | * Signature: (I[BIIJJ[B[B)I 114 | */ 115 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvRequestAddGroup 116 | (JNIEnv *, jclass, jint, jbyteArray, jint, jint, jlong, jlong, jbyteArray, jbyteArray); 117 | 118 | /* 119 | * Class: org_itxtech_mirainative_Bridge 120 | * Method: pEvRequestAddFriend 121 | * Signature: (I[BIIJ[B[B)I 122 | */ 123 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvRequestAddFriend 124 | (JNIEnv *, jclass, jint, jbyteArray, jint, jint, jlong, jbyteArray, jbyteArray); 125 | 126 | /* 127 | * Class: org_itxtech_mirainative_Bridge 128 | * Method: pEvFriendAdd 129 | * Signature: (I[BIIJ)I 130 | */ 131 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_pEvFriendAdd 132 | (JNIEnv *, jclass, jint, jbyteArray, jint, jint, jlong); 133 | 134 | /* 135 | * Class: org_itxtech_mirainative_Bridge 136 | * Method: callIntMethod 137 | * Signature: (I[B)I 138 | */ 139 | JNIEXPORT jint JNICALL Java_org_itxtech_mirainative_Bridge_callIntMethod 140 | (JNIEnv *, jclass, jint, jbyteArray); 141 | 142 | /* 143 | * Class: org_itxtech_mirainative_Bridge 144 | * Method: callStringMethod 145 | * Signature: (I[B)[B 146 | */ 147 | JNIEXPORT jbyteArray JNICALL Java_org_itxtech_mirainative_Bridge_callStringMethod 148 | (JNIEnv *, jclass, jint, jbyteArray); 149 | 150 | /* 151 | * Class: org_itxtech_mirainative_Bridge 152 | * Method: processMessage 153 | * Signature: ()V 154 | */ 155 | JNIEXPORT void JNICALL Java_org_itxtech_mirainative_Bridge_processMessage 156 | (JNIEnv *, jclass); 157 | 158 | #ifdef __cplusplus 159 | } 160 | #endif 161 | #endif 162 | -------------------------------------------------------------------------------- /native/resource.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTXTech/mirai-native/980b5e64086ac734544528536274b696fe3afe0e/native/resource.h -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { url 'https://maven.aliyun.com/repository/public' } 4 | maven { url 'https://plugins.gradle.org/m2/' } 5 | mavenCentral() 6 | } 7 | } 8 | rootProject.name = 'mirai-native' 9 | 10 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/Bridge.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | @file:Suppress("UNUSED_PARAMETER", "unused") 26 | 27 | package org.itxtech.mirainative 28 | 29 | import org.itxtech.mirainative.bridge.MiraiBridge 30 | import org.itxtech.mirainative.bridge.MiraiImpl 31 | import java.nio.charset.Charset 32 | 33 | object Bridge { 34 | const val PRI_MSG_SUBTYPE_FRIEND = 11 35 | const val PRI_MSG_SUBTYPE_ONLINE_STATE = 1 36 | const val PRI_MSG_SUBTYPE_GROUP = 2 37 | const val PRI_MSG_SUBTYPE_DISCUSS = 3 38 | 39 | const val PERM_SUBTYPE_CANCEL_ADMIN = 1 40 | const val PERM_SUBTYPE_SET_ADMIN = 2 41 | 42 | const val MEMBER_LEAVE_QUIT = 1 43 | const val MEMBER_LEAVE_KICK = 2 44 | 45 | const val MEMBER_JOIN_PERMITTED = 1 46 | const val MEMBER_JOIN_INVITED_BY_ADMIN = 2 47 | 48 | const val REQUEST_GROUP_APPLY = 1 //他人申请 49 | const val REQUEST_GROUP_INVITED = 2 //受邀 50 | 51 | const val GROUP_UNMUTE = 1 52 | const val GROUP_MUTE = 2 53 | 54 | // Helper 55 | 56 | fun syncWorkingDir() = setCurrentDirectory(System.getProperty("user.dir").toNative()) 57 | 58 | // Native 59 | 60 | @JvmStatic 61 | external fun shutdown(): Int 62 | 63 | @JvmStatic 64 | external fun setCurrentDirectory(dir: ByteArray): Int 65 | 66 | @JvmStatic 67 | external fun loadNativePlugin(file: ByteArray, id: Int): Int 68 | 69 | @JvmStatic 70 | external fun freeNativePlugin(id: Int): Int 71 | 72 | @JvmStatic 73 | external fun pEvPrivateMessage( 74 | pluginId: Int, 75 | method: ByteArray, 76 | subType: Int, 77 | msgId: Int, 78 | fromAccount: Long, 79 | msg: ByteArray, 80 | font: Int 81 | ): Int 82 | 83 | @JvmStatic 84 | external fun pEvGroupMessage( 85 | pluginId: Int, 86 | method: ByteArray, 87 | subType: Int, 88 | msgId: Int, 89 | fromGroup: Long, 90 | fromAccount: Long, 91 | fromAnonymous: ByteArray, 92 | msg: ByteArray, 93 | font: Int 94 | ): Int 95 | 96 | @JvmStatic 97 | external fun pEvGroupAdmin( 98 | pluginId: Int, 99 | method: ByteArray, 100 | subType: Int, 101 | time: Int, 102 | fromGroup: Long, 103 | beingOperateAccount: Long 104 | ): Int 105 | 106 | @JvmStatic 107 | external fun pEvGroupMember( 108 | pluginId: Int, 109 | method: ByteArray, 110 | subType: Int, 111 | time: Int, 112 | fromGroup: Long, 113 | fromAccount: Long, 114 | beingOperateAccount: Long 115 | ): Int 116 | 117 | @JvmStatic 118 | external fun pEvGroupBan( 119 | pluginId: Int, 120 | method: ByteArray, 121 | subType: Int, 122 | time: Int, 123 | fromGroup: Long, 124 | fromAccount: Long, 125 | beingOperateAccount: Long, 126 | duration: Long 127 | ): Int 128 | 129 | @JvmStatic 130 | external fun pEvRequestAddGroup( 131 | pluginId: Int, 132 | method: ByteArray, 133 | subType: Int, 134 | time: Int, 135 | fromGroup: Long, 136 | fromAccount: Long, 137 | msg: ByteArray, 138 | flag: ByteArray 139 | ): Int 140 | 141 | @JvmStatic 142 | external fun pEvRequestAddFriend( 143 | pluginId: Int, 144 | method: ByteArray, 145 | subType: Int, 146 | time: Int, 147 | fromAccount: Long, 148 | msg: ByteArray, 149 | flag: ByteArray 150 | ): Int 151 | 152 | @JvmStatic 153 | external fun pEvFriendAdd(pluginId: Int, method: ByteArray, subType: Int, time: Int, fromAccount: Long): Int 154 | 155 | @JvmStatic 156 | external fun callIntMethod(pluginId: Int, method: ByteArray): Int 157 | 158 | @JvmStatic 159 | external fun callStringMethod(pluginId: Int, method: ByteArray): ByteArray 160 | 161 | @JvmStatic 162 | external fun processMessage() 163 | 164 | // Bridge 165 | 166 | @JvmStatic 167 | fun sendPrivateMessage(pluginId: Int, account: Long, msg: ByteArray) = 168 | MiraiBridge.sendPrivateMessage(pluginId, account, msg.fromNative()) 169 | 170 | @JvmStatic 171 | fun sendGroupMessage(pluginId: Int, group: Long, msg: ByteArray) = 172 | MiraiBridge.sendGroupMessage(pluginId, group, msg.fromNative()) 173 | 174 | @JvmStatic 175 | fun addLog(pluginId: Int, priority: Int, type: ByteArray, content: ByteArray) { 176 | MiraiBridge.addLog(pluginId, priority, type.fromNative(), content.fromNative()) 177 | } 178 | 179 | @JvmStatic 180 | fun getPluginDataDir(pluginId: Int) = MiraiBridge.getPluginDataDir(pluginId).toNative() 181 | 182 | @JvmStatic 183 | fun getLoginQQ(pluginId: Int) = MiraiBridge.getLoginQQ(pluginId) 184 | 185 | @JvmStatic 186 | fun getLoginNick(pluginId: Int) = MiraiBridge.getLoginNick(pluginId).toNative() 187 | 188 | 189 | @JvmStatic 190 | fun setGroupBan(pluginId: Int, group: Long, member: Long, duration: Long) = 191 | MiraiBridge.setGroupBan(pluginId, group, member, duration.toInt()) 192 | 193 | @JvmStatic 194 | fun setGroupCard(pluginId: Int, group: Long, member: Long, card: ByteArray) = 195 | MiraiBridge.setGroupCard(pluginId, group, member, card.fromNative()) 196 | 197 | @JvmStatic 198 | fun setGroupLeave(pluginId: Int, group: Long, dismiss: Boolean) = 199 | MiraiBridge.setGroupLeave(pluginId, group) 200 | 201 | @JvmStatic 202 | fun setGroupSpecialTitle(pluginId: Int, group: Long, member: Long, title: ByteArray, duration: Long) = 203 | MiraiBridge.setGroupSpecialTitle(pluginId, group, member, title.fromNative(), duration) 204 | 205 | @JvmStatic 206 | fun setGroupWholeBan(pluginId: Int, group: Long, enable: Boolean) = 207 | MiraiBridge.setGroupWholeBan(pluginId, group, enable) 208 | 209 | @JvmStatic 210 | fun recallMsg(pluginId: Int, msgId: Long) = MiraiBridge.recallMessage(pluginId, msgId) 211 | 212 | @JvmStatic 213 | fun getFriendList(pluginId: Int, reserved: Boolean) = MiraiBridge.getFriendList(pluginId).toNative() 214 | 215 | @JvmStatic 216 | fun getGroupInfo(pluginId: Int, groupId: Long, cache: Boolean) = 217 | MiraiBridge.getGroupInfo(pluginId, groupId).toNative() 218 | 219 | @JvmStatic 220 | fun getGroupList(pluginId: Int) = MiraiBridge.getGroupList(pluginId).toNative() 221 | 222 | @JvmStatic 223 | fun getGroupMemberInfo(pluginId: Int, group: Long, member: Long, cache: Boolean) = 224 | MiraiBridge.getGroupMemberInfo(pluginId, group, member).toNative() 225 | 226 | @JvmStatic 227 | fun getGroupMemberList(pluginId: Int, group: Long) = MiraiBridge.getGroupMemberList(pluginId, group).toNative() 228 | 229 | @JvmStatic 230 | fun setGroupAddRequest( 231 | pluginId: Int, 232 | requestId: ByteArray, 233 | reqType: Int, 234 | fbType: Int, 235 | reason: ByteArray 236 | ) = MiraiBridge.setGroupAddRequest(pluginId, requestId.fromNative(), reqType, fbType, reason.fromNative()) 237 | 238 | @JvmStatic 239 | fun setFriendAddRequest(pluginId: Int, requestId: ByteArray, type: Int, remark: ByteArray) = 240 | MiraiBridge.setFriendAddRequest(pluginId, requestId.fromNative(), type, remark.fromNative()) 241 | 242 | @JvmStatic 243 | fun getStrangerInfo(pluginId: Int, account: Long, cache: Boolean) = 244 | MiraiBridge.getStrangerInfo(pluginId, account).toNative() 245 | 246 | @JvmStatic 247 | fun getImage(pluginId: Int, image: ByteArray) = 248 | MiraiBridge.getImage(pluginId, image.fromNative()).toNative() 249 | 250 | @JvmStatic 251 | fun getRecord(pluginId: Int, file: ByteArray, format: ByteArray) = 252 | MiraiBridge.getRecord(pluginId, file.fromNative(), format.fromNative()).toNative() 253 | 254 | @JvmStatic 255 | fun setGroupAnonymousBan(pluginId: Int, group: Long, id: ByteArray, duration: Long) = 256 | MiraiBridge.setGroupAnonymousBan(pluginId, group, id.fromNative(), duration) 257 | 258 | // Placeholder methods which mirai hasn't supported yet 259 | 260 | @JvmStatic 261 | fun setGroupAnonymous(pluginId: Int, group: Long, enable: Boolean) = 0 262 | 263 | @JvmStatic 264 | fun setGroupAdmin(pluginId: Int, group: Long, account: Long, admin: Boolean) = 265 | MiraiBridge.setGroupAdmin(pluginId, group, account, admin) 266 | //true => set, false => revoke 267 | 268 | // Wont' Implement 269 | 270 | @JvmStatic 271 | fun sendLike(pluginId: Int, account: Long, times: Int) = 0 272 | 273 | @JvmStatic 274 | fun getCookies(pluginId: Int, domain: ByteArray) = "".toNative() 275 | 276 | @JvmStatic 277 | fun getCsrfToken(pluginId: Int) = "".toNative() 278 | 279 | @JvmStatic 280 | fun sendDiscussMessage(pluginId: Int, group: Long, msg: ByteArray) = 0 281 | 282 | @JvmStatic 283 | fun setDiscussLeave(pluginId: Int, group: Long) = 0 284 | 285 | // Mirai Unique Methods 286 | 287 | @JvmStatic 288 | fun quoteMessage(pluginId: Int, msgId: Int, msg: ByteArray) = 289 | MiraiImpl.quoteMessage(pluginId, msgId, msg.fromNative()) 290 | 291 | @JvmStatic 292 | fun forwardMessage(pluginId: Int, type: Int, id: Long, strategy: ByteArray, msg: ByteArray) = 293 | MiraiImpl.forwardMessage(pluginId, type, id, strategy.fromNative(), msg.fromNative()) 294 | 295 | @JvmStatic 296 | fun setGroupKick(pluginId: Int, group: Long, member: Long, reject: Boolean, message: ByteArray) = 297 | MiraiImpl.setGroupKick(pluginId, group, member, message.fromNative()) 298 | 299 | @JvmStatic 300 | fun getGroupEntranceAnnouncement(pluginId: Int, group: Long) = 301 | MiraiImpl.getGroupEntranceAnnouncement(pluginId, group).toNative() 302 | 303 | @JvmStatic 304 | fun setGroupEntranceAnnouncement(pluginId: Int, group: Long, a: ByteArray) = 305 | MiraiImpl.setGroupEntranceAnnouncement(pluginId, group, a.fromNative()) 306 | 307 | } 308 | 309 | fun String.toNative() = toByteArray(Charset.forName("GB18030")) 310 | 311 | fun ByteArray.fromNative() = String(this, Charset.forName("GB18030")) 312 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/MiraiNative.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative 26 | 27 | import io.ktor.util.* 28 | import kotlinx.coroutines.* 29 | import kotlinx.serialization.json.Json 30 | import net.mamoe.mirai.Bot 31 | import net.mamoe.mirai.console.extension.PluginComponentStorage 32 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescriptionBuilder 33 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin 34 | import net.mamoe.mirai.utils.ExternalResource 35 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 36 | import org.itxtech.mirainative.manager.CacheManager 37 | import org.itxtech.mirainative.manager.EventManager 38 | import org.itxtech.mirainative.manager.LibraryManager 39 | import org.itxtech.mirainative.manager.PluginManager 40 | import org.itxtech.mirainative.ui.FloatingWindow 41 | import org.itxtech.mirainative.ui.Tray 42 | import org.itxtech.mirainative.util.ConfigMan 43 | import java.io.File 44 | import java.io.FileOutputStream 45 | import java.math.BigInteger 46 | import java.security.MessageDigest 47 | import java.util.jar.Manifest 48 | 49 | object MiraiNative : KotlinPlugin( 50 | JvmPluginDescriptionBuilder("MiraiNative", "2.0.1") 51 | .id("org.itxtech.mirainative") 52 | .author("iTX Technologies") 53 | .info("强大的 mirai 原生插件加载器。") 54 | .build() 55 | ) { 56 | private val lib: File by lazy { File(dataFolder.absolutePath + File.separatorChar + "libraries").also { it.mkdirs() } } 57 | private val dll: File by lazy { File(dataFolder.absolutePath + File.separatorChar + "CQP.dll") } 58 | val imageDataPath: File by lazy { File("data" + File.separatorChar + "image").also { it.mkdirs() } } 59 | val recDataPath: File by lazy { File("data" + File.separatorChar + "record").also { it.mkdirs() } } 60 | 61 | @OptIn(DelicateCoroutinesApi::class) 62 | private val dispatcher = newSingleThreadContext("MiraiNative Main") + SupervisorJob() 63 | 64 | @OptIn(DelicateCoroutinesApi::class) 65 | val menuDispatcher = newSingleThreadContext("MiraiNative Menu") 66 | 67 | @OptIn(DelicateCoroutinesApi::class) 68 | val eventDispatcher = 69 | newFixedThreadPoolContext(Runtime.getRuntime().availableProcessors() * 2, "MiraiNative Events") 70 | 71 | var botOnline = false 72 | val bot: Bot by lazy { Bot.instances.first() } 73 | 74 | private fun ByteArray.checksum() = BigInteger(1, MessageDigest.getInstance("MD5").digest(this)) 75 | 76 | val json = Json { 77 | isLenient = true 78 | ignoreUnknownKeys = true 79 | allowSpecialFloatingPointValues = true 80 | useArrayPolymorphism = true 81 | } 82 | 83 | private fun checkNativeLibs() { 84 | logger.info("正在加载 Mirai Native Bridge ${dll.absolutePath}") 85 | LibraryManager.load(dll.absolutePath) 86 | 87 | lib.listFiles()?.forEach { file -> 88 | if (file.absolutePath.endsWith(".dll")) { 89 | logger.info("正在加载外部库 " + file.absolutePath) 90 | LibraryManager.load(file.absolutePath) 91 | } 92 | } 93 | } 94 | 95 | fun setBotOnline() { 96 | if (!botOnline) { 97 | botOnline = true 98 | nativeLaunch { 99 | ConfigMan.init() 100 | logger.info("Mirai Native 正启用所有插件。") 101 | PluginManager.enablePlugins() 102 | } 103 | } 104 | } 105 | 106 | override fun PluginComponentStorage.onLoad() { 107 | //暂时只支持 x86 平台运行,不兼容 amd64 108 | val mode = System.getProperty("sun.arch.data.model") 109 | if (mode != "32") { 110 | logger.warning("当前运行环境 $mode 可能不与 Mirai Native 兼容,推荐使用 32位 JRE 运行 Mirai Native。") 111 | logger.warning("如果您正在开发或调试其他环境下的 Mirai Native,请忽略此警告。") 112 | } 113 | 114 | val nativeLib = getResourceAsStream("CQP.dll")!! 115 | if (!dll.exists()) { 116 | logger.error("找不到 ${dll.absolutePath},写出自带的 CQP.dll。") 117 | val cqp = FileOutputStream(dll) 118 | nativeLib.copyTo(cqp) 119 | cqp.close() 120 | } else if (nativeLib.readBytes().checksum() != dll.readBytes().checksum()) { 121 | logger.warning("${dll.absolutePath} 与 Mirai Native 内置的 CQP.dll 的校验和不同。") 122 | logger.warning("如运行时出现问题,请尝试删除 ${dll.absolutePath} 并重启 mirai。") 123 | } 124 | 125 | initDataDir() 126 | } 127 | 128 | private fun libPath(d: String) = System.getProperty("java.library.path") 129 | .substringBefore(";") + File.separatorChar + "data" + File.separatorChar + d 130 | 131 | private fun File.mkdirsOrExists() = if (exists()) true else mkdirs() 132 | 133 | private fun initDataDir() { 134 | if (!File(libPath("image")).mkdirsOrExists() || !File(libPath("record")).mkdirsOrExists()) { 135 | logger.warning("图片或语音文件夹创建失败,可能没有使用管理员权限运行。位置:${libPath("")}") 136 | } 137 | File(imageDataPath, "MIRAI_NATIVE_IMAGE_DATA").createNewFile() 138 | File(recDataPath, "MIRAI_NATIVE_RECORD_DATA").createNewFile() 139 | } 140 | 141 | fun getDataFile(type: String, name: String): ExternalResource? { 142 | if (name.startsWith("base64://")) { 143 | return name.split("base64://", limit = 2)[1].decodeBase64Bytes().toExternalResource() 144 | } 145 | arrayOf( 146 | "data" + File.separatorChar + type + File.separatorChar, 147 | libPath(type) + File.separatorChar, 148 | "" 149 | ).forEach { 150 | val f = File(it + name).absoluteFile 151 | if (f.exists()) { 152 | return f.toExternalResource() 153 | } 154 | } 155 | return null 156 | } 157 | 158 | private suspend fun CoroutineScope.processMessage() { 159 | while (isActive) { 160 | Bridge.processMessage() 161 | delay(10) 162 | } 163 | } 164 | 165 | override fun onEnable() { 166 | Tray.create() 167 | FloatingWindow.create() 168 | 169 | checkNativeLibs() 170 | PluginManager.loadPlugins() 171 | 172 | nativeLaunch { processMessage() } 173 | launch(menuDispatcher) { processMessage() } 174 | 175 | PluginManager.registerCommands() 176 | EventManager.registerEvents() 177 | 178 | if (Bot.instances.isNotEmpty() && Bot.instances.first().isOnline) { 179 | setBotOnline() 180 | } 181 | 182 | launch { 183 | while (isActive) { 184 | CacheManager.checkCacheLimit(ConfigMan.config.cacheExpiration) 185 | delay(60000L) //1min 186 | } 187 | } 188 | } 189 | 190 | override fun onDisable() { 191 | ConfigMan.save() 192 | CacheManager.clear() 193 | Tray.close() 194 | FloatingWindow.close() 195 | runBlocking { 196 | PluginManager.unloadPlugins().join() 197 | nativeLaunch { Bridge.shutdown() }.join() 198 | dispatcher.cancel() 199 | dispatcher[Job]?.join() 200 | } 201 | } 202 | 203 | fun nativeLaunch(b: suspend CoroutineScope.() -> Unit) = launch(context = dispatcher, block = b) 204 | 205 | fun launchEvent(b: suspend CoroutineScope.() -> Unit) = launch(context = eventDispatcher, block = b) 206 | 207 | fun getVersion(): String { 208 | var version = description.version.toString() 209 | val mf = javaClass.classLoader.getResources("META-INF/MANIFEST.MF") 210 | while (mf.hasMoreElements()) { 211 | val manifest = Manifest(mf.nextElement().openStream()) 212 | if ("iTXTech MiraiNative" == manifest.mainAttributes.getValue("Name")) { 213 | version += "-" + manifest.mainAttributes.getValue("Revision") 214 | } 215 | } 216 | return version 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/bridge/ForwardMessageDecoder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.bridge 26 | 27 | import io.ktor.util.* 28 | import io.ktor.utils.io.core.* 29 | import kotlinx.coroutines.runBlocking 30 | import net.mamoe.mirai.contact.Contact 31 | import net.mamoe.mirai.contact.Group 32 | import net.mamoe.mirai.contact.nameCardOrNick 33 | import net.mamoe.mirai.message.data.ForwardMessage 34 | import net.mamoe.mirai.message.data.ForwardMessageBuilder 35 | import net.mamoe.mirai.message.data.RawForwardMessage 36 | import net.mamoe.mirai.message.data.buildForwardMessage 37 | import net.mamoe.mirai.utils.MiraiExperimentalApi 38 | import org.itxtech.mirainative.MiraiNative 39 | import org.itxtech.mirainative.bridge.MiraiBridge.readString 40 | import org.itxtech.mirainative.message.ChainCodeConverter 41 | 42 | object ForwardMessageDecoder { 43 | /** 44 | * 结构 45 | * Short => 消息条数 46 | * Entry => MessageEntry 47 | */ 48 | fun decode(contact: Contact, strategy: String, data: String): ForwardMessage { 49 | val pk = ByteReadPacket(data.decodeBase64Bytes()) 50 | return runBlocking { 51 | return@runBlocking buildForwardMessage(contact, decodeStrategy(strategy)) { 52 | for (i in 1..pk.readShort()) { 53 | MessageEntry(pk.readLong(), pk.readString(), pk.readInt(), pk.readString()).append(this, contact) 54 | } 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Strategy格式 61 | * Title, Brief, Source, Summary 62 | * Preview数目,Strings 63 | */ 64 | private fun decodeStrategy(strategy: String): ForwardMessage.DisplayStrategy { 65 | if (strategy == "") { 66 | return ForwardMessage.DisplayStrategy 67 | } 68 | val pk = ByteReadPacket(strategy.decodeBase64Bytes()) 69 | val title = pk.readString() 70 | val brief = pk.readString() 71 | val source = pk.readString() 72 | val summary = pk.readString() 73 | val pre = pk.readShort() 74 | val seq = arrayListOf() 75 | for (i in 1..pre) { 76 | seq.add(pk.readString()) 77 | } 78 | return object : ForwardMessage.DisplayStrategy { 79 | override fun generateTitle(forward: RawForwardMessage) = 80 | if (title == "") super.generateTitle(forward) else title 81 | 82 | override fun generateBrief(forward: RawForwardMessage) = 83 | if (brief == "") super.generateBrief(forward) else brief 84 | 85 | override fun generateSource(forward: RawForwardMessage) = 86 | if (source == "") super.generateSource(forward) else source 87 | 88 | override fun generateSummary(forward: RawForwardMessage) = 89 | if (summary == "") super.generateSummary(forward) else summary 90 | 91 | override fun generatePreview(forward: RawForwardMessage): List = 92 | if (pre == 0.toShort()) super.generatePreview(forward) else seq 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * 当sender=0时为bot 99 | * 当time=0时为当前时间 100 | */ 101 | data class MessageEntry(val sender: Long, val name: String, val time: Int, val msg: String) { 102 | @OptIn(MiraiExperimentalApi::class) 103 | suspend fun append(builder: ForwardMessageBuilder, contact: Contact) { 104 | val s = if (sender == 0L) MiraiNative.bot.id else sender 105 | builder.add( 106 | ForwardMessage.Node( 107 | s, 108 | if (time == 0) builder.currentTime++ else time, 109 | getName(name, s, contact), 110 | ChainCodeConverter.codeToChain(msg, contact) 111 | ) 112 | ) 113 | } 114 | 115 | private fun getName(name: String, sender: Long, contact: Contact): String { 116 | if (name != "") { 117 | return name 118 | } 119 | if (contact is Group && contact.contains(sender)) { 120 | return contact[sender]!!.nameCardOrNick 121 | } 122 | if (sender == MiraiNative.bot.id) { 123 | return MiraiNative.bot.nick 124 | } 125 | val friend = MiraiNative.bot.getFriend(sender) 126 | if (friend != null) { 127 | return friend.nameCardOrNick 128 | } 129 | return sender.toString() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/bridge/MiraiBridge.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.bridge 26 | 27 | import io.ktor.client.* 28 | import io.ktor.client.engine.okhttp.* 29 | import io.ktor.client.plugins.* 30 | import io.ktor.client.request.* 31 | import io.ktor.client.statement.* 32 | import io.ktor.http.* 33 | import io.ktor.util.* 34 | import io.ktor.util.cio.* 35 | import io.ktor.utils.io.* 36 | import io.ktor.utils.io.core.* 37 | import kotlinx.coroutines.launch 38 | import kotlinx.coroutines.runBlocking 39 | import net.mamoe.mirai.Mirai 40 | import net.mamoe.mirai.contact.NormalMember 41 | import net.mamoe.mirai.contact.getMember 42 | import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent 43 | import net.mamoe.mirai.event.events.MemberJoinRequestEvent 44 | import net.mamoe.mirai.event.events.NewFriendRequestEvent 45 | import net.mamoe.mirai.message.data.Image 46 | import net.mamoe.mirai.message.data.Image.Key.queryUrl 47 | import org.itxtech.mirainative.Bridge 48 | import org.itxtech.mirainative.MiraiNative 49 | import org.itxtech.mirainative.fromNative 50 | import org.itxtech.mirainative.manager.CacheManager 51 | import org.itxtech.mirainative.manager.PluginManager 52 | import org.itxtech.mirainative.message.ChainCodeConverter 53 | import org.itxtech.mirainative.plugin.FloatingWindowEntry 54 | import org.itxtech.mirainative.plugin.NativePlugin 55 | import org.itxtech.mirainative.toNative 56 | import org.itxtech.mirainative.util.ConfigMan 57 | import java.io.File 58 | import java.math.BigInteger 59 | import java.nio.charset.Charset 60 | import java.security.MessageDigest 61 | import kotlin.io.use 62 | import kotlin.text.toByteArray 63 | 64 | object MiraiBridge { 65 | val client = HttpClient(OkHttp) { 66 | install(UserAgent) { 67 | agent = 68 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.146 Safari/537.36" 69 | } 70 | 71 | install(HttpTimeout) { 72 | connectTimeoutMillis = 10000 73 | requestTimeoutMillis = 60000 74 | socketTimeoutMillis = 60000 75 | } 76 | 77 | engine { 78 | config { 79 | retryOnConnectionFailure(true) 80 | } 81 | } 82 | } 83 | 84 | fun logError(id: Int, e: String, err: Exception? = null) { 85 | val plugin = PluginManager.plugins[id] 86 | val info = if (plugin == null) { 87 | e.replace("%0", "$id(未找到该插件)") 88 | } else { 89 | e.replace("%0", plugin.detailedIdentifier) 90 | } 91 | if (err == null) { 92 | MiraiNative.logger.error(Exception(info)) 93 | } else { 94 | MiraiNative.logger.error(info, err) 95 | } 96 | } 97 | 98 | fun verifyCall(pluginId: Int): Boolean { 99 | if (MiraiNative.botOnline) { 100 | return true 101 | } 102 | logError(pluginId, "插件 %0 在机器人登录之前调用了API。") 103 | return false 104 | } 105 | 106 | inline fun call( 107 | exportName: String, 108 | pluginId: Int, 109 | defaultValue: T, 110 | errMsg: String = "", 111 | block: () -> T 112 | ): T { 113 | if (ConfigMan.config.verboseNativeApiLog) { 114 | val plugin = PluginManager.plugins[pluginId] 115 | MiraiNative.logger.verbose("插件 ${plugin?.detailedIdentifier ?: "$pluginId(未找到该插件)"} 调用了 $exportName 。") 116 | } 117 | if (verifyCall(pluginId)) { 118 | try { 119 | return block() 120 | } catch (e: Exception) { 121 | logError(pluginId, errMsg, e) 122 | } 123 | } 124 | return defaultValue 125 | } 126 | 127 | fun sendPrivateMessage(pluginId: Int, id: Long, message: String) = call("CQ_sendPrivateMsg", pluginId, 0) { 128 | val internalId = CacheManager.nextId() 129 | MiraiNative.launch { 130 | CacheManager.findUser(id)?.apply { 131 | val chain = ChainCodeConverter.codeToChain(message, this) 132 | sendMessage(chain).apply { 133 | CacheManager.cacheMessage(source, internalId, chain) 134 | } 135 | } 136 | } 137 | return internalId 138 | } 139 | 140 | fun sendGroupMessage(pluginId: Int, id: Long, message: String) = call("CQ_sendGroupMsg", pluginId, 0) { 141 | val internalId = CacheManager.nextId() 142 | MiraiNative.launch { 143 | val contact = MiraiNative.bot.getGroup(id) 144 | val chain = ChainCodeConverter.codeToChain(message, contact) 145 | contact?.sendMessage(chain)?.apply { 146 | CacheManager.cacheMessage(source, internalId, chain) 147 | } 148 | } 149 | return internalId 150 | } 151 | 152 | fun setGroupBan(pluginId: Int, groupId: Long, memberId: Long, duration: Int) = call("CQ_setGroupBan", pluginId, 0) { 153 | MiraiNative.launch { 154 | if (duration == 0) { 155 | MiraiNative.bot.getGroup(groupId)?.get(memberId)?.unmute() 156 | } else { 157 | MiraiNative.bot.getGroup(groupId)?.get(memberId)?.mute(duration) 158 | } 159 | } 160 | return 0 161 | } 162 | 163 | fun setGroupCard(pluginId: Int, groupId: Long, memberId: Long, card: String) = 164 | call("CQ_setGroupCard", pluginId, 0) { 165 | MiraiNative.bot.getGroup(groupId)?.get(memberId)?.nameCard = card 166 | return 0 167 | } 168 | 169 | fun setGroupLeave(pluginId: Int, groupId: Long) = call("CQ_setGroupLeave", pluginId, 0) { 170 | MiraiNative.launch { 171 | MiraiNative.bot.getGroup(groupId)?.quit() 172 | } 173 | return 0 174 | } 175 | 176 | fun setGroupSpecialTitle(pluginId: Int, group: Long, member: Long, title: String, duration: Long) = 177 | call("CQ_setGroupSpecialTitle", pluginId, 0) { 178 | MiraiNative.bot.getGroup(group)?.get(member)?.specialTitle = title 179 | return 0 180 | } 181 | 182 | fun setGroupWholeBan(pluginId: Int, group: Long, enable: Boolean) = call("CQ_setGroupWholeBan", pluginId, 0) { 183 | MiraiNative.bot.getGroup(group)?.settings?.isMuteAll = enable 184 | return 0 185 | } 186 | 187 | fun getStrangerInfo(pluginId: Int, account: Long) = call("CQ_getStrangerInfo", pluginId, "") { 188 | return@call runBlocking { 189 | val profile = Mirai.queryProfile(MiraiNative.bot, account) 190 | return@runBlocking buildPacket { 191 | writeLong(account) 192 | writeString(profile.nickname) 193 | writeInt(profile.sex.ordinal) 194 | writeInt(profile.age) 195 | writeInt(profile.qLevel) 196 | writeString(profile.email) 197 | writeString(profile.sign) 198 | }.encodeBase64() 199 | } 200 | } 201 | 202 | fun getFriendList(pluginId: Int) = call("CQ_getFriendList", pluginId, "") { 203 | val list = MiraiNative.bot.friends 204 | return buildPacket { 205 | writeInt(list.size) 206 | list.forEach { qq -> 207 | writeShortLVPacket { 208 | writeLong(qq.id) 209 | writeString(qq.nick) 210 | writeString(qq.remark) 211 | } 212 | } 213 | }.encodeBase64() 214 | } 215 | 216 | fun getGroupInfo(pluginId: Int, id: Long) = call("CQ_getGroupInfo", pluginId, "") { 217 | val info = MiraiNative.bot.getGroup(id) 218 | return if (info != null) { 219 | buildPacket { 220 | writeLong(id) 221 | writeString(info.name) 222 | writeInt(info.members.size + 1) 223 | //TODO: 上限 224 | writeInt(1000) 225 | }.encodeBase64() 226 | } else "" 227 | } 228 | 229 | fun getGroupList(pluginId: Int) = call("CQ_getGroupList", pluginId, "") { 230 | val list = MiraiNative.bot.groups 231 | return buildPacket { 232 | writeInt(list.size) 233 | list.forEach { 234 | writeShortLVPacket { 235 | writeLong(it.id) 236 | writeString(it.name) 237 | } 238 | } 239 | }.encodeBase64() 240 | } 241 | 242 | fun getGroupMemberInfo(pluginId: Int, groupId: Long, memberId: Long) = 243 | call("CQ_getGroupMemberInfoV2", pluginId, "") { 244 | val member = MiraiNative.bot.getGroup(groupId)?.get(memberId) ?: return "" 245 | return@call runBlocking { 246 | return@runBlocking buildPacket { 247 | writeMember(member) 248 | }.encodeBase64() 249 | } 250 | } 251 | 252 | fun getGroupMemberList(pluginId: Int, groupId: Long) = call("CQ_getGroupMemberList", pluginId, "") { 253 | val group = MiraiNative.bot.getGroup(groupId) ?: return "" 254 | return@call runBlocking { 255 | return@runBlocking buildPacket { 256 | writeInt(group.members.size) 257 | group.members.forEach { 258 | writeShortLVPacket { 259 | writeMember(it) 260 | } 261 | } 262 | }.encodeBase64() 263 | } 264 | } 265 | 266 | fun setGroupAddRequest(pluginId: Int, requestId: String, reqType: Int, type: Int, reason: String) = 267 | call("CQ_setGroupAddRequestV2", pluginId, 0) { 268 | MiraiNative.launch { 269 | if (reqType == Bridge.REQUEST_GROUP_APPLY) { 270 | (CacheManager.getEvent(requestId) as? MemberJoinRequestEvent)?.apply { 271 | when (type) {//1通过,2拒绝,3忽略 272 | 1 -> accept() 273 | 2 -> reject(message = reason) 274 | 3 -> ignore() 275 | } 276 | } 277 | } else { 278 | (CacheManager.getEvent(requestId) as? BotInvitedJoinGroupRequestEvent)?.apply { 279 | when (type) {//1通过,2忽略 280 | 1 -> accept() 281 | 2 -> ignore() 282 | } 283 | } 284 | } 285 | } 286 | return 0 287 | } 288 | 289 | fun setFriendAddRequest(pluginId: Int, requestId: String, type: Int, remark: String) = 290 | call("CQ_setFriendAddRequest", pluginId, 0) { 291 | MiraiNative.launch { 292 | (CacheManager.getEvent(requestId) as? NewFriendRequestEvent)?.apply { 293 | when (type) {//1通过,2拒绝 294 | 1 -> accept() 295 | 2 -> reject() 296 | } 297 | } 298 | } 299 | return 0 300 | } 301 | 302 | fun getImage(pluginId: Int, image: String): String = 303 | call("CQ_getImage", pluginId, "", "Error occurred when plugin %0 downloading image $image") { 304 | return@call runBlocking { 305 | val img = image.replace(".mnimg,type=flash","").replace(".mnimg", "") // fix when get flash image 306 | val u = Image(img).queryUrl() 307 | if (u != "") { 308 | client.prepareGet(u).execute { response -> 309 | if (response.status.isSuccess()) { 310 | val md = MessageDigest.getInstance("MD5") 311 | val basename = MiraiNative.imageDataPath.absolutePath + File.separatorChar + 312 | BigInteger(1, md.digest(img.toByteArray())) 313 | .toString(16).padStart(32, '0') 314 | val ext = when (response.headers[HttpHeaders.ContentType]) { 315 | "image/gif" -> "gif" 316 | "image/png" -> "png" 317 | "image/jpeg" -> "jpg" 318 | "image/x-bitmap" -> "bmp" 319 | "image/tiff" -> "tiff" 320 | else -> "jpg" 321 | } 322 | val file = File("$basename.$ext") 323 | response.bodyAsChannel().copyAndClose(file.writeChannel()) 324 | file.absolutePath 325 | } else { 326 | "" 327 | } 328 | } 329 | } else { 330 | "" 331 | } 332 | } 333 | } 334 | 335 | fun getRecord(pluginId: Int, record: String, format: String) = 336 | call("CQ_getRecordV2", pluginId, "", "Error occurred when plugin %0 downloading record $record") { 337 | return runBlocking { 338 | val rec = CacheManager.getRecord(record.replace(".mnrec", "")) 339 | if (rec != null) { 340 | val file = File( 341 | MiraiNative.recDataPath.absolutePath + File.separatorChar + 342 | BigInteger(1, rec.fileMd5).toString(16) 343 | .padStart(32, '0') + ".silk" 344 | ) 345 | client.prepareGet(rec.urlForDownload).execute { response -> 346 | if (response.status.isSuccess()) { 347 | response.bodyAsChannel().copyAndClose(file.writeChannel()) 348 | file.absolutePath 349 | } else { 350 | "" 351 | } 352 | } 353 | } else { 354 | "" 355 | } 356 | } 357 | } 358 | 359 | fun setGroupAnonymousBan(pluginId: Int, group: Long, id: String, duration: Long) = 360 | call("CQ_setGroupAnonymousBan", pluginId, 0) { 361 | runBlocking { 362 | CacheManager.findAnonymousMember(group, id)?.mute(duration.toInt()) 363 | } 364 | return 0 365 | } 366 | 367 | fun setGroupAdmin(pluginId: Int, group: Long, account: Long, admin: Boolean) = 368 | call("CQ_setGroupAdmin", pluginId, 0) { 369 | runBlocking { 370 | MiraiNative.bot.getGroup(group)?.getMember(account)?.modifyAdmin(admin) 371 | } 372 | return 0 373 | } 374 | 375 | fun addLog(pluginId: Int, priority: Int, type: String, content: String) { 376 | NativeLoggerHelper.log(PluginManager.plugins[pluginId]!!, priority, type, content) 377 | } 378 | 379 | fun getPluginDataDir(pluginId: Int) = call("CQ_getAppDirectory", pluginId, "") { 380 | return PluginManager.plugins[pluginId]!!.appDir.absolutePath + File.separatorChar 381 | } 382 | 383 | fun getLoginQQ(pluginId: Int) = call("CQ_getLoginQQ", pluginId, 0L) { 384 | return MiraiNative.bot.id 385 | } 386 | 387 | fun getLoginNick(pluginId: Int) = call("CQ_getLoginNick", pluginId, "") { 388 | return MiraiNative.bot.nick 389 | } 390 | 391 | fun recallMessage(pluginId: Int, id: Long) = call("CQ_deleteMsg", pluginId, -1) { 392 | return if (CacheManager.recall(id.toInt())) 0 else -1 393 | } 394 | 395 | fun updateFwe(pluginId: Int, fwe: FloatingWindowEntry) { 396 | val pk = ByteReadPacket( 397 | Bridge.callStringMethod(pluginId, fwe.status.function.toNative()).fromNative().decodeBase64Bytes() 398 | ) 399 | fwe.data = pk.readString() 400 | fwe.unit = pk.readString() 401 | fwe.color = pk.readInt() 402 | } 403 | 404 | fun ByteReadPacket.readString(): String { 405 | return String(readBytes(readShort().toInt())) 406 | } 407 | 408 | private inline fun BytePacketBuilder.writeShortLVPacket( 409 | lengthOffset: ((Long) -> Long) = { it }, 410 | builder: BytePacketBuilder.() -> Unit 411 | ): Int = 412 | BytePacketBuilder().apply(builder).build().use { 413 | val length = lengthOffset.invoke(it.remaining) 414 | writeShort(length.toShort()) 415 | writePacket(it) 416 | return length.toInt() 417 | } 418 | 419 | private fun BytePacketBuilder.writeString(string: String) { 420 | val b = string.toByteArray(Charset.forName("GB18030")) 421 | writeShort(b.size.toShort()) 422 | writeFully(b) 423 | } 424 | 425 | private fun BytePacketBuilder.writeBool(bool: Boolean) { 426 | writeInt(if (bool) 1 else 0) 427 | } 428 | 429 | private suspend fun BytePacketBuilder.writeMember(member: NormalMember) { 430 | // val profile = member.queryProfile() 431 | writeLong(member.group.id) 432 | writeLong(member.id) 433 | writeString(member.nick) 434 | writeString(member.nameCard) 435 | writeInt(0) // TODO: 性别 436 | writeInt(0) // TODO: 年龄 437 | writeString("未知") // TODO: 地区 438 | writeInt(member.joinTimestamp) 439 | writeInt(member.lastSpeakTimestamp) 440 | writeString("") // TODO: 等级名称 441 | writeInt(member.permission.ordinal + 1) 442 | writeBool(false) // TODO: 不良记录成员 443 | writeString(member.specialTitle) 444 | writeInt(-1) // TODO: 头衔过期时间 445 | writeBool(false) // TODO: 允许修改名片 446 | } 447 | } 448 | 449 | object NativeLoggerHelper { 450 | const val LOG_DEBUG = 0 451 | const val LOG_INFO = 10 452 | const val LOG_INFO_SUCC = 11 453 | const val LOG_INFO_RECV = 12 454 | const val LOG_INFO_SEND = 13 455 | const val LOG_WARNING = 20 456 | const val LOG_ERROR = 21 457 | const val LOG_FATAL = 22 458 | 459 | private fun getLogger() = MiraiNative.logger 460 | 461 | fun log(plugin: NativePlugin, priority: Int, type: String, content: String) { 462 | var c = "[" + plugin.getName() 463 | if ("" != type) { 464 | c += " $type" 465 | } 466 | c += "] $content" 467 | when (priority) { 468 | LOG_DEBUG -> getLogger().debug(c) 469 | LOG_INFO, LOG_INFO_RECV, LOG_INFO_SUCC, LOG_INFO_SEND -> getLogger().info( 470 | c 471 | ) 472 | LOG_WARNING -> getLogger().warning(c) 473 | LOG_ERROR -> getLogger().error(c) 474 | LOG_FATAL -> getLogger().error("[FATAL] $c") 475 | } 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/bridge/MiraiImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.bridge 26 | 27 | import kotlinx.coroutines.flow.filter 28 | import kotlinx.coroutines.flow.firstOrNull 29 | import kotlinx.coroutines.launch 30 | import kotlinx.coroutines.runBlocking 31 | import net.mamoe.mirai.contact.announcement.Announcement.Companion.publishAnnouncement 32 | import net.mamoe.mirai.contact.announcement.AnnouncementParameters 33 | import net.mamoe.mirai.message.data.MessageSource.Key.quote 34 | import net.mamoe.mirai.message.data.MessageSourceKind 35 | import net.mamoe.mirai.message.data.kind 36 | import org.itxtech.mirainative.MiraiNative 37 | import org.itxtech.mirainative.bridge.MiraiBridge.call 38 | import org.itxtech.mirainative.manager.CacheManager 39 | import org.itxtech.mirainative.message.ChainCodeConverter 40 | 41 | object MiraiImpl { 42 | fun quoteMessage(pluginId: Int, msgId: Int, message: String) = call("mQuoteMessage", pluginId, 0) { 43 | val internalId = CacheManager.nextId() 44 | MiraiNative.launch { 45 | val src = CacheManager.getMessage(msgId) 46 | if (src != null) { 47 | if (src.kind != MessageSourceKind.GROUP) { 48 | if (src.fromId != MiraiNative.bot.id) { 49 | val f = MiraiNative.bot.getFriend(src.fromId) 50 | val chain = src.quote() + ChainCodeConverter.codeToChain(message, f) 51 | f?.sendMessage(chain)?.apply { 52 | CacheManager.cacheMessage(source, internalId, chain) 53 | } 54 | } 55 | } else { 56 | val group = MiraiNative.bot.getGroup(src.targetId) 57 | if (src.fromId != MiraiNative.bot.id) { 58 | val chain = src.quote() + ChainCodeConverter.codeToChain(message, group) 59 | group?.sendMessage(chain)?.apply { 60 | CacheManager.cacheMessage(source, internalId, chain) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | return internalId 67 | } 68 | 69 | fun forwardMessage(pluginId: Int, type: Int, id: Long, strategy: String, msg: String) = 70 | call("mForwardMessage", pluginId, 0) { 71 | val contact = if (type == 0) MiraiNative.bot.getFriend(id) else MiraiNative.bot.getGroup(id) 72 | val internalId = CacheManager.nextId() 73 | MiraiNative.launch { 74 | contact?.sendMessage(ForwardMessageDecoder.decode(contact, strategy, msg)) 75 | } 76 | return internalId 77 | } 78 | 79 | fun setGroupKick(pluginId: Int, groupId: Long, memberId: Long, message: String) = 80 | call(if (message.isBlank()) "CQ_setGroupKick" else "mSetGroupKick", pluginId, 0) { 81 | MiraiNative.launch { 82 | MiraiNative.bot.getGroup(groupId)?.get(memberId)?.kick(message) 83 | } 84 | return 0 85 | } 86 | 87 | fun getGroupEntranceAnnouncement(pluginId: Int, id: Long) = 88 | call("mGetGroupEntranceAnnouncement", pluginId, "") { 89 | return@call runBlocking { 90 | MiraiNative.bot.getGroup(id)?.announcements?.asFlow()?.filter { it.parameters.sendToNewMember } 91 | ?.firstOrNull()?.content ?: "" 92 | } 93 | } 94 | 95 | fun setGroupEntranceAnnouncement(pluginId: Int, id: Long, a: String) = 96 | call("mSetGroupEntranceAnnouncement", pluginId, 0) { 97 | MiraiNative.launch { 98 | MiraiNative.bot.getGroup(id)?.publishAnnouncement( 99 | a, 100 | AnnouncementParameters.DEFAULT.builder().sendToNewMember(true).build() 101 | ) 102 | } 103 | return 0 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/bridge/NativeBridge.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.bridge 26 | 27 | import org.itxtech.mirainative.Bridge 28 | import org.itxtech.mirainative.MiraiNative 29 | import org.itxtech.mirainative.fromNative 30 | import org.itxtech.mirainative.manager.PluginManager 31 | import org.itxtech.mirainative.plugin.Event 32 | import org.itxtech.mirainative.plugin.NativePlugin 33 | import org.itxtech.mirainative.toNative 34 | import org.itxtech.mirainative.util.ConfigMan 35 | import java.io.File 36 | 37 | object NativeBridge { 38 | private fun getPlugins() = PluginManager.plugins 39 | 40 | private fun getLogger() = MiraiNative.logger 41 | 42 | fun getPluginInfo(plugin: NativePlugin) = Bridge.callStringMethod(plugin.id, "pluginInfo".toNative()).fromNative() 43 | 44 | fun loadPlugin(plugin: NativePlugin, file: File): Int { 45 | val code = Bridge.loadNativePlugin( 46 | file.absolutePath.replace("\\", "\\\\").toNative(), 47 | plugin.id 48 | ) 49 | val info = "插件 ${plugin.file.name} 已被加载,返回值为 $code 。" 50 | if (code == 0) { 51 | getLogger().info(info) 52 | } else { 53 | getLogger().error(info) 54 | } 55 | Bridge.syncWorkingDir() 56 | return code 57 | } 58 | 59 | fun unloadPlugin(plugin: NativePlugin) = Bridge.freeNativePlugin(plugin.id).apply { 60 | getLogger().info("插件 ${plugin.detailedIdentifier} 已被卸载,返回值为 $this 。") 61 | } 62 | 63 | fun disablePlugin(plugin: NativePlugin) { 64 | if (plugin.loaded && plugin.enabled) { 65 | singleEvent(plugin, Event.EVENT_DISABLE, true, "_eventDisable") 66 | } 67 | } 68 | 69 | fun enablePlugin(plugin: NativePlugin) { 70 | if (plugin.loaded && !plugin.enabled) { 71 | singleEvent(plugin, Event.EVENT_ENABLE, true, "_eventEnable") 72 | } 73 | } 74 | 75 | fun startPlugin(plugin: NativePlugin) { 76 | singleEvent(plugin, Event.EVENT_STARTUP, true, "_eventStartup") 77 | } 78 | 79 | fun exitPlugin(plugin: NativePlugin) { 80 | singleEvent(plugin, Event.EVENT_EXIT, true, "_eventExit") 81 | } 82 | 83 | fun updateInfo(plugin: NativePlugin) { 84 | if (ConfigMan.config.verboseNativeApiLog) { 85 | getLogger().verbose("正在调用插件 ${plugin.detailedIdentifier} 的 AppInfo 方法。") 86 | } 87 | val info = Bridge.callStringMethod(plugin.id, "AppInfo".toNative()).fromNative() 88 | if ("" != info) { 89 | plugin.setInfo(info) 90 | } 91 | } 92 | 93 | // Events 94 | 95 | private inline fun singleEvent( 96 | plugin: NativePlugin, 97 | ev: Int, 98 | ignoreState: Boolean, 99 | defaultMethod: String, 100 | block: NativePlugin.(evName: ByteArray) -> Int = { Bridge.callIntMethod(plugin.id, it) } 101 | ): Boolean { 102 | val eventName = plugin.getEventOrDefault(ev, defaultMethod) 103 | return (plugin.shouldCallEvent(ev, ignoreState).apply { 104 | if (this && ConfigMan.config.verboseNativeApiLog) { 105 | getLogger().verbose("正在调用插件 ${plugin.detailedIdentifier} 的 $eventName 事件。") 106 | } 107 | } && block(plugin, eventName.toNative()) == 1) 108 | } 109 | 110 | private inline fun event(ev: Int, defaultMethod: String, block: NativePlugin.(evName: ByteArray) -> Int) { 111 | for (plugin in getPlugins().values) { 112 | if (singleEvent(plugin, ev, false, defaultMethod, block)) { 113 | break 114 | } 115 | } 116 | } 117 | 118 | fun eventPrivateMessage( 119 | subType: Int, 120 | msgId: Int, 121 | fromAccount: Long, 122 | msg: String, 123 | font: Int 124 | ) { 125 | event(Event.EVENT_PRI_MSG, "_eventPrivateMsg") { 126 | Bridge.pEvPrivateMessage( 127 | id, 128 | it, 129 | subType, 130 | msgId, 131 | fromAccount, 132 | processMessage(Event.EVENT_PRI_MSG, msg).toNative(), 133 | font 134 | ) 135 | } 136 | } 137 | 138 | fun eventGroupMessage( 139 | subType: Int, 140 | msgId: Int, 141 | fromGroup: Long, 142 | fromAccount: Long, 143 | fromAnonymous: String, 144 | msg: String, 145 | font: Int 146 | ) { 147 | event(Event.EVENT_GROUP_MSG, "_eventGroupMsg") { 148 | Bridge.pEvGroupMessage( 149 | id, 150 | it, 151 | subType, 152 | msgId, 153 | fromGroup, 154 | fromAccount, 155 | fromAnonymous.toNative(), 156 | processMessage(Event.EVENT_GROUP_MSG, msg).toNative(), 157 | font 158 | ) 159 | } 160 | } 161 | 162 | fun eventGroupAdmin(subType: Int, time: Int, fromGroup: Long, beingOperateAccount: Long) { 163 | event(Event.EVENT_GROUP_ADMIN, "_eventSystem_GroupAdmin") { 164 | Bridge.pEvGroupAdmin(id, it, subType, time, fromGroup, beingOperateAccount) 165 | } 166 | } 167 | 168 | fun eventGroupMemberLeave(subType: Int, time: Int, fromGroup: Long, fromAccount: Long, beingOperateAccount: Long) { 169 | event(Event.EVENT_GROUP_MEMBER_DEC, "_eventSystem_GroupMemberDecrease") { 170 | Bridge.pEvGroupMember(id, it, subType, time, fromGroup, fromAccount, beingOperateAccount) 171 | } 172 | } 173 | 174 | fun eventGroupBan( 175 | subType: Int, 176 | time: Int, 177 | fromGroup: Long, 178 | fromAccount: Long, 179 | beingOperateAccount: Long, 180 | duration: Long 181 | ) { 182 | event(Event.EVENT_GROUP_BAN, "_eventSystem_GroupBan") { 183 | Bridge.pEvGroupBan(id, it, subType, time, fromGroup, fromAccount, beingOperateAccount, duration) 184 | } 185 | } 186 | 187 | fun eventGroupMemberJoin( 188 | subType: Int, 189 | time: Int, 190 | fromGroup: Long, 191 | fromAccount: Long, 192 | beingOperateAccount: Long 193 | ) { 194 | event(Event.EVENT_GROUP_MEMBER_INC, "_eventSystem_GroupMemberIncrease") { 195 | Bridge.pEvGroupMember(id, it, subType, time, fromGroup, fromAccount, beingOperateAccount) 196 | } 197 | } 198 | 199 | fun eventRequestAddGroup( 200 | subType: Int, 201 | time: Int, 202 | fromGroup: Long, 203 | fromAccount: Long, 204 | msg: String, 205 | flag: String 206 | ) { 207 | event(Event.EVENT_REQUEST_GROUP, "_eventRequest_AddGroup") { 208 | Bridge.pEvRequestAddGroup(id, it, subType, time, fromGroup, fromAccount, msg.toNative(), flag.toNative()) 209 | } 210 | } 211 | 212 | fun eventRequestAddFriend( 213 | subType: Int, 214 | time: Int, 215 | fromAccount: Long, 216 | msg: String, 217 | flag: String 218 | ) { 219 | event(Event.EVENT_REQUEST_FRIEND, "_eventRequest_AddFriend") { 220 | Bridge.pEvRequestAddFriend(id, it, subType, time, fromAccount, msg.toNative(), flag.toNative()) 221 | } 222 | } 223 | 224 | fun eventFriendAdd(subType: Int, time: Int, fromAccount: Long) { 225 | event(Event.EVENT_FRIEND_ADD, "_eventRequest_AddFriend") { 226 | Bridge.pEvFriendAdd(id, it, subType, time, fromAccount) 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/manager/CacheManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | @file:Suppress("NOTHING_TO_INLINE") 26 | 27 | package org.itxtech.mirainative.manager 28 | 29 | import kotlinx.atomicfu.atomic 30 | import kotlinx.coroutines.launch 31 | import net.mamoe.mirai.contact.AnonymousMember 32 | import net.mamoe.mirai.contact.User 33 | import net.mamoe.mirai.event.events.BotEvent 34 | import net.mamoe.mirai.event.events.GroupMessageEvent 35 | import net.mamoe.mirai.event.events.GroupTempMessageEvent 36 | import net.mamoe.mirai.message.data.MessageChain 37 | import net.mamoe.mirai.message.data.MessageSource 38 | import net.mamoe.mirai.message.data.MessageSource.Key.recall 39 | import net.mamoe.mirai.message.data.OnlineAudio 40 | import net.mamoe.mirai.message.data.source 41 | import org.itxtech.mirainative.MiraiNative 42 | 43 | class CacheWrapper( 44 | val obj: T, 45 | val creationTime: Long = System.currentTimeMillis() 46 | ) 47 | 48 | inline fun hashMapWrapperOf() = hashMapOf>() 49 | 50 | inline operator fun MutableMap>.set(key: K, value: V) { 51 | put(key, CacheWrapper(value)) 52 | } 53 | 54 | inline fun MutableMap>.getObj(key: K): V? = get(key)?.obj 55 | 56 | inline fun MutableMap>.checkExpiration(exp: Int) { 57 | values.removeIf { it.creationTime + exp >= System.currentTimeMillis() } 58 | } 59 | 60 | object CacheManager { 61 | private val msgCache = hashMapWrapperOf() 62 | private val evCache = hashMapWrapperOf() 63 | private val senders = hashMapWrapperOf() 64 | private val anonymousMembers = hashMapOf>>() 65 | private val records = hashMapWrapperOf() 66 | private val internalId = atomic(0) 67 | 68 | fun checkCacheLimit(exp: Int) { 69 | msgCache.checkExpiration(exp) 70 | evCache.checkExpiration(exp) 71 | senders.checkExpiration(exp) 72 | records.checkExpiration(exp) 73 | anonymousMembers.forEach { it.value.checkExpiration(exp) } 74 | } 75 | 76 | fun nextId() = internalId.getAndIncrement() 77 | 78 | fun cacheEvent(event: BotEvent, id: Int = nextId()) = id.apply { evCache[this] = event }.toString() 79 | 80 | fun getEvent(id: String): BotEvent? = evCache.getObj(id.toInt()).also { evCache.remove(id.toInt()) } 81 | 82 | fun cacheMessage(source: MessageSource, id: Int = nextId(), chain: MessageChain? = null): Int { 83 | msgCache[id] = source 84 | chain?.forEach { 85 | if (it is OnlineAudio) { 86 | records[it.filename] = it 87 | } 88 | } 89 | return id 90 | } 91 | 92 | fun cacheMember(member: User) { 93 | senders[member.id] = member 94 | } 95 | 96 | fun cacheTempMessage(message: GroupTempMessageEvent, id: Int = nextId()): Int { 97 | cacheMember(message.sender) 98 | return cacheMessage(message.message.source, id, message.message) 99 | } 100 | 101 | fun cacheAnonymousMember(ev: GroupMessageEvent) { 102 | if (anonymousMembers[ev.group.id] == null) { 103 | anonymousMembers[ev.group.id] = hashMapOf() 104 | } 105 | val sender = ev.sender as AnonymousMember 106 | anonymousMembers[ev.group.id]!![sender.anonymousId] = sender 107 | } 108 | 109 | fun recall(id: Int): Boolean { 110 | val message = msgCache.getObj(id) ?: return false 111 | msgCache.remove(id) 112 | MiraiNative.launch { 113 | message.recall() 114 | } 115 | return true 116 | } 117 | 118 | fun getMessage(id: Int): MessageSource? = msgCache.getObj(id) 119 | 120 | fun getRecord(name: String): OnlineAudio? = records.getObj(name.replace(".mnrec", "")) 121 | 122 | fun findUser(id: Long): User? { 123 | var member = MiraiNative.bot.getFriend(id) ?: senders.getObj(id) 124 | if (member == null) { 125 | member = MiraiNative.bot.strangers[id] 126 | } 127 | if (member == null) { 128 | MiraiNative.bot.groups.forEach { 129 | if (it[id] != null) { 130 | return it[id] 131 | } 132 | } 133 | } 134 | return member 135 | } 136 | 137 | fun findAnonymousMember(group: Long, id: String): AnonymousMember? = anonymousMembers[group]?.getObj(id) 138 | 139 | fun clear() { 140 | msgCache.clear() 141 | evCache.clear() 142 | senders.clear() 143 | anonymousMembers.clear() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/manager/EventManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.manager 26 | 27 | import net.mamoe.mirai.contact.AnonymousMember 28 | import net.mamoe.mirai.contact.MemberPermission 29 | import net.mamoe.mirai.event.events.* 30 | import net.mamoe.mirai.event.globalEventChannel 31 | import net.mamoe.mirai.message.data.source 32 | import net.mamoe.mirai.utils.MiraiExperimentalApi 33 | import org.itxtech.mirainative.Bridge 34 | import org.itxtech.mirainative.MiraiNative 35 | import org.itxtech.mirainative.MiraiNative.launchEvent 36 | import org.itxtech.mirainative.MiraiNative.setBotOnline 37 | import org.itxtech.mirainative.bridge.NativeBridge 38 | import org.itxtech.mirainative.message.ChainCodeConverter 39 | import org.itxtech.mirainative.message.ChainCodeConverter.escape 40 | 41 | object EventManager { 42 | @OptIn(MiraiExperimentalApi::class) 43 | fun registerEvents() { 44 | with(MiraiNative.globalEventChannel()) { 45 | subscribeAlways { 46 | setBotOnline() 47 | } 48 | 49 | subscribeAlways { 50 | setBotOnline() 51 | } 52 | 53 | // 消息事件 54 | subscribeAlways { 55 | launchEvent { 56 | NativeBridge.eventPrivateMessage( 57 | Bridge.PRI_MSG_SUBTYPE_FRIEND, 58 | CacheManager.cacheMessage(message.source, chain = message), 59 | sender.id, 60 | ChainCodeConverter.chainToCode(message), 61 | 0 62 | ) 63 | } 64 | } 65 | subscribeAlways { 66 | if (sender is AnonymousMember) { 67 | CacheManager.cacheAnonymousMember(this) 68 | } 69 | launchEvent { 70 | NativeBridge.eventGroupMessage( 71 | 1, 72 | CacheManager.cacheMessage(message.source, chain = message), 73 | group.id, 74 | sender.id, 75 | if (sender is AnonymousMember) (sender as AnonymousMember).anonymousId else "",//可能不兼容某些插件 76 | ChainCodeConverter.chainToCode(message), 77 | 0 78 | ) 79 | } 80 | } 81 | subscribeAlways { msg -> 82 | launchEvent { 83 | NativeBridge.eventPrivateMessage( 84 | Bridge.PRI_MSG_SUBTYPE_GROUP, 85 | CacheManager.cacheTempMessage(msg), 86 | sender.id, 87 | ChainCodeConverter.chainToCode(message), 88 | 0 89 | ) 90 | } 91 | } 92 | subscribeAlways { 93 | launchEvent { 94 | NativeBridge.eventPrivateMessage( 95 | Bridge.PRI_MSG_SUBTYPE_ONLINE_STATE, 96 | CacheManager.cacheMessage(message.source, chain = message), 97 | sender.id, 98 | ChainCodeConverter.chainToCode(message), 99 | 0 100 | ) 101 | } 102 | } 103 | 104 | // 权限事件 105 | subscribeAlways { 106 | launchEvent { 107 | NativeBridge.eventGroupAdmin( 108 | if (new == MemberPermission.MEMBER) Bridge.PERM_SUBTYPE_CANCEL_ADMIN else Bridge.PERM_SUBTYPE_SET_ADMIN, 109 | getTimestamp(), group.id, member.id 110 | ) 111 | } 112 | } 113 | subscribeAlways { 114 | launchEvent { 115 | NativeBridge.eventGroupAdmin( 116 | if (new == MemberPermission.MEMBER) Bridge.PERM_SUBTYPE_CANCEL_ADMIN else Bridge.PERM_SUBTYPE_SET_ADMIN, 117 | getTimestamp(), group.id, bot.id 118 | ) 119 | } 120 | } 121 | 122 | // 加群事件 123 | subscribeAlways { ev -> 124 | launchEvent { 125 | NativeBridge.eventGroupMemberJoin( 126 | if (ev is MemberJoinEvent.Invite) Bridge.MEMBER_JOIN_PERMITTED else Bridge.MEMBER_JOIN_INVITED_BY_ADMIN, 127 | getTimestamp(), group.id, if (ev is MemberJoinEvent.Invite) ev.invitor.id else 0, member.id 128 | ) 129 | } 130 | } 131 | subscribeAlways { ev -> 132 | launchEvent { 133 | NativeBridge.eventRequestAddGroup( 134 | Bridge.REQUEST_GROUP_APPLY, 135 | getTimestamp(), 136 | groupId, 137 | fromId, 138 | message.escape(false), 139 | CacheManager.cacheEvent(ev) 140 | ) 141 | } 142 | } 143 | subscribeAlways { ev -> 144 | launchEvent { 145 | NativeBridge.eventRequestAddGroup( 146 | Bridge.REQUEST_GROUP_INVITED, 147 | getTimestamp(), groupId, invitorId, "", CacheManager.cacheEvent(ev) 148 | ) 149 | } 150 | } 151 | subscribeAlways { 152 | launchEvent { 153 | NativeBridge.eventGroupMemberJoin( 154 | Bridge.MEMBER_JOIN_INVITED_BY_ADMIN, 155 | getTimestamp(), 156 | group.id, 157 | if (this@subscribeAlways is BotJoinGroupEvent.Invite) invitor.id else 0, 158 | bot.id 159 | ) 160 | } 161 | } 162 | 163 | //加好友事件 164 | subscribeAlways { ev -> 165 | launchEvent { 166 | NativeBridge.eventRequestAddFriend( 167 | 1, 168 | getTimestamp(), 169 | fromId, 170 | message.escape(false), 171 | CacheManager.cacheEvent(ev) 172 | ) 173 | } 174 | } 175 | subscribeAlways { 176 | launchEvent { 177 | NativeBridge.eventFriendAdd(1, getTimestamp(), friend.id) 178 | } 179 | } 180 | 181 | // 退群事件 182 | subscribeAlways { 183 | CacheManager.cacheMember(member) 184 | launchEvent { 185 | NativeBridge.eventGroupMemberLeave( 186 | if (it is MemberLeaveEvent.Kick) Bridge.MEMBER_LEAVE_KICK else Bridge.MEMBER_LEAVE_QUIT, 187 | getTimestamp(), 188 | group.id, 189 | if (it is MemberLeaveEvent.Kick) it.operator?.id ?: bot.id else 0, 190 | member.id 191 | ) 192 | } 193 | } 194 | subscribeAlways { 195 | launchEvent { 196 | NativeBridge.eventGroupMemberLeave( 197 | if (it is BotLeaveEvent.Kick) Bridge.MEMBER_LEAVE_KICK else Bridge.MEMBER_LEAVE_QUIT, 198 | getTimestamp(), group.id, if (it is BotLeaveEvent.Kick) it.operator.id else 0, bot.id 199 | ) 200 | } 201 | } 202 | 203 | // 禁言事件 204 | subscribeAlways { 205 | launchEvent { 206 | NativeBridge.eventGroupBan( 207 | Bridge.GROUP_MUTE, 208 | getTimestamp(), 209 | group.id, 210 | operator?.id ?: bot.id, 211 | member.id, 212 | durationSeconds.toLong() 213 | ) 214 | } 215 | } 216 | subscribeAlways { 217 | launchEvent { 218 | NativeBridge.eventGroupBan( 219 | Bridge.GROUP_UNMUTE, 220 | getTimestamp(), 221 | group.id, 222 | operator?.id ?: bot.id, 223 | member.id, 224 | 0 225 | ) 226 | } 227 | } 228 | subscribeAlways { 229 | launchEvent { 230 | NativeBridge.eventGroupBan( 231 | Bridge.GROUP_MUTE, 232 | getTimestamp(), 233 | group.id, 234 | operator.id, 235 | bot.id, 236 | durationSeconds.toLong() 237 | ) 238 | } 239 | } 240 | subscribeAlways { 241 | launchEvent { 242 | NativeBridge.eventGroupBan( 243 | Bridge.GROUP_UNMUTE, 244 | getTimestamp(), 245 | group.id, 246 | operator.id, 247 | bot.id, 248 | 0 249 | ) 250 | } 251 | } 252 | subscribeAlways { 253 | launchEvent { 254 | NativeBridge.eventGroupBan( 255 | if (new) Bridge.GROUP_MUTE else Bridge.GROUP_UNMUTE, 256 | getTimestamp(), 257 | group.id, 258 | operator?.id ?: bot.id, 259 | 0, 260 | 0 261 | ) 262 | } 263 | } 264 | } 265 | } 266 | 267 | fun getTimestamp() = System.currentTimeMillis().toInt() 268 | } 269 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/manager/LibraryManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.manager 26 | 27 | object LibraryManager { 28 | private val libs = arrayListOf() 29 | 30 | fun load(file: String) { 31 | if (!libs.contains(file)) { 32 | libs.add(file) 33 | System.load(file) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/manager/PluginManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.manager 26 | 27 | import kotlinx.atomicfu.atomic 28 | import kotlinx.coroutines.Job 29 | import kotlinx.coroutines.launch 30 | import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register 31 | import net.mamoe.mirai.console.command.CommandSender 32 | import net.mamoe.mirai.console.command.CompositeCommand 33 | import net.mamoe.mirai.console.util.ConsoleExperimentalApi 34 | import org.itxtech.mirainative.Bridge 35 | import org.itxtech.mirainative.MiraiNative 36 | import org.itxtech.mirainative.bridge.NativeBridge 37 | import org.itxtech.mirainative.plugin.NativePlugin 38 | import org.itxtech.mirainative.plugin.PluginInfo 39 | import org.itxtech.mirainative.toNative 40 | import org.itxtech.mirainative.ui.Tray 41 | import org.itxtech.mirainative.util.NpmHelper 42 | import java.io.File 43 | 44 | object PluginManager { 45 | private val pluginId = atomic(0) 46 | val plugins = hashMapOf() 47 | private val pl: File by lazy { File(MiraiNative.dataFolder.absolutePath + File.separatorChar + "plugins").also { it.mkdirs() } } 48 | 49 | fun loadPlugins() { 50 | if (!MiraiNative.dataFolder.isDirectory) { 51 | MiraiNative.logger.error("数据文件夹不是一个文件夹!" + MiraiNative.dataFolder.absolutePath) 52 | } else { 53 | MiraiNative.nativeLaunch { 54 | pl.listFiles()?.forEach { file -> 55 | if (file.isFile && file.absolutePath.endsWith("dll") && !file.absolutePath.endsWith("CQP.dll")) { 56 | readPlugin(file) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | fun unloadPlugins(): Job { 64 | MiraiNative.logger.info("正停用所有插件并调用Exit事件。") 65 | return MiraiNative.nativeLaunch { 66 | val it = plugins.values.iterator() 67 | while (it.hasNext()) { 68 | val p = it.next() 69 | unloadPlugin(p, false) 70 | it.remove() 71 | } 72 | plugins.clear() 73 | } 74 | } 75 | 76 | fun getPluginByIdentifier(id: String): NativePlugin? { 77 | plugins.values.forEach { 78 | if (it.identifier == id) { 79 | return it 80 | } 81 | } 82 | return null 83 | } 84 | 85 | fun readPluginFromFile(f: String): Boolean { 86 | val file = File(pl.absolutePath + File.separatorChar + f) 87 | if (f.endsWith(".dll") && file.isFile && file.exists()) { 88 | readPlugin(file) 89 | return true 90 | } 91 | return false 92 | } 93 | 94 | fun readPlugin(file: File) { 95 | plugins.values.forEach { 96 | if (it.loaded && it.file == file) { 97 | MiraiNative.logger.error("DLL ${file.absolutePath} 已被加载,无法重复加载。") 98 | return 99 | } 100 | } 101 | MiraiNative.nativeLaunch { 102 | val plugin = NativePlugin(file, pluginId.value) 103 | loadPlugin(plugin) 104 | plugins[pluginId.getAndIncrement()] = plugin 105 | Tray.update() 106 | } 107 | } 108 | 109 | fun loadPlugin(plugin: NativePlugin) { 110 | if (plugin.loaded) { 111 | return 112 | } 113 | lateinit var suffix: String 114 | val file = if (plugin.reloadable) { 115 | suffix = ".tmp" 116 | plugin.tempFile = File(plugin.file.absolutePath.replace(".dev.dll", suffix)) 117 | plugin.file.copyTo(plugin.tempFile!!, true) 118 | } else { 119 | suffix = ".dll" 120 | plugin.file 121 | } 122 | if (NativeBridge.loadPlugin(plugin, file) == 0) { 123 | val json = File(file.parent + File.separatorChar + file.name.replace(suffix, ".json")) 124 | 125 | try { 126 | plugin.pluginInfo = MiraiNative.json.decodeFromString( 127 | PluginInfo.serializer(), 128 | if (json.exists()) json.readText() else NativeBridge.getPluginInfo(plugin) 129 | ) 130 | } catch (ignored: Throwable) { 131 | } 132 | if (plugin.pluginInfo == null) { 133 | MiraiNative.logger.warning("无法找到 ${plugin.file.name} 的插件信息。") 134 | } 135 | plugin.loaded = true 136 | NativeBridge.updateInfo(plugin) 137 | } 138 | } 139 | 140 | fun unloadPlugin(plugin: NativePlugin, remove: Boolean = true) { 141 | with(plugin) { 142 | if (loaded) { 143 | disablePlugin(this) 144 | NativeBridge.exitPlugin(this) 145 | NativeBridge.unloadPlugin(this) 146 | loaded = false 147 | enabled = false 148 | started = false 149 | entries.forEach { it.vaild = false } 150 | entries.clear() 151 | events.clear() 152 | tempFile?.delete() 153 | if (remove) plugins.remove(id) 154 | Tray.update() 155 | } 156 | } 157 | } 158 | 159 | fun reloadPlugin(plugin: NativePlugin) { 160 | if (plugin.loaded) { 161 | if (plugin.reloadable) { 162 | unloadPlugin(plugin) 163 | loadPlugin(plugin) 164 | plugins[plugin.id] = plugin 165 | Tray.update() 166 | } else { 167 | MiraiNative.logger.error("插件 ${plugin.detailedIdentifier} 不可重新加载。文件名必须以 \".dev.dll\" 结尾。") 168 | } 169 | } 170 | } 171 | 172 | fun enablePlugin(plugin: NativePlugin): Boolean { 173 | if (MiraiNative.botOnline && !plugin.enabled) { 174 | if (!plugin.started) { 175 | NativeBridge.startPlugin(plugin) 176 | plugin.started = true 177 | } 178 | NativeBridge.enablePlugin(plugin) 179 | plugin.enabled = true 180 | Tray.update() 181 | return true 182 | } 183 | return false 184 | } 185 | 186 | fun disablePlugin(plugin: NativePlugin): Boolean { 187 | if (plugin.enabled) { 188 | NativeBridge.disablePlugin(plugin) 189 | plugin.enabled = false 190 | Tray.update() 191 | return true 192 | } 193 | return false 194 | } 195 | 196 | fun enablePlugins() { 197 | MiraiNative.nativeLaunch { 198 | plugins.values.forEach { 199 | if (it.autoEnable) { 200 | enablePlugin(it) 201 | } 202 | } 203 | } 204 | } 205 | 206 | @OptIn(ConsoleExperimentalApi::class) 207 | object NpmCommand : CompositeCommand( 208 | MiraiNative, "npm", 209 | description = "Mirai Native 插件管理器" 210 | ) { 211 | @Description("列出所有 Mirai Native 插件") 212 | @SubCommand 213 | suspend fun CommandSender.list() { 214 | sendMessage(buildString { 215 | appendLine("共加载了 ${plugins.size} 个 Mirai Native 插件") 216 | plugins.values.forEach { p -> 217 | if (p.pluginInfo != null) { 218 | appendLine( 219 | "Id:${p.id} 标识符:${p.identifier} 名称:${p.pluginInfo!!.name} 版本:${p.pluginInfo!!.version} ${ 220 | NpmHelper.state( 221 | p 222 | ) 223 | }" 224 | ) 225 | } else { 226 | appendLine("Id:${p.id} 标识符:${p.identifier} (JSON文件缺失)${NpmHelper.state(p)}") 227 | } 228 | } 229 | }) 230 | } 231 | 232 | @Description("启用指定 Mirai Native 插件") 233 | @SubCommand 234 | suspend fun CommandSender.enable(@Name("插件Id") id: Int) { 235 | sendMessage(buildString { 236 | if (!MiraiNative.botOnline) { 237 | appendLine("Bot 还未上线,无法调用 Enable 事件。") 238 | } else { 239 | if (plugins.containsKey(id)) { 240 | val p = plugins[id]!! 241 | MiraiNative.nativeLaunch { 242 | enablePlugin(p) 243 | } 244 | appendLine("插件 ${p.identifier} 已启用。") 245 | } else { 246 | appendLine("Id $id 不存在。") 247 | } 248 | } 249 | }) 250 | } 251 | 252 | @Description("停用指定 Mirai Native 插件") 253 | @SubCommand 254 | suspend fun CommandSender.disable(@Name("插件Id") id: Int) { 255 | sendMessage(buildString { 256 | if (plugins.containsKey(id)) { 257 | val p = plugins[id]!! 258 | MiraiNative.nativeLaunch { 259 | disablePlugin(p) 260 | } 261 | appendLine("插件 ${p.identifier} 已禁用。") 262 | } else { 263 | appendLine("Id $id 不存在。") 264 | } 265 | }) 266 | } 267 | 268 | @Description("调用指定 Mirai Native 插件的菜单方法") 269 | @SubCommand 270 | suspend fun CommandSender.menu(@Name("插件Id") id: Int, @Name("方法名") method: String) { 271 | sendMessage(buildString { 272 | if (plugins[id]?.verifyMenuFunc(method) == true) { 273 | MiraiNative.launch(MiraiNative.menuDispatcher) { 274 | Bridge.callIntMethod(id, method.toNative()) 275 | } 276 | appendLine("已调用 Id $id 的 $method 方法。") 277 | } else { 278 | appendLine("Id $id 不存在,或未注册该菜单入口。") 279 | } 280 | }) 281 | } 282 | 283 | @Description("查看指定 Mirai Native 插件的详细信息") 284 | @SubCommand 285 | suspend fun CommandSender.info(@Name("插件Id") id: Int) { 286 | sendMessage(buildString { 287 | if (plugins.containsKey(id)) { 288 | appendLine(NpmHelper.summary(plugins[id]!!)) 289 | } else { 290 | appendLine("Id $id 不存在。") 291 | } 292 | }) 293 | } 294 | 295 | @Description("加载指定DLL文件") 296 | @SubCommand 297 | suspend fun CommandSender.load(@Name("DLL文件名") file: String) { 298 | sendMessage(buildString { 299 | if (!readPluginFromFile(file)) { 300 | appendLine("文件 $file 不存在。") 301 | } 302 | }) 303 | } 304 | 305 | @Description("卸载指定 Mirai Native 插件") 306 | @SubCommand 307 | suspend fun CommandSender.unload(@Name("插件Id") id: Int) { 308 | sendMessage(buildString { 309 | if (plugins.containsKey(id)) { 310 | MiraiNative.nativeLaunch { 311 | unloadPlugin(plugins[id]!!) 312 | } 313 | } else { 314 | appendLine("Id $id 不存在。") 315 | } 316 | }) 317 | } 318 | 319 | @Description("重新载入指定 Mirai Native 插件") 320 | @SubCommand 321 | suspend fun CommandSender.reload(@Name("插件Id") id: Int) { 322 | sendMessage(buildString { 323 | if (plugins.containsKey(id)) { 324 | MiraiNative.nativeLaunch { 325 | reloadPlugin(plugins[id]!!) 326 | } 327 | } else { 328 | appendLine("Id $id 不存在。") 329 | } 330 | }) 331 | } 332 | } 333 | 334 | fun registerCommands() { 335 | NpmCommand.register() 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/message/ChainCodeConverter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.message 26 | 27 | import io.ktor.client.call.* 28 | import io.ktor.client.request.* 29 | import io.ktor.client.statement.* 30 | import io.ktor.util.* 31 | import net.mamoe.mirai.contact.AudioSupported 32 | import net.mamoe.mirai.contact.Contact 33 | import net.mamoe.mirai.contact.Group 34 | import net.mamoe.mirai.message.data.* 35 | import net.mamoe.mirai.message.data.PokeMessage.Key.ChuoYiChuo 36 | import net.mamoe.mirai.utils.ExternalResource 37 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 38 | import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage 39 | import net.mamoe.mirai.utils.MiraiExperimentalApi 40 | import org.itxtech.mirainative.MiraiNative 41 | import org.itxtech.mirainative.bridge.MiraiBridge 42 | import org.itxtech.mirainative.manager.CacheManager 43 | import org.itxtech.mirainative.util.Music 44 | import org.itxtech.mirainative.util.NeteaseMusic 45 | import org.itxtech.mirainative.util.QQMusic 46 | 47 | @OptIn(MiraiExperimentalApi::class) 48 | object ChainCodeConverter { 49 | private val MSG_EMPTY = PlainText("") 50 | 51 | fun String.escape(comma: Boolean): String { 52 | val s = replace("&", "&") 53 | .replace("[", "[") 54 | .replace("]", "]") 55 | return if (comma) s.replace(",", ",") else s 56 | } 57 | 58 | fun String.unescape(comma: Boolean): String { 59 | val s = replace("&", "&") 60 | .replace("[", "[") 61 | .replace("]", "]") 62 | return if (comma) s.replace(",", ",") else s 63 | } 64 | 65 | private fun String.toMap() = HashMap().apply { 66 | this@toMap.split(",").forEach { 67 | val parts = it.split(delimiters = arrayOf("="), limit = 2) 68 | this[parts[0].trim()] = parts[1].unescape(true).trim() 69 | } 70 | } 71 | 72 | private suspend inline fun String.useExternalResource(block: (ExternalResource) -> T): T { 73 | return MiraiBridge.client.get(this).body().toExternalResource().use(block) 74 | } 75 | 76 | private suspend fun String.toMessageInternal(contact: Contact?): Message { 77 | if (startsWith("[CQ:") && endsWith("]")) { 78 | val parts = substring(4, length - 1).split(delimiters = arrayOf(","), limit = 2) 79 | val args = if (parts.size == 2) { 80 | parts[1].toMap() 81 | } else { 82 | HashMap() 83 | } 84 | when (parts[0]) { 85 | "at" -> { 86 | if (args["qq"] == "all") { 87 | return AtAll 88 | } else { 89 | return if (contact !is Group) { 90 | MiraiNative.logger.debug("不能在私聊中发送 At。") 91 | MSG_EMPTY 92 | } else { 93 | val member = contact.get(args["qq"]!!.toLong()) 94 | if (member == null) { 95 | MiraiNative.logger.debug("无法找到群员:${args["qq"]}") 96 | MSG_EMPTY 97 | } else { 98 | At(member) 99 | } 100 | } 101 | } 102 | } 103 | "face" -> { 104 | return Face(args["id"]!!.toInt()) 105 | } 106 | "emoji" -> { 107 | return PlainText(String(Character.toChars(args["id"]!!.toInt()))) 108 | } 109 | "image" -> { 110 | var image: Image? = null 111 | if (args.containsKey("file")) { 112 | image = if (args["file"]!!.endsWith(".mnimg")) { 113 | Image(args["file"]!!.replace(".mnimg", "")) 114 | } else { 115 | MiraiNative.getDataFile("image", args["file"]!!)?.use { 116 | contact!!.uploadImage(it) 117 | } 118 | } 119 | } else if (args.containsKey("url")) { 120 | image = args["url"]!!.useExternalResource { 121 | it.uploadAsImage(contact!!) 122 | } 123 | } 124 | if (image != null) { 125 | if (args["type"] == "flash") { 126 | return image.flash() 127 | } 128 | return image 129 | } 130 | return MSG_EMPTY 131 | } 132 | "share" -> { 133 | return RichMessageHelper.share( 134 | args["url"]!!, 135 | args["title"], 136 | args["content"], 137 | args["image"] 138 | ) 139 | } 140 | "contact" -> { 141 | return if (args["type"] == "qq") { 142 | RichMessageHelper.contactQQ(args["id"]!!.toLong()) 143 | } else { 144 | RichMessageHelper.contactGroup(args["id"]!!.toLong()) 145 | } 146 | } 147 | "music" -> { 148 | when (args["type"]) { 149 | "qq" -> return QQMusic.send(args["id"]!!) 150 | "163" -> return NeteaseMusic.send(args["id"]!!) 151 | "custom" -> return Music.custom( 152 | args["url"]!!, 153 | args["audio"]!!, 154 | args["title"]!!, 155 | args["content"], 156 | args["image"] 157 | ) 158 | } 159 | } 160 | "shake" -> { 161 | return ChuoYiChuo 162 | } 163 | "poke" -> { 164 | PokeMessage.values.forEach { 165 | if (it.pokeType == args["type"]!!.toInt() && it.id == args["id"]!!.toInt()) { 166 | return it 167 | } 168 | } 169 | return MSG_EMPTY 170 | } 171 | "xml" -> { 172 | return xmlMessage(args["data"]!!) 173 | } 174 | "json" -> { 175 | return jsonMessage(args["data"]!!) 176 | } 177 | "app" -> { 178 | return LightApp(args["data"]!!) 179 | } 180 | "rich" -> { 181 | return SimpleServiceMessage(args["id"]!!.toInt(), args["data"]!!) 182 | } 183 | "record" -> { 184 | var rec: Audio? = null 185 | if (contact is AudioSupported) { 186 | if (args.containsKey("file")) { 187 | rec = if (args["file"]!!.endsWith(".mnrec")) { 188 | CacheManager.getRecord(args["file"]!!) 189 | } else { 190 | MiraiNative.getDataFile("record", args["file"]!!)?.use { 191 | contact.uploadAudio(it) 192 | } 193 | } 194 | } else if (args.containsKey("url")) { 195 | rec = args["url"]!!.useExternalResource { 196 | contact.uploadAudio(it) 197 | } 198 | } 199 | } 200 | return rec ?: MSG_EMPTY 201 | } 202 | "dice" -> { 203 | return Dice(args["type"]!!.toInt()) 204 | } 205 | else -> { 206 | MiraiNative.logger.debug("不支持的 CQ码:${parts[0]}") 207 | } 208 | } 209 | return MSG_EMPTY 210 | } 211 | return PlainText(unescape(false)) 212 | } 213 | 214 | fun chainToCode(chain: MessageChain): String { 215 | return chain.joinToString(separator = "") { 216 | when (it) { 217 | is At -> "[CQ:at,qq=${it.target}]" 218 | is AtAll -> "[CQ:at,qq=all]" 219 | is PlainText -> it.content.escape(false) 220 | is Face -> "[CQ:face,id=${it.id}]" 221 | is VipFace -> "[CQ:vipface,id=${it.kind.id},name=${it.kind.name},count=${it.count}]" 222 | is Image -> "[CQ:image,file=${it.imageId}.mnimg]" // Real file not supported 223 | is RichMessage -> { 224 | val content = it.content.escape(true) 225 | return@joinToString when (it) { 226 | is LightApp -> "[CQ:app,data=$content]" 227 | is ServiceMessage -> when (it.serviceId) { 228 | 60 -> "[CQ:xml,data=$content]" 229 | 1 -> "[CQ:json,data=$content]" 230 | else -> "[CQ:rich,data=${content},id=${it.serviceId}]" 231 | } 232 | else -> "[CQ:rich,data=$content]" // Which is impossible 233 | } 234 | } 235 | is Audio -> "[CQ:record,file=${it.filename}.mnrec]" 236 | is PokeMessage -> "[CQ:poke,id=${it.id},type=${it.pokeType},name=${it.name}]" 237 | is FlashImage -> "[CQ:image,file=${it.image.imageId}.mnimg,type=flash]" 238 | is Dice -> "[CQ:dice,type=${it.value}]" 239 | is MarketFace -> "[CQ:bface,id=${it.id},name=${it.name}]" 240 | else -> ""//error("不支持的消息类型:${it::class.simpleName}") 241 | } 242 | } 243 | } 244 | 245 | suspend fun codeToChain(message: String, contact: Contact?): MessageChain { 246 | return buildMessageChain { 247 | if (message.contains("[CQ:")) { 248 | var interpreting = false 249 | val sb = StringBuilder() 250 | var index = 0 251 | message.forEach { c: Char -> 252 | if (c == '[') { 253 | if (interpreting) { 254 | MiraiNative.logger.error("CQ消息解析失败:$message,索引:$index") 255 | return@forEach 256 | } else { 257 | interpreting = true 258 | if (sb.isNotEmpty()) { 259 | val lastMsg = sb.toString() 260 | sb.delete(0, sb.length) 261 | +lastMsg.toMessageInternal(contact) 262 | } 263 | sb.append(c) 264 | } 265 | } else if (c == ']') { 266 | if (!interpreting) { 267 | MiraiNative.logger.error("CQ消息解析失败:$message,索引:$index") 268 | return@forEach 269 | } else { 270 | interpreting = false 271 | sb.append(c) 272 | if (sb.isNotEmpty()) { 273 | val lastMsg = sb.toString() 274 | sb.delete(0, sb.length) 275 | +lastMsg.toMessageInternal(contact) 276 | } 277 | } 278 | } else { 279 | sb.append(c) 280 | } 281 | index++ 282 | } 283 | if (sb.isNotEmpty()) { 284 | +sb.toString().toMessageInternal(contact) 285 | } 286 | } else { 287 | +PlainText(message.unescape(false)) 288 | } 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/message/RichMessageHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.message 26 | 27 | import net.mamoe.mirai.message.data.ServiceMessage 28 | import net.mamoe.mirai.message.data.SimpleServiceMessage 29 | import net.mamoe.mirai.message.data.buildXmlMessage 30 | import net.mamoe.mirai.utils.MiraiExperimentalApi 31 | import org.itxtech.mirainative.MiraiNative 32 | 33 | @OptIn(MiraiExperimentalApi::class) 34 | object RichMessageHelper { 35 | fun share(u: String, title: String?, content: String?, image: String?) = buildXmlMessage(60) { 36 | templateId = 12345 37 | serviceId = 1 38 | action = "web" 39 | brief = "[分享] " + (title ?: "") 40 | url = u 41 | item { 42 | layout = 2 43 | if (image != null) { 44 | picture(image) 45 | } 46 | if (title != null) { 47 | title(title) 48 | } 49 | if (content != null) { 50 | summary(content) 51 | } 52 | } 53 | } 54 | 55 | fun contactQQ(id: Long): ServiceMessage { 56 | val nick = MiraiNative.bot.getFriend(id)?.nick 57 | return xmlMessage( 58 | "" + 59 | "" + 63 | "推荐联系人
" + 64 | "" + 65 | "" + 66 | "$nick帐号:$id
" 67 | ) 68 | } 69 | 70 | fun contactGroup(id: Long): SimpleServiceMessage { 71 | val group = MiraiNative.bot.getGroup(id) 72 | // TODO: 创建人,链接 73 | val founder = "未知创建人" 74 | val url = "https://github.com/mamoe/mirai" 75 | return xmlMessage( 76 | "\n" + 77 | "推荐群
" + 79 | "" + 80 | "${group?.name}创建人:$founder
" 81 | ) 82 | } 83 | } 84 | 85 | @OptIn(MiraiExperimentalApi::class) 86 | fun xmlMessage(content: String) = SimpleServiceMessage(60, content) 87 | 88 | @OptIn(MiraiExperimentalApi::class) 89 | fun jsonMessage(content: String) = SimpleServiceMessage(60, content) 90 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/plugin/NativePlugin.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.plugin 26 | 27 | import kotlinx.coroutines.delay 28 | import kotlinx.coroutines.isActive 29 | import org.itxtech.mirainative.MiraiNative 30 | import org.itxtech.mirainative.bridge.MiraiBridge 31 | import org.itxtech.mirainative.ui.FloatingWindow 32 | import java.io.File 33 | 34 | data class NativePlugin(val file: File, val id: Int) { 35 | var autoEnable = true 36 | var loaded = false 37 | var enabled = false 38 | var started = false 39 | var api = -1 40 | var identifier: String = file.name 41 | val appDir: File by lazy { 42 | File(MiraiNative.dataFolder.absolutePath + File.separatorChar + "data" + File.separatorChar + identifier).also { it.mkdirs() } 43 | } 44 | var pluginInfo: PluginInfo? = null 45 | set(v) { 46 | v!!.event.forEach { 47 | events[it.type] = it.function 48 | } 49 | if (v.status.isNotEmpty()) { 50 | registerFws(v.status) 51 | } 52 | field = v 53 | } 54 | val events = hashMapOf() 55 | val entries = arrayListOf() 56 | 57 | val reloadable 58 | get() = file.name.endsWith(".dev.dll") 59 | var tempFile: File? = null 60 | 61 | val detailedIdentifier 62 | get() = "\"$identifier\" (${file.name}) (ID: $id)" 63 | 64 | private fun registerFws(fws: ArrayList) { 65 | fws.forEach { 66 | val entry = FloatingWindowEntry(it) 67 | entries.add(entry) 68 | MiraiNative.nativeLaunch { 69 | while (isActive && loaded && entry.vaild) { 70 | if (enabled && entry.visible && FloatingWindow.visible) { 71 | MiraiBridge.updateFwe(id, entry) 72 | } 73 | delay(it.period.toLong()) 74 | } 75 | } 76 | } 77 | } 78 | 79 | fun getName() = pluginInfo?.name ?: identifier 80 | 81 | fun setInfo(i: String) { 82 | val parts = i.split(",") 83 | if (parts.size == 2) { 84 | api = parts[0].toInt() 85 | identifier = parts[1] 86 | } 87 | } 88 | 89 | fun getEventOrDefault(key: Int, default: String) = events.getOrDefault(key, default) 90 | 91 | fun shouldCallEvent(key: Int, ignoreState: Boolean = false): Boolean { 92 | if (!enabled && !ignoreState) { 93 | return false 94 | } 95 | return events.containsKey(key) 96 | } 97 | 98 | fun processMessage(key: Int, msg: String): String { 99 | //TODO: regex 100 | return msg 101 | } 102 | 103 | fun verifyMenuFunc(name: String): Boolean { 104 | if (pluginInfo == null) { 105 | return true 106 | } 107 | pluginInfo!!.menu.forEach { 108 | if (it.function == name) { 109 | return true 110 | } 111 | } 112 | return false 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/plugin/PluginInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.plugin 26 | 27 | import kotlinx.serialization.Serializable 28 | 29 | @Serializable 30 | data class PluginInfo( 31 | val ret: Int = 1, 32 | val apiver: Int, 33 | val name: String, 34 | val version: String = "", 35 | val version_id: Int = 0, 36 | val author: String = "", 37 | val description: String = "", 38 | val event: ArrayList = ArrayList(), 39 | val menu: ArrayList = ArrayList(), 40 | val status: ArrayList = ArrayList(), 41 | val auth: IntArray = IntArray(0) 42 | ) 43 | 44 | @Serializable 45 | data class Event( 46 | val id: Int, 47 | val type: Int, 48 | val name: String, 49 | val function: String, 50 | val priority: Int, 51 | val regex: Regex? = null 52 | ) { 53 | companion object { 54 | const val EVENT_STARTUP = 1001 55 | const val EVENT_EXIT = 1002 56 | const val EVENT_ENABLE = 1003 57 | const val EVENT_DISABLE = 1004 58 | 59 | const val EVENT_PRI_MSG = 21 60 | const val EVENT_GROUP_MSG = 2 61 | const val EVENT_DISCUSS_MSG = 4 62 | 63 | const val EVENT_GROUP_UPLOAD = 11 64 | const val EVENT_GROUP_ADMIN = 101 65 | const val EVENT_GROUP_MEMBER_DEC = 102 66 | const val EVENT_GROUP_MEMBER_INC = 103 67 | const val EVENT_GROUP_BAN = 104 68 | 69 | const val EVENT_FRIEND_ADD = 105 70 | 71 | const val EVENT_REQUEST_FRIEND = 301 72 | const val EVENT_REQUEST_GROUP = 302 73 | } 74 | } 75 | 76 | @Serializable 77 | data class Menu( 78 | val name: String, 79 | val function: String 80 | ) 81 | 82 | @Serializable 83 | data class Status( 84 | val id: Int, 85 | val name: String, 86 | val title: String, 87 | val function: String, 88 | val period: Int 89 | ) 90 | 91 | @Serializable 92 | data class Regex( 93 | val key: ArrayList, 94 | val expression: ArrayList 95 | ) 96 | 97 | data class FloatingWindowEntry(val status: Status) { 98 | var data = "" 99 | var unit = "" 100 | var color = 0 101 | var visible = false 102 | var vaild = true 103 | } 104 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/ui/FloatingWindow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.ui 26 | 27 | import kotlinx.coroutines.delay 28 | import kotlinx.coroutines.isActive 29 | import kotlinx.coroutines.launch 30 | import org.itxtech.mirainative.MiraiNative 31 | import org.itxtech.mirainative.manager.PluginManager 32 | import java.awt.GraphicsEnvironment 33 | import java.awt.event.ComponentAdapter 34 | import java.awt.event.ComponentEvent 35 | import javax.swing.JFrame 36 | import javax.swing.JPanel 37 | import javax.swing.JTextArea 38 | 39 | object FloatingWindow { 40 | private var window: JFrame? = null 41 | private var text: JTextArea? = null 42 | 43 | val visible: Boolean 44 | get() = window?.isVisible ?: false 45 | 46 | fun create() { 47 | try { 48 | val panel = JPanel() 49 | 50 | window = JFrame("Mirai Native 悬浮窗").apply { 51 | setSize(250, 150) 52 | isResizable = false 53 | isAlwaysOnTop = true 54 | 55 | val rect = 56 | GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration.bounds 57 | setLocation( 58 | (rect.maxX / 20 * 19.5 - width).toInt(), 59 | (rect.maxY / 20 * 19 - height).toInt() 60 | ) 61 | 62 | add(panel) 63 | 64 | addComponentListener(object : ComponentAdapter() { 65 | override fun componentHidden(e: ComponentEvent) { 66 | Tray.update() 67 | } 68 | }) 69 | } 70 | 71 | text = JTextArea().apply { 72 | setLocation(0, 0) 73 | setSize(250, 150) 74 | isEditable = false 75 | panel.add(this) 76 | } 77 | 78 | MiraiNative.launch { 79 | while (isActive) { 80 | update() 81 | delay(100) 82 | } 83 | } 84 | } catch (e: Throwable) { 85 | MiraiNative.logger.error(e) 86 | } 87 | } 88 | 89 | fun close() { 90 | if (window?.isVisible == true) { 91 | window!!.isVisible = false 92 | } 93 | window = null 94 | text = null 95 | } 96 | 97 | private fun update() { 98 | if (text != null) { 99 | val t = StringBuilder() 100 | PluginManager.plugins.values.forEach { p -> 101 | if (p.entries.isNotEmpty()) { 102 | p.entries.forEach { e -> 103 | if (p.enabled && e.visible) { 104 | t.append(e.status.name).append(": ").append(e.data).append(" ").appendLine(e.unit) 105 | } 106 | } 107 | } 108 | } 109 | text!!.text = t.toString() 110 | } 111 | } 112 | 113 | fun toggle() { 114 | if (window != null) { 115 | window!!.isVisible = !window!!.isVisible 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/ui/Tray.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.ui 26 | 27 | import kotlinx.coroutines.launch 28 | import org.itxtech.mirainative.Bridge 29 | import org.itxtech.mirainative.MiraiNative 30 | import org.itxtech.mirainative.manager.PluginManager 31 | import org.itxtech.mirainative.toNative 32 | import org.itxtech.mirainative.util.ConfigMan 33 | import org.itxtech.mirainative.util.NpmHelper 34 | import java.awt.* 35 | import java.awt.event.MouseAdapter 36 | import java.awt.event.MouseEvent 37 | import javax.imageio.ImageIO 38 | import javax.swing.JOptionPane 39 | 40 | object Tray { 41 | private var icon: TrayIcon? = null 42 | 43 | fun create() { 44 | try { 45 | if (SystemTray.isSupported()) { 46 | icon = TrayIcon(ImageIO.read(MiraiNative.getResourceAsStream("icon.jpg")), "Mirai Native 插件菜单").apply { 47 | addMouseListener(object : MouseAdapter() { 48 | override fun mouseClicked(e: MouseEvent?) { 49 | if (e?.button == 1 && !FloatingWindow.visible) { 50 | FloatingWindow.toggle() 51 | } 52 | } 53 | }) 54 | popupMenu = PopupMenu() 55 | SystemTray.getSystemTray().add(this) 56 | } 57 | update() 58 | } 59 | } catch (e: Throwable) { 60 | MiraiNative.logger.error(e) 61 | } 62 | } 63 | 64 | fun close() { 65 | if (icon != null) { 66 | SystemTray.getSystemTray().remove(icon) 67 | } 68 | } 69 | 70 | fun update() { 71 | if (icon != null) { 72 | icon!!.popupMenu = PopupMenu().apply { 73 | add(MenuItem().apply { 74 | fun lbl() = if (FloatingWindow.visible) "隐藏悬浮窗" else "显示悬浮窗" 75 | label = lbl() 76 | addActionListener { 77 | FloatingWindow.toggle() 78 | label = lbl() 79 | } 80 | }) 81 | 82 | addSeparator() 83 | 84 | add(MenuItem("NPM").apply { isEnabled = false }) 85 | 86 | val npm = Menu("插件管理") 87 | add(npm) 88 | 89 | add(MenuItem("加载 DLL").apply { 90 | addActionListener { 91 | val file = JOptionPane.showInputDialog("请输入位于 MiraiNative 目录下的 DLL文件名。") 92 | if (file != null) { 93 | if (!PluginManager.readPluginFromFile(file)) { 94 | JOptionPane.showMessageDialog( 95 | null, 96 | "加载 DLL 文件出错 “$file”。", 97 | "错误", 98 | JOptionPane.ERROR_MESSAGE 99 | ) 100 | } 101 | } 102 | } 103 | }) 104 | 105 | addSeparator() 106 | 107 | add(MenuItem("开发").apply { isEnabled = false }) 108 | add(MenuItem().apply { 109 | fun lbl() = (if (ConfigMan.config.verboseNativeApiLog) "禁用" else "启用") + "输出插件调用日志" 110 | label = lbl() 111 | addActionListener { 112 | ConfigMan.config.verboseNativeApiLog = !ConfigMan.config.verboseNativeApiLog 113 | label = lbl() 114 | } 115 | }) 116 | 117 | addSeparator() 118 | 119 | add(MenuItem("插件菜单").apply { isEnabled = false }) 120 | 121 | PluginManager.plugins.values.forEach { plugin -> 122 | if (plugin.loaded) { 123 | val p = Menu(NpmHelper.name(plugin)) 124 | npm.add(p) 125 | 126 | p.add(MenuItem("Id:" + plugin.id + " 版本:" + if (plugin.pluginInfo != null) plugin.pluginInfo!!.version else "未知").apply { 127 | isEnabled = false 128 | }) 129 | 130 | p.add(MenuItem(NpmHelper.state(plugin)).apply { isEnabled = false }) 131 | 132 | if (plugin.entries.isNotEmpty()) { 133 | p.add(Menu("状态").apply { 134 | plugin.entries.forEach { e -> 135 | add(MenuItem().apply { 136 | fun lbl() = e.status.name + ":" + if (e.visible) "显示" else "隐藏" 137 | label = lbl() 138 | addActionListener { 139 | e.visible = !e.visible 140 | label = lbl() 141 | } 142 | }) 143 | } 144 | }) 145 | } 146 | 147 | p.add(MenuItem("信息").apply { 148 | addActionListener { 149 | JOptionPane.showMessageDialog( 150 | null, 151 | NpmHelper.summary(plugin), 152 | "插件信息 " + NpmHelper.name(plugin), 153 | JOptionPane.INFORMATION_MESSAGE 154 | ) 155 | } 156 | }) 157 | 158 | p.add(MenuItem("卸载").apply { 159 | addActionListener { 160 | MiraiNative.nativeLaunch { 161 | PluginManager.unloadPlugin(plugin) 162 | } 163 | } 164 | }) 165 | 166 | 167 | if (plugin.reloadable) { 168 | p.add(MenuItem("重新加载").apply { 169 | addActionListener { 170 | MiraiNative.nativeLaunch { 171 | PluginManager.reloadPlugin(plugin) 172 | } 173 | } 174 | }) 175 | } 176 | 177 | p.add(MenuItem(if (plugin.enabled) "禁用" else "启用").apply { 178 | isEnabled = MiraiNative.botOnline 179 | addActionListener { 180 | MiraiNative.nativeLaunch { 181 | if (plugin.enabled) { 182 | PluginManager.disablePlugin(plugin) 183 | } else { 184 | PluginManager.enablePlugin(plugin) 185 | } 186 | } 187 | } 188 | }) 189 | 190 | if (plugin.pluginInfo != null && plugin.pluginInfo!!.menu.count() > 0) { 191 | add(Menu(plugin.pluginInfo!!.name).apply { 192 | plugin.pluginInfo!!.menu.forEach { m -> 193 | val item = MenuItem(m.name) 194 | item.addActionListener { 195 | MiraiNative.launch(MiraiNative.menuDispatcher) { 196 | Bridge.callIntMethod(plugin.id, m.function.toNative()) 197 | } 198 | } 199 | add(item) 200 | } 201 | }) 202 | } 203 | } 204 | } 205 | 206 | addSeparator() 207 | 208 | add(MenuItem("关于").apply { 209 | addActionListener { 210 | JOptionPane.showMessageDialog( 211 | null, "Mirai Native " + MiraiNative.getVersion() + 212 | "\nhttps://github.com/iTXTech/mirai-native" + 213 | "\n遵循 AGPL-3.0 协议开源" + 214 | "\n作者 PeratX@iTXTech.org" + 215 | "\n“流泪猫猫头”图标版权所有" + 216 | "\n版权所有 (C) 2020 iTX Technologies", 217 | "关于 Mirai Native", JOptionPane.INFORMATION_MESSAGE 218 | ) 219 | } 220 | }) 221 | } 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/util/Configuration.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.util 26 | 27 | import kotlinx.serialization.Serializable 28 | import kotlinx.serialization.json.Json 29 | import org.itxtech.mirainative.MiraiNative 30 | import org.itxtech.mirainative.manager.PluginManager 31 | import org.itxtech.mirainative.ui.FloatingWindow 32 | import java.io.File 33 | 34 | object ConfigMan { 35 | private val file = File(MiraiNative.dataFolder.absolutePath + File.separatorChar + "config.json") 36 | val config: Configuration by lazy { 37 | if (file.exists()) { 38 | MiraiNative.json.decodeFromString(Configuration.serializer(), file.readText()) 39 | } else { 40 | Configuration() 41 | } 42 | } 43 | 44 | fun init() { 45 | if (config.fwState && !FloatingWindow.visible) { 46 | FloatingWindow.toggle() 47 | } 48 | config.plugins.forEach { e -> 49 | val p = PluginManager.getPluginByIdentifier(e.id) 50 | if (p != null) { 51 | p.autoEnable = e.enable 52 | e.visibleFwes.forEach { f -> 53 | p.entries.forEach { 54 | if (it.status.title == f) { 55 | it.visible = true 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | fun save() { 64 | config.fwState = FloatingWindow.visible 65 | config.plugins = ArrayList() 66 | PluginManager.plugins.values.forEach { p -> 67 | val entry = PluginEntry() 68 | entry.id = p.identifier 69 | if (MiraiNative.botOnline) { 70 | entry.enable = p.enabled 71 | } 72 | p.entries.forEach { e -> 73 | if (e.visible) { 74 | entry.visibleFwes.add(e.status.title) 75 | } 76 | } 77 | config.plugins.add(entry) 78 | } 79 | file.writeText(Json.encodeToString(Configuration.serializer(), config)) 80 | } 81 | } 82 | 83 | @Serializable 84 | data class Configuration( 85 | var verboseNativeApiLog: Boolean = false, 86 | var fwState: Boolean = false, 87 | var cacheExpiration: Int = 60000, 88 | var plugins: ArrayList = ArrayList() 89 | ) 90 | 91 | @Serializable 92 | data class PluginEntry( 93 | var id: String = "", 94 | var enable: Boolean = true, 95 | var visibleFwes: ArrayList = ArrayList() 96 | ) 97 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/util/Music.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.util 26 | 27 | import io.ktor.client.request.* 28 | import io.ktor.client.statement.* 29 | import kotlinx.serialization.json.* 30 | import net.mamoe.mirai.message.data.Message 31 | import net.mamoe.mirai.message.data.MusicKind 32 | import net.mamoe.mirai.message.data.MusicShare 33 | import net.mamoe.mirai.message.data.SimpleServiceMessage 34 | import net.mamoe.mirai.utils.MiraiExperimentalApi 35 | import org.itxtech.mirainative.bridge.MiraiBridge 36 | import org.itxtech.mirainative.message.xmlMessage 37 | 38 | abstract class MusicProvider { 39 | val http = MiraiBridge.client 40 | 41 | abstract suspend fun send(id: String): Message 42 | } 43 | 44 | object Music { 45 | @OptIn(MiraiExperimentalApi::class) 46 | fun custom(url: String, audio: String, title: String, content: String?, image: String?) = 47 | xmlMessage( 48 | "" + 49 | "" + 52 | "" 57 | ) 58 | } 59 | 60 | object QQMusic : MusicProvider() { 61 | suspend fun search(name: String, page: Int, cnt: Int): JsonElement { 62 | val result = 63 | http.get("https://c.y.qq.com/soso/fcgi-bin/client_search_cp?aggr=1&cr=1&flag_qc=0&p=$page&n=$cnt&w=$name").bodyAsText() 64 | return Json.parseToJsonElement(result.substring(8, result.length - 1)) 65 | } 66 | 67 | suspend fun getPlayUrl(mid: String): String { 68 | val result = http.get( 69 | "https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg?&jsonpCallback=MusicJsonCallback&cid=205361747&songmid=" + 70 | mid + "&filename=C400" + mid + ".m4a&guid=7549058080" 71 | ).bodyAsText() 72 | val json = 73 | Json.parseToJsonElement(result).jsonObject.getValue("data").jsonObject.getValue("items").jsonArray[0].jsonObject 74 | if (json["subcode"]?.jsonPrimitive?.int == 0) { 75 | return "http://aqqmusic.tc.qq.com/amobile.music.tc.qq.com/C400$mid.m4a?guid=7549058080&vkey=${json["vkey"]!!.jsonPrimitive.content}&uin=0&fromtag=38" 76 | } 77 | return "" 78 | } 79 | 80 | suspend fun getSongInfo(id: String = "", mid: String = ""): JsonObject { 81 | val result = http.get( 82 | "https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&" + 83 | "platform=yqq.json&needNewCode=0&data=" + 84 | "{%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:" + 85 | "{%22song_type%22:0,%22song_mid%22:%22$mid%22,%22song_id%22:$id},%22module%22:%22music.pf_song_detail_svr%22}}" 86 | ).bodyAsText() 87 | return Json.parseToJsonElement(result).jsonObject.getValue("songinfo").jsonObject.getValue("data").jsonObject 88 | } 89 | 90 | override suspend fun send(id: String): Message { 91 | val info = getSongInfo(id) 92 | val trackInfo = info.getValue("track_info").jsonObject 93 | val url = getPlayUrl(trackInfo.getValue("file").jsonObject["media_mid"]!!.jsonPrimitive.content) 94 | val albumId = trackInfo.getValue("album").jsonObject["id"]!!.jsonPrimitive.content 95 | return MusicShare( 96 | kind = MusicKind.QQMusic, 97 | title = trackInfo["name"]!!.jsonPrimitive.content, 98 | summary = trackInfo.getValue("singer").jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content, 99 | jumpUrl = "https://i.y.qq.com/v8/playsong.html?_wv=1&songid=$id&souce=qqshare&source=qqshare&ADTAG=qqshare", 100 | pictureUrl = "http://imgcache.qq.com/music/photo/album_500/${albumId.substring(albumId.length - 2)}/500_albumpic_${albumId}_0.jpg", 101 | musicUrl = url, 102 | ) 103 | /*return toXmlMessage( 104 | trackInfo["name"]!!.jsonPrimitive.content, 105 | trackInfo.getValue("singer").jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content, 106 | id, 107 | trackInfo.getValue("album").jsonObject["id"]!!.jsonPrimitive.content, 108 | url 109 | )*/ 110 | } 111 | 112 | /*fun toXmlMessage(song: String, singer: String, songId: String, albumId: String, playUrl: String) = 113 | xmlMessage( 114 | "" + 115 | "" + 118 | "" 123 | )*/ 124 | } 125 | 126 | @OptIn(MiraiExperimentalApi::class) 127 | object NeteaseMusic : MusicProvider() { 128 | override suspend fun send(id: String): SimpleServiceMessage { 129 | TODO("Not yet implemented") 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/kotlin/org/itxtech/mirainative/util/NpmHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Mirai Native 4 | * 5 | * Copyright (C) 2020-2022 iTX Technologies 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the 10 | * License, or (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public License 18 | * along with this program. If not, see . 19 | * 20 | * @author PeratX 21 | * @website https://github.com/iTXTech/mirai-native 22 | * 23 | */ 24 | 25 | package org.itxtech.mirainative.util 26 | 27 | import org.itxtech.mirainative.plugin.NativePlugin 28 | 29 | object NpmHelper { 30 | fun state(p: NativePlugin, h: Boolean = true) = 31 | (if (h) "状态:" else "") + (if (p.enabled) "已启用 " else "已禁用 ") + (if (p.loaded) "已加载" else "已卸载") 32 | 33 | fun name(p: NativePlugin) = if (p.pluginInfo != null) p.pluginInfo!!.name else p.identifier 34 | 35 | fun summary(p: NativePlugin) = buildString { 36 | val i = p.pluginInfo 37 | appendLine("标识符:${p.identifier}") 38 | appendLine("状态:${state(p, false)}") 39 | if (i == null) { 40 | appendLine("Id:${p.id} (插件信息缺失)") 41 | appendLine("CQ API:${p.api}") 42 | } else { 43 | appendLine("Id:${p.id}") 44 | appendLine("CQ API:${p.api} CQ API(JSON):${i.apiver}") 45 | appendLine("名称:${i.name}") 46 | appendLine("版本:${i.version} 版本号:${i.version_id}") 47 | appendLine("描述:${i.description}") 48 | appendLine("作者:${i.author}") 49 | appendLine("注册了 ${i.event.size} 个事件") 50 | i.event.forEach { ev -> 51 | appendLine("类型:${ev.type} 描述:${ev.name} 方法名:${ev.function}") 52 | } 53 | appendLine("注册了 ${i.status.size} 个悬浮窗项目") 54 | i.status.forEach { s -> 55 | appendLine("名称:${s.name} 标题:${s.title} 方法名:${s.function}") 56 | } 57 | appendLine("注册了 ${i.menu.size} 个菜单入口") 58 | i.menu.forEach { m -> 59 | appendLine("名称:${m.name} 方法名:${m.function}") 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/resources/CQP.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTXTech/mirai-native/980b5e64086ac734544528536274b696fe3afe0e/src/main/resources/CQP.dll -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin: -------------------------------------------------------------------------------- 1 | org.itxtech.mirainative.MiraiNative 2 | -------------------------------------------------------------------------------- /src/main/resources/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTXTech/mirai-native/980b5e64086ac734544528536274b696fe3afe0e/src/main/resources/icon.jpg --------------------------------------------------------------------------------