Merge remote-tracking branch 'origin/master'

This commit is contained in:
Him188 2020-02-26 10:11:18 +08:00
commit 1c95434e65
11 changed files with 190 additions and 72 deletions

View File

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

View File

@ -116,7 +116,8 @@ fun main() {
```
使用此方式释放session及其相关资源Bot不会被释放
**不使用的Session应当被释放否则Session持续保存Bot收到的消息将会导致内存泄露**
**不使用的Session应当被释放否则Session持续保存Bot收到的消息**
**长时间30分钟未被使用的Session会被系统自动释放以避免内存泄露**
#### 请求:

View File

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

View File

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

View File

@ -159,7 +159,7 @@ object MiraiConsole {
commandStr = "/$commandStr"
}
if (!CommandManager.runCommand(command.sender, commandStr)) {
logger("未知指令 $commandStr")
command.sender.sendMessage("未知指令 $commandStr")
}
}
}

View File

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

View File

@ -237,6 +237,7 @@ object DefaultCommands {
onCommand {
CommandManager.getCommands().let {
var size = 0
appendMessage("")//\n
it.toSet().forEach {
++size
appendMessage("-> " + it.name + " :" + it.description)

View File

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

View File

@ -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? {

View File

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