mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-27 08:50:15 +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
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<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>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
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