mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-09 09:50:16 +08:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
1c95434e65
17
README.md
17
README.md
@ -2,8 +2,7 @@
|
||||
<img width="160" src="http://img.mamoe.net/2020/02/16/a759783b42f72.png" alt="logo"></br>
|
||||
|
||||
<img width="95" src="http://img.mamoe.net/2020/02/16/c4aece361224d.png" alt="title">
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
[![Gitter](https://badges.gitter.im/mamoe/mirai.svg)](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
@ -89,6 +88,11 @@ TIM PC (2.3.2 版本,2019 年 8 月)协议的实现
|
||||
(目前不再更新此协议,请关注上文的安卓协议)
|
||||
|
||||
## 加入开发
|
||||
### 基于mirai的项目-如其他语言的SDK, 功能的拓展(无排名)
|
||||
|
||||
- [mirai-native](https://github.com/iTXTech/mirai-native) 支持酷Q插件在mirai上运行
|
||||
- [python-mirai](https://github.com/Chenwe-i-lin/python-mirai) 基于`Mirai-http-api`的 Mirai Framework for Python
|
||||
- [node-mirai](https://github.com/RedBeanN/node-mirai) Mirai的NodeJs SDK
|
||||
|
||||
我们欢迎一切形式的贡献。
|
||||
我们也期待有更多人能加入 `Mirai` 的开发。
|
||||
@ -102,8 +106,7 @@ TIM PC (2.3.2 版本,2019 年 8 月)协议的实现
|
||||
特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 为开源项目提供免费的 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 的授权
|
||||
[<img src=".github/jetbrains-variant-3.png" width="200"/>](https://www.jetbrains.com/?from=mirai)
|
||||
|
||||
### 第三方类库
|
||||
|
||||
### 第三方类库(无排名)
|
||||
- [kotlin-stdlib](https://github.com/JetBrains/kotlin)
|
||||
- [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines)
|
||||
- [kotlinx-io](https://github.com/Kotlin/kotlinx-io)
|
||||
@ -133,16 +136,16 @@ TIM PC (2.3.2 版本,2019 年 8 月)协议的实现
|
||||
------
|
||||
|
||||
Copyright (C) 2019-2020 mamoe and Mirai contributors
|
||||
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
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 Affero General Public License for more details.
|
||||
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
@ -116,7 +116,8 @@ fun main() {
|
||||
```
|
||||
|
||||
使用此方式释放session及其相关资源(Bot不会被释放)
|
||||
**不使用的Session应当被释放,否则Session持续保存Bot收到的消息,将会导致内存泄露**
|
||||
**不使用的Session应当被释放,否则Session持续保存Bot收到的消息**
|
||||
**长时间(30分钟)未被使用的Session会被系统自动释放,以避免内存泄露**
|
||||
|
||||
#### 请求:
|
||||
|
||||
|
@ -15,8 +15,7 @@ import net.mamoe.mirai.api.http.queue.MessageQueue
|
||||
import net.mamoe.mirai.event.Listener
|
||||
import net.mamoe.mirai.event.events.BotEvent
|
||||
import net.mamoe.mirai.event.subscribeAlways
|
||||
import net.mamoe.mirai.event.subscribeMessages
|
||||
import net.mamoe.mirai.message.MessagePacket
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
@ -57,7 +56,13 @@ internal object SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(sessionKey: String) = allSession[sessionKey]
|
||||
fun createAuthedSession(bot: Bot, originKey: String): AuthedSession = AuthedSession(bot, originKey, EmptyCoroutineContext).also { session ->
|
||||
closeSession(originKey)
|
||||
allSession[originKey] = session
|
||||
}
|
||||
|
||||
operator fun get(sessionKey: String) = allSession[sessionKey]?.also {
|
||||
if (it is AuthedSession) it.latestUsed = currentTimeSeconds }
|
||||
|
||||
fun containSession(sessionKey: String): Boolean = allSession.containsKey(sessionKey)
|
||||
|
||||
@ -76,14 +81,12 @@ internal object SessionManager {
|
||||
* 需使用[SessionManager]
|
||||
*/
|
||||
abstract class Session internal constructor(
|
||||
coroutineContext: CoroutineContext
|
||||
coroutineContext: CoroutineContext,
|
||||
val key: String = generateSessionKey()
|
||||
) : CoroutineScope {
|
||||
val supervisorJob = SupervisorJob(coroutineContext[Job])
|
||||
final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
|
||||
|
||||
val key: String = generateSessionKey()
|
||||
|
||||
|
||||
internal open fun close() {
|
||||
supervisorJob.complete()
|
||||
}
|
||||
@ -101,16 +104,33 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses
|
||||
* 任何[TempSession]认证后转化为一个[AuthedSession]
|
||||
* 在这一步[AuthedSession]应该已经有assigned的bot
|
||||
*/
|
||||
class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext) : Session(coroutineContext) {
|
||||
class AuthedSession internal constructor(val bot: Bot, originKey: String, coroutineContext: CoroutineContext) : Session(coroutineContext, originKey) {
|
||||
|
||||
companion object {
|
||||
const val CHECK_TIME = 1800L // 1800s aka 30min
|
||||
}
|
||||
|
||||
val messageQueue = MessageQueue()
|
||||
private val _listener: Listener<BotEvent>
|
||||
private val releaseJob: Job //手动释放将会在下一次检查时回收Session
|
||||
|
||||
internal var latestUsed = currentTimeSeconds
|
||||
|
||||
init {
|
||||
_listener = bot.subscribeAlways{ this.run(messageQueue::add) }
|
||||
releaseJob = launch {
|
||||
while (true) {
|
||||
delay(CHECK_TIME * 1000)
|
||||
if (currentTimeSeconds - latestUsed >= CHECK_TIME) {
|
||||
SessionManager.closeSession(this@AuthedSession)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
messageQueue.clear()
|
||||
_listener.complete()
|
||||
super.close()
|
||||
}
|
||||
|
@ -35,10 +35,7 @@ fun Application.authModule() {
|
||||
|
||||
miraiVerify<BindDTO>("/verify", verifiedSessionKey = false) {
|
||||
val bot = getBotOrThrow(it.qq)
|
||||
with(SessionManager) {
|
||||
closeSession(it.sessionKey)
|
||||
allSession[it.sessionKey] = AuthedSession(bot, EmptyCoroutineContext)
|
||||
}
|
||||
SessionManager.createAuthedSession(bot, it.sessionKey)
|
||||
call.respondStateCode(StateCode.Success)
|
||||
}
|
||||
|
||||
|
@ -159,7 +159,7 @@ object MiraiConsole {
|
||||
commandStr = "/$commandStr"
|
||||
}
|
||||
if (!CommandManager.runCommand(command.sender, commandStr)) {
|
||||
logger("未知指令 $commandStr")
|
||||
command.sender.sendMessage("未知指令 $commandStr")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ object CommandManager {
|
||||
val allNames = mutableListOf(command.name).also { it.addAll(command.alias) }
|
||||
allNames.forEach {
|
||||
if (registeredCommand.containsKey(it)) {
|
||||
error("net.mamoe.mirai.Command Name(or Alias) $it is already registered, consider if same function plugin was installed")
|
||||
error("Command Name(or Alias) $it is already registered, consider if same functional plugin was installed")
|
||||
}
|
||||
}
|
||||
allNames.forEach {
|
||||
@ -74,6 +74,8 @@ object CommandManager {
|
||||
)
|
||||
) {
|
||||
PluginManager.onCommand(this, args)
|
||||
} else {
|
||||
sender.sendMessage(this.usage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
sender.sendMessage("在运行指令时出现了未知错误")
|
||||
@ -104,13 +106,13 @@ interface CommandSender {
|
||||
}
|
||||
|
||||
abstract class CommandSenderImpl : CommandSender {
|
||||
private val builder = StringBuilder()
|
||||
internal val builder = StringBuilder()
|
||||
|
||||
override fun appendMessage(message: String) {
|
||||
builder.append(message).append("\n")
|
||||
}
|
||||
|
||||
internal suspend fun flushMessage() {
|
||||
internal open suspend fun flushMessage() {
|
||||
if (!builder.isEmpty()) {
|
||||
sendMessage(builder.toString().removeSuffix("\n"))
|
||||
}
|
||||
@ -125,6 +127,11 @@ object ConsoleCommandSender : CommandSenderImpl() {
|
||||
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() {
|
||||
@ -156,6 +163,8 @@ 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()
|
||||
}
|
||||
@ -163,7 +172,8 @@ interface Command {
|
||||
abstract class BlockingCommand(
|
||||
override val name: String,
|
||||
override val alias: List<String> = listOf(),
|
||||
override val description: String = ""
|
||||
override val description: String = "",
|
||||
override val usage: String = ""
|
||||
) : Command {
|
||||
/**
|
||||
* 最高优先级监听器
|
||||
@ -186,6 +196,7 @@ 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 {
|
||||
@ -201,6 +212,7 @@ 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) {
|
||||
@ -209,7 +221,7 @@ class CommandBuilder internal constructor() {
|
||||
|
||||
fun register(): Command {
|
||||
if (name == null || onCommand == null) {
|
||||
error("net.mamoe.mirai.CommandBuilder not complete")
|
||||
error("CommandBuilder not complete")
|
||||
}
|
||||
if (alias == null) {
|
||||
alias = listOf()
|
||||
@ -218,6 +230,7 @@ class CommandBuilder internal constructor() {
|
||||
name!!,
|
||||
alias!!,
|
||||
description,
|
||||
usage,
|
||||
onCommand!!
|
||||
).also { it.register() }
|
||||
}
|
||||
|
@ -237,6 +237,7 @@ object DefaultCommands {
|
||||
onCommand {
|
||||
CommandManager.getCommands().let {
|
||||
var size = 0
|
||||
appendMessage("")//\n
|
||||
it.toSet().forEach {
|
||||
++size
|
||||
appendMessage("-> " + it.name + " :" + it.description)
|
||||
|
@ -184,9 +184,11 @@ class WithDefaultWriteLoader<T : Any>(
|
||||
prop: KProperty<*>
|
||||
): ReadWriteProperty<Any, T> {
|
||||
val defaultValue by lazy { defaultValue.invoke() }
|
||||
config.setIfAbsent(prop.name, defaultValue)
|
||||
if (save) {
|
||||
config.save()
|
||||
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 {
|
||||
|
@ -31,6 +31,9 @@ abstract class PluginBase(coroutineContext: CoroutineContext) : CoroutineScope {
|
||||
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() }
|
||||
}
|
||||
@ -68,12 +71,14 @@ abstract class PluginBase(coroutineContext: CoroutineContext) : CoroutineScope {
|
||||
this.onEnable()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加载一个data folder中的Config
|
||||
* 这个config是read-write的
|
||||
*/
|
||||
fun loadConfig(fileName: String): Config {
|
||||
return Config.load(File(fileName))
|
||||
return Config.load(dataFolder.absolutePath + fileName)
|
||||
}
|
||||
|
||||
|
||||
@JvmOverloads
|
||||
internal fun disable(throwable: CancellationException? = null) {
|
||||
this.coroutineContext[Job]!!.cancelChildren(throwable)
|
||||
@ -87,18 +92,43 @@ abstract class PluginBase(coroutineContext: CoroutineContext) : CoroutineScope {
|
||||
this.onLoad()
|
||||
}
|
||||
|
||||
fun getPluginManager() = PluginManager
|
||||
val pluginManager = PluginManager
|
||||
|
||||
val logger: MiraiLogger by lazy {
|
||||
DefaultLogger(pluginDescription.name)
|
||||
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 PluginManager.getFileInJarByName(
|
||||
this.pluginDescription.name,
|
||||
fileName
|
||||
)
|
||||
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(
|
||||
@ -361,7 +391,7 @@ object PluginManager {
|
||||
|
||||
|
||||
/**
|
||||
* 根据插件名字找Jar resources中的文件
|
||||
* 根据插件名字找Jar中的文件
|
||||
* null => 没找到
|
||||
*/
|
||||
fun getFileInJarByName(pluginName: String, toFind: String): InputStream? {
|
||||
|
@ -10,19 +10,29 @@
|
||||
package net.mamoe.mirai.imageplugin
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import net.mamoe.mirai.console.command.registerCommand
|
||||
import net.mamoe.mirai.console.plugins.Config
|
||||
import net.mamoe.mirai.console.plugins.ConfigSection
|
||||
import net.mamoe.mirai.console.plugins.PluginBase
|
||||
import net.mamoe.mirai.console.plugins.withDefaultWriteSave
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.sendMessage
|
||||
import net.mamoe.mirai.event.events.BotOnlineEvent
|
||||
import net.mamoe.mirai.event.events.MemberPermissionChangeEvent
|
||||
import net.mamoe.mirai.event.subscribeAlways
|
||||
import net.mamoe.mirai.event.subscribeGroupMessages
|
||||
import net.mamoe.mirai.event.subscribeMessages
|
||||
import net.mamoe.mirai.message.data.Image
|
||||
import net.mamoe.mirai.message.data.sendTo
|
||||
import net.mamoe.mirai.message.upload
|
||||
import net.mamoe.mirai.message.uploadAsImage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import org.jsoup.Jsoup
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.awt.RenderingHints
|
||||
import java.awt.image.BufferedImage
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
|
||||
class ImageSenderMain : PluginBase() {
|
||||
|
||||
@ -30,42 +40,60 @@ class ImageSenderMain : PluginBase() {
|
||||
lateinit var normal: List<ConfigSection>
|
||||
lateinit var r18: List<ConfigSection>
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@MiraiExperimentalAPI
|
||||
|
||||
val config by lazy {
|
||||
loadConfig("setting.yml")
|
||||
}
|
||||
|
||||
val Normal_Image_Trigger by config.withDefaultWriteSave { "色图" }
|
||||
val R18_Image_Trigger by config.withDefaultWriteSave { "不够色" }
|
||||
val Image_Resize_Max_Width_Height by config.withDefaultWriteSave { 800 }
|
||||
|
||||
val groupsAllowNormal by lazy {
|
||||
config.getLongList("Allow_Normal_Image_Groups").toMutableList()
|
||||
}
|
||||
|
||||
val groupsAllowR18 by lazy {
|
||||
config.getLongList("Allow_R18_Image_Groups").toMutableList()
|
||||
}
|
||||
|
||||
override fun onDisable() {
|
||||
config["Allow_R18_Image_Groups"] = groupsAllowR18
|
||||
config["Allow_Normal_Image_Groups"] = groupsAllowNormal
|
||||
config.save()
|
||||
}
|
||||
|
||||
override fun onEnable() {
|
||||
logger.info("Image Sender plugin enabled")
|
||||
GlobalScope.subscribeAlways<BotOnlineEvent> {
|
||||
registerCommands()
|
||||
subscribeAlways<MemberPermissionChangeEvent> {
|
||||
logger.info("${this.bot.uin} login succeed, it will be controlled by Image Sender Plugin")
|
||||
this.bot.subscribeMessages {
|
||||
(contains("色图")) {
|
||||
try {
|
||||
with(normal.random()) {
|
||||
getImage(
|
||||
subject, this.getString("url"), this.getString("pid")
|
||||
).plus(this.getString("tags")).send()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
reply(e.message ?: "unknown error")
|
||||
}
|
||||
this.bot.subscribeGroupMessages {
|
||||
(contains(Normal_Image_Trigger)) {
|
||||
sendImage(subject, normal.random())
|
||||
}
|
||||
|
||||
(contains("不够色")) {
|
||||
try {
|
||||
with(r18.random()) {
|
||||
getImage(
|
||||
subject, this.getString("url"), this.getString("pid")
|
||||
).plus(this.getString("tags")).send()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
reply(e.message ?: "unknown error")
|
||||
}
|
||||
(contains(R18_Image_Trigger)) {
|
||||
sendImage(subject, r18.random())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getImage(contact: Contact, url: String, pid: String): Image {
|
||||
return withTimeoutOrNull(20 * 1000) {
|
||||
private fun sendImage(contact: Contact, configSection: ConfigSection) {
|
||||
launch {
|
||||
try {
|
||||
logger.info("正在推送图片")
|
||||
getImage(
|
||||
contact, configSection.getString("url"), configSection.getString("pid"), 800
|
||||
).plus(configSection.getString("tags")).sendTo(contact)
|
||||
} catch (e: Exception) {
|
||||
contact.sendMessage(e.message ?: "unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getImage(contact: Contact, url: String, pid: String, maxWidthOrHeight: Int): Image {
|
||||
val bodyStream = withTimeoutOrNull(20 * 1000) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Jsoup
|
||||
.connect(url)
|
||||
@ -78,14 +106,35 @@ class ImageSenderMain : PluginBase() {
|
||||
.maxBodySize(100000000)
|
||||
.execute().also { check(it.statusCode() == 200) { "Failed to download image" } }
|
||||
}
|
||||
}?.bodyStream()?.uploadAsImage(contact) ?: error("Unable to download image")
|
||||
}?.bodyStream() ?: error("Failed to download image")
|
||||
if (maxWidthOrHeight < 1) {
|
||||
return bodyStream.uploadAsImage(contact)
|
||||
}
|
||||
val image = withContext(Dispatchers.IO) {
|
||||
ImageIO.read(bodyStream)
|
||||
}
|
||||
if (image.width.coerceAtLeast(image.height) <= maxWidthOrHeight) {
|
||||
return image.upload(contact)
|
||||
}
|
||||
val rate = (maxWidthOrHeight.toFloat() / image.width.coerceAtLeast(image.height))
|
||||
val newWidth = (image.width * rate).toInt()
|
||||
val newHeight = (image.height * rate).toInt()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val dimg = BufferedImage(newWidth, newHeight, image.type)
|
||||
val g = dimg.createGraphics()
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
|
||||
g.drawImage(image, 0, 0, newWidth, newHeight, 0, 0, image.width, image.height, null)
|
||||
g.dispose()
|
||||
dimg
|
||||
}.upload(contact)
|
||||
}
|
||||
|
||||
|
||||
override fun onLoad() {
|
||||
logger.info("loading local image data")
|
||||
|
||||
try {
|
||||
images = Config.load(getResources(fileName = "data.yml")!!, "yml")
|
||||
images = getResourcesConfig("data.yml")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logger.info("无法加载本地图片")
|
||||
@ -97,8 +146,10 @@ class ImageSenderMain : PluginBase() {
|
||||
logger.info("R18 * " + r18.size)
|
||||
}
|
||||
|
||||
fun registerCommands() {
|
||||
registerCommand {
|
||||
|
||||
override fun onDisable() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user