mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-05 18:22:27 +08:00
Separate mirai-console
series from main repository
This commit is contained in:
parent
5510c1be7d
commit
86d1ecf3f0
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@ -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
|
143
LICENSE
143
LICENSE
@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
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
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
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
|
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
|
software for all its users.
|
||||||
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.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
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
|
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.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
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:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
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
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
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
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
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
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
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
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
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
|
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,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
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
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
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
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
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.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
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
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
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) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
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 <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
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
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
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".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
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.
|
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
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
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
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
44
build.gradle
Normal file
44
build.gradle
Normal file
@ -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" }
|
||||||
|
}
|
||||||
|
}
|
20
gradle.properties
Normal file
20
gradle.properties
Normal file
@ -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
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -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
|
183
gradlew
vendored
Normal file
183
gradlew
vendored
Normal file
@ -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" "$@"
|
100
gradlew.bat
vendored
Normal file
100
gradlew.bat
vendored
Normal file
@ -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
|
6
mirai-console-graphical/README.md
Normal file
6
mirai-console-graphical/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
### Mirai Console Graphical
|
||||||
|
支持windows/mac
|
||||||
|
有正式UI界面实现的CONSOLE
|
||||||
|
优点: 适合新手/完全不懂编程的/界面美丽
|
||||||
|
缺点: 不能在linux服务器运行
|
||||||
|
所使用插件系统与terminal版本一致 可以来回切换
|
48
mirai-console-graphical/build.gradle.kts
Normal file
48
mirai-console-graphical/build.gradle.kts
Normal file
@ -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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
kotlinOptions.jvmTarget = "1.8"
|
||||||
|
}
|
@ -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<String>) {
|
||||||
|
launch<MiraiGraphicalUI>(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
class MiraiGraphicalUI : App(Decorator::class, PrimaryStyleSheet::class) {
|
||||||
|
|
||||||
|
override fun init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
MiraiConsole.start(find<MiraiGraphicalUIController>())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
|
MiraiConsole.stop()
|
||||||
|
}
|
||||||
|
}
|
@ -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<Long, BotModel>()
|
||||||
|
val mainLog = observableListOf<String>()
|
||||||
|
|
||||||
|
|
||||||
|
val botList = observableListOf<BotModel>()
|
||||||
|
val pluginList: ObservableList<PluginModel> 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<VerificationCodeFragment>(Scope(model)).openModal(
|
||||||
|
modality = Modality.APPLICATION_MODAL,
|
||||||
|
resizable = false
|
||||||
|
)
|
||||||
|
return model.code.value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pushBotAdminStatus(identity: Long, admins: List<Long>) = Platform.runLater {
|
||||||
|
cache[identity]?.admins?.setAll(admins)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLoginSolver(): LoginSolver = loginSolver
|
||||||
|
|
||||||
|
private fun getPluginsFromConsole(): ObservableList<PluginModel> =
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
@ -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<Bot>(null)
|
||||||
|
var bot: Bot by botProperty
|
||||||
|
|
||||||
|
val logHistory = observableListOf<String>()
|
||||||
|
val admins = observableListOf<Long>()
|
||||||
|
}
|
||||||
|
|
||||||
|
class BotViewModel(botModel: BotModel? = null) : ItemViewModel<BotModel>(botModel) {
|
||||||
|
val bot = bind(BotModel::botProperty)
|
||||||
|
val logHistory = bind(BotModel::logHistory)
|
||||||
|
val admins = bind(BotModel::admins)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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<PluginModel>() {
|
||||||
|
constructor(plugin: PluginDescription) : this(plugin.name, plugin.version, plugin.author, plugin.info)
|
||||||
|
|
||||||
|
val enabledProperty = SimpleBooleanProperty(this, "enabledProperty")
|
||||||
|
var enabled by enabledProperty
|
||||||
|
}
|
@ -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<VerificationCode>(code) {
|
||||||
|
constructor() : this(VerificationCode())
|
||||||
|
|
||||||
|
val code = bind(VerificationCode::codeProperty)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<String>, 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<String>, op: JFXPasswordField.() -> Unit = {}) =
|
||||||
|
jfxPasswordfield().apply {
|
||||||
|
bind(property)
|
||||||
|
op(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T> EventTarget.jfxListView(values: ObservableList<T>? = null, op: ListView<T>.() -> Unit = {}) =
|
||||||
|
JFXListView<T>().attachTo(this, op) {
|
||||||
|
if (values != null) {
|
||||||
|
if (values is SortedFilteredList<T>) values.bindTo(it)
|
||||||
|
else it.items = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : RecursiveTreeObject<T>?> EventTarget.jfxTreeTableView(
|
||||||
|
items: ObservableList<T>? = null,
|
||||||
|
op: JFXTreeTableView<T>.() -> Unit = {}
|
||||||
|
) = JFXTreeTableView<T>(RecursiveTreeItem(items, RecursiveTreeObject<T>::getChildren)).attachTo(this, op)
|
@ -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<PrimaryView>().root)
|
||||||
|
}
|
@ -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<MiraiGraphicalUIController>()
|
||||||
|
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 = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<MiraiGraphicalUIController>()
|
||||||
|
val plugins = controller.pluginList
|
||||||
|
|
||||||
|
override val root = jfxTreeTableView(plugins) {
|
||||||
|
columns.addAll(
|
||||||
|
JFXTreeTableColumn<PluginModel, String>("插件名").apply {
|
||||||
|
prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.1))
|
||||||
|
},
|
||||||
|
JFXTreeTableColumn<PluginModel, String>("版本").apply {
|
||||||
|
prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.1))
|
||||||
|
},
|
||||||
|
JFXTreeTableColumn<PluginModel, String>("作者").apply {
|
||||||
|
prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.1))
|
||||||
|
},
|
||||||
|
JFXTreeTableColumn<PluginModel, String>("介绍").apply {
|
||||||
|
prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.6))
|
||||||
|
},
|
||||||
|
JFXTreeTableColumn<PluginModel, String>("操作").apply {
|
||||||
|
prefWidthProperty().bind(this@jfxTreeTableView.widthProperty().multiply(0.08))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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<MiraiGraphicalUIController>()
|
||||||
|
|
||||||
|
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<BotModel>() {
|
||||||
|
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<LoginView>().root
|
||||||
|
|
||||||
|
tab("Plugins").content = find<PluginsView>().root
|
||||||
|
|
||||||
|
tab("Settings").content = find<SettingsView>().root
|
||||||
|
|
||||||
|
logTab("Main", controller.mainLog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TabPane.logTab(
|
||||||
|
text: String? = null,
|
||||||
|
logs: ObservableList<String>,
|
||||||
|
op: Tab.() -> Unit = {}
|
||||||
|
) = tab(text) {
|
||||||
|
listview(logs) {
|
||||||
|
|
||||||
|
fitToParentSize()
|
||||||
|
cellFormat {
|
||||||
|
graphic = label(it) {
|
||||||
|
maxWidthProperty().bind(this@listview.widthProperty())
|
||||||
|
isWrapText = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
also(op)
|
||||||
|
}
|
@ -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<MiraiGraphicalUIController>()
|
||||||
|
|
||||||
|
override val root = form {
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
field {
|
||||||
|
jfxButton("撤掉") { }
|
||||||
|
jfxButton("保存") { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset("插件目录") {
|
||||||
|
field {
|
||||||
|
jfxTextfield("...") { isEditable = false }
|
||||||
|
jfxButton("打开目录")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset("最大日志容量") {
|
||||||
|
field {
|
||||||
|
jfxTextfield("...") {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
mirai-console-graphical/src/main/resources/character.png
Normal file
BIN
mirai-console-graphical/src/main/resources/character.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
mirai-console-graphical/src/main/resources/logo.png
Normal file
BIN
mirai-console-graphical/src/main/resources/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
6
mirai-console-terminal/README.md
Normal file
6
mirai-console-terminal/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
### Mirai Console Terminal
|
||||||
|
支持windows/mac/linux
|
||||||
|
在terminal环境下的Console, 由控制台富文本实现简易UI
|
||||||
|
优点: 可以在linux环境下运行/简洁使用效率高
|
||||||
|
缺点: 需要有略微的terminal知识
|
||||||
|
所使用插件系统与graphical版本一致 可以来回切换
|
46
mirai-console-terminal/build.gradle.kts
Normal file
46
mirai-console-terminal/build.gradle.kts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
plugins {
|
||||||
|
id("kotlinx-serialization")
|
||||||
|
id("kotlin")
|
||||||
|
id("java")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(plugin = "com.github.johnrengelman.shadow")
|
||||||
|
|
||||||
|
|
||||||
|
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>() {
|
||||||
|
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
|
||||||
|
}
|
@ -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<String>) {
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<Long>) {
|
||||||
|
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<Long, LimitLinkedQueue<String>>().also {
|
||||||
|
it[0L] = LimitLinkedQueue(cacheLogSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
val botAdminCount = ConcurrentHashMap<Long, Int>()
|
||||||
|
|
||||||
|
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<String>) {
|
||||||
|
//this.cleanPage()
|
||||||
|
currentHeight = 6
|
||||||
|
var logsToDraw = 0
|
||||||
|
var vara = 0
|
||||||
|
val toPrint = mutableListOf<String>()
|
||||||
|
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<T>(
|
||||||
|
val limit: Int = 50
|
||||||
|
) : ConcurrentLinkedDeque<T>() {
|
||||||
|
override fun push(e: T) {
|
||||||
|
if (size >= limit) {
|
||||||
|
this.pollLast()
|
||||||
|
}
|
||||||
|
return super.push(e)
|
||||||
|
}
|
||||||
|
}
|
87
mirai-console/README.MD
Normal file
87
mirai-console/README.MD
Normal file
@ -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 兼容性最高, 在其他都表现不佳的时候请使用</br>
|
||||||
|
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 <b>Plugins</b>, tells the bot what to do
|
||||||
|
#### Mirai Console 支持插件系统, 你可以自己开发或使用公开的插件来逻辑化机器人, 如群管
|
||||||
|
<br>
|
||||||
|
|
||||||
|
#### download 下载
|
||||||
|
#### how to get/write plugins 如何获取/写插件
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### how to use(如何使用)
|
||||||
|
#### how to run Mirai Console
|
||||||
|
<ul>
|
||||||
|
<li>download mirai-console.jar</li>
|
||||||
|
<li>open command line/terminal</li>
|
||||||
|
<li>create a folder and put mirai-console.jar in</li>
|
||||||
|
<li>cd that folder</li>
|
||||||
|
<li>"java -jar mirai-console.jar"</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>下载mirai-console.jar</li>
|
||||||
|
<li>打开终端</li>
|
||||||
|
<li>在任何地方创建一个文件夹, 并放入mirai-console.jar</li>
|
||||||
|
<li>在终端中打开该文件夹"cd"</li>
|
||||||
|
<li>输入"java -jar mirai-console.jar"</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
#### how to add plugins
|
||||||
|
<ul>
|
||||||
|
<li>After first time of running mirai console</li>
|
||||||
|
<li>/plugins/folder will be created next to mirai-console.jar</li>
|
||||||
|
<li>put plugin(.jar) into /plugins/</li>
|
||||||
|
<li>restart mirai console</li>
|
||||||
|
<li>checking logger and check if the plugin is loaded successfully</li>
|
||||||
|
<li>if the plugin has it own Config file, it normally appears in /plugins/{pluginName}/</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>在首次运行mirai console后</li>
|
||||||
|
<li>mirai-console.jar 的同级会出现/plugins/文件夹</li>
|
||||||
|
<li>将插件(.jar)放入/plugins/文件夹</li>
|
||||||
|
<li>重启mirai console</li>
|
||||||
|
<li>在开启后检查日志, 是否成功加载</li>
|
||||||
|
<li>如该插件有配置文件, 配置文件一般会创建在/plugins/插件名字/ 文件夹下</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
113
mirai-console/build.gradle.kts
Normal file
113
mirai-console/build.gradle.kts
Normal file
@ -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<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>() {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<FullCommand> = 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)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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<String, Command> = mutableMapOf()
|
||||||
|
|
||||||
|
fun getCommands(): Collection<Command> {
|
||||||
|
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<String>(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<String>
|
||||||
|
val description: String
|
||||||
|
val usage: String
|
||||||
|
|
||||||
|
suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean
|
||||||
|
fun register()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BlockingCommand(
|
||||||
|
override val name: String,
|
||||||
|
override val alias: List<String> = listOf(),
|
||||||
|
override val description: String = "",
|
||||||
|
override val usage: String = ""
|
||||||
|
) : Command {
|
||||||
|
/**
|
||||||
|
* 最高优先级监听器
|
||||||
|
* 如果 return `false` 这次指令不会被 [PluginBase] 的全局 onCommand 监听器监听
|
||||||
|
* */
|
||||||
|
final override suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
onCommandBlocking(sender, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun onCommandBlocking(sender: CommandSender, args: List<String>): Boolean
|
||||||
|
|
||||||
|
override fun register() {
|
||||||
|
CommandManager.register(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnonymousCommand internal constructor(
|
||||||
|
override val name: String,
|
||||||
|
override val alias: List<String>,
|
||||||
|
override val description: String,
|
||||||
|
override val usage: String = "",
|
||||||
|
val onCommand: suspend CommandSender.(args: List<String>) -> Boolean
|
||||||
|
) : Command {
|
||||||
|
override suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean {
|
||||||
|
return onCommand.invoke(sender, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun register() {
|
||||||
|
CommandManager.register(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandBuilder internal constructor() {
|
||||||
|
var name: String? = null
|
||||||
|
var alias: List<String>? = null
|
||||||
|
var description: String = ""
|
||||||
|
var usage: String = "use /help for help"
|
||||||
|
var onCommand: (suspend CommandSender.(args: List<String>) -> Boolean)? = null
|
||||||
|
|
||||||
|
fun onCommand(commandProcess: suspend CommandSender.(args: List<String>) -> 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()
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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<String>
|
||||||
|
fun getIntList(key: String): List<Int>
|
||||||
|
fun getFloatList(key: String): List<Float>
|
||||||
|
fun getDoubleList(key: String): List<Double>
|
||||||
|
fun getLongList(key: String): List<Long>
|
||||||
|
fun getConfigSectionList(key: String): List<ConfigSection>
|
||||||
|
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<String, Any>
|
||||||
|
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 <reified T : Any> Config.getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||||
|
return smartCast(property)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline operator fun <reified T : Any> Config.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||||
|
this[property.name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 带有默认值的代理 */
|
||||||
|
inline fun <reified T : Any> Config.withDefault(
|
||||||
|
noinline defaultValue: () -> T
|
||||||
|
): ReadWriteProperty<Any, T> {
|
||||||
|
val default by lazy { defaultValue.invoke() }
|
||||||
|
return object : ReadWriteProperty<Any, T> {
|
||||||
|
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 <reified T : Any> Config.withDefaultWrite(
|
||||||
|
noinline defaultValue: () -> T
|
||||||
|
): WithDefaultWriteLoader<T> {
|
||||||
|
return WithDefaultWriteLoader(
|
||||||
|
T::class,
|
||||||
|
this,
|
||||||
|
defaultValue,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 带有默认值且如果为空会写入保存的代理 */
|
||||||
|
inline fun <reified T : Any> Config.withDefaultWriteSave(
|
||||||
|
noinline defaultValue: () -> T
|
||||||
|
): WithDefaultWriteLoader<T> {
|
||||||
|
return WithDefaultWriteLoader(T::class, this, defaultValue, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
class WithDefaultWriteLoader<T : Any>(
|
||||||
|
private val _class: KClass<T>,
|
||||||
|
private val config: Config,
|
||||||
|
private val defaultValue: () -> T,
|
||||||
|
private val save: Boolean
|
||||||
|
) {
|
||||||
|
operator fun provideDelegate(
|
||||||
|
thisRef: Any,
|
||||||
|
prop: KProperty<*>
|
||||||
|
): ReadWriteProperty<Any, T> {
|
||||||
|
val defaultValue by lazy { defaultValue.invoke() }
|
||||||
|
if (!config.contains(prop.name)) {
|
||||||
|
config[prop.name] = defaultValue
|
||||||
|
if (save) {
|
||||||
|
config.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object : ReadWriteProperty<Any, T> {
|
||||||
|
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 <reified T : Any> Config.smartCast(property: KProperty<*>): T {
|
||||||
|
return _smartCast(property.name, T::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
|
||||||
|
fun <T : Any> Config._smartCast(propertyName: String, _class: KClass<T>): 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<String, Any> {
|
||||||
|
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<String, Any>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String> {
|
||||||
|
return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntList(key: String): List<Int> {
|
||||||
|
return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString().toInt() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFloatList(key: String): List<Float> {
|
||||||
|
return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString().toFloat() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDoubleList(key: String): List<Double> {
|
||||||
|
return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString().toDouble() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLongList(key: String): List<Long> {
|
||||||
|
return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map { it.toString().toLong() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConfigSectionList(key: String): List<ConfigSection> {
|
||||||
|
return ((get(key) ?: error("ConfigSection does not contain $key ")) as List<*>).map {
|
||||||
|
if (it is ConfigSection) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
ConfigSectionDelegation(
|
||||||
|
Collections.synchronizedMap(
|
||||||
|
it as MutableMap<String, Any>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String, Any>(),
|
||||||
|
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<String, Any> {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun save() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setIfAbsent(key: String, value: Any) {
|
||||||
|
this.putIfAbsent(key, value)//atomic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class ConfigSectionDelegation(
|
||||||
|
private val delegate: MutableMap<String, Any>
|
||||||
|
) : ConfigSection, MutableMap<String, Any> 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<String, Any> {
|
||||||
|
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<MutableMap.MutableEntry<String, Any>> get() = content.entries
|
||||||
|
override val keys: MutableSet<String> get() = content.keys
|
||||||
|
override val values: MutableCollection<Any> 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<out String, Any>) = 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<String, Any> {
|
||||||
|
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<ConfigSectionImpl>(
|
||||||
|
content,
|
||||||
|
object : TypeReference<ConfigSectionImpl>() {},
|
||||||
|
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<LinkedHashMap<String, Any>>(content) as LinkedHashMap<String, Any>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<String>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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<String>,//插件的依赖
|
||||||
|
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<String>();
|
||||||
|
|
||||||
|
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<String, PluginBase> = mutableMapOf()
|
||||||
|
private val pluginDescriptions: MutableMap<String, PluginDescription> = mutableMapOf()
|
||||||
|
|
||||||
|
fun onCommand(command: Command, args: List<String>) {
|
||||||
|
nameToPluginBaseMap.values.forEach {
|
||||||
|
it.onCommand(command, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllPluginDescriptions(): Collection<PluginDescription> {
|
||||||
|
return pluginDescriptions.values
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试加载全部插件
|
||||||
|
*/
|
||||||
|
fun loadPlugins() {
|
||||||
|
val pluginsFound: MutableMap<String, PluginDescription> = mutableMapOf()
|
||||||
|
val pluginsLocation: MutableMap<String, File> = 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<String>,
|
||||||
|
existDepends: MutableList<String>
|
||||||
|
) {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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<String>) {
|
||||||
|
MiraiConsole.start(MiraiConsoleUIPure())
|
||||||
|
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
||||||
|
MiraiConsole.stop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<Long>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLoginSolver(): LoginSolver {
|
||||||
|
return DefaultLoginSolver(
|
||||||
|
reader = object : LoginSolverInputReader {
|
||||||
|
override suspend fun read(question: String): String? {
|
||||||
|
return requestInput(question)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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<Long>())
|
||||||
|
BOT_MANAGERS[this.uin.toString()] =
|
||||||
|
(BOT_MANAGERS.getLongList(this.uin.toString()) as MutableList<Long>).apply { add(long) }
|
||||||
|
BotManagers.config.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bot.removeManager(long: Long) {
|
||||||
|
BOT_MANAGERS.putIfAbsent(this.uin.toString(), mutableListOf<Long>())
|
||||||
|
BOT_MANAGERS[this.uin.toString()] =
|
||||||
|
(BOT_MANAGERS.getLongList(this.uin.toString()) as MutableList<Long>).apply { add(long) }
|
||||||
|
BotManagers.config.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bot.getManagers(): List<Long> {
|
||||||
|
BOT_MANAGERS.putIfAbsent(this.uin.toString(), mutableListOf<Long>())
|
||||||
|
return BOT_MANAGERS.getLongList(this.uin.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Bot.checkManager(long: Long): Boolean {
|
||||||
|
return this.getManagers().contains(long)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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<Long>
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由UI层创建一个LoginSolver
|
||||||
|
*/
|
||||||
|
fun createLoginSolver(): LoginSolver
|
||||||
|
|
||||||
|
}
|
47
settings.gradle
Normal file
47
settings.gradle
Normal file
@ -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')
|
Loading…
Reference in New Issue
Block a user