diff --git a/README.md b/README.md
index 5e5c018f7..dd411b366 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,7 @@
-
-
+
----
[](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