Skip to content

Commit

Permalink
Implement http api, fix #15
Browse files Browse the repository at this point in the history
  • Loading branch information
Under-estimate committed Jan 10, 2023
1 parent 0094bc6 commit 63cab02
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 9 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,26 @@
| recordInterval | 整数 | 记录在线人数的时间间隔(秒) |
| recordLimit | 整数 | 最长保留的在线人数记录时间(秒) |
| fontPath | 字符串(默认为空) | 指定渲染图片时所使用的字体文件,如果指定了字体文件并且被成功加载,则不会使用`fontName`配置项(此配置项正常情况下无需使用。如果无法使用系统字体,请使用此配置项指定字体文件([#14](https://github.com/Under-estimate/McMotd/issues/14))) |
| httpServerPort | 整数(默认为0) | http服务器的运行端口号,设置为0以禁用http服务器功能 |
| httpServerMapping | 字典(默认`{}`) | http服务器中`minecraft服务器名``minecraft服务器地址`的对应关系 |

## HTTP API
要开启插件的HTTP API功能,需要将配置文件中的`httpServerPort`设置为非零的可用端口,并配置`httpServerMapping`
示例配置:
> httpServerPort: 8092
> httpServerMapping:
>   hypixel: hypixel.net
>   earthmc: org.earthmc.net
以上述配置启动McMotd后,访问`http://localhost:8092/info/hypixel` 将会返回与`/mcp hypixel.net`相同的图片结果;访问`http://localhost:8092/info/earthmc` 将会返回与`/mcp org.earthmc.net`相同的图片结果。访问配置文件中未定义的服务器名(如`http://localhost:8092/info/foo` )将不会返回有效的结果

## FAQ
### Q: 在QQ群中发送命令没反应
A: 请检查是否安装了[chat-command](https://github.com/project-mirai/chat-command )插件,如果没有安装请看[这里](#如何安装 )

### Q: `UninitializedPropertyAccessException:lateinit property FONT has not been initialized`
### Q: UninitializedPropertyAccessException:lateinit property FONT has not been initialized
A: 如果您正在使用Linux运行mirai,请检查是否安装了中文字体

### Q: Could not find artifact io.ktor:xxxxx:jar:2.2.2 in https://maven.aliyun.com/repository/public
A: 如果您正在使用[Mirai Console Loader](https://github.com/iTXTech/mirai-console-loader ),请在`/config/Console/PluginDependencies.yml`中添加
>   \- 'https://repo.maven.apache.org/maven2/'
6 changes: 4 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ plugins {
}

group = "org.zrnq"
version = "1.1.13"
version = "1.1.14"

repositories {
maven("https://maven.aliyun.com/repository/public")
mavenCentral()
maven("https://maven.aliyun.com/repository/public")
}


dependencies {
implementation(kotlin("reflect"))
implementation("com.alibaba:fastjson:1.2.83")
implementation("dnsjava:dnsjava:3.5.0")
implementation("io.ktor:ktor-server-core:2.2.2")
implementation("io.ktor:ktor-server-netty:2.2.2")
}

tasks.create("CopyToLib", Copy::class) {
Expand Down
56 changes: 51 additions & 5 deletions src/main/kotlin/org/zrnq/mcmotd/ImageUtil.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package org.zrnq.mcmotd

import org.zrnq.mclient.MClientOptions
import org.zrnq.mcmotd.ColorScheme.toTransparent
import java.awt.*
import java.awt.image.BufferedImage
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.tan
import kotlin.random.Random

object ImageUtil {
fun BufferedImage.appendPlayerHistory(address : String) : BufferedImage {
Expand All @@ -32,8 +36,7 @@ object ImageUtil {
g.drawString("在线人数趋势", 20, 20)

if(history.size <= 1) {
g.color = Color(254, 71, 81)
g.paintTextCC("-=没有足够的数据来绘制图表,稍后再来吧=-", width / 2, height / 2)
g.drawErrorMessage("没有足够的数据来绘制图表,稍后再来吧", 0, 0, width, height)
return result
}

Expand Down Expand Up @@ -85,11 +88,11 @@ object ImageUtil {
it.add(minX to maxY)
}

g.paint = GradientPaint(minX.toFloat(), minY.toFloat(), Color(132, 188, 60, 150),
minX.toFloat(), maxY.toFloat(), Color(132, 188, 60, 0))
g.paint = GradientPaint(minX.toFloat(), minY.toFloat(), ColorScheme.darkGreen,
minX.toFloat(), maxY.toFloat(), ColorScheme.darkGreen.toTransparent())
g.fillPolygon(polygon.map { it.first }.toIntArray(), polygon.map { it.second }.toIntArray(), polygon.size)

g.color = Color(132, 188, 60)
g.color = ColorScheme.brightGreen
g.stroke = BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)

g.drawPolyline(plot.map { it.first }.toIntArray(), plot.map { it.second }.toIntArray(), plot.size)
Expand All @@ -110,4 +113,47 @@ object ImageUtil {
fun Graphics2D.paintTextCC(str : String, x : Int, y : Int) {
drawString(str, x - fontMetrics.stringWidth(str) / 2, y + fontMetrics.ascent - fontMetrics.height / 2)
}

fun Graphics2D.fillStripedRect(color1 : Color, color2 : Color, x : Int, y : Int, width : Int, height : Int) {
val angle = 45.0
val stripeWidth = 20
val offset = Random.Default.nextInt(0, stripeWidth * 2)
setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
color = color1
fillRect(x, y, width, height)
color = color2
val deviation = height / tan(Math.toRadians(angle))
val limit = ceil((width + deviation) / stripeWidth).toInt()
for(i in 0..limit step 2) {
val base = x + i * stripeWidth - offset
fillPolygon(
intArrayOf(base, base + stripeWidth, base + stripeWidth - deviation.toInt(), base - deviation.toInt()),
intArrayOf(y, y, y + height, y + height),
4
)
}
}

fun Graphics2D.drawErrorMessage(msg : String, x : Int, y : Int, width: Int, height: Int) {
font = MClientOptions.FONT
fillStripedRect(Color.black, ColorScheme.darkRed, x, y, width, height)
val msgRectWidth = fontMetrics.stringWidth(msg) + 30
val msgRectHeight = fontMetrics.height + 20
color = Color.black
fillRect(x + (width - msgRectWidth) / 2, y + (height - msgRectHeight) / 2, msgRectWidth, msgRectHeight)
color = ColorScheme.darkRed
drawRect(x + (width - msgRectWidth) / 2, y + (height - msgRectHeight) / 2, msgRectWidth, msgRectHeight)
color = ColorScheme.brightRed
paintTextCC(msg, x + width / 2, y + height / 2)
}
}

object ColorScheme {
val brightRed = Color(254, 71, 81)
val darkRed = Color(201, 42, 42)
val brightGreen = Color(132, 188, 60)
val darkGreen = Color(132, 188, 60, 150)

fun Color.toTransparent()
= Color(red, green, blue, 0)
}
65 changes: 64 additions & 1 deletion src/main/kotlin/org/zrnq/mcmotd/McMotd.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package org.zrnq.mcmotd

import com.alibaba.fastjson.parser.ParserConfig
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
Expand All @@ -10,15 +17,21 @@ import net.mamoe.mirai.utils.info
import org.zrnq.mclient.MClientOptions
import org.zrnq.mclient.output.APIOutputHandler
import org.zrnq.mclient.pingInternal
import org.zrnq.mclient.renderBasicInfoImage
import org.zrnq.mcmotd.ImageUtil.appendPlayerHistory
import org.zrnq.mcmotd.ImageUtil.drawErrorMessage
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.util.*
import javax.imageio.ImageIO

lateinit var miraiLogger : MiraiLogger

object McMotd : KotlinPlugin(
JvmPluginDescription(
id = "org.zrnq.mcmotd",
name = "Minecraft MOTD Fetcher",
version = "1.1.13",
version = "1.1.14",
) {
author("ZRnQ")
info("""以图片的形式获取指定Minecraft服务器的基本信息""")
Expand All @@ -36,6 +49,7 @@ object McMotd : KotlinPlugin(
RecordCommand.register()
MClientOptions.loadPluginConfig()
startRecord()
configureHttpServer()
}

override fun onDisable() {
Expand All @@ -45,6 +59,7 @@ object McMotd : KotlinPlugin(
DelCommand.unregister()
RecordCommand.unregister()
stopRecord()
stopHttpServer()
}

private lateinit var timer : Timer
Expand All @@ -67,5 +82,53 @@ object McMotd : KotlinPlugin(
private fun stopRecord() {
timer.cancel()
}

private var httpServer : ApplicationEngine? = null

private fun configureHttpServer() {
if(PluginConfig.httpServerPort == 0) return
logger.info("Starting embedded http server on http://localhost:${PluginConfig.httpServerPort}")
httpServer = embeddedServer(Netty, PluginConfig.httpServerPort, module = Application::mcmotdHttpServer).start(false)
}

private fun stopHttpServer() {
if(httpServer != null)
httpServer!!.stop()
}
}

fun Application.mcmotdHttpServer() {
routing {
configureRouting()
}
}

suspend fun PipelineContext<*, ApplicationCall>.respondImage(image : BufferedImage)
= call.respondBytes(ContentType.Image.PNG, HttpStatusCode.OK) {
ByteArrayOutputStream().also { stream ->
ImageIO.write(image, "png", stream)
}.toByteArray()
}

suspend fun PipelineContext<*, ApplicationCall>.respondErrorImage(msg : String)
= respondImage(BufferedImage(1000, 200, BufferedImage.TYPE_INT_RGB).also {
it.createGraphics().drawErrorMessage(msg, 0, 0, 1000, 200)
})
fun Route.configureRouting() {
route("/info") {
get("{server?}") {
val servername = call.parameters["server"] ?: return@get respondErrorImage("未指定服务器名")
if(!PluginConfig.httpServerMapping.containsKey(servername))
return@get respondErrorImage("指定的服务器名没有在配置文件中定义")
var error : String? = null
var image : BufferedImage? = null
val target = PluginConfig.httpServerMapping[servername]!!
pingInternal(target, APIOutputHandler(McMotd.logger, { error = it }, { image = renderBasicInfoImage(it).appendPlayerHistory(target) }))
if(image == null) {
McMotd.logger.warning("Http请求失败:$error")
return@get respondErrorImage("服务器信息获取失败")
}
return@get respondImage(image!!)
}
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/org/zrnq/mcmotd/PluginConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ object PluginConfig : AutoSavePluginConfig("mcmotd") {
val recordOnlinePlayer by value(mutableListOf<String>())
val recordInterval by value(300)
val recordLimit by value(21600)

val httpServerPort by value(0)
val httpServerMapping by value(mutableMapOf<String, String>())
}

0 comments on commit 63cab02

Please sign in to comment.