Separate mirai-console series from main repository

This commit is contained in:
Him188 2020-02-27 13:30:15 +08:00
parent 6110cf8f4e
commit c6ae8e32c2
35 changed files with 0 additions and 3369 deletions

View File

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

View File

@ -1,45 +0,0 @@
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"
dependencies {
api(project(":mirai-core"))
api(project(":mirai-core-qqandroid"))
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

@ -1,36 +0,0 @@
/*
* 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

@ -1,94 +0,0 @@
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

@ -1,19 +0,0 @@
package net.mamoe.mirai.console.graphical.model
import javafx.beans.property.SimpleObjectProperty
import net.mamoe.mirai.Bot
import tornadofx.*
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

@ -1,17 +0,0 @@
package net.mamoe.mirai.console.graphical.model
import javafx.beans.property.SimpleStringProperty
import tornadofx.setValue
import tornadofx.getValue
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

@ -1,20 +0,0 @@
package net.mamoe.mirai.console.graphical.model
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleStringProperty
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

@ -1,17 +0,0 @@
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

@ -1,47 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,48 +0,0 @@
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.*
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

@ -1,9 +0,0 @@
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

@ -1,50 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -1,99 +0,0 @@
package net.mamoe.mirai.console.graphical.view
import com.jfoenix.controls.*
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

@ -1,36 +0,0 @@
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.*
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

@ -1,19 +0,0 @@
package net.mamoe.mirai.console.graphical.view
import javafx.scene.Parent
import tornadofx.*
class VerificationCodeFragment : Fragment() {
override val root = vbox {
//TODO: 显示验证码
form {
fieldset {
field("验证码") {
textfield()
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

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

View File

@ -1,44 +0,0 @@
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"
dependencies {
api(project(":mirai-core"))
api(project(":mirai-core-qqandroid"))
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

@ -1,31 +0,0 @@
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

@ -1,662 +0,0 @@
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)
}
}

View File

@ -1,88 +0,0 @@
# 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

@ -1,46 +0,0 @@
plugins {
id("kotlinx-serialization")
id("kotlin")
id("java")
}
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"
}
}
dependencies {
api(project(":mirai-core"))
api(project(":mirai-core-qqandroid"))
// 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
}

View File

@ -1,218 +0,0 @@
/*
* 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

@ -1,242 +0,0 @@
/*
* 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.Group
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.quote
import net.mamoe.mirai.message.data.toMessage
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import java.lang.StringBuilder
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

@ -1,265 +0,0 @@
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.message.GroupMessage
import net.mamoe.mirai.utils.SimpleLogger
import java.lang.StringBuilder
import java.util.NoSuchElementException
/**
* 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

@ -1,525 +0,0 @@
/*
* 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 tornadofx.c
import java.io.File
import java.io.InputStream
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.collections.LinkedHashMap
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

@ -1,410 +0,0 @@
/*
* 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.DefaultLogger
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

@ -1,16 +0,0 @@
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

@ -1,81 +0,0 @@
/*
* 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

@ -1,41 +0,0 @@
package net.mamoe.mirai.console.utils
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiProperties
import net.mamoe.mirai.console.utils.BotManagers.BOT_MANAGERS
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 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

@ -1,75 +0,0 @@
/*
* 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
}

View File

@ -42,9 +42,6 @@ include(':mirai-core')
//include(':mirai-core-timpc')
include(':mirai-core-qqandroid')
include(':mirai-japt')
include(':mirai-console')
include(':mirai-console-terminal')
//include(':mirai-api')
include(':mirai-api-http')
include(':mirai-demos:mirai-demo-1')