Separate mirai-console series from main repository

This commit is contained in:
Him188 2020-02-27 13:18:33 +08:00
parent 5510c1be7d
commit 86d1ecf3f0
44 changed files with 3950 additions and 80 deletions

42
.gitignore vendored Normal file
View 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
View File

@ -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/>.

View File

@ -1,2 +1,3 @@
# mirai-console
mirai的高效率QQ机器人框架
# mirai-japt
Mirai 的 Java API

44
build.gradle Normal file
View 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
View 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

Binary file not shown.

View 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
View 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
View 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

View File

@ -0,0 +1,6 @@
### Mirai Console Graphical
支持windows/mac
有正式UI界面实现的CONSOLE
优点: 适合新手/完全不懂编程的/界面美丽
缺点: 不能在linux服务器运行
所使用插件系统与terminal版本一致 可以来回切换

View 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"
}

View File

@ -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()
}
}

View File

@ -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.
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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 = ""
}
}
}
}
}

View File

@ -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))
}
)
}
}

View File

@ -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)
}

View File

@ -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("...") {
}
}
}
}
}

View File

@ -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()
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,6 @@
### Mirai Console Terminal
支持windows/mac/linux
在terminal环境下的Console, 由控制台富文本实现简易UI
优点: 可以在linux环境下运行/简洁使用效率高
缺点: 需要有略微的terminal知识
所使用插件系统与graphical版本一致 可以来回切换

View 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
}

View File

@ -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()
})
}
}
}

View File

@ -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
View 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>

View 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())
}
}
}

View File

@ -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)
}*/
}
}

View File

@ -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()
}

View File

@ -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
}
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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()
})
}
}
}

View File

@ -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)
}
}
)
}
}

View File

@ -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)
}

View File

@ -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
*
* identitylog所属的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
View 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')