mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-24 15:00:38 +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="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">
|
<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)
|
[![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` 的开发。
|
我们也期待有更多人能加入 `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 的授权
|
特别感谢 [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)
|
[<img src=".github/jetbrains-variant-3.png" width="200"/>](https://www.jetbrains.com/?from=mirai)
|
||||||
|
|
||||||
### 第三方类库
|
### 第三方类库(无排名)
|
||||||
|
|
||||||
- [kotlin-stdlib](https://github.com/JetBrains/kotlin)
|
- [kotlin-stdlib](https://github.com/JetBrains/kotlin)
|
||||||
- [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines)
|
- [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines)
|
||||||
- [kotlinx-io](https://github.com/Kotlin/kotlinx-io)
|
- [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
|
Copyright (C) 2019-2020 mamoe and Mirai contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as
|
it under the terms of the GNU Affero General Public License as
|
||||||
published by the Free Software Foundation, either version 3 of the
|
published by the Free Software Foundation, either version 3 of the
|
||||||
License, or (at your option) any later version.
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
@ -116,7 +116,8 @@ fun main() {
|
|||||||
```
|
```
|
||||||
|
|
||||||
使用此方式释放session及其相关资源(Bot不会被释放)
|
使用此方式释放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.Listener
|
||||||
import net.mamoe.mirai.event.events.BotEvent
|
import net.mamoe.mirai.event.events.BotEvent
|
||||||
import net.mamoe.mirai.event.subscribeAlways
|
import net.mamoe.mirai.event.subscribeAlways
|
||||||
import net.mamoe.mirai.event.subscribeMessages
|
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||||
import net.mamoe.mirai.message.MessagePacket
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
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)
|
fun containSession(sessionKey: String): Boolean = allSession.containsKey(sessionKey)
|
||||||
|
|
||||||
@ -76,14 +81,12 @@ internal object SessionManager {
|
|||||||
* 需使用[SessionManager]
|
* 需使用[SessionManager]
|
||||||
*/
|
*/
|
||||||
abstract class Session internal constructor(
|
abstract class Session internal constructor(
|
||||||
coroutineContext: CoroutineContext
|
coroutineContext: CoroutineContext,
|
||||||
|
val key: String = generateSessionKey()
|
||||||
) : CoroutineScope {
|
) : CoroutineScope {
|
||||||
val supervisorJob = SupervisorJob(coroutineContext[Job])
|
val supervisorJob = SupervisorJob(coroutineContext[Job])
|
||||||
final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
|
final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
|
||||||
|
|
||||||
val key: String = generateSessionKey()
|
|
||||||
|
|
||||||
|
|
||||||
internal open fun close() {
|
internal open fun close() {
|
||||||
supervisorJob.complete()
|
supervisorJob.complete()
|
||||||
}
|
}
|
||||||
@ -101,16 +104,33 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses
|
|||||||
* 任何[TempSession]认证后转化为一个[AuthedSession]
|
* 任何[TempSession]认证后转化为一个[AuthedSession]
|
||||||
* 在这一步[AuthedSession]应该已经有assigned的bot
|
* 在这一步[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()
|
val messageQueue = MessageQueue()
|
||||||
private val _listener: Listener<BotEvent>
|
private val _listener: Listener<BotEvent>
|
||||||
|
private val releaseJob: Job //手动释放将会在下一次检查时回收Session
|
||||||
|
|
||||||
|
internal var latestUsed = currentTimeSeconds
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_listener = bot.subscribeAlways{ this.run(messageQueue::add) }
|
_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() {
|
override fun close() {
|
||||||
|
messageQueue.clear()
|
||||||
_listener.complete()
|
_listener.complete()
|
||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
|
@ -35,10 +35,7 @@ fun Application.authModule() {
|
|||||||
|
|
||||||
miraiVerify<BindDTO>("/verify", verifiedSessionKey = false) {
|
miraiVerify<BindDTO>("/verify", verifiedSessionKey = false) {
|
||||||
val bot = getBotOrThrow(it.qq)
|
val bot = getBotOrThrow(it.qq)
|
||||||
with(SessionManager) {
|
SessionManager.createAuthedSession(bot, it.sessionKey)
|
||||||
closeSession(it.sessionKey)
|
|
||||||
allSession[it.sessionKey] = AuthedSession(bot, EmptyCoroutineContext)
|
|
||||||
}
|
|
||||||
call.respondStateCode(StateCode.Success)
|
call.respondStateCode(StateCode.Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ object MiraiConsole {
|
|||||||
commandStr = "/$commandStr"
|
commandStr = "/$commandStr"
|
||||||
}
|
}
|
||||||
if (!CommandManager.runCommand(command.sender, 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) }
|
val allNames = mutableListOf(command.name).also { it.addAll(command.alias) }
|
||||||
allNames.forEach {
|
allNames.forEach {
|
||||||
if (registeredCommand.containsKey(it)) {
|
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 {
|
allNames.forEach {
|
||||||
@ -74,6 +74,8 @@ object CommandManager {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
PluginManager.onCommand(this, args)
|
PluginManager.onCommand(this, args)
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(this.usage)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
sender.sendMessage("在运行指令时出现了未知错误")
|
sender.sendMessage("在运行指令时出现了未知错误")
|
||||||
@ -104,13 +106,13 @@ interface CommandSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class CommandSenderImpl : CommandSender {
|
abstract class CommandSenderImpl : CommandSender {
|
||||||
private val builder = StringBuilder()
|
internal val builder = StringBuilder()
|
||||||
|
|
||||||
override fun appendMessage(message: String) {
|
override fun appendMessage(message: String) {
|
||||||
builder.append(message).append("\n")
|
builder.append(message).append("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun flushMessage() {
|
internal open suspend fun flushMessage() {
|
||||||
if (!builder.isEmpty()) {
|
if (!builder.isEmpty()) {
|
||||||
sendMessage(builder.toString().removeSuffix("\n"))
|
sendMessage(builder.toString().removeSuffix("\n"))
|
||||||
}
|
}
|
||||||
@ -125,6 +127,11 @@ object ConsoleCommandSender : CommandSenderImpl() {
|
|||||||
override suspend fun sendMessage(message: String) {
|
override suspend fun sendMessage(message: String) {
|
||||||
MiraiConsole.logger("[Command]", 0, message)
|
MiraiConsole.logger("[Command]", 0, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun flushMessage() {
|
||||||
|
super.flushMessage()
|
||||||
|
builder.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class ContactCommandSender(val contact: Contact) : CommandSenderImpl() {
|
open class ContactCommandSender(val contact: Contact) : CommandSenderImpl() {
|
||||||
@ -156,6 +163,8 @@ interface Command {
|
|||||||
val name: String
|
val name: String
|
||||||
val alias: List<String>
|
val alias: List<String>
|
||||||
val description: String
|
val description: String
|
||||||
|
val usage: String
|
||||||
|
|
||||||
suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean
|
suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean
|
||||||
fun register()
|
fun register()
|
||||||
}
|
}
|
||||||
@ -163,7 +172,8 @@ interface Command {
|
|||||||
abstract class BlockingCommand(
|
abstract class BlockingCommand(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val alias: List<String> = listOf(),
|
override val alias: List<String> = listOf(),
|
||||||
override val description: String = ""
|
override val description: String = "",
|
||||||
|
override val usage: String = ""
|
||||||
) : Command {
|
) : Command {
|
||||||
/**
|
/**
|
||||||
* 最高优先级监听器
|
* 最高优先级监听器
|
||||||
@ -186,6 +196,7 @@ class AnonymousCommand internal constructor(
|
|||||||
override val name: String,
|
override val name: String,
|
||||||
override val alias: List<String>,
|
override val alias: List<String>,
|
||||||
override val description: String,
|
override val description: String,
|
||||||
|
override val usage: String = "",
|
||||||
val onCommand: suspend CommandSender.(args: List<String>) -> Boolean
|
val onCommand: suspend CommandSender.(args: List<String>) -> Boolean
|
||||||
) : Command {
|
) : Command {
|
||||||
override suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean {
|
override suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean {
|
||||||
@ -201,6 +212,7 @@ class CommandBuilder internal constructor() {
|
|||||||
var name: String? = null
|
var name: String? = null
|
||||||
var alias: List<String>? = null
|
var alias: List<String>? = null
|
||||||
var description: String = ""
|
var description: String = ""
|
||||||
|
var usage: String = "use /help for help"
|
||||||
var onCommand: (suspend CommandSender.(args: List<String>) -> Boolean)? = null
|
var onCommand: (suspend CommandSender.(args: List<String>) -> Boolean)? = null
|
||||||
|
|
||||||
fun onCommand(commandProcess: suspend CommandSender.(args: List<String>) -> Boolean) {
|
fun onCommand(commandProcess: suspend CommandSender.(args: List<String>) -> Boolean) {
|
||||||
@ -209,7 +221,7 @@ class CommandBuilder internal constructor() {
|
|||||||
|
|
||||||
fun register(): Command {
|
fun register(): Command {
|
||||||
if (name == null || onCommand == null) {
|
if (name == null || onCommand == null) {
|
||||||
error("net.mamoe.mirai.CommandBuilder not complete")
|
error("CommandBuilder not complete")
|
||||||
}
|
}
|
||||||
if (alias == null) {
|
if (alias == null) {
|
||||||
alias = listOf()
|
alias = listOf()
|
||||||
@ -218,6 +230,7 @@ class CommandBuilder internal constructor() {
|
|||||||
name!!,
|
name!!,
|
||||||
alias!!,
|
alias!!,
|
||||||
description,
|
description,
|
||||||
|
usage,
|
||||||
onCommand!!
|
onCommand!!
|
||||||
).also { it.register() }
|
).also { it.register() }
|
||||||
}
|
}
|
||||||
|
@ -237,6 +237,7 @@ object DefaultCommands {
|
|||||||
onCommand {
|
onCommand {
|
||||||
CommandManager.getCommands().let {
|
CommandManager.getCommands().let {
|
||||||
var size = 0
|
var size = 0
|
||||||
|
appendMessage("")//\n
|
||||||
it.toSet().forEach {
|
it.toSet().forEach {
|
||||||
++size
|
++size
|
||||||
appendMessage("-> " + it.name + " :" + it.description)
|
appendMessage("-> " + it.name + " :" + it.description)
|
||||||
|
@ -184,9 +184,11 @@ class WithDefaultWriteLoader<T : Any>(
|
|||||||
prop: KProperty<*>
|
prop: KProperty<*>
|
||||||
): ReadWriteProperty<Any, T> {
|
): ReadWriteProperty<Any, T> {
|
||||||
val defaultValue by lazy { defaultValue.invoke() }
|
val defaultValue by lazy { defaultValue.invoke() }
|
||||||
config.setIfAbsent(prop.name, defaultValue)
|
if (!config.contains(prop.name)) {
|
||||||
if (save) {
|
config[prop.name] = defaultValue
|
||||||
config.save()
|
if (save) {
|
||||||
|
config.save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return object : ReadWriteProperty<Any, T> {
|
return object : ReadWriteProperty<Any, T> {
|
||||||
override fun getValue(thisRef: Any, property: KProperty<*>): T {
|
override fun getValue(thisRef: Any, property: KProperty<*>): T {
|
||||||
|
@ -31,6 +31,9 @@ abstract class PluginBase(coroutineContext: CoroutineContext) : CoroutineScope {
|
|||||||
private val supervisorJob = SupervisorJob()
|
private val supervisorJob = SupervisorJob()
|
||||||
final override val coroutineContext: CoroutineContext = coroutineContext + supervisorJob
|
final override val coroutineContext: CoroutineContext = coroutineContext + supervisorJob
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件被分配的data folder, 如果插件改名了 data folder 也会变 请注意
|
||||||
|
*/
|
||||||
val dataFolder: File by lazy {
|
val dataFolder: File by lazy {
|
||||||
File(PluginManager.pluginsPath + pluginDescription.name).also { it.mkdir() }
|
File(PluginManager.pluginsPath + pluginDescription.name).also { it.mkdir() }
|
||||||
}
|
}
|
||||||
@ -68,12 +71,14 @@ abstract class PluginBase(coroutineContext: CoroutineContext) : CoroutineScope {
|
|||||||
this.onEnable()
|
this.onEnable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载一个data folder中的Config
|
||||||
|
* 这个config是read-write的
|
||||||
|
*/
|
||||||
fun loadConfig(fileName: String): Config {
|
fun loadConfig(fileName: String): Config {
|
||||||
return Config.load(File(fileName))
|
return Config.load(dataFolder.absolutePath + fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
internal fun disable(throwable: CancellationException? = null) {
|
internal fun disable(throwable: CancellationException? = null) {
|
||||||
this.coroutineContext[Job]!!.cancelChildren(throwable)
|
this.coroutineContext[Job]!!.cancelChildren(throwable)
|
||||||
@ -87,18 +92,43 @@ abstract class PluginBase(coroutineContext: CoroutineContext) : CoroutineScope {
|
|||||||
this.onLoad()
|
this.onLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPluginManager() = PluginManager
|
val pluginManager = PluginManager
|
||||||
|
|
||||||
val logger: MiraiLogger by lazy {
|
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? {
|
fun getResources(fileName: String): InputStream? {
|
||||||
return PluginManager.getFileInJarByName(
|
return try {
|
||||||
this.pluginDescription.name,
|
this.javaClass.classLoader.getResourceAsStream(fileName)
|
||||||
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(
|
class PluginDescription(
|
||||||
@ -361,7 +391,7 @@ object PluginManager {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据插件名字找Jar resources中的文件
|
* 根据插件名字找Jar中的文件
|
||||||
* null => 没找到
|
* null => 没找到
|
||||||
*/
|
*/
|
||||||
fun getFileInJarByName(pluginName: String, toFind: String): InputStream? {
|
fun getFileInJarByName(pluginName: String, toFind: String): InputStream? {
|
||||||
|
@ -10,19 +10,29 @@
|
|||||||
package net.mamoe.mirai.imageplugin
|
package net.mamoe.mirai.imageplugin
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
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.Config
|
||||||
import net.mamoe.mirai.console.plugins.ConfigSection
|
import net.mamoe.mirai.console.plugins.ConfigSection
|
||||||
import net.mamoe.mirai.console.plugins.PluginBase
|
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.Contact
|
||||||
|
import net.mamoe.mirai.contact.sendMessage
|
||||||
import net.mamoe.mirai.event.events.BotOnlineEvent
|
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.subscribeAlways
|
||||||
|
import net.mamoe.mirai.event.subscribeGroupMessages
|
||||||
import net.mamoe.mirai.event.subscribeMessages
|
import net.mamoe.mirai.event.subscribeMessages
|
||||||
import net.mamoe.mirai.message.data.Image
|
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.message.uploadAsImage
|
||||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import java.io.File
|
import java.awt.RenderingHints
|
||||||
import java.net.URL
|
import java.awt.image.BufferedImage
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
|
|
||||||
class ImageSenderMain : PluginBase() {
|
class ImageSenderMain : PluginBase() {
|
||||||
|
|
||||||
@ -30,42 +40,60 @@ class ImageSenderMain : PluginBase() {
|
|||||||
lateinit var normal: List<ConfigSection>
|
lateinit var normal: List<ConfigSection>
|
||||||
lateinit var r18: 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() {
|
override fun onEnable() {
|
||||||
logger.info("Image Sender plugin enabled")
|
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")
|
logger.info("${this.bot.uin} login succeed, it will be controlled by Image Sender Plugin")
|
||||||
this.bot.subscribeMessages {
|
this.bot.subscribeGroupMessages {
|
||||||
(contains("色图")) {
|
(contains(Normal_Image_Trigger)) {
|
||||||
try {
|
sendImage(subject, normal.random())
|
||||||
with(normal.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)) {
|
||||||
(contains("不够色")) {
|
sendImage(subject, r18.random())
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getImage(contact: Contact, url: String, pid: String): Image {
|
private fun sendImage(contact: Contact, configSection: ConfigSection) {
|
||||||
return withTimeoutOrNull(20 * 1000) {
|
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) {
|
withContext(Dispatchers.IO) {
|
||||||
Jsoup
|
Jsoup
|
||||||
.connect(url)
|
.connect(url)
|
||||||
@ -78,14 +106,35 @@ class ImageSenderMain : PluginBase() {
|
|||||||
.maxBodySize(100000000)
|
.maxBodySize(100000000)
|
||||||
.execute().also { check(it.statusCode() == 200) { "Failed to download image" } }
|
.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() {
|
override fun onLoad() {
|
||||||
logger.info("loading local image data")
|
logger.info("loading local image data")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
images = Config.load(getResources(fileName = "data.yml")!!, "yml")
|
images = getResourcesConfig("data.yml")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
logger.info("无法加载本地图片")
|
logger.info("无法加载本地图片")
|
||||||
@ -97,8 +146,10 @@ class ImageSenderMain : PluginBase() {
|
|||||||
logger.info("R18 * " + r18.size)
|
logger.info("R18 * " + r18.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun registerCommands() {
|
||||||
|
registerCommand {
|
||||||
|
|
||||||
override fun onDisable() {
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user