diff --git a/README.md b/README.md index 5e5c018f7..dd411b366 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ logo
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 的授权 [](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 . diff --git a/mirai-api-http/README_CH.md b/mirai-api-http/README_CH.md index ff595681d..05a7cd35e 100644 --- a/mirai-api-http/README_CH.md +++ b/mirai-api-http/README_CH.md @@ -116,7 +116,8 @@ fun main() { ``` 使用此方式释放session及其相关资源(Bot不会被释放) -**不使用的Session应当被释放,否则Session持续保存Bot收到的消息,将会导致内存泄露** +**不使用的Session应当被释放,否则Session持续保存Bot收到的消息** +**长时间(30分钟)未被使用的Session会被系统自动释放,以避免内存泄露** #### 请求: diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt index 32f7aeeb4..3ab8d40c6 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt @@ -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 + 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() } diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt index aa229181d..1d495a088 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt @@ -35,10 +35,7 @@ fun Application.authModule() { miraiVerify("/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) } diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt index 5ee1b1b5b..4f634f5bc 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt @@ -159,7 +159,7 @@ object MiraiConsole { commandStr = "/$commandStr" } if (!CommandManager.runCommand(command.sender, commandStr)) { - logger("未知指令 $commandStr") + command.sender.sendMessage("未知指令 $commandStr") } } } diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt index 53e7f5175..17bc58c21 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt @@ -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 val description: String + val usage: String + suspend fun onCommand(sender: CommandSender, args: List): Boolean fun register() } @@ -163,7 +172,8 @@ interface Command { abstract class BlockingCommand( override val name: String, override val alias: List = 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, override val description: String, + override val usage: String = "", val onCommand: suspend CommandSender.(args: List) -> Boolean ) : Command { override suspend fun onCommand(sender: CommandSender, args: List): Boolean { @@ -201,6 +212,7 @@ class CommandBuilder internal constructor() { var name: String? = null var alias: List? = null var description: String = "" + var usage: String = "use /help for help" var onCommand: (suspend CommandSender.(args: List) -> Boolean)? = null fun onCommand(commandProcess: suspend CommandSender.(args: List) -> 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() } } diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt index 6f13bd9b0..651eb423b 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/DefaultCommands.kt @@ -237,6 +237,7 @@ object DefaultCommands { onCommand { CommandManager.getCommands().let { var size = 0 + appendMessage("")//\n it.toSet().forEach { ++size appendMessage("-> " + it.name + " :" + it.description) diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt index e80676438..ed52dae92 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt @@ -184,9 +184,11 @@ class WithDefaultWriteLoader( prop: KProperty<*> ): ReadWriteProperty { 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 { override fun getValue(thisRef: Any, property: KProperty<*>): T { diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt index f43aa1685..e5e35c34e 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt @@ -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? { diff --git a/mirai-plugins/image-sender/src/main/java/net/mamoe/mirai/imageplugin/ImageSenderMain.kt b/mirai-plugins/image-sender/src/main/java/net/mamoe/mirai/imageplugin/ImageSenderMain.kt index 2e575f86d..61e3e6ea4 100644 --- a/mirai-plugins/image-sender/src/main/java/net/mamoe/mirai/imageplugin/ImageSenderMain.kt +++ b/mirai-plugins/image-sender/src/main/java/net/mamoe/mirai/imageplugin/ImageSenderMain.kt @@ -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 lateinit var r18: List - @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 { + registerCommands() + subscribeAlways { 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() { - + } } + } \ No newline at end of file diff --git a/mirai-plugins/image-sender/src/main/java/net/mamoe/mirai/imageplugin/Test.kt b/mirai-plugins/image-sender/src/main/resources/Test.kt similarity index 100% rename from mirai-plugins/image-sender/src/main/java/net/mamoe/mirai/imageplugin/Test.kt rename to mirai-plugins/image-sender/src/main/resources/Test.kt