diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..d040a2409
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,42 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+target/
+build/
+
+.idea/
+*.iml
+/.idea/
+.idea/*
+/.idea/*
+
+/test
+
+
+.gradle/
+
+
+local.properties
+
+# Maven publishing credits
+keys.properties
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index f288702d2..bae94e189 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
@@ -7,17 +7,15 @@
Preamble
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
+our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
+software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -72,7 +60,7 @@ modification follow.
0. Definitions.
- "This License" refers to version 3 of the GNU General Public License.
+ "This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
- 13. Use with the GNU Affero General Public License.
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
+under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
+Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
+GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
+versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
+ it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU General Public License
+ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
\ No newline at end of file
diff --git a/README.md b/README.md
index 028628d83..d577f7c14 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
-# mirai-console
-mirai的高效率QQ机器人框架
+# mirai-japt
+
+Mirai 的 Java API
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..a2c036fd6
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,44 @@
+buildscript {
+ repositories {
+ mavenLocal()
+ maven { url 'https://mirrors.huaweicloud.com/repository/maven' }
+ jcenter()
+ mavenCentral()
+ google()
+ maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
+ maven { url 'https://dl.bintray.com/kotlin/kotlin-dev' }
+ }
+
+ dependencies {
+ classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
+ classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
+ classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' // don't use any other.
+ }
+}
+
+try {
+ def keyProps = new Properties()
+ def keyFile = file("local.properties")
+ if (keyFile.exists()) keyFile.withInputStream { keyProps.load(it) }
+ if (!keyProps.getProperty("sdk.dir", "").isEmpty()) {
+ project.ext.set("isAndroidSDKAvailable", true)
+ } else {
+ project.ext.set("isAndroidSDKAvailable", false)
+ }
+} catch (Exception ignored) {
+}
+
+allprojects {
+ group = "net.mamoe"
+
+ repositories {
+ mavenLocal()
+ maven { url "https://mirrors.huaweicloud.com/repository/maven" }
+ jcenter()
+ mavenCentral()
+ google()
+ maven { url "https://dl.bintray.com/kotlin/kotlin-eap" }
+ maven { url "https://dl.bintray.com/kotlin/kotlin-dev" }
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..f94f42aab
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,20 @@
+# style guide
+kotlin.code.style=official
+# config
+mirai_version=0.22.0
+mirai_console_version=0.3.0
+kotlin.incremental.multiplatform=true
+kotlin.parallel.tasks.in.project=true
+# kotlin
+kotlinVersion=1.3.61
+# kotlin libraries
+serializationVersion=0.14.0
+coroutinesVersion=1.3.3
+atomicFuVersion=0.14.1
+kotlinXIoVersion=0.1.16
+coroutinesIoVersion=0.1.16
+# utility
+ktorVersion=1.3.1
+klockVersion=1.7.0
+# gradle plugin
+protobufJavaVersion=3.10.0
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..87b738cbd
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..ed4f40dea
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Feb 27 13:09:44 CST 2020
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100644
index 000000000..2fe81a7d9
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..9618d8d96
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/mirai-console-graphical/README.md b/mirai-console-graphical/README.md
new file mode 100644
index 000000000..cc26c6c15
--- /dev/null
+++ b/mirai-console-graphical/README.md
@@ -0,0 +1,6 @@
+### Mirai Console Graphical
+支持windows/mac
+有正式UI界面实现的CONSOLE
+优点: 适合新手/完全不懂编程的/界面美丽
+缺点: 不能在linux服务器运行
+所使用插件系统与terminal版本一致 可以来回切换
\ No newline at end of file
diff --git a/mirai-console-graphical/build.gradle.kts b/mirai-console-graphical/build.gradle.kts
new file mode 100644
index 000000000..4477f0e1b
--- /dev/null
+++ b/mirai-console-graphical/build.gradle.kts
@@ -0,0 +1,48 @@
+plugins {
+ id("kotlinx-serialization")
+ id("org.openjfx.javafxplugin") version "0.0.8"
+ id("kotlin")
+ id("java")
+}
+
+javafx {
+ version = "13.0.2"
+ modules = listOf("javafx.controls")
+ //mainClassName = "Application"
+}
+
+apply(plugin = "com.github.johnrengelman.shadow")
+
+val kotlinVersion: String by rootProject.ext
+val atomicFuVersion: String by rootProject.ext
+val coroutinesVersion: String by rootProject.ext
+val kotlinXIoVersion: String by rootProject.ext
+val coroutinesIoVersion: String by rootProject.ext
+
+val klockVersion: String by rootProject.ext
+val ktorVersion: String by rootProject.ext
+
+val serializationVersion: String by rootProject.ext
+
+fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
+
+fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
+
+val mirai_version: String by rootProject.ext
+
+dependencies {
+ implementation("net.mamoe:mirai-core-jvm:$mirai_version")
+ implementation("net.mamoe:mirai-core-qqandroid-jvm:$mirai_version")
+
+ // api(project(":mirai-api-http"))
+ api(project(":mirai-console"))
+ runtimeOnly(files("../mirai-core-qqandroid/build/classes/kotlin/jvm/main"))
+ api(group = "no.tornado", name = "tornadofx", version = "1.7.19")
+ api(group = "com.jfoenix", name = "jfoenix", version = "9.0.8")
+ api("org.bouncycastle:bcprov-jdk15on:1.64")
+ // classpath is not set correctly by IDE
+}
+
+tasks.withType {
+ kotlinOptions.jvmTarget = "1.8"
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt
new file mode 100644
index 000000000..51e61f081
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.console.graphical
+
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
+import net.mamoe.mirai.console.graphical.styleSheet.PrimaryStyleSheet
+import net.mamoe.mirai.console.graphical.view.Decorator
+import tornadofx.App
+import tornadofx.find
+import tornadofx.launch
+
+fun main(args: Array) {
+ launch(args)
+}
+
+class MiraiGraphicalUI : App(Decorator::class, PrimaryStyleSheet::class) {
+
+ override fun init() {
+ super.init()
+
+ MiraiConsole.start(find())
+ }
+
+ override fun stop() {
+ super.stop()
+ MiraiConsole.stop()
+ }
+}
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt
new file mode 100644
index 000000000..159b62e92
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/controller/MiraiGraphicalUIController.kt
@@ -0,0 +1,94 @@
+package net.mamoe.mirai.console.graphical.controller
+
+import javafx.application.Platform
+import javafx.collections.ObservableList
+import javafx.stage.Modality
+import kotlinx.io.core.IoBuffer
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.graphical.model.BotModel
+import net.mamoe.mirai.console.graphical.model.ConsoleInfo
+import net.mamoe.mirai.console.graphical.model.PluginModel
+import net.mamoe.mirai.console.graphical.model.VerificationCodeModel
+import net.mamoe.mirai.console.graphical.view.VerificationCodeFragment
+import net.mamoe.mirai.console.utils.MiraiConsoleUI
+import net.mamoe.mirai.utils.LoginSolver
+import tornadofx.*
+
+class MiraiGraphicalUIController : Controller(), MiraiConsoleUI {
+
+ private val loginSolver = GraphicalLoginSolver()
+ private val cache = mutableMapOf()
+ val mainLog = observableListOf()
+
+
+ val botList = observableListOf()
+ val pluginList: ObservableList by lazy(::getPluginsFromConsole)
+
+ val consoleInfo = ConsoleInfo()
+
+ fun login(qq: String, psd: String) {
+ MiraiConsole.CommandProcessor.runConsoleCommandBlocking("/login $qq $psd")
+ }
+
+ fun sendCommand(command: String) = MiraiConsole.CommandProcessor.runConsoleCommandBlocking(command)
+
+ override fun pushLog(identity: Long, message: String) = Platform.runLater {
+ when (identity) {
+ 0L -> mainLog.add(message)
+ else -> cache[identity]?.logHistory?.add(message)
+ }
+ }
+
+ override fun prePushBot(identity: Long) = Platform.runLater {
+ BotModel(identity).also {
+ cache[identity] = it
+ botList.add(it)
+ }
+ }
+
+ override fun pushBot(bot: Bot) = Platform.runLater {
+ cache[bot.uin]?.bot = bot
+ }
+
+ override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) {
+ Platform.runLater {
+ consoleInfo.consoleVersion = consoleVersion
+ consoleInfo.consoleBuild = consoleBuild
+ consoleInfo.coreVersion = coreVersion
+ }
+ }
+
+ override suspend fun requestInput(question: String): String {
+ val model = VerificationCodeModel()
+ find(Scope(model)).openModal(
+ modality = Modality.APPLICATION_MODAL,
+ resizable = false
+ )
+ return model.code.value
+ }
+
+ override fun pushBotAdminStatus(identity: Long, admins: List) = Platform.runLater {
+ cache[identity]?.admins?.setAll(admins)
+ }
+
+ override fun createLoginSolver(): LoginSolver = loginSolver
+
+ private fun getPluginsFromConsole(): ObservableList =
+ MiraiConsole.pluginManager.getAllPluginDescriptions().map(::PluginModel).toObservable()
+
+}
+
+class GraphicalLoginSolver : LoginSolver() {
+ override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
+ TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+ }
+
+ override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
+ TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+ }
+
+ override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
+ TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+ }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt
new file mode 100644
index 000000000..54de54c76
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt
@@ -0,0 +1,22 @@
+package net.mamoe.mirai.console.graphical.model
+
+import javafx.beans.property.SimpleObjectProperty
+import net.mamoe.mirai.Bot
+import tornadofx.ItemViewModel
+import tornadofx.getValue
+import tornadofx.observableListOf
+import tornadofx.setValue
+
+class BotModel(val uin: Long) {
+ val botProperty = SimpleObjectProperty(null)
+ var bot: Bot by botProperty
+
+ val logHistory = observableListOf()
+ val admins = observableListOf()
+}
+
+class BotViewModel(botModel: BotModel? = null) : ItemViewModel(botModel) {
+ val bot = bind(BotModel::botProperty)
+ val logHistory = bind(BotModel::logHistory)
+ val admins = bind(BotModel::admins)
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt
new file mode 100644
index 000000000..ea3182f48
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/ConsoleInfo.kt
@@ -0,0 +1,17 @@
+package net.mamoe.mirai.console.graphical.model
+
+import javafx.beans.property.SimpleStringProperty
+import tornadofx.getValue
+import tornadofx.setValue
+
+class ConsoleInfo {
+
+ val consoleVersionProperty = SimpleStringProperty()
+ var consoleVersion by consoleVersionProperty
+
+ val consoleBuildProperty = SimpleStringProperty()
+ var consoleBuild by consoleBuildProperty
+
+ val coreVersionProperty = SimpleStringProperty()
+ var coreVersion by coreVersionProperty
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/PluginModel.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/PluginModel.kt
new file mode 100644
index 000000000..9402a435d
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/PluginModel.kt
@@ -0,0 +1,19 @@
+package net.mamoe.mirai.console.graphical.model
+
+import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject
+import javafx.beans.property.SimpleBooleanProperty
+import net.mamoe.mirai.console.plugins.PluginDescription
+import tornadofx.getValue
+import tornadofx.setValue
+
+class PluginModel(
+ val name: String,
+ val version: String,
+ val author: String,
+ val description: String
+) : RecursiveTreeObject() {
+ constructor(plugin: PluginDescription) : this(plugin.name, plugin.version, plugin.author, plugin.info)
+
+ val enabledProperty = SimpleBooleanProperty(this, "enabledProperty")
+ var enabled by enabledProperty
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt
new file mode 100644
index 000000000..2eeadb35f
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/VerificationCodeModel.kt
@@ -0,0 +1,17 @@
+package net.mamoe.mirai.console.graphical.model
+
+import javafx.beans.property.SimpleStringProperty
+import tornadofx.ItemViewModel
+import tornadofx.getValue
+import tornadofx.setValue
+
+class VerificationCode {
+ val codeProperty = SimpleStringProperty("")
+ var code: String by codeProperty
+}
+
+class VerificationCodeModel(code: VerificationCode) : ItemViewModel(code) {
+ constructor() : this(VerificationCode())
+
+ val code = bind(VerificationCode::codeProperty)
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/LoginViewStyleSheet.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/LoginViewStyleSheet.kt
new file mode 100644
index 000000000..6e0b1f69c
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/LoginViewStyleSheet.kt
@@ -0,0 +1,47 @@
+package net.mamoe.mirai.console.graphical.styleSheet
+
+import javafx.scene.Cursor
+import javafx.scene.effect.BlurType
+import javafx.scene.effect.DropShadow
+import javafx.scene.paint.Color
+import javafx.scene.text.FontWeight
+import tornadofx.*
+
+class LoginViewStyleSheet : Stylesheet() {
+
+ companion object {
+ val vBox by csselement("VBox")
+ }
+
+ init {
+
+ vBox {
+ maxWidth = 500.px
+ maxHeight = 500.px
+
+ backgroundColor += c("39c5BB", 0.3)
+ backgroundRadius += box(15.px)
+
+ padding = box(50.px, 100.px)
+ spacing = 25.px
+
+ borderRadius += box(15.px)
+ effect = DropShadow(BlurType.THREE_PASS_BOX, Color.GRAY, 10.0, 0.0, 15.0, 15.0)
+ }
+
+ textField {
+ prefHeight = 30.px
+ textFill = Color.BLACK
+ fontWeight = FontWeight.BOLD
+ }
+
+ button {
+ backgroundColor += c("00BCD4", 0.8)
+ padding = box(10.px, 0.px)
+ prefWidth = 500.px
+ textFill = Color.WHITE
+ fontWeight = FontWeight.BOLD
+ cursor = Cursor.HAND
+ }
+ }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/PrimaryStyleSheet.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/PrimaryStyleSheet.kt
new file mode 100644
index 000000000..8f47b7a39
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/PrimaryStyleSheet.kt
@@ -0,0 +1,21 @@
+package net.mamoe.mirai.console.graphical.styleSheet
+
+import tornadofx.*
+
+class PrimaryStyleSheet : Stylesheet() {
+ companion object {
+ val jfxTitle by cssclass("jfx-decorator-buttons-container")
+ val container by cssclass("jfx-decorator-content-container")
+ }
+
+ init {
+ jfxTitle {
+ backgroundColor += c("00BCD4")
+ }
+
+ container {
+ borderColor += box(c("00BCD4"))
+ borderWidth += box(0.px, 4.px, 4.px, 4.px)
+ }
+ }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt
new file mode 100644
index 000000000..394cd82fa
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt
@@ -0,0 +1,56 @@
+package net.mamoe.mirai.console.graphical.util
+
+import com.jfoenix.controls.*
+import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject
+import javafx.beans.value.ObservableValue
+import javafx.collections.ObservableList
+import javafx.event.EventTarget
+import javafx.scene.Node
+import javafx.scene.control.Button
+import javafx.scene.control.ListView
+import javafx.scene.control.TabPane
+import tornadofx.SortedFilteredList
+import tornadofx.attachTo
+import tornadofx.bind
+
+internal fun EventTarget.jfxTabPane(op: TabPane.() -> Unit = {}) = JFXTabPane().attachTo(this, op)
+
+internal fun EventTarget.jfxButton(text: String = "", graphic: Node? = null, op: Button.() -> Unit = {}) =
+ JFXButton(text).attachTo(this, op) {
+ if (graphic != null) it.graphic = graphic
+ }
+
+fun EventTarget.jfxTextfield(value: String? = null, op: JFXTextField.() -> Unit = {}) =
+ JFXTextField().attachTo(this, op) {
+ if (value != null) it.text = value
+ }
+
+fun EventTarget.jfxTextfield(property: ObservableValue, op: JFXTextField.() -> Unit = {}) =
+ jfxTextfield().apply {
+ bind(property)
+ op(this)
+ }
+
+fun EventTarget.jfxPasswordfield(value: String? = null, op: JFXPasswordField.() -> Unit = {}) =
+ JFXPasswordField().attachTo(this, op) {
+ if (value != null) it.text = value
+ }
+
+fun EventTarget.jfxPasswordfield(property: ObservableValue, op: JFXPasswordField.() -> Unit = {}) =
+ jfxPasswordfield().apply {
+ bind(property)
+ op(this)
+ }
+
+internal fun EventTarget.jfxListView(values: ObservableList? = null, op: ListView.() -> Unit = {}) =
+ JFXListView().attachTo(this, op) {
+ if (values != null) {
+ if (values is SortedFilteredList) values.bindTo(it)
+ else it.items = values
+ }
+ }
+
+fun ?> EventTarget.jfxTreeTableView(
+ items: ObservableList? = null,
+ op: JFXTreeTableView.() -> Unit = {}
+) = JFXTreeTableView(RecursiveTreeItem(items, RecursiveTreeObject::getChildren)).attachTo(this, op)
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt
new file mode 100644
index 000000000..5531abe04
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt
@@ -0,0 +1,9 @@
+package net.mamoe.mirai.console.graphical.view
+
+import com.jfoenix.controls.JFXDecorator
+import tornadofx.View
+
+class Decorator : View() {
+
+ override val root = JFXDecorator(primaryStage, find().root)
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt
new file mode 100644
index 000000000..46b9d59dc
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt
@@ -0,0 +1,50 @@
+package net.mamoe.mirai.console.graphical.view
+
+import javafx.beans.property.SimpleStringProperty
+import javafx.geometry.Pos
+import javafx.scene.image.Image
+import kotlinx.coroutines.runBlocking
+import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
+import net.mamoe.mirai.console.graphical.styleSheet.LoginViewStyleSheet
+import net.mamoe.mirai.console.graphical.util.jfxButton
+import net.mamoe.mirai.console.graphical.util.jfxPasswordfield
+import net.mamoe.mirai.console.graphical.util.jfxTextfield
+import tornadofx.*
+
+class LoginView : View("CNM") {
+
+ private val controller = find()
+ private val qq = SimpleStringProperty("")
+ private val psd = SimpleStringProperty("")
+
+ override val root = borderpane {
+
+ addStylesheet(LoginViewStyleSheet::class)
+
+ center = vbox {
+
+ imageview(Image(LoginView::class.java.classLoader.getResourceAsStream("character.png"))) {
+ alignment = Pos.CENTER
+ }
+
+ jfxTextfield(qq) {
+ promptText = "QQ"
+ isLabelFloat = true
+ }
+
+ jfxPasswordfield(psd) {
+ promptText = "Password"
+ isLabelFloat = true
+ }
+
+ jfxButton("Login").action {
+ runAsync {
+ runBlocking { controller.login(qq.value, psd.value) }
+ }.ui {
+ qq.value = ""
+ psd.value = ""
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt
new file mode 100644
index 000000000..7351b2480
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PluginsView.kt
@@ -0,0 +1,33 @@
+package net.mamoe.mirai.console.graphical.view
+
+import com.jfoenix.controls.JFXTreeTableColumn
+import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
+import net.mamoe.mirai.console.graphical.model.PluginModel
+import net.mamoe.mirai.console.graphical.util.jfxTreeTableView
+import tornadofx.View
+
+class PluginsView : View() {
+
+ private val controller = find()
+ val plugins = controller.pluginList
+
+ override val root = jfxTreeTableView(plugins) {
+ columns.addAll(
+ JFXTreeTableColumn("插件名").apply {
+ prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.1))
+ },
+ JFXTreeTableColumn("版本").apply {
+ prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.1))
+ },
+ JFXTreeTableColumn("作者").apply {
+ prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.1))
+ },
+ JFXTreeTableColumn("介绍").apply {
+ prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.6))
+ },
+ JFXTreeTableColumn("操作").apply {
+ prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.08))
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt
new file mode 100644
index 000000000..a8aef22eb
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt
@@ -0,0 +1,99 @@
+package net.mamoe.mirai.console.graphical.view
+
+import com.jfoenix.controls.JFXListCell
+import javafx.collections.ObservableList
+import javafx.scene.control.Tab
+import javafx.scene.control.TabPane
+import javafx.scene.image.Image
+import javafx.scene.input.KeyCode
+import kotlinx.coroutines.runBlocking
+import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
+import net.mamoe.mirai.console.graphical.model.BotModel
+import net.mamoe.mirai.console.graphical.util.jfxListView
+import net.mamoe.mirai.console.graphical.util.jfxTabPane
+import tornadofx.*
+
+class PrimaryView : View() {
+
+ private val controller = find()
+
+ override val root = borderpane {
+
+ prefWidth = 1000.0
+ prefHeight = 650.0
+
+ left = vbox {
+
+ imageview(Image(PrimaryView::class.java.classLoader.getResourceAsStream("logo.png")))
+
+ // bot list
+ jfxListView(controller.botList) {
+ fitToParentSize()
+
+ setCellFactory {
+ object : JFXListCell() {
+ init {
+ onDoubleClick {
+ (center as TabPane).logTab(
+ text = item.uin.toString(),
+ logs = item.logHistory
+ ).select()
+ }
+ }
+
+ override fun updateItem(item: BotModel?, empty: Boolean) {
+ super.updateItem(item, empty)
+ if (item != null && !empty) {
+ graphic = null
+ text = item.uin.toString()
+ } else {
+ graphic = null
+ text = ""
+ }
+ }
+ }
+ }
+ }
+
+ // command input
+ textfield {
+ setOnKeyPressed {
+ if (it.code == KeyCode.ENTER) {
+ runAsync {
+ runBlocking { controller.sendCommand(text) }
+ }.ui { text = "" }
+ }
+ }
+ }
+ }
+
+ center = jfxTabPane {
+
+ tab("Login").content = find().root
+
+ tab("Plugins").content = find().root
+
+ tab("Settings").content = find().root
+
+ logTab("Main", controller.mainLog)
+ }
+ }
+}
+
+private fun TabPane.logTab(
+ text: String? = null,
+ logs: ObservableList,
+ op: Tab.() -> Unit = {}
+) = tab(text) {
+ listview(logs) {
+
+ fitToParentSize()
+ cellFormat {
+ graphic = label(it) {
+ maxWidthProperty().bind(this@listview.widthProperty())
+ isWrapText = true
+ }
+ }
+ }
+ also(op)
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt
new file mode 100644
index 000000000..d7d2d8302
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/SettingsView.kt
@@ -0,0 +1,39 @@
+package net.mamoe.mirai.console.graphical.view
+
+import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
+import net.mamoe.mirai.console.graphical.util.jfxButton
+import net.mamoe.mirai.console.graphical.util.jfxTextfield
+import tornadofx.View
+import tornadofx.field
+import tornadofx.fieldset
+import tornadofx.form
+
+class SettingsView : View() {
+
+ private val controller = find()
+
+ override val root = form {
+
+ fieldset {
+ field {
+ jfxButton("撤掉") { }
+ jfxButton("保存") { }
+ }
+ }
+
+ fieldset("插件目录") {
+ field {
+ jfxTextfield("...") { isEditable = false }
+ jfxButton("打开目录")
+ }
+ }
+
+ fieldset("最大日志容量") {
+ field {
+ jfxTextfield("...") {
+
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/VerificationCodeFragment.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/VerificationCodeFragment.kt
new file mode 100644
index 000000000..aaabb90e1
--- /dev/null
+++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/VerificationCodeFragment.kt
@@ -0,0 +1,18 @@
+package net.mamoe.mirai.console.graphical.view
+
+import tornadofx.*
+
+class VerificationCodeFragment : Fragment() {
+
+ override val root = vbox {
+ //TODO: 显示验证码
+
+ form {
+ fieldset {
+ field("验证码") {
+ textfield()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/mirai-console-graphical/src/main/resources/character.png b/mirai-console-graphical/src/main/resources/character.png
new file mode 100644
index 000000000..b55153f1e
Binary files /dev/null and b/mirai-console-graphical/src/main/resources/character.png differ
diff --git a/mirai-console-graphical/src/main/resources/logo.png b/mirai-console-graphical/src/main/resources/logo.png
new file mode 100644
index 000000000..bded5f137
Binary files /dev/null and b/mirai-console-graphical/src/main/resources/logo.png differ
diff --git a/mirai-console-terminal/README.md b/mirai-console-terminal/README.md
new file mode 100644
index 000000000..bd7980bfe
--- /dev/null
+++ b/mirai-console-terminal/README.md
@@ -0,0 +1,6 @@
+### Mirai Console Terminal
+支持windows/mac/linux
+在terminal环境下的Console, 由控制台富文本实现简易UI
+优点: 可以在linux环境下运行/简洁使用效率高
+缺点: 需要有略微的terminal知识
+所使用插件系统与graphical版本一致 可以来回切换
\ No newline at end of file
diff --git a/mirai-console-terminal/build.gradle.kts b/mirai-console-terminal/build.gradle.kts
new file mode 100644
index 000000000..f25916e58
--- /dev/null
+++ b/mirai-console-terminal/build.gradle.kts
@@ -0,0 +1,46 @@
+plugins {
+ id("kotlinx-serialization")
+ id("kotlin")
+ id("java")
+}
+
+
+apply(plugin = "com.github.johnrengelman.shadow")
+
+
+tasks.withType() {
+ manifest {
+ attributes["Main-Class"] = "net.mamoe.mirai.console.MiraiConsoleTerminalLoader"
+ }
+}
+
+val kotlinVersion: String by rootProject.ext
+val atomicFuVersion: String by rootProject.ext
+val coroutinesVersion: String by rootProject.ext
+val kotlinXIoVersion: String by rootProject.ext
+val coroutinesIoVersion: String by rootProject.ext
+
+val klockVersion: String by rootProject.ext
+val ktorVersion: String by rootProject.ext
+
+val serializationVersion: String by rootProject.ext
+
+fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
+
+fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
+
+
+val mirai_version: String by rootProject.ext
+
+dependencies {
+ implementation("net.mamoe:mirai-core-jvm:$mirai_version")
+ implementation("net.mamoe:mirai-core-qqandroid-jvm:$mirai_version")
+
+ // api(project(":mirai-api-http"))
+ api(project(":mirai-console"))
+ runtimeOnly(files("../mirai-core-qqandroid/build/classes/kotlin/jvm/main"))
+ runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main"))
+ api(group = "com.googlecode.lanterna", name = "lanterna", version = "3.0.2")
+ api("org.bouncycastle:bcprov-jdk15on:1.64")
+ // classpath is not set correctly by IDE
+}
\ No newline at end of file
diff --git a/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalLoader.kt b/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalLoader.kt
new file mode 100644
index 000000000..9da5cf122
--- /dev/null
+++ b/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalLoader.kt
@@ -0,0 +1,31 @@
+package net.mamoe.mirai.console
+
+import net.mamoe.mirai.console.pure.MiraiConsoleUIPure
+import kotlin.concurrent.thread
+
+class MiraiConsoleTerminalLoader {
+ companion object {
+ @JvmStatic
+ fun main(args: Array) {
+ if (args.contains("pure") || args.contains("-pure") || System.getProperty(
+ "os.name",
+ ""
+ ).toLowerCase().contains("windows")
+ ) {
+ println("[MiraiConsoleTerminalLoader]: 将以Pure[兼容模式]启动Console")
+ MiraiConsole.start(MiraiConsoleUIPure())
+ } else {
+ MiraiConsoleTerminalUI.start()
+ thread {
+ MiraiConsole.start(
+ MiraiConsoleTerminalUI
+ )
+ }
+ }
+ Runtime.getRuntime().addShutdownHook(thread(start = false) {
+ MiraiConsole.stop()
+ MiraiConsoleTerminalUI.exit()
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt b/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt
new file mode 100644
index 000000000..ebefb9e1a
--- /dev/null
+++ b/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt
@@ -0,0 +1,662 @@
+package net.mamoe.mirai.console
+
+import com.googlecode.lanterna.SGR
+import com.googlecode.lanterna.TerminalSize
+import com.googlecode.lanterna.TextColor
+import com.googlecode.lanterna.graphics.TextGraphics
+import com.googlecode.lanterna.input.KeyStroke
+import com.googlecode.lanterna.input.KeyType
+import com.googlecode.lanterna.terminal.DefaultTerminalFactory
+import com.googlecode.lanterna.terminal.Terminal
+import com.googlecode.lanterna.terminal.TerminalResizeListener
+import com.googlecode.lanterna.terminal.swing.SwingTerminal
+import com.googlecode.lanterna.terminal.swing.SwingTerminalFrame
+import kotlinx.coroutines.*
+import kotlinx.coroutines.io.close
+import kotlinx.io.core.IoBuffer
+import kotlinx.io.core.use
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.cleanPage
+import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.drawLog
+import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.redrawLogs
+import net.mamoe.mirai.console.utils.MiraiConsoleUI
+import net.mamoe.mirai.utils.LoginSolver
+import net.mamoe.mirai.utils.createCharImg
+import net.mamoe.mirai.utils.writeChannel
+import java.io.File
+import java.io.OutputStream
+import java.io.PrintStream
+import java.nio.charset.Charset
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentLinkedDeque
+import javax.imageio.ImageIO
+import kotlin.concurrent.thread
+import kotlin.system.exitProcess
+
+/**
+ * 此文件不推荐任何人看
+ * 可能导致
+ * 1:心肌梗死
+ * 2:呼吸困难
+ * 3:想要重写但是发现改任何一个看似不合理的地方都会崩
+ *
+ * @author NaturalHG
+ *
+ */
+
+fun String.actualLength(): Int {
+ var x = 0
+ this.forEach {
+ if (it.isChineseChar()) {
+ x += 2
+ } else {
+ x += 1
+ }
+ }
+ return x
+}
+
+fun String.getSubStringIndexByActualLength(widthMax: Int): Int {
+ var index = 0
+ var currentLength = 0
+ this.forEach {
+ if (it.isChineseChar()) {
+ currentLength += 2
+ } else {
+ currentLength += 1
+ }
+ if (currentLength > widthMax) {
+ return@forEach
+ }
+ ++index
+ }
+ if (index < 2) {
+ index = 2
+ }
+ return index
+}
+
+fun Char.isChineseChar(): Boolean {
+ return this.toString().isChineseChar()
+}
+
+fun String.isChineseChar(): Boolean {
+ return this.matches(Regex("[\u4e00-\u9fa5]"))
+}
+
+
+object MiraiConsoleTerminalUI : MiraiConsoleUI {
+ val cacheLogSize = 50
+ var mainTitle = "Mirai Console v0.01 Core v0.15"
+
+ override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) {
+ mainTitle = "Mirai Console(Terminal) $consoleVersion $consoleBuild Core $coreVersion"
+ }
+
+ override fun pushLog(identity: Long, message: String) {
+ log[identity]!!.push(message)
+ if (identity == screens[currentScreenId]) {
+ drawLog(message)
+ }
+ }
+
+ override fun prePushBot(identity: Long) {
+ log[identity] = LimitLinkedQueue(cacheLogSize)
+ }
+
+ override fun pushBot(bot: Bot) {
+ botAdminCount[bot.uin] = 0
+ screens.add(bot.uin)
+ drawFrame(this.getScreenName(currentScreenId))
+ if (terminal is SwingTerminalFrame) {
+ terminal.flush()
+ }
+ }
+
+ var requesting = false
+ var requestResult: String? = null
+ override suspend fun requestInput(question: String): String {
+ requesting = true
+ while (requesting) {
+ delay(100)//不然会卡死 迷惑吧
+ }
+ return requestResult!!
+ }
+
+
+ suspend fun provideInput(input: String) {
+ if (requesting) {
+ requestResult = input
+ requesting = false
+ } else {
+ MiraiConsole.CommandProcessor.runConsoleCommand(commandBuilder.toString())
+ }
+ }
+
+
+ override fun pushBotAdminStatus(identity: Long, admins: List) {
+ botAdminCount[identity] = admins.size
+ }
+
+ override fun createLoginSolver(): LoginSolver {
+ return object : LoginSolver() {
+ override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? {
+ val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() }
+ withContext(Dispatchers.IO) {
+ tempFile.createNewFile()
+ pushLog(0, "[Login Solver]需要图片验证码登录, 验证码为 4 字母")
+ try {
+ tempFile.writeChannel().apply {
+ writeFully(data)
+ close()
+ }
+ pushLog(0, "请查看文件 ${tempFile.absolutePath}")
+ } catch (e: Exception) {
+ error("[Login Solver]验证码无法保存[Error0001]")
+ }
+ }
+
+ var toLog = ""
+ tempFile.inputStream().use {
+ val img = ImageIO.read(it)
+ if (img == null) {
+ toLog += "无法创建字符图片. 请查看文件\n"
+ } else {
+ toLog += img.createCharImg((terminal.terminalSize.columns / 1.5).toInt())
+ }
+ }
+ pushLog(0, "$toLog[Login Solver]请输验证码. ${tempFile.absolutePath}")
+ return requestInput("[Login Solver]请输入 4 位字母验证码. 若要更换验证码, 请直接回车")!!
+ .takeUnless { it.isEmpty() || it.length != 4 }
+ .also {
+ pushLog(0, "[Login Solver]正在提交[$it]中...")
+ }
+ }
+
+ override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
+ pushLog(0, "[Login Solver]需要滑动验证码")
+ pushLog(0, "[Login Solver]请在任意浏览器中打开以下链接并完成验证码. ")
+ pushLog(0, "[Login Solver]完成后请输入任意字符 ")
+ pushLog(0, url)
+ return requestInput("[Login Solver]完成后请输入任意字符").also {
+ pushLog(0, "[Login Solver]正在提交中")
+ }
+ }
+
+ override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
+ pushLog(0, "[Login Solver]需要进行账户安全认证")
+ pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题")
+ pushLog(0, "[Login Solver]完成以下账号认证即可成功登录|理论本认证在mirai每个账户中最多出现1次")
+ pushLog(0, "[Login Solver]请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符")
+ pushLog(0, "[Login Solver]这步操作将在后续的版本中优化")
+ pushLog(0, url)
+ return requestInput("[Login Solver]完成后请输入任意字符").also {
+ pushLog(0, "[Login Solver]正在提交中...")
+ }
+ }
+
+ }
+ }
+
+ val log = ConcurrentHashMap>().also {
+ it[0L] = LimitLinkedQueue(cacheLogSize)
+ }
+
+ val botAdminCount = ConcurrentHashMap()
+
+ private val screens = mutableListOf(0L)
+ private var currentScreenId = 0
+
+
+ lateinit var terminal: Terminal
+ lateinit var textGraphics: TextGraphics
+
+ var hasStart = false
+ private lateinit var internalPrinter: PrintStream
+ fun start() {
+ if (hasStart) {
+ return
+ }
+
+ internalPrinter = System.out
+
+
+ hasStart = true
+ val defaultTerminalFactory = DefaultTerminalFactory(internalPrinter, System.`in`, Charset.defaultCharset())
+ try {
+ terminal = defaultTerminalFactory.createTerminal()
+ terminal.enterPrivateMode()
+ terminal.clearScreen()
+ terminal.setCursorVisible(false)
+ } catch (e: Exception) {
+ try {
+ terminal = SwingTerminalFrame("Mirai Console")
+ terminal.enterPrivateMode()
+ terminal.clearScreen()
+ terminal.setCursorVisible(false)
+ } catch (e: Exception) {
+ error("can not create terminal")
+ }
+ }
+ textGraphics = terminal.newTextGraphics()
+
+ /*
+ var lastRedrawTime = 0L
+ var lastNewWidth = 0
+ var lastNewHeight = 0
+
+ terminal.addResizeListener(TerminalResizeListener { terminal1: Terminal, newSize: TerminalSize ->
+ try {
+ if (lastNewHeight == newSize.rows
+ &&
+ lastNewWidth == newSize.columns
+ ) {
+ return@TerminalResizeListener
+ }
+ lastNewHeight = newSize.rows
+ lastNewWidth = newSize.columns
+ terminal.clearScreen()
+ if(terminal !is SwingTerminalFrame) {
+ Thread.sleep(300)
+ }
+ update()
+ redrawCommand()
+ redrawLogs(log[screens[currentScreenId]]!!)
+ }catch (ignored:Exception){
+
+ }
+ })
+
+ */
+ var lastJob: Job? = null
+ terminal.addResizeListener(TerminalResizeListener { terminal1: Terminal, newSize: TerminalSize ->
+ lastJob = GlobalScope.launch {
+ try {
+ delay(300)
+ if (lastJob == coroutineContext[Job]) {
+ terminal.clearScreen()
+ //inited = false
+ update()
+ redrawCommand()
+ redrawLogs(log[screens[currentScreenId]]!!)
+ }
+ } catch (e: Exception) {
+ pushLog(0, "[UI ERROR] ${e.message}")
+ }
+ }
+ })
+
+ if (terminal !is SwingTerminalFrame) {
+ System.setOut(PrintStream(object : OutputStream() {
+ var builder = java.lang.StringBuilder()
+ override fun write(b: Int) {
+ with(b.toChar()) {
+ if (this == '\n') {
+ pushLog(0, builder.toString())
+ builder = java.lang.StringBuilder()
+ } else {
+ builder.append(this)
+ }
+ }
+ }
+ }))
+ }
+
+ System.setErr(System.out)
+
+ try {
+ update()
+ } catch (e: Exception) {
+ pushLog(0, "[UI ERROR] ${e.message}")
+ }
+
+ val charList = listOf(',', '.', '/', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '=', '+', '!', ' ')
+ thread {
+ while (true) {
+ try {
+ var keyStroke: KeyStroke = terminal.readInput()
+
+ when (keyStroke.keyType) {
+ KeyType.ArrowLeft -> {
+ currentScreenId =
+ getLeftScreenId()
+ clearRows(2)
+ cleanPage()
+ update()
+ }
+ KeyType.ArrowRight -> {
+ currentScreenId =
+ getRightScreenId()
+ clearRows(2)
+ cleanPage()
+ update()
+ }
+ KeyType.Enter -> {
+ runBlocking {
+ provideInput(commandBuilder.toString())
+ }
+ emptyCommand()
+ }
+ KeyType.Escape -> {
+ exit()
+ }
+ else -> {
+ if (keyStroke.character != null) {
+ if (keyStroke.character.toInt() == 8) {
+ deleteCommandChar()
+ }
+ if (keyStroke.character.isLetterOrDigit() || charList.contains(keyStroke.character)) {
+ addCommandChar(keyStroke.character)
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ pushLog(0, "[UI ERROR] ${e.message}")
+ }
+ }
+ }
+ }
+
+ private fun getLeftScreenId(): Int {
+ var newId = currentScreenId - 1
+ if (newId < 0) {
+ newId = screens.size - 1
+ }
+ return newId
+ }
+
+ private fun getRightScreenId(): Int {
+ var newId = 1 + currentScreenId
+ if (newId >= screens.size) {
+ newId = 0
+ }
+ return newId
+ }
+
+ private fun getScreenName(id: Int): String {
+ return when (screens[id]) {
+ 0L -> {
+ "Console Screen"
+ }
+ else -> {
+ "Bot: ${screens[id]}"
+ }
+ }
+ }
+
+
+ fun clearRows(row: Int) {
+ textGraphics.putString(
+ 0, row, " ".repeat(
+ terminal.terminalSize.columns
+ )
+ )
+ }
+
+ fun drawFrame(
+ title: String
+ ) {
+ val width = terminal.terminalSize.columns
+ val height = terminal.terminalSize.rows
+ terminal.setBackgroundColor(TextColor.ANSI.DEFAULT)
+
+ textGraphics.foregroundColor = TextColor.ANSI.WHITE
+ textGraphics.backgroundColor = TextColor.ANSI.GREEN
+ textGraphics.putString((width - mainTitle.actualLength()) / 2, 1, mainTitle, SGR.BOLD)
+ textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
+ textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
+ textGraphics.putString(2, 3, "-".repeat(width - 4))
+ textGraphics.putString(2, 5, "-".repeat(width - 4))
+ textGraphics.putString(2, height - 4, "-".repeat(width - 4))
+ textGraphics.putString(2, height - 3, "|>>>")
+ textGraphics.putString(width - 3, height - 3, "|")
+ textGraphics.putString(2, height - 2, "-".repeat(width - 4))
+
+ textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
+ textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
+ val leftName =
+ getScreenName(getLeftScreenId())
+ // clearRows(2)
+ textGraphics.putString((width - title.actualLength()) / 2 - "$leftName << ".length, 2, "$leftName << ")
+ textGraphics.foregroundColor = TextColor.ANSI.WHITE
+ textGraphics.backgroundColor = TextColor.ANSI.YELLOW
+ textGraphics.putString((width - title.actualLength()) / 2, 2, title, SGR.BOLD)
+ textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
+ textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
+ val rightName =
+ getScreenName(getRightScreenId())
+ textGraphics.putString((width + title.actualLength()) / 2 + 1, 2, ">> $rightName")
+ }
+
+ fun drawMainFrame(
+ onlineBotCount: Number
+ ) {
+ drawFrame("Console Screen")
+ val width = terminal.terminalSize.columns
+ textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
+ textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
+ clearRows(4)
+ textGraphics.putString(2, 4, "|Online Bots: $onlineBotCount")
+ textGraphics.putString(
+ width - 2 - "Powered By Mamoe Technologies|".actualLength(),
+ 4,
+ "Powered By Mamoe Technologies|"
+ )
+ }
+
+ fun drawBotFrame(
+ qq: Long,
+ adminCount: Number
+ ) {
+ drawFrame("Bot: $qq")
+ val width = terminal.terminalSize.columns
+ textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
+ textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
+ clearRows(4)
+ textGraphics.putString(2, 4, "|Admins: $adminCount")
+ textGraphics.putString(width - 2 - "Add admins via commands|".actualLength(), 4, "Add admins via commands|")
+ }
+
+
+ object LoggerDrawer {
+ var currentHeight = 6
+
+ fun drawLog(string: String, flush: Boolean = true) {
+ val maxHeight = terminal.terminalSize.rows - 4
+ val heightNeed = (string.actualLength() / (terminal.terminalSize.columns - 6)) + 1
+ if (heightNeed - 1 > maxHeight) {
+ pushLog(0, "[UI ERROR]: 您的屏幕太小, 有一条超长LOG无法显示")
+ return//拒绝打印
+ }
+ if (currentHeight + heightNeed > maxHeight) {
+ cleanPage()//翻页
+ }
+ if (string.contains("\n")) {
+ string.split("\n").forEach {
+ drawLog(string, false)
+ }
+ } else {
+ val width = terminal.terminalSize.columns - 6
+ var x = string
+ while (true) {
+ if (x == "") {
+ break
+ }
+ val toWrite = if (x.actualLength() > width) {
+ val index = x.getSubStringIndexByActualLength(width)
+ x.substring(0, index).also {
+ x = if (index < x.length) {
+ x.substring(index)
+ } else {
+ ""
+ }
+ }
+ } else {
+ x.also {
+ x = ""
+ }
+ }
+ try {
+ textGraphics.foregroundColor = TextColor.ANSI.GREEN
+ textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
+ textGraphics.putString(
+ 3,
+ currentHeight, toWrite, SGR.ITALIC
+ )
+ } catch (ignored: Exception) {
+ //
+ }
+ ++currentHeight
+ }
+ }
+ if (flush && terminal is SwingTerminalFrame) {
+ terminal.flush()
+ }
+ }
+
+
+ fun cleanPage() {
+ for (index in 6 until terminal.terminalSize.rows - 4) {
+ clearRows(index)
+ }
+ currentHeight = 6
+ }
+
+
+ fun redrawLogs(toDraw: Queue) {
+ //this.cleanPage()
+ currentHeight = 6
+ var logsToDraw = 0
+ var vara = 0
+ val toPrint = mutableListOf()
+ toDraw.forEach {
+ val heightNeed = (it.actualLength() / (terminal.terminalSize.columns - 6)) + 1
+ vara += heightNeed
+ if (currentHeight + vara < terminal.terminalSize.rows - 4) {
+ logsToDraw++
+ toPrint.add(it)
+ } else {
+ return@forEach
+ }
+ }
+ toPrint.reversed().forEach {
+ drawLog(it, false)
+ }
+ if (terminal is SwingTerminalFrame) {
+ terminal.flush()
+ }
+ }
+ }
+
+
+ var commandBuilder = StringBuilder()
+ fun redrawCommand() {
+ val height = terminal.terminalSize.rows
+ val width = terminal.terminalSize.columns
+ clearRows(height - 3)
+ textGraphics.foregroundColor = TextColor.ANSI.DEFAULT
+ textGraphics.putString(2, height - 3, "|>>>")
+ textGraphics.putString(width - 3, height - 3, "|")
+ textGraphics.foregroundColor = TextColor.ANSI.WHITE
+ textGraphics.backgroundColor = TextColor.ANSI.BLACK
+ textGraphics.putString(7, height - 3, commandBuilder.toString())
+ if (terminal is SwingTerminalFrame) {
+ terminal.flush()
+ }
+ textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
+ }
+
+ private fun addCommandChar(
+ c: Char
+ ) {
+ if (!requesting && commandBuilder.isEmpty() && c != '/') {
+ addCommandChar('/')
+ }
+ textGraphics.foregroundColor = TextColor.ANSI.WHITE
+ textGraphics.backgroundColor = TextColor.ANSI.BLACK
+ val height = terminal.terminalSize.rows
+ commandBuilder.append(c)
+ if (terminal is SwingTerminalFrame) {
+ redrawCommand()
+ } else {
+ textGraphics.putString(6 + commandBuilder.length, height - 3, c.toString())
+ }
+ textGraphics.backgroundColor = TextColor.ANSI.DEFAULT
+ }
+
+ private fun deleteCommandChar() {
+ if (!commandBuilder.isEmpty()) {
+ commandBuilder = StringBuilder(commandBuilder.toString().substring(0, commandBuilder.length - 1))
+ }
+ val height = terminal.terminalSize.rows
+ if (terminal is SwingTerminalFrame) {
+ redrawCommand()
+ } else {
+ textGraphics.putString(7 + commandBuilder.length, height - 3, " ")
+ }
+ }
+
+
+ var lastEmpty: Job? = null
+ private fun emptyCommand() {
+ commandBuilder = StringBuilder()
+ if (terminal is SwingTerminal) {
+ redrawCommand()
+ terminal.flush()
+ } else {
+ lastEmpty = GlobalScope.launch {
+ try {
+ delay(100)
+ if (lastEmpty == coroutineContext[Job]) {
+ terminal.clearScreen()
+ //inited = false
+ update()
+ redrawCommand()
+ redrawLogs(log[screens[currentScreenId]]!!)
+ }
+ } catch (e: Exception) {
+ pushLog(0, "[UI ERROR] ${e.message}")
+ }
+ }
+ }
+ }
+
+ fun update() {
+ when (screens[currentScreenId]) {
+ 0L -> {
+ drawMainFrame(screens.size - 1)
+ }
+ else -> {
+ drawBotFrame(
+ screens[currentScreenId],
+ 0
+ )
+ }
+ }
+ redrawLogs(log[screens[currentScreenId]]!!)
+ }
+
+ fun exit() {
+ try {
+ terminal.exitPrivateMode()
+ terminal.close()
+ exitProcess(0)
+ } catch (ignored: Exception) {
+
+ }
+ }
+}
+
+
+class LimitLinkedQueue(
+ val limit: Int = 50
+) : ConcurrentLinkedDeque() {
+ override fun push(e: T) {
+ if (size >= limit) {
+ this.pollLast()
+ }
+ return super.push(e)
+ }
+}
diff --git a/mirai-console/README.MD b/mirai-console/README.MD
new file mode 100644
index 000000000..6d5db89bc
--- /dev/null
+++ b/mirai-console/README.MD
@@ -0,0 +1,87 @@
+# Mirai Console
+你可以在全平台运行Mirai高效率机器人框架
+### Mirai Console提供了6个版本以满足各种需要
+#### 所有版本的Mirai Console API相同 插件系统相同
+
+| 名字 | 介绍 |
+|:------------------------|:------------------------------|
+| Mirai-Console-Pure | 最纯净版, CLI环境, 通过标准输入与标准输出 交互 |
+| Mirai-Console-Terminal | (UNIX)Terminal环境 提供简洁的富文本控制台 |
+| Mirai-Console-Android | 安卓APP (TODO) |
+| Mirai-Console-Graphical | JavaFX的图形化界面 (.jar/.exe/.dmg) |
+| Mirai-Console-WebPanel | Web Panel操作(TODO) |
+| Mirai-Console-Ios | IOS APP (TODO) |
+
+
+### 如何选择版本
+1: Mirai-Console-Pure 兼容性最高, 在其他都表现不佳的时候请使用
+2: 以系统区分
+```kotlin
+ return when(operatingSystem){
+ WINDOWS -> listOf("Graphical","WebPanel","Pure")
+ MAC_OS -> listOf("Graphical","Terminal","WebPanel","Pure")
+ LINUX -> listOf("Terminal","Pure")
+ ANDROID -> listOf("Android","Pure","WebPanel")
+ IOS -> listOf("Ios")
+ else -> listOf("Pure")
+ }
+```
+3: 以策略区分
+```kotlin
+ return when(task){
+ 体验 -> listOf("Graphical","Terminal","WebPanel","Android","Pure")
+ 测试插件 -> listOf("Pure")
+ 调试插件 -> byOperatingSystem()
+ 稳定挂机 -> listOf("Terminal","Pure")
+ else -> listOf("Pure")
+ }
+```
+
+
+#### More Importantly, Mirai Console support Plugins, tells the bot what to do
+#### Mirai Console 支持插件系统, 你可以自己开发或使用公开的插件来逻辑化机器人, 如群管
+
+
+#### download 下载
+#### how to get/write plugins 如何获取/写插件
+
+
+
+### how to use(如何使用)
+#### how to run Mirai Console
+
+ - download mirai-console.jar
+ - open command line/terminal
+ - create a folder and put mirai-console.jar in
+ - cd that folder
+ - "java -jar mirai-console.jar"
+
+
+
+ - 下载mirai-console.jar
+ - 打开终端
+ - 在任何地方创建一个文件夹, 并放入mirai-console.jar
+ - 在终端中打开该文件夹"cd"
+ - 输入"java -jar mirai-console.jar"
+
+
+#### how to add plugins
+
+ - After first time of running mirai console
+ - /plugins/folder will be created next to mirai-console.jar
+ - put plugin(.jar) into /plugins/
+ - restart mirai console
+ - checking logger and check if the plugin is loaded successfully
+ - if the plugin has it own Config file, it normally appears in /plugins/{pluginName}/
+
+
+
+ - 在首次运行mirai console后
+ - mirai-console.jar 的同级会出现/plugins/文件夹
+ - 将插件(.jar)放入/plugins/文件夹
+ - 重启mirai console
+ - 在开启后检查日志, 是否成功加载
+ - 如该插件有配置文件, 配置文件一般会创建在/plugins/插件名字/ 文件夹下
+
+
+
diff --git a/mirai-console/build.gradle.kts b/mirai-console/build.gradle.kts
new file mode 100644
index 000000000..07795b6a4
--- /dev/null
+++ b/mirai-console/build.gradle.kts
@@ -0,0 +1,113 @@
+import java.util.*
+
+plugins {
+ id("kotlinx-serialization")
+ id("kotlin")
+ id("java")
+ `maven-publish`
+ id("com.jfrog.bintray")
+}
+
+
+apply(plugin = "com.github.johnrengelman.shadow")
+
+val kotlinVersion: String by rootProject.ext
+val atomicFuVersion: String by rootProject.ext
+val coroutinesVersion: String by rootProject.ext
+val kotlinXIoVersion: String by rootProject.ext
+val coroutinesIoVersion: String by rootProject.ext
+
+val klockVersion: String by rootProject.ext
+val ktorVersion: String by rootProject.ext
+
+val serializationVersion: String by rootProject.ext
+
+fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
+
+fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
+
+tasks.withType() {
+ manifest {
+ attributes["Main-Class"] = "net.mamoe.mirai.console.pure.MiraiConsolePureLoader"
+ }
+}
+
+
+val mirai_version: String by rootProject.ext
+
+dependencies {
+ implementation("net.mamoe:mirai-core-jvm:$mirai_version")
+ implementation("net.mamoe:mirai-core-qqandroid-jvm:$mirai_version")
+
+
+ // api(project(":mirai-api-http"))
+ runtimeOnly(files("../mirai-core-qqandroid/build/classes/kotlin/jvm/main"))
+ runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main"))
+ api(kotlin("serialization"))
+ api(group = "com.alibaba", name = "fastjson", version = "1.2.62")
+ api(group = "org.yaml", name = "snakeyaml", version = "1.25")
+ api(group = "com.moandjiezana.toml", name = "toml4j", version = "0.7.2")
+ api("org.bouncycastle:bcprov-jdk15on:1.64")
+
+ implementation("no.tornado:tornadofx:1.7.19")
+ // classpath is not set correctly by IDE
+}
+
+val mirai_console_version: String by project.ext
+version = mirai_console_version
+
+description = "Console with plugin support for mirai"
+bintray {
+ val keyProps = Properties()
+ val keyFile = file("../keys.properties")
+ if (keyFile.exists()) keyFile.inputStream().use { keyProps.load(it) }
+ if (keyFile.exists()) keyFile.inputStream().use { keyProps.load(it) }
+
+ user = keyProps.getProperty("bintrayUser")
+ key = keyProps.getProperty("bintrayKey")
+ setPublications("mavenJava")
+ setConfigurations("archives")
+
+ pkg.apply {
+ repo = "mirai"
+ name = "mirai-console"
+ setLicenses("AGPLv3")
+ publicDownloadNumbers = true
+ vcsUrl = "https://github.com/mamoe/mirai"
+ }
+}
+
+@Suppress("DEPRECATION")
+val sourcesJar by tasks.registering(Jar::class) {
+ classifier = "sources"
+ from(sourceSets.main.get().allSource)
+}
+
+publishing {
+ /*
+ repositories {
+ maven {
+ // change to point to your repo, e.g. http://my.org/repo
+ url = uri("$buildDir/repo")
+ }
+ }*/
+ publications {
+ register("mavenJava", MavenPublication::class) {
+ from(components["java"])
+
+ groupId = rootProject.group.toString()
+ artifactId = "mirai-console"
+ version = mirai_console_version
+
+ pom.withXml {
+ val root = asNode()
+ root.appendNode("description", description)
+ root.appendNode("name", project.name)
+ root.appendNode("url", "https://github.com/mamoe/mirai")
+ root.children().last()
+ }
+
+ artifact(sourcesJar.get())
+ }
+ }
+}
\ No newline at end of file
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt
new file mode 100644
index 000000000..4f634f5bc
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.console
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.Channel
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.console.MiraiConsole.CommandProcessor.processNextCommandLine
+import net.mamoe.mirai.console.command.CommandManager
+import net.mamoe.mirai.console.command.CommandSender
+import net.mamoe.mirai.console.command.ConsoleCommandSender
+import net.mamoe.mirai.console.command.DefaultCommands
+import net.mamoe.mirai.console.plugins.PluginManager
+import net.mamoe.mirai.console.plugins.loadAsConfig
+import net.mamoe.mirai.console.plugins.withDefaultWrite
+import net.mamoe.mirai.console.utils.MiraiConsoleUI
+import net.mamoe.mirai.utils.cryptor.ECDH
+import java.io.File
+
+
+object MiraiConsole {
+ /**
+ * 发布的版本号 统一修改位置
+ */
+ const val version = "0.1.0"
+ const val coreVersion = "v0.18.0"
+ const val build = "Alpha"
+
+
+ /**
+ * 获取从Console登陆上的Bot, Bots
+ * */
+ val bots get() = Bot.instances
+
+ fun getBotByUIN(uin: Long): Bot? {
+ bots.forEach {
+ if (it.get()?.uin == uin) {
+ return it.get()
+ }
+ }
+ return null
+ }
+
+ /**
+ * PluginManager
+ */
+ val pluginManager: PluginManager get() = PluginManager
+
+ /**
+ * 与前端交互所使用的Logger
+ */
+ var logger = UIPushLogger
+
+ /**
+ * Console运行路径
+ */
+ var path: String = System.getProperty("user.dir")
+
+ /**
+ * Console前端接口
+ */
+ lateinit var frontEnd: MiraiConsoleUI
+
+
+ /**
+ * 启动Console
+ */
+ var start = false
+
+ fun start(
+ frontEnd: MiraiConsoleUI
+ ) {
+ if (start) {
+ return
+ }
+ start = true
+
+ /* 加载ECDH */
+ try {
+ ECDH()
+ } catch (ignored: Exception) {
+ }
+ //Security.removeProvider("BC")
+
+
+ /* 初始化前端 */
+ this.frontEnd = frontEnd
+ frontEnd.pushVersion(version, build, coreVersion)
+ logger("Mirai-console [$version $build | core version $coreVersion] is still in testing stage, major features are available")
+ logger("Mirai-console now running under $path")
+ logger("Get news in github: https://github.com/mamoe/mirai")
+ logger("Mirai为开源项目,请自觉遵守开源项目协议")
+ logger("Powered by Mamoe Technologies and contributors")
+
+ /* 依次启用功能 */
+ DefaultCommands()
+ HTTPAPIAdaptar()
+ pluginManager.loadPlugins()
+ CommandProcessor.start()
+
+ /* 通知启动完成 */
+ logger("Mirai-console 启动完成")
+ logger("\"/login qqnumber qqpassword \" to login a bot")
+ logger("\"/login qq号 qq密码 \" 来登录一个BOT")
+ }
+
+ fun stop() {
+ PluginManager.disableAllPlugins()
+ try {
+ bots.forEach {
+ it.get()?.close()
+ }
+ } catch (ignored: Exception) {
+
+ }
+ }
+
+
+ object CommandProcessor : Job by {
+ GlobalScope.launch(start = CoroutineStart.LAZY) {
+ processNextCommandLine()
+ }
+ }() {
+
+ internal class FullCommand(
+ val sender: CommandSender,
+ val commandStr: String
+ )
+
+ private val commandChannel: Channel = Channel()
+
+ suspend fun runConsoleCommand(command: String) {
+ commandChannel.send(
+ FullCommand(ConsoleCommandSender, command)
+ )
+ }
+
+ suspend fun runCommand(sender: CommandSender, command: String) {
+ commandChannel.send(
+ FullCommand(sender, command)
+ )
+ }
+
+ fun runConsoleCommandBlocking(command: String) = runBlocking { runConsoleCommand(command) }
+
+ fun runCommandBlocking(sender: CommandSender, command: String) = runBlocking { runCommand(sender, command) }
+
+ private suspend fun processNextCommandLine() {
+ for (command in commandChannel) {
+ var commandStr = command.commandStr
+ if (!commandStr.startsWith("/")) {
+ commandStr = "/$commandStr"
+ }
+ if (!CommandManager.runCommand(command.sender, commandStr)) {
+ command.sender.sendMessage("未知指令 $commandStr")
+ }
+ }
+ }
+ }
+
+ object UIPushLogger {
+ operator fun invoke(any: Any? = null) {
+ invoke(
+ "[Mirai$version $build]",
+ 0L,
+ any
+ )
+ }
+
+ operator fun invoke(identityStr: String, identity: Long, any: Any? = null) {
+ if (any != null) {
+ frontEnd.pushLog(identity, "$identityStr: $any")
+ }
+ }
+ }
+
+}
+
+object MiraiProperties {
+ var config = File("${MiraiConsole.path}/mirai.properties").loadAsConfig()
+
+ var HTTP_API_ENABLE: Boolean by config.withDefaultWrite { true }
+ var HTTP_API_PORT: Int by config.withDefaultWrite { 8080 }
+ /*
+ var HTTP_API_AUTH_KEY: String by config.withDefaultWriteSave {
+ "InitKey" + generateSessionKey()
+ }*/
+}
+
+object HTTPAPIAdaptar {
+ operator fun invoke() {
+ /*
+ if (MiraiProperties.HTTP_API_ENABLE) {
+ if (MiraiProperties.HTTP_API_AUTH_KEY.startsWith("InitKey")) {
+ MiraiConsole.logger("请尽快更改初始生成的HTTP API AUTHKEY")
+ }
+ MiraiConsole.logger("正在启动HTTPAPI; 端口=" + MiraiProperties.HTTP_API_PORT)
+ MiraiHttpAPIServer.logger = SimpleLogger("HTTP API") { _, message, e ->
+ MiraiConsole.logger("[Mirai HTTP API]", 0, message)
+ }
+ MiraiHttpAPIServer.start(
+ MiraiProperties.HTTP_API_PORT,
+ MiraiProperties.HTTP_API_AUTH_KEY
+ )
+ MiraiConsole.logger("HTTPAPI启动完成; 端口= " + MiraiProperties.HTTP_API_PORT)
+ }*/
+ }
+}
+
+
+
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt
new file mode 100644
index 000000000..2fe0ca77e
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.console.command
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.plugins.PluginManager
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.sendMessage
+import net.mamoe.mirai.message.GroupMessage
+import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.utils.MiraiExperimentalAPI
+
+object CommandManager {
+ private val registeredCommand: MutableMap = mutableMapOf()
+
+ fun getCommands(): Collection {
+ return registeredCommand.values
+ }
+
+
+ fun register(command: Command) {
+ val allNames = mutableListOf(command.name).also { it.addAll(command.alias) }
+ allNames.forEach {
+ if (registeredCommand.containsKey(it)) {
+ error("Command Name(or Alias) $it is already registered, consider if same functional plugin was installed")
+ }
+ }
+ allNames.forEach {
+ registeredCommand[it] = command
+ }
+ }
+
+ fun unregister(command: Command) {
+ val allNames = mutableListOf(command.name).also { it.addAll(command.alias) }
+ allNames.forEach {
+ registeredCommand.remove(it)
+ }
+ }
+
+ fun unregister(commandName: String) {
+ registeredCommand.remove(commandName)
+ }
+
+ /*
+ * Index: MiraiConsole
+ * */
+ internal suspend fun runCommand(sender: CommandSender, fullCommand: String): Boolean {
+ val blocks = fullCommand.split(" ")
+ val commandHead = blocks[0].replace("/", "")
+ if (!registeredCommand.containsKey(commandHead)) {
+ return false
+ }
+ val args = blocks.subList(1, blocks.size)
+ registeredCommand[commandHead]?.run {
+ try {
+ if (onCommand(
+ sender,
+ blocks.subList(1, blocks.size)
+ )
+ ) {
+ PluginManager.onCommand(this, args)
+ } else {
+ sender.sendMessage(this.usage)
+ }
+ } catch (e: Exception) {
+ sender.sendMessage("在运行指令时出现了未知错误")
+ e.printStackTrace()
+ } finally {
+ (sender as CommandSenderImpl).flushMessage()
+ }
+ }
+ return true
+ }
+
+}
+
+interface CommandSender {
+ /**
+ * 立刻发送一条Message
+ */
+ suspend fun sendMessage(messageChain: MessageChain)
+
+ suspend fun sendMessage(message: String)
+ /**
+ * 写入要发送的内容 所有内容最后会被以一条发出, 不管成功与否
+ */
+ fun appendMessage(message: String)
+
+ fun sendMessageBlocking(messageChain: MessageChain) = runBlocking { sendMessage(messageChain) }
+ fun sendMessageBlocking(message: String) = runBlocking { sendMessage(message) }
+}
+
+abstract class CommandSenderImpl : CommandSender {
+ internal val builder = StringBuilder()
+
+ override fun appendMessage(message: String) {
+ builder.append(message).append("\n")
+ }
+
+ internal open suspend fun flushMessage() {
+ if (!builder.isEmpty()) {
+ sendMessage(builder.toString().removeSuffix("\n"))
+ }
+ }
+}
+
+object ConsoleCommandSender : CommandSenderImpl() {
+ override suspend fun sendMessage(messageChain: MessageChain) {
+ MiraiConsole.logger("[Command]", 0, messageChain.toString())
+ }
+
+ override suspend fun sendMessage(message: String) {
+ MiraiConsole.logger("[Command]", 0, message)
+ }
+
+ override suspend fun flushMessage() {
+ super.flushMessage()
+ builder.clear()
+ }
+}
+
+open class ContactCommandSender(val contact: Contact) : CommandSenderImpl() {
+ override suspend fun sendMessage(messageChain: MessageChain) {
+ contact.sendMessage(messageChain)
+ }
+
+ override suspend fun sendMessage(message: String) {
+ contact.sendMessage(message)
+ }
+}
+
+/**
+ * 弃用中
+ * */
+class GroupCommandSender(val toQuote: GroupMessage, contact: Contact) : ContactCommandSender(contact) {
+ @MiraiExperimentalAPI
+ override suspend fun sendMessage(message: String) {
+ toQuote.quoteReply(message)
+ }
+
+ @MiraiExperimentalAPI
+ override suspend fun sendMessage(messageChain: MessageChain) {
+ toQuote.quoteReply(messageChain)
+ }
+}
+
+interface Command {
+ val name: String
+ val alias: List
+ val description: String
+ val usage: String
+
+ suspend fun onCommand(sender: CommandSender, args: List): Boolean
+ fun register()
+}
+
+abstract class BlockingCommand(
+ override val name: String,
+ override val alias: List = listOf(),
+ override val description: String = "",
+ override val usage: String = ""
+) : Command {
+ /**
+ * 最高优先级监听器
+ * 如果 return `false` 这次指令不会被 [PluginBase] 的全局 onCommand 监听器监听
+ * */
+ final override suspend fun onCommand(sender: CommandSender, args: List): Boolean {
+ return withContext(Dispatchers.IO) {
+ onCommandBlocking(sender, args)
+ }
+ }
+
+ abstract fun onCommandBlocking(sender: CommandSender, args: List): Boolean
+
+ override fun register() {
+ CommandManager.register(this)
+ }
+}
+
+class AnonymousCommand internal constructor(
+ override val name: String,
+ override val alias: List,
+ override val description: String,
+ override val usage: String = "",
+ val onCommand: suspend CommandSender.(args: List) -> Boolean
+) : Command {
+ override suspend fun onCommand(sender: CommandSender, args: List): Boolean {
+ return onCommand.invoke(sender, args)
+ }
+
+ override fun register() {
+ CommandManager.register(this)
+ }
+}
+
+class CommandBuilder internal constructor() {
+ var name: String? = null
+ var alias: List? = null
+ var description: String = ""
+ var usage: String = "use /help for help"
+ var onCommand: (suspend CommandSender.(args: List) -> Boolean)? = null
+
+ fun onCommand(commandProcess: suspend CommandSender.(args: List) -> Boolean) {
+ onCommand = commandProcess
+ }
+
+ fun register(): Command {
+ if (name == null || onCommand == null) {
+ error("CommandBuilder not complete")
+ }
+ if (alias == null) {
+ alias = listOf()
+ }
+ return AnonymousCommand(
+ name!!,
+ alias!!,
+ description,
+ usage,
+ onCommand!!
+ ).also { it.register() }
+ }
+}
+
+fun registerCommand(builder: CommandBuilder.() -> Unit): Command {
+ return CommandBuilder().apply(builder).register()
+}
+
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt
new file mode 100644
index 000000000..86cc19441
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt
@@ -0,0 +1,263 @@
+package net.mamoe.mirai.console.command
+
+import kotlinx.coroutines.runBlocking
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.plugins.PluginManager
+import net.mamoe.mirai.console.utils.addManager
+import net.mamoe.mirai.console.utils.checkManager
+import net.mamoe.mirai.console.utils.getManagers
+import net.mamoe.mirai.console.utils.removeManager
+import net.mamoe.mirai.contact.sendMessage
+import net.mamoe.mirai.event.subscribeMessages
+import net.mamoe.mirai.utils.SimpleLogger
+import java.util.*
+
+
+/**
+ * Some defaults commands are recommend to be replaced by plugin provided commands
+ */
+
+object DefaultCommands {
+ operator fun invoke() {
+ registerCommand {
+ name = "manager"
+ description = "Add a manager"
+ onCommand { it ->
+ if (this !is ConsoleCommandSender) {
+ sendMessage("请在后台使用该指令")
+ return@onCommand false
+ }
+ if (it.size < 2) {
+ MiraiConsole.logger("[Bot Manager]", 0, "/manager add [bot ID] [Manager ID]")
+ MiraiConsole.logger("[Bot Manager]", 0, "/manager remove [bot ID] [Manager ID]")
+ MiraiConsole.logger("[Bot Manager]", 0, "/manager list [bot ID]")
+ return@onCommand true
+ }
+ val botId = try {
+ it[1].toLong()
+ } catch (e: Exception) {
+ MiraiConsole.logger("[Bot Manager]", 0, it[1] + " 不是一个Bot的ID")
+ return@onCommand false
+ }
+ val bot = MiraiConsole.getBotByUIN(botId)
+ if (bot == null) {
+ MiraiConsole.logger("[Bot Manager]", 0, "$botId 没有在Console中登陆")
+ return@onCommand false
+ }
+ when (it[0]) {
+ "add" -> {
+ if (it.size < 3) {
+ MiraiConsole.logger("[Bot Manager]", 0, "/manager add [bot ID] [Manager ID]")
+ return@onCommand true
+ }
+ val adminID = try {
+ it[2].toLong()
+ } catch (e: Exception) {
+ MiraiConsole.logger("[Bot Manager]", 0, it[2] + " 不是一个ID")
+ return@onCommand false
+ }
+ bot.addManager(adminID)
+ MiraiConsole.logger("[Bot Manager]", 0, it[2] + "增加成功")
+ }
+ "remove" -> {
+ if (it.size < 3) {
+ MiraiConsole.logger("[Bot Manager]", 0, "/manager remove [bot ID] [Manager ID]")
+ return@onCommand true
+ }
+ val adminID = try {
+ it[2].toLong()
+ } catch (e: Exception) {
+ MiraiConsole.logger("[Bot Manager]", 0, it[1] + " 不是一个ID")
+ return@onCommand false
+ }
+ if (!bot.checkManager(adminID)) {
+ MiraiConsole.logger("[Bot Manager]", 0, it[2] + "本身不是一个Manager")
+ return@onCommand true
+ }
+ bot.removeManager(adminID)
+ MiraiConsole.logger("[Bot Manager]", 0, it[2] + "移除成功")
+ }
+ "list" -> {
+ bot.getManagers().forEach {
+ MiraiConsole.logger("[Bot Manager]", 0, " -> $it")
+ }
+ }
+ }
+ return@onCommand true
+ }
+ }
+
+ registerCommand {
+ name = "login"
+ description = "机器人登陆"
+ onCommand {
+ if (this !is ConsoleCommandSender) {
+ sendMessage("请在后台使用该指令")
+ return@onCommand false
+ }
+ if (it.size < 2) {
+ MiraiConsole.logger("\"/login qqnumber qqpassword \" to login a bot")
+ MiraiConsole.logger("\"/login qq号 qq密码 \" 来登录一个BOT")
+ return@onCommand false
+ }
+ val qqNumber = it[0].toLong()
+ val qqPassword = it[1]
+ MiraiConsole.logger("[Bot Login]", 0, "login...")
+ try {
+ MiraiConsole.frontEnd.prePushBot(qqNumber)
+ val bot = Bot(qqNumber, qqPassword) {
+ this.loginSolver = MiraiConsole.frontEnd.createLoginSolver()
+ this.botLoggerSupplier = {
+ SimpleLogger("BOT $qqNumber]") { _, message, e ->
+ MiraiConsole.logger("[BOT $qqNumber]", qqNumber, message)
+ if (e != null) {
+ MiraiConsole.logger("[NETWORK ERROR]", qqNumber, e.toString())//因为在一页 所以可以不打QQ
+ e.printStackTrace()
+ }
+ }
+ }
+ this.networkLoggerSupplier = {
+ SimpleLogger("BOT $qqNumber") { _, message, e ->
+ MiraiConsole.logger("[NETWORK]", qqNumber, message)//因为在一页 所以可以不打QQ
+ if (e != null) {
+ MiraiConsole.logger("[NETWORK ERROR]", qqNumber, e.toString())//因为在一页 所以可以不打QQ
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+ bot.login()
+ bot.subscribeMessages {
+ this.startsWith("/") {
+ if (bot.checkManager(this.sender.id)) {
+ val sender = ContactCommandSender(this.subject)
+ MiraiConsole.CommandProcessor.runCommand(
+ sender, it
+ )
+ }
+ }
+ }
+ sendMessage("$qqNumber login successes")
+ MiraiConsole.frontEnd.pushBot(bot)
+ } catch (e: Exception) {
+ sendMessage("$qqNumber login failed -> " + e.message)
+ }
+ true
+ }
+ }
+
+ registerCommand {
+ name = "status"
+ description = "获取状态"
+ onCommand {
+ when (it.size) {
+ 0 -> {
+ sendMessage("当前有" + MiraiConsole.bots.size + "个BOT在线")
+ }
+ 1 -> {
+ val bot = it[0]
+ var find = false
+ MiraiConsole.bots.forEach {
+ if (it.get()?.uin.toString().contains(bot)) {
+ find = true
+ appendMessage("" + it.get()?.uin + ": 在线中; 好友数量:" + it.get()?.qqs?.size + "; 群组数量:" + it.get()?.groups?.size)
+ }
+ }
+ if (!find) {
+ sendMessage("没有找到BOT$bot")
+ }
+ }
+ }
+ true
+ }
+ }
+
+
+ registerCommand {
+ name = "say"
+ description = "聊天功能演示"
+ onCommand {
+ if (it.size < 2) {
+ MiraiConsole.logger("say [好友qq号或者群号] [文本消息] //将默认使用第一个BOT")
+ MiraiConsole.logger("say [bot号] [好友qq号或者群号] [文本消息]")
+ return@onCommand false
+ }
+ val bot: Bot? = if (it.size == 2) {
+ if (MiraiConsole.bots.size == 0) {
+ MiraiConsole.logger("还没有BOT登录")
+ return@onCommand false
+ }
+ MiraiConsole.bots[0].get()
+ } else {
+ MiraiConsole.getBotByUIN(it[0].toLong())
+ }
+ if (bot == null) {
+ MiraiConsole.logger("没有找到BOT")
+ return@onCommand false
+ }
+ val target = it[it.size - 2].toLong()
+ val message = it[it.size - 1]
+ try {
+ val contact = bot[target]
+ runBlocking {
+ contact.sendMessage(message)
+ MiraiConsole.logger("消息已推送")
+ }
+ } catch (e: NoSuchElementException) {
+ MiraiConsole.logger("没有找到群或好友 号码为${target}")
+ return@onCommand false
+ }
+ true
+ }
+ }
+
+
+ registerCommand {
+ name = "plugins"
+ alias = listOf("plugin")
+ description = "获取插件列表"
+ onCommand {
+ PluginManager.getAllPluginDescriptions().let {
+ it.forEach {
+ appendMessage("\t" + it.name + " v" + it.version + " by" + it.author + " " + it.info)
+ }
+ appendMessage("加载了" + it.size + "个插件")
+ true
+ }
+ }
+ }
+
+ registerCommand {
+ name = "command"
+ alias = listOf("commands", "help", "helps")
+ description = "获取指令列表"
+ onCommand {
+ CommandManager.getCommands().let {
+ var size = 0
+ appendMessage("")//\n
+ it.toSet().forEach {
+ ++size
+ appendMessage("-> " + it.name + " :" + it.description)
+ }
+ appendMessage("""共有${size}条指令""")
+ }
+ true
+ }
+ }
+
+ registerCommand {
+ name = "about"
+ description = "About Mirai-Console"
+ onCommand {
+ appendMessage("v${MiraiConsole.version} ${MiraiConsole.build} is still in testing stage, major features are available")
+ appendMessage("now running under ${MiraiConsole.path}")
+ appendMessage("在Github中获取项目最新进展: https://github.com/mamoe/mirai")
+ appendMessage("Mirai为开源项目,请自觉遵守开源项目协议")
+ appendMessage("Powered by Mamoe Technologies and contributors")
+ true
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt
new file mode 100644
index 000000000..dffc93092
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt
@@ -0,0 +1,523 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.console.plugins
+
+import com.alibaba.fastjson.JSON
+import com.alibaba.fastjson.JSONObject
+import com.alibaba.fastjson.TypeReference
+import com.alibaba.fastjson.parser.Feature
+import com.moandjiezana.toml.Toml
+import com.moandjiezana.toml.TomlWriter
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UnstableDefault
+import net.mamoe.mirai.utils.io.encodeToString
+import org.yaml.snakeyaml.Yaml
+import java.io.File
+import java.io.InputStream
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+import kotlin.reflect.full.isSubclassOf
+
+
+/**
+ * TODO: support all config types
+ * only JSON is now supported
+ *
+ */
+
+interface Config {
+ fun getConfigSection(key: String): ConfigSection
+ fun getString(key: String): String
+ fun getInt(key: String): Int
+ fun getFloat(key: String): Float
+ fun getDouble(key: String): Double
+ fun getLong(key: String): Long
+ fun getBoolean(key: String): Boolean
+ fun getList(key: String): List<*>
+ fun getStringList(key: String): List
+ fun getIntList(key: String): List
+ fun getFloatList(key: String): List
+ fun getDoubleList(key: String): List
+ fun getLongList(key: String): List
+ fun getConfigSectionList(key: String): List
+ operator fun set(key: String, value: Any)
+ operator fun get(key: String): Any?
+ operator fun contains(key: String): Boolean
+ fun exist(key: String): Boolean
+ fun setIfAbsent(key: String, value: Any)
+ fun asMap(): Map
+ fun save()
+
+ companion object {
+ fun load(fileName: String): Config {
+ return load(
+ File(
+ fileName.replace(
+ "//",
+ "/"
+ )
+ )
+ )
+ }
+
+ /**
+ * create a read-write config
+ * */
+ fun load(file: File): Config {
+ if (!file.exists()) {
+ file.createNewFile()
+ }
+ return when (file.extension.toLowerCase()) {
+ "json" -> JsonConfig(file)
+ "yml" -> YamlConfig(file)
+ "yaml" -> YamlConfig(file)
+ "mirai" -> YamlConfig(file)
+ "ini" -> TomlConfig(file)
+ "toml" -> TomlConfig(file)
+ "properties" -> TomlConfig(file)
+ "property" -> TomlConfig(file)
+ "data" -> TomlConfig(file)
+ else -> error("Unsupported file config type ${file.extension.toLowerCase()}")
+ }
+ }
+
+ /**
+ * create a read-only config
+ */
+ fun load(content: String, type: String): Config {
+ return when (type.toLowerCase()) {
+ "json" -> JsonConfig(content)
+ "yml" -> YamlConfig(content)
+ "yaml" -> YamlConfig(content)
+ "mirai" -> YamlConfig(content)
+ "ini" -> TomlConfig(content)
+ "toml" -> TomlConfig(content)
+ "properties" -> TomlConfig(content)
+ "property" -> TomlConfig(content)
+ "data" -> TomlConfig(content)
+ else -> error("Unsupported file config type $content")
+ }
+ }
+
+ /**
+ * create a read-only config
+ */
+ fun load(inputStream: InputStream, type: String): Config {
+ return load(inputStream.readBytes().encodeToString(), type)
+ }
+
+ }
+}
+
+
+fun File.loadAsConfig(): Config {
+ return Config.load(this)
+}
+
+/* 最简单的代理 */
+inline operator fun Config.getValue(thisRef: Any?, property: KProperty<*>): T {
+ return smartCast(property)
+}
+
+inline operator fun Config.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ this[property.name] = value
+}
+
+/* 带有默认值的代理 */
+inline fun Config.withDefault(
+ noinline defaultValue: () -> T
+): ReadWriteProperty {
+ val default by lazy { defaultValue.invoke() }
+ return object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>): T {
+ if (this@withDefault.exist(property.name)) {//unsafe
+ return this@withDefault.smartCast(property)
+ }
+ return default
+ }
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
+ this@withDefault[property.name] = value
+ }
+ }
+}
+
+/* 带有默认值且如果为空会写入的代理 */
+inline fun Config.withDefaultWrite(
+ noinline defaultValue: () -> T
+): WithDefaultWriteLoader {
+ return WithDefaultWriteLoader(
+ T::class,
+ this,
+ defaultValue,
+ false
+ )
+}
+
+/* 带有默认值且如果为空会写入保存的代理 */
+inline fun Config.withDefaultWriteSave(
+ noinline defaultValue: () -> T
+): WithDefaultWriteLoader {
+ return WithDefaultWriteLoader(T::class, this, defaultValue, true)
+}
+
+class WithDefaultWriteLoader(
+ private val _class: KClass,
+ private val config: Config,
+ private val defaultValue: () -> T,
+ private val save: Boolean
+) {
+ operator fun provideDelegate(
+ thisRef: Any,
+ prop: KProperty<*>
+ ): ReadWriteProperty {
+ val defaultValue by lazy { defaultValue.invoke() }
+ if (!config.contains(prop.name)) {
+ config[prop.name] = defaultValue
+ if (save) {
+ config.save()
+ }
+ }
+ return object : ReadWriteProperty {
+ override fun getValue(thisRef: Any, property: KProperty<*>): T {
+ if (config.exist(property.name)) {//unsafe
+ return config._smartCast(property.name, _class)
+ }
+ return defaultValue
+ }
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
+ config[property.name] = value
+ }
+ }
+ }
+}
+
+inline fun Config.smartCast(property: KProperty<*>): T {
+ return _smartCast(property.name, T::class)
+}
+
+@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
+fun Config._smartCast(propertyName: String, _class: KClass): T {
+ return when (_class) {
+ String::class -> this.getString(propertyName)
+ Int::class -> this.getInt(propertyName)
+ Float::class -> this.getFloat(propertyName)
+ Double::class -> this.getDouble(propertyName)
+ Long::class -> this.getLong(propertyName)
+ Boolean::class -> this.getBoolean(propertyName)
+ else -> when {
+ _class.isSubclassOf(ConfigSection::class) -> this.getConfigSection(propertyName)
+ _class == List::class || _class == MutableList::class -> {
+ val list = this.getList(propertyName)
+ return if (list.isEmpty()) {
+ list
+ } else {
+ when (list[0]!!::class) {
+ String::class -> getStringList(propertyName)
+ Int::class -> getIntList(propertyName)
+ Float::class -> getFloatList(propertyName)
+ Double::class -> getDoubleList(propertyName)
+ Long::class -> getLongList(propertyName)
+ //不去支持getConfigSectionList(propertyName)
+ // LinkedHashMap::class -> getConfigSectionList(propertyName)//faster approach
+ else -> {
+ //if(list[0]!! is ConfigSection || list[0]!! is Map<*,*>){
+ // getConfigSectionList(propertyName)
+ //}else {
+ error("unsupported type" + list[0]!!::class)
+ //}
+ }
+ }
+ } as T
+ }
+ else -> {
+ error("unsupported type")
+ }
+ }
+ } as T
+}
+
+
+interface ConfigSection : Config, MutableMap {
+ override fun getConfigSection(key: String): ConfigSection {
+ val content = get(key) ?: error("ConfigSection does not contain $key ")
+ if (content is ConfigSection) {
+ return content
+ }
+ return ConfigSectionDelegation(
+ Collections.synchronizedMap(
+ (get(key) ?: error("ConfigSection does not contain $key ")) as LinkedHashMap
+ )
+ )
+ }
+
+ override fun getString(key: String): String {
+ return (get(key) ?: error("ConfigSection does not contain $key ")).toString()
+ }
+
+ override fun getInt(key: String): Int {
+ return (get(key) ?: error("ConfigSection does not contain $key ")).toString().toInt()
+ }
+
+ override fun getFloat(key: String): Float {
+ return (get(key) ?: error("ConfigSection does not contain $key ")).toString().toFloat()
+ }
+
+ override fun getBoolean(key: String): Boolean {
+ return (get(key) ?: error("ConfigSection does not contain $key ")).toString().toBoolean()
+ }
+
+ override fun getDouble(key: String): Double {
+ return (get(key) ?: error("ConfigSection does not contain $key ")).toString().toDouble()
+ }
+
+ override fun getLong(key: String): Long {
+ return (get(key) ?: error("ConfigSection does not contain $key ")).toString().toLong()
+ }
+
+ override fun getList(key: String): List<*> {
+ return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>)
+ }
+
+ override fun getStringList(key: String): List {
+ return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString() }
+ }
+
+ override fun getIntList(key: String): List {
+ return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString().toInt() }
+ }
+
+ override fun getFloatList(key: String): List {
+ return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString().toFloat() }
+ }
+
+ override fun getDoubleList(key: String): List {
+ return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString().toDouble() }
+ }
+
+ override fun getLongList(key: String): List {
+ return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString().toLong() }
+ }
+
+ override fun getConfigSectionList(key: String): List {
+ return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map {
+ if (it is ConfigSection) {
+ it
+ } else {
+ ConfigSectionDelegation(
+ Collections.synchronizedMap(
+ it as MutableMap
+ )
+ )
+ }
+ }
+ }
+
+ override fun exist(key: String): Boolean {
+ return get(key) != null
+ }
+
+ override fun setIfAbsent(key: String, value: Any) {
+ if (!exist(key)) set(key, value)
+ }
+}
+
+@Serializable
+open class ConfigSectionImpl() : ConcurrentHashMap(),
+ ConfigSection {
+ override fun set(key: String, value: Any) {
+ super.put(key, value)
+ }
+
+ override operator fun get(key: String): Any? {
+ return super.get(key)
+ }
+
+ @Suppress("RedundantOverride")
+ override fun contains(key: String): Boolean {
+ return super.contains(key)
+ }
+
+ override fun exist(key: String): Boolean {
+ return containsKey(key)
+ }
+
+ override fun asMap(): Map {
+ return this
+ }
+
+ override fun save() {
+
+ }
+
+ override fun setIfAbsent(key: String, value: Any) {
+ this.putIfAbsent(key, value)//atomic
+ }
+}
+
+open class ConfigSectionDelegation(
+ private val delegate: MutableMap
+) : ConfigSection, MutableMap by delegate {
+ override fun set(key: String, value: Any) {
+ delegate.put(key, value)
+ }
+
+ override fun contains(key: String): Boolean {
+ return delegate.containsKey(key)
+ }
+
+ override fun asMap(): Map {
+ return delegate
+ }
+
+ override fun save() {
+
+ }
+}
+
+
+interface FileConfig : Config {
+ fun deserialize(content: String): ConfigSection
+
+ fun serialize(config: ConfigSection): String
+}
+
+
+abstract class FileConfigImpl internal constructor(
+ private val rawContent: String
+) : FileConfig,
+ ConfigSection {
+
+ internal var file: File? = null
+
+
+ constructor(file: File) : this(file.readText()) {
+ this.file = file
+ }
+
+
+ private val content by lazy {
+ deserialize(rawContent)
+ }
+
+
+ override val size: Int get() = content.size
+ override val entries: MutableSet> get() = content.entries
+ override val keys: MutableSet get() = content.keys
+ override val values: MutableCollection get() = content.values
+ override fun containsKey(key: String): Boolean = content.containsKey(key)
+ override fun containsValue(value: Any): Boolean = content.containsValue(value)
+ override fun put(key: String, value: Any): Any? = content.put(key, value)
+ override fun isEmpty(): Boolean = content.isEmpty()
+ override fun putAll(from: Map) = content.putAll(from)
+ override fun clear() = content.clear()
+ override fun remove(key: String): Any? = content.remove(key)
+
+ override fun save() {
+ if (isReadOnly()) {
+ error("Config is readonly")
+ }
+ if (!((file?.exists())!!)) {
+ file?.createNewFile()
+ }
+ file?.writeText(serialize(content))
+ }
+
+ fun isReadOnly() = file == null
+
+ override fun contains(key: String): Boolean {
+ return content.contains(key)
+ }
+
+ override fun get(key: String): Any? {
+ return content[key]
+ }
+
+ override fun set(key: String, value: Any) {
+ content[key] = value
+ }
+
+ override fun asMap(): Map {
+ return content.asMap()
+ }
+
+}
+
+class JsonConfig internal constructor(
+ content: String
+) : FileConfigImpl(content) {
+ constructor(file: File) : this(file.readText()) {
+ this.file = file
+ }
+
+ @UnstableDefault
+ override fun deserialize(content: String): ConfigSection {
+ if (content.isEmpty() || content.isBlank() || content == "{}") {
+ return ConfigSectionImpl()
+ }
+ return JSON.parseObject(
+ content,
+ object : TypeReference() {},
+ Feature.OrderedField
+ )
+ }
+
+ @UnstableDefault
+ override fun serialize(config: ConfigSection): String {
+ return JSONObject.toJSONString(config)
+ }
+}
+
+class YamlConfig internal constructor(content: String) : FileConfigImpl(content) {
+ constructor(file: File) : this(file.readText()) {
+ this.file = file
+ }
+
+ override fun deserialize(content: String): ConfigSection {
+ if (content.isEmpty() || content.isBlank()) {
+ return ConfigSectionImpl()
+ }
+ return ConfigSectionDelegation(
+ Collections.synchronizedMap(
+ Yaml().load>(content) as LinkedHashMap
+ )
+ )
+ }
+
+ override fun serialize(config: ConfigSection): String {
+ return Yaml().dumpAsMap(config)
+ }
+
+}
+
+class TomlConfig internal constructor(content: String) : FileConfigImpl(content) {
+ constructor(file: File) : this(file.readText()) {
+ this.file = file
+ }
+
+ override fun deserialize(content: String): ConfigSection {
+ if (content.isEmpty() || content.isBlank()) {
+ return ConfigSectionImpl()
+ }
+ return ConfigSectionDelegation(
+ Collections.synchronizedMap(
+ Toml().read(content).toMap()
+ )
+ )
+ }
+
+ override fun serialize(config: ConfigSection): String {
+ return TomlWriter().write(config)
+ }
+}
\ No newline at end of file
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt
new file mode 100644
index 000000000..b8eef079a
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt
@@ -0,0 +1,411 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.console.plugins
+
+import kotlinx.coroutines.*
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.command.Command
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.SimpleLogger
+import net.mamoe.mirai.utils.io.encodeToString
+import java.io.File
+import java.io.InputStream
+import java.net.URL
+import java.net.URLClassLoader
+import java.util.jar.JarFile
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+
+abstract class PluginBase(coroutineContext: CoroutineContext) : CoroutineScope {
+ constructor() : this(EmptyCoroutineContext)
+
+ private val supervisorJob = SupervisorJob()
+ final override val coroutineContext: CoroutineContext = coroutineContext + supervisorJob
+
+ /**
+ * 插件被分配的data folder, 如果插件改名了 data folder 也会变 请注意
+ */
+ val dataFolder: File by lazy {
+ File(PluginManager.pluginsPath + pluginDescription.name).also { it.mkdir() }
+ }
+
+ /**
+ * 当一个插件被加载时调用
+ */
+ open fun onLoad() {
+
+ }
+
+ /**
+ * 当所有插件全部被加载后被调用
+ */
+ open fun onEnable() {
+
+ }
+
+ /**
+ * 当插件关闭前被调用
+ */
+ open fun onDisable() {
+
+ }
+
+ /**
+ * 当任意指令被使用
+ */
+ open fun onCommand(command: Command, args: List) {
+
+ }
+
+
+ internal fun enable() {
+ this.onEnable()
+ }
+
+ /**
+ * 加载一个data folder中的Config
+ * 这个config是read-write的
+ */
+ fun loadConfig(fileName: String): Config {
+ return Config.load(dataFolder.absolutePath + fileName)
+ }
+
+ @JvmOverloads
+ internal fun disable(throwable: CancellationException? = null) {
+ this.coroutineContext[Job]!!.cancelChildren(throwable)
+ this.onDisable()
+ }
+
+ private lateinit var pluginDescription: PluginDescription
+
+ internal fun init(pluginDescription: PluginDescription) {
+ this.pluginDescription = pluginDescription
+ this.onLoad()
+ }
+
+ val pluginManager = PluginManager
+
+ val logger: MiraiLogger by lazy {
+ SimpleLogger("Plugin ${pluginDescription.name}") { _, message, e ->
+ MiraiConsole.logger("[${pluginDescription.name}]", 0, message)
+ if (e != null) {
+ MiraiConsole.logger("[${pluginDescription.name}]", 0, e.toString())
+ e.printStackTrace()
+ }
+ }
+ }
+
+ /**
+ * 加载一个插件jar, resources中的东西
+ */
+ fun getResources(fileName: String): InputStream? {
+ return try {
+ this.javaClass.classLoader.getResourceAsStream(fileName)
+ } catch (e: Exception) {
+ PluginManager.getFileInJarByName(
+ this.pluginDescription.name,
+ fileName
+ )
+ }
+ }
+
+ /**
+ * 加载一个插件jar, resources中的Config
+ * 这个Config是read-only的
+ */
+ fun getResourcesConfig(fileName: String): Config {
+ if (fileName.contains(".")) {
+ error("Unknown Config Type")
+ }
+ return Config.load(getResources(fileName) ?: error("Config Not Found"), fileName.split(".")[1])
+ }
+
+}
+
+class PluginDescription(
+ val name: String,
+ val author: String,
+ val basePath: String,
+ val version: String,
+ val info: String,
+ val depends: List,//插件的依赖
+ internal var loaded: Boolean = false,
+ internal var noCircularDepend: Boolean = true
+) {
+
+ override fun toString(): String {
+ return "name: $name\nauthor: $author\npath: $basePath\nver: $version\ninfo: $info\ndepends: $depends"
+ }
+
+ companion object {
+ fun readFromContent(content_: String): PluginDescription {
+ val content = content_.split("\n")
+
+ var name = "Plugin"
+ var author = "Unknown"
+ var basePath = "net.mamoe.mirai.PluginMain"
+ var info = "Unknown"
+ var version = "1.0.0"
+ val depends = mutableListOf();
+
+ content.forEach {
+ val line = it.trim()
+ val lowercaseLine = line.toLowerCase()
+ if (it.contains(":")) {
+ when {
+ lowercaseLine.startsWith("name") -> {
+ name = line.substringAfter(":").trim()
+ }
+ lowercaseLine.startsWith("author") -> {
+ author = line.substringAfter(":").trim()
+ }
+ lowercaseLine.startsWith("info") || lowercaseLine.startsWith("information") -> {
+ info = line.substringAfter(":").trim()
+ }
+ lowercaseLine.startsWith("main") || lowercaseLine.startsWith("path") || lowercaseLine.startsWith(
+ "basepath"
+ ) -> {
+ basePath = line.substringAfter(":").trim()
+ }
+ lowercaseLine.startsWith("version") || lowercaseLine.startsWith("ver") -> {
+ version = line.substringAfter(":").trim()
+ }
+ }
+ } else if (line.startsWith("-")) {
+ depends.add(line.substringAfter("-").trim())
+ }
+ }
+ return PluginDescription(
+ name,
+ author,
+ basePath,
+ version,
+ info,
+ depends
+ )
+ }
+ }
+}
+
+internal class PluginClassLoader(file: File, parent: ClassLoader) :
+ URLClassLoader(arrayOf(file.toURI().toURL()), parent)
+
+object PluginManager {
+ internal val pluginsPath = System.getProperty("user.dir") + "/plugins/".replace("//", "/").also {
+ File(it).mkdirs()
+ }
+
+ val logger = SimpleLogger("Plugin Manager") { _, message, e ->
+ MiraiConsole.logger("[Plugin Manager]", 0, message)
+ }
+
+ //已完成加载的
+ private val nameToPluginBaseMap: MutableMap = mutableMapOf()
+ private val pluginDescriptions: MutableMap = mutableMapOf()
+
+ fun onCommand(command: Command, args: List) {
+ nameToPluginBaseMap.values.forEach {
+ it.onCommand(command, args)
+ }
+ }
+
+ fun getAllPluginDescriptions(): Collection {
+ return pluginDescriptions.values
+ }
+
+ /**
+ * 尝试加载全部插件
+ */
+ fun loadPlugins() {
+ val pluginsFound: MutableMap = mutableMapOf()
+ val pluginsLocation: MutableMap = mutableMapOf()
+
+ logger.info("""开始加载${pluginsPath}下的插件""")
+
+ File(pluginsPath).listFiles()?.forEach { file ->
+ if (file != null && file.extension == "jar") {
+ val jar = JarFile(file)
+ val pluginYml =
+ jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull()
+ if (pluginYml == null) {
+ logger.info("plugin.yml not found in jar " + jar.name + ", it will not be consider as a Plugin")
+ } else {
+ val description =
+ PluginDescription.readFromContent(
+ URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().inputStream.use {
+ it.readBytes().encodeToString()
+ })
+ pluginsFound[description.name] = description
+ pluginsLocation[description.name] = file
+ }
+ }
+ }
+
+ fun checkNoCircularDepends(
+ target: PluginDescription,
+ needDepends: List,
+ existDepends: MutableList
+ ) {
+
+ if (!target.noCircularDepend) {
+ return
+ }
+
+ existDepends.add(target.name)
+
+ if (needDepends.any { existDepends.contains(it) }) {
+ target.noCircularDepend = false
+ }
+
+ existDepends.addAll(needDepends)
+
+ needDepends.forEach {
+ if (pluginsFound.containsKey(it)) {
+ checkNoCircularDepends(pluginsFound[it]!!, pluginsFound[it]!!.depends, existDepends)
+ }
+ }
+ }
+
+
+ pluginsFound.values.forEach {
+ checkNoCircularDepends(it, it.depends, mutableListOf())
+ }
+
+ //load
+
+
+ fun loadPlugin(description: PluginDescription): Boolean {
+ if (!description.noCircularDepend) {
+ logger.error("Failed to load plugin " + description.name + " because it has circular dependency")
+ return false
+ }
+
+ //load depends first
+ description.depends.forEach { dependent ->
+ if (!pluginsFound.containsKey(dependent)) {
+ logger.error("Failed to load plugin " + description.name + " because it need " + dependent + " as dependency")
+ return false
+ }
+ val depend = pluginsFound[dependent]!!
+ //还没有加载
+ if (!depend.loaded && !loadPlugin(pluginsFound[dependent]!!)) {
+ logger.error("Failed to load plugin " + description.name + " because " + dependent + " as dependency failed to load")
+ return false
+ }
+ }
+ //在这里所有的depends都已经加载了
+
+
+ //real load
+ logger.info("loading plugin " + description.name)
+
+ try {
+ val pluginClass = try {
+ PluginClassLoader(
+ (pluginsLocation[description.name]!!),
+ this.javaClass.classLoader
+ )
+ .loadClass(description.basePath)
+ } catch (e: ClassNotFoundException) {
+ logger.info("failed to find Main: " + description.basePath + " checking if it's kotlin's path")
+ PluginClassLoader(
+ (pluginsLocation[description.name]!!),
+ this.javaClass.classLoader
+ )
+ .loadClass("${description.basePath}Kt")
+ }
+ return try {
+ val subClass = pluginClass.asSubclass(PluginBase::class.java)
+ val plugin: PluginBase =
+ subClass.kotlin.objectInstance ?: subClass.getDeclaredConstructor().newInstance()
+ description.loaded = true
+ logger.info("successfully loaded plugin " + description.name + " version " + description.version + " by " + description.author)
+ logger.info(description.info)
+
+ nameToPluginBaseMap[description.name] = plugin
+ pluginDescriptions[description.name] = description
+ plugin.init(description)
+ true
+ } catch (e: ClassCastException) {
+ logger.error("failed to load plugin " + description.name + " , Main class does not extends PluginBase ")
+ false
+ }
+ } catch (e: ClassNotFoundException) {
+ e.printStackTrace()
+ logger.error("failed to load plugin " + description.name + " , Main class not found under " + description.basePath)
+ return false
+ }
+ }
+
+ pluginsFound.values.forEach {
+ loadPlugin(it)
+ }
+
+ nameToPluginBaseMap.values.forEach {
+ it.enable()
+ }
+
+ logger.info("""加载了${nameToPluginBaseMap.size}个插件""")
+
+ }
+
+
+ @JvmOverloads
+ fun disableAllPlugins(throwable: CancellationException? = null) {
+ nameToPluginBaseMap.values.forEach {
+ it.disable(throwable)
+ }
+ }
+
+ /**
+ * 根据插件名字找Jar的文件
+ * null => 没找到
+ */
+ fun getJarPath(pluginName: String): File? {
+ File(pluginsPath).listFiles()?.forEach { file ->
+ if (file != null && file.extension == "jar") {
+ val jar = JarFile(file)
+ val pluginYml =
+ jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull()
+ if (pluginYml != null) {
+ val description =
+ PluginDescription.readFromContent(
+ URL("jar:file:" + file.absoluteFile + "!/" + pluginYml.name).openConnection().inputStream.use {
+ it.readBytes().encodeToString()
+ })
+ if (description.name.toLowerCase() == pluginName.toLowerCase()) {
+ return file
+ }
+ }
+ }
+ }
+ return null
+ }
+
+
+ /**
+ * 根据插件名字找Jar中的文件
+ * null => 没找到
+ */
+ fun getFileInJarByName(pluginName: String, toFind: String): InputStream? {
+ val jarFile = getJarPath(pluginName)
+ if (jarFile == null) {
+ return null
+ }
+ val jar = JarFile(jarFile)
+ val toFindFile =
+ jar.entries().asSequence().filter { it.name == toFind }.firstOrNull() ?: return null
+ return URL("jar:file:" + jarFile.absoluteFile + "!/" + toFindFile.name).openConnection().inputStream
+ }
+}
+
+
+
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt
new file mode 100644
index 000000000..deac0fce0
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt
@@ -0,0 +1,16 @@
+package net.mamoe.mirai.console.pure
+
+import net.mamoe.mirai.console.MiraiConsole
+import kotlin.concurrent.thread
+
+class MiraiConsolePureLoader {
+ companion object {
+ @JvmStatic
+ fun main(args: Array) {
+ MiraiConsole.start(MiraiConsoleUIPure())
+ Runtime.getRuntime().addShutdownHook(thread(start = false) {
+ MiraiConsole.stop()
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleUIPure.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleUIPure.kt
new file mode 100644
index 000000000..388f5551e
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleUIPure.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.console.pure
+
+import kotlinx.coroutines.delay
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.utils.MiraiConsoleUI
+import net.mamoe.mirai.utils.DefaultLoginSolver
+import net.mamoe.mirai.utils.LoginSolver
+import net.mamoe.mirai.utils.LoginSolverInputReader
+import kotlin.concurrent.thread
+
+class MiraiConsoleUIPure : MiraiConsoleUI {
+ var requesting = false
+ var requestStr = ""
+
+ init {
+ thread {
+ while (true) {
+ val input = readLine() ?: return@thread
+ if (requesting) {
+ requestStr = input
+ requesting = false
+ } else {
+ MiraiConsole.CommandProcessor.runConsoleCommandBlocking(input)
+ }
+ }
+ }
+ }
+
+ override fun pushLog(identity: Long, message: String) {
+ println(message)
+ }
+
+ override fun prePushBot(identity: Long) {
+
+ }
+
+ override fun pushBot(bot: Bot) {
+
+ }
+
+ override fun pushVersion(consoleVersion: String, consoleBuild: String, coreVersion: String) {
+
+ }
+
+ override suspend fun requestInput(question: String): String {
+ requesting = true
+ while (true) {
+ delay(50)
+ if (!requesting) {
+ return requestStr
+ }
+ }
+ }
+
+ override fun pushBotAdminStatus(identity: Long, admins: List) {
+
+ }
+
+ override fun createLoginSolver(): LoginSolver {
+ return DefaultLoginSolver(
+ reader = object : LoginSolverInputReader {
+ override suspend fun read(question: String): String? {
+ return requestInput(question)
+ }
+ }
+ )
+ }
+
+}
+
+
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt
new file mode 100644
index 000000000..c4709b8df
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/BotHelper.kt
@@ -0,0 +1,40 @@
+package net.mamoe.mirai.console.utils
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.console.MiraiConsole
+import net.mamoe.mirai.console.plugins.ConfigSection
+import net.mamoe.mirai.console.plugins.ConfigSectionImpl
+import net.mamoe.mirai.console.plugins.loadAsConfig
+import net.mamoe.mirai.console.plugins.withDefaultWriteSave
+import net.mamoe.mirai.console.utils.BotManagers.BOT_MANAGERS
+import java.io.File
+
+object BotManagers {
+ val config = File("${MiraiConsole.path}/bot.yml").loadAsConfig()
+ val BOT_MANAGERS: ConfigSection by config.withDefaultWriteSave { ConfigSectionImpl() }
+}
+
+fun Bot.addManager(long: Long) {
+ BOT_MANAGERS.putIfAbsent(this.uin.toString(), mutableListOf())
+ BOT_MANAGERS[this.uin.toString()] =
+ (BOT_MANAGERS.getLongList(this.uin.toString()) as MutableList).apply { add(long) }
+ BotManagers.config.save()
+}
+
+fun Bot.removeManager(long: Long) {
+ BOT_MANAGERS.putIfAbsent(this.uin.toString(), mutableListOf())
+ BOT_MANAGERS[this.uin.toString()] =
+ (BOT_MANAGERS.getLongList(this.uin.toString()) as MutableList).apply { add(long) }
+ BotManagers.config.save()
+}
+
+fun Bot.getManagers(): List {
+ BOT_MANAGERS.putIfAbsent(this.uin.toString(), mutableListOf())
+ return BOT_MANAGERS.getLongList(this.uin.toString())
+}
+
+fun Bot.checkManager(long: Long): Boolean {
+ return this.getManagers().contains(long)
+}
+
+
diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/MiraiConsoleUI.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/MiraiConsoleUI.kt
new file mode 100644
index 000000000..a82d8a0bc
--- /dev/null
+++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/utils/MiraiConsoleUI.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.console.utils
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.utils.LoginSolver
+
+/**
+ * 只需要实现一个这个 传入MiraiConsole 就可以绑定UI层与Console层
+ * 注意线程
+ */
+
+interface MiraiConsoleUI {
+ /**
+ * 让UI层展示一条log
+ *
+ * identity:log所属的screen, Main=0; Bot=Bot.uin
+ */
+ fun pushLog(
+ identity: Long,
+ message: String
+ )
+
+ /**
+ * 让UI层准备接受新增的一个BOT
+ */
+ fun prePushBot(
+ identity: Long
+ )
+
+ /**
+ * 让UI层接受一个新的bot
+ * */
+ fun pushBot(
+ bot: Bot
+ )
+
+
+ fun pushVersion(
+ consoleVersion: String,
+ consoleBuild: String,
+ coreVersion: String
+ )
+
+ /**
+ * 让UI层提供一个Input
+ * 这个Input 不 等于 Command
+ *
+ */
+ suspend fun requestInput(
+ question: String
+ ): String
+
+
+ /**
+ * 让UI层更新BOT管理员的数据
+ */
+ fun pushBotAdminStatus(
+ identity: Long,
+ admins: List
+ )
+
+ /**
+ * 由UI层创建一个LoginSolver
+ */
+ fun createLoginSolver(): LoginSolver
+
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..36b061578
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,47 @@
+pluginManagement {
+ resolutionStrategy {
+ eachPlugin {
+ switch (requested.id.id) {
+ case "org.jetbrains.kotlin.multiplatform": useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}"); break
+ case "com.android.library": useModule("com.android.tools.build:gradle:${requested.version}"); break
+ case "com.jfrog.bintray": useModule("com.jfrog.bintray.gradle:gradle-bintray-plugin:${requested.version}")
+ }
+ }
+ }
+
+ repositories {
+ mavenLocal()
+ jcenter()
+ google()
+ mavenCentral()
+ maven { url "https://plugins.gradle.org/m2/" }
+ maven { url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies" }
+ }
+}
+
+rootProject.name = 'mirai-console'
+
+include(':mirai-console')
+include(':mirai-console-terminal')
+
+try{
+ def javaVersion = System.getProperty("java.version")
+ def versionPos = javaVersion.indexOf(".")
+ if (versionPos==-1) versionPos = javaVersion.indexOf("-")
+ if (versionPos==-1){
+ println("jdk version unknown")
+ }else{
+ def javaVersionNum = javaVersion.substring(0, versionPos).toInteger()
+ if (javaVersionNum >= 11) {
+ include(':mirai-console-graphical')
+ } else {
+ println("jdk版本为 "+ javaVersionNum)
+ println("当前使用的 JDK 版本为 ${System.getProperty("java.version")}, 最低需要 JDK 11 才能引入模块 `:mirai-console-graphical`")
+ }
+ }
+}catch(Exception ignored){
+ println("无法确定 JDK 版本, 将不会引入 `:mirai-console-graphical`")
+}
+
+
+enableFeaturePreview('GRADLE_METADATA')
\ No newline at end of file