diff --git a/example.png b/example.png new file mode 100644 index 0000000..3d368b4 Binary files /dev/null and b/example.png differ diff --git a/src/main/kotlin/cn/rtast/fancybot/Const.kt b/src/main/kotlin/cn/rtast/fancybot/Const.kt index e11b9fc..2cc9040 100644 --- a/src/main/kotlin/cn/rtast/fancybot/Const.kt +++ b/src/main/kotlin/cn/rtast/fancybot/Const.kt @@ -26,7 +26,8 @@ val DEFAULT_CONFIG = Config( accessToken = "114514", port = 6760, listeningGroups = listOf(114514, 1919810), - qweatherKey = "114514" + qweatherKey = "114514", + githubKey = "1919810" ) val ADMINS = listOf( diff --git a/src/main/kotlin/cn/rtast/fancybot/FancyBot.kt b/src/main/kotlin/cn/rtast/fancybot/FancyBot.kt index 828d179..c42d720 100644 --- a/src/main/kotlin/cn/rtast/fancybot/FancyBot.kt +++ b/src/main/kotlin/cn/rtast/fancybot/FancyBot.kt @@ -9,6 +9,7 @@ package cn.rtast.fancybot import cn.rtast.fancybot.commands.* import cn.rtast.fancybot.commands.misc.BVParseCommand +import cn.rtast.fancybot.commands.misc.GitHubParseCommand import cn.rtast.fancybot.commands.misc.ImageURLCommand import cn.rtast.fancybot.commands.misc.ReverseGIFCommand import cn.rtast.fancybot.entity.bili.CardShare @@ -38,6 +39,14 @@ class FancyBot : OBMessage { val msg = message.rawMessage val groupId = message.groupId println("$sender($senderId: $groupId): $msg") + + if (message.rawMessage.startsWith("https://github.com/") || + message.rawMessage.startsWith("git@github.com:") || + message.rawMessage.split("/").size == 2 + ) { + GitHubParseCommand.parse(this, message) + } + if (message.rawMessage.startsWith("BV") || message.rawMessage.startsWith("https://www.bilibili.com") || message.rawMessage.contains("https://b23.tv/") || diff --git a/src/main/kotlin/cn/rtast/fancybot/commands/misc/BVParseCommand.kt b/src/main/kotlin/cn/rtast/fancybot/commands/misc/BVParseCommand.kt index 6dcab14..137147c 100644 --- a/src/main/kotlin/cn/rtast/fancybot/commands/misc/BVParseCommand.kt +++ b/src/main/kotlin/cn/rtast/fancybot/commands/misc/BVParseCommand.kt @@ -10,7 +10,9 @@ package cn.rtast.fancybot.commands.misc import cn.rtast.fancybot.entity.bili.BVID import cn.rtast.fancybot.util.Http import cn.rtast.fancybot.util.Resources +import cn.rtast.fancybot.util.drawCustomImage import cn.rtast.fancybot.util.str.encodeToBase64 +import cn.rtast.fancybot.util.str.setTruncat import cn.rtast.rob.entity.GroupMessage import cn.rtast.rob.util.ob.MessageChain import cn.rtast.rob.util.ob.OBMessage @@ -18,8 +20,6 @@ import okhttp3.OkHttpClient import okhttp3.Request import java.awt.Color import java.awt.Font -import java.awt.Graphics2D -import java.awt.geom.RoundRectangle2D import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream import java.net.URI @@ -59,35 +59,6 @@ object BVParseCommand { } } - private fun drawImage( - image: BufferedImage, - g2d: Graphics2D, - x: Int, - y: Int, - maxWidth: Double, - maxHeight: Double, - clip: Boolean - ) { - val originalWidth = image.getWidth(null) - val originalHeight = image.getHeight(null) - val widthScale = maxWidth / originalWidth - val heightScale = maxHeight / originalHeight - val scale = minOf(widthScale, heightScale) - val targetWidth = (originalWidth * scale).toInt() - val targetHeight = (originalHeight * scale).toInt() - if (clip) { - g2d.clip = RoundRectangle2D.Float( - x.toFloat(), - y.toFloat(), - targetWidth.toFloat(), - targetHeight.toFloat(), - 30.toFloat(), - 30.toFloat() - ) - } - g2d.drawImage(image, x, y, targetWidth, targetHeight, null) - } - private fun createResponseImage( title: String, author: String, @@ -119,35 +90,24 @@ object BVParseCommand { g2d.drawString(reply, 300, 290) g2d.drawString(share, 300, 200) // draw video title - val maxWidth = 500 - val fontMetrics = g2d.fontMetrics - val textWidth = fontMetrics.stringWidth(title) - val truncatedText = if (textWidth > maxWidth) { - val ellipsisWidth = fontMetrics.stringWidth("...") - val width = maxWidth - ellipsisWidth - var endIndex = title.length - while (fontMetrics.stringWidth(title.substring(0, endIndex)) > width && endIndex > 0) { - endIndex-- - } - title.substring(0, endIndex) + "..." - } else title + val truncatedText = setTruncat(title, g2d) g2d.font = titleFont g2d.drawString(truncatedText, 20, 70) // draw author name g2d.drawString(author, 40, 580) // draw icons - this.drawImage(favoriteIcon, g2d, 230, 340, 50.0, 50.0, false) - this.drawImage(replyIcon, g2d, 230, 250, 50.0, 50.0, false) - this.drawImage(shareIcon, g2d, 230, 160, 50.0, 50.0, false) - this.drawImage(viewIcon, g2d, 40, 340, 50.0, 50.0, false) - this.drawImage(coinIcon, g2d, 40, 250, 50.0, 50.0, false) - this.drawImage(likeIcon, g2d, 40, 160, 50.0, 50.0, false) + g2d.drawCustomImage(favoriteIcon, 230, 340, 50.0, 50.0, false) + g2d.drawCustomImage(replyIcon, 230, 250, 50.0, 50.0, false) + g2d.drawCustomImage(shareIcon, 230, 160, 50.0, 50.0, false) + g2d.drawCustomImage(viewIcon, 40, 340, 50.0, 50.0, false) + g2d.drawCustomImage(coinIcon, 40, 250, 50.0, 50.0, false) + g2d.drawCustomImage(likeIcon, 40, 160, 50.0, 50.0, false) // draw 22 logo - this.drawImage(twoTwoLogo, g2d, 800, 450, 90.0, 160.0, false) + g2d.drawCustomImage(twoTwoLogo, 800, 450, 90.0, 160.0, false) // draw author face - this.drawImage(faceImage, g2d, 40, 480, 60.0, 60.0, true) + g2d.drawCustomImage(faceImage, 40, 480, 60.0, 60.0, true) // draw cover image - this.drawImage(coverImage, g2d, 430, 140, 600.0, 300.0, true) + g2d.drawCustomImage(coverImage, 430, 140, 600.0, 300.0, true) g2d.dispose() val byteArrayOutputStream = ByteArrayOutputStream() ImageIO.write(backgroundImage, "png", byteArrayOutputStream) diff --git a/src/main/kotlin/cn/rtast/fancybot/commands/misc/GitHubParseCommand.kt b/src/main/kotlin/cn/rtast/fancybot/commands/misc/GitHubParseCommand.kt new file mode 100644 index 0000000..f541a55 --- /dev/null +++ b/src/main/kotlin/cn/rtast/fancybot/commands/misc/GitHubParseCommand.kt @@ -0,0 +1,125 @@ +/* + * Copyright © 2024 RTAkland + * Author: RTAkland + * Date: 2024/9/12 + */ + + +package cn.rtast.fancybot.commands.misc + +import cn.rtast.fancybot.configManager +import cn.rtast.fancybot.entity.github.RepoInfo +import cn.rtast.fancybot.util.Http +import cn.rtast.fancybot.util.Resources +import cn.rtast.fancybot.util.drawCustomImage +import cn.rtast.fancybot.util.str.encodeToBase64 +import cn.rtast.fancybot.util.str.setTruncat +import cn.rtast.rob.entity.GroupMessage +import cn.rtast.rob.util.ob.MessageChain +import cn.rtast.rob.util.ob.OBMessage +import java.awt.Color +import java.awt.Font +import java.awt.RenderingHints +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.net.URI +import javax.imageio.ImageIO + +object GitHubParseCommand { + + private const val REPO_INFO_API = "https://api.github.com/repos" + private const val CANVAS_WIDTH = 1620 + private const val CANVAS_HEIGHT = 1000 + private val githubLogo = ImageIO.read(Resources.loadFromResources("github/favicon.png")) + private val starIcon = ImageIO.read(Resources.loadFromResources("github/star.png")) + private val issueIcon = ImageIO.read(Resources.loadFromResources("github/issue.png")) + private val forkIcon = ImageIO.read(Resources.loadFromResources("github/fork.png")) + private val customFont = Font("Serif", Font.ITALIC, 50).deriveFont(Font.ITALIC).deriveFont(Font.BOLD) + private val titleCustomFont = Font("Serif", Font.ITALIC, 75).deriveFont(Font.ITALIC).deriveFont(Font.BOLD) + private val descriptionCustomFont = Font("Serif", Font.ITALIC, 50).deriveFont(Font.ITALIC).deriveFont(Font.BOLD) + + private val languageColors = mapOf( + "Kotlin" to Color(169, 123, 255), + "Java" to Color(176, 114, 25), + "Python" to Color(53, 114, 165), + "JavaScript" to Color(241, 224, 90), + "C++" to Color(243, 75, 125), + "Go" to Color(0, 173, 216), + "Ruby" to Color(112, 21, 22), + "PHP" to Color(79, 93, 149), + "TypeScript" to Color(43, 116, 137), + "Swift" to Color(240, 81, 56), + "Unknown" to Color(211, 211, 211) + ) + + private fun Int.formatNumber(): String { + return when { + this >= 1_000_000 -> String.format("%.1fM", this / 1_000_000.0) + this >= 10_000 -> String.format("%.1fk", this / 1_000.0) + this >= 1_000 -> String.format("%dk", this / 1_000) + else -> this.toString() + } + } + + private fun getLanguageColor(language: String): Color { + return languageColors[language] ?: languageColors["Unknown"]!! + } + + private fun getRepoStat(username: String, repo: String): RepoInfo { + return Http.get( + "$REPO_INFO_API/$username/$repo", + headers = mapOf("Authorization" to "Bearer ${configManager.githubKey}") + ) + } + + private fun createImage(repoStat: RepoInfo): String { + val canvas = BufferedImage(CANVAS_WIDTH, CANVAS_HEIGHT, BufferedImage.TYPE_INT_RGB) + val g2d = canvas.createGraphics() + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + g2d.font = customFont + g2d.color = Color.WHITE + g2d.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT) + g2d.color = this.getLanguageColor(repoStat.language) + g2d.fillRect(0, CANVAS_HEIGHT - 20, CANVAS_WIDTH, 20) + g2d.drawCustomImage(githubLogo, 1300, 780, 100.0, 100.0, false) // draw github logo + g2d.drawCustomImage(starIcon, 100, 730, 70.0, 70.0, false) // draw star icon + g2d.drawCustomImage(issueIcon, 500, 730, 70.0, 70.0, false) // draw issue icon + g2d.drawCustomImage(forkIcon, 900, 730, 70.0, 70.0, false) // draw fork icon + g2d.color = Color(60, 60, 60) + g2d.drawString("Stars", 160, 840) + g2d.drawString("Issues", 560, 840) + g2d.drawString("Forks", 960, 840) + g2d.drawString(repoStat.starsCount.formatNumber(), 200, 780) + g2d.drawString(repoStat.openIssueCount.formatNumber(), 600, 780) + g2d.drawString(repoStat.forksCount.formatNumber(), 1000, 780) + g2d.font = titleCustomFont + val truncatedFullNameText = setTruncat(repoStat.fullName, g2d, 1000) + g2d.drawString(truncatedFullNameText, 80, 280) + g2d.font = descriptionCustomFont + val truncatedDescription = setTruncat(repoStat.description, g2d, 1000) + g2d.drawString(truncatedDescription, 20, 450) + val avatarImage = ImageIO.read(URI(repoStat.owner.avatarUrl).toURL()) + g2d.drawCustomImage(avatarImage, 1200, 300, 300.0, 300.0, true) + val byteArrayOutputStream = ByteArrayOutputStream() + ImageIO.write(canvas, "png", byteArrayOutputStream) + val imageBytes = byteArrayOutputStream.toByteArray() + return imageBytes.encodeToBase64() + } + + suspend fun parse(listener: OBMessage, message: GroupMessage) { + val path = message.rawMessage + .replace(".git", "") + .replace("https://github.com/", "") + .replace("git@github.com:", "") + .split("/") + val user = path.first() + val repo = path.last() + val repoStat = this.getRepoStat(user, repo) + val image = this.createImage(repoStat) + val msg = MessageChain.Builder() + .addReply(message.messageId) + .addImage(image, true) + .build() + listener.sendGroupMessage(message.groupId, msg) + } +} \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/entity/Config.kt b/src/main/kotlin/cn/rtast/fancybot/entity/Config.kt index 734671b..aec54ae 100644 --- a/src/main/kotlin/cn/rtast/fancybot/entity/Config.kt +++ b/src/main/kotlin/cn/rtast/fancybot/entity/Config.kt @@ -16,5 +16,6 @@ data class Config( val accessToken: String, val port: Int, val listeningGroups: List, - val qweatherKey: String + val qweatherKey: String, + val githubKey: String, ) \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/entity/github/RepoInfo.kt b/src/main/kotlin/cn/rtast/fancybot/entity/github/RepoInfo.kt new file mode 100644 index 0000000..f6ebb23 --- /dev/null +++ b/src/main/kotlin/cn/rtast/fancybot/entity/github/RepoInfo.kt @@ -0,0 +1,29 @@ +/* + * Copyright © 2024 RTAkland + * Author: RTAkland + * Date: 2024/9/12 + */ + + +package cn.rtast.fancybot.entity.github + +import com.google.gson.annotations.SerializedName + +data class RepoInfo( + val owner: Owner, + @SerializedName("full_name") + val fullName: String, + val description: String, + @SerializedName("forks_count") + val forksCount: Int, + val language: String, + @SerializedName("stargazers_count") + val starsCount: Int, + @SerializedName("open_issues_count") + val openIssueCount: Int +) { + data class Owner( + @SerializedName("avatar_url") + val avatarUrl: String, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/util/Image.kt b/src/main/kotlin/cn/rtast/fancybot/util/Image.kt new file mode 100644 index 0000000..c4404ac --- /dev/null +++ b/src/main/kotlin/cn/rtast/fancybot/util/Image.kt @@ -0,0 +1,64 @@ +/* + * Copyright © 2024 RTAkland + * Author: RTAkland + * Date: 2024/9/12 + */ + + +package cn.rtast.fancybot.util + +import java.awt.Graphics2D +import java.awt.geom.RoundRectangle2D +import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import javax.imageio.ImageIO + +fun Graphics2D.drawCustomImage( + image: BufferedImage, + x: Int, + y: Int, + maxWidth: Double, + maxHeight: Double, + clip: Boolean +) { + val originalWidth = image.getWidth(null) + val originalHeight = image.getHeight(null) + val widthScale = maxWidth / originalWidth + val heightScale = maxHeight / originalHeight + val scale = minOf(widthScale, heightScale) + val targetWidth = (originalWidth * scale).toInt() + val targetHeight = (originalHeight * scale).toInt() + if (clip) { + this.clip = RoundRectangle2D.Float( + x.toFloat(), + y.toFloat(), + targetWidth.toFloat(), + targetHeight.toFloat(), + 30.toFloat(), + 30.toFloat() + ) + } + this.drawImage(image, x, y, targetWidth, targetHeight, null) +} + +fun BufferedImage.scaleImage(size: Pair): BufferedImage { + val scaledImage = BufferedImage(size.first, size.second, this.type) + val graphics2d = scaledImage.createGraphics() + graphics2d.drawImage(this, 0, 0, size.first, size.second, null) + graphics2d.dispose() + return scaledImage +} + +fun ByteArray.toBufferedImage(): BufferedImage { + ByteArrayInputStream(this).use { inputStream -> + return ImageIO.read(inputStream) + } +} + +fun BufferedImage.toByteArray(): ByteArray { + ByteArrayOutputStream().use { outputStream -> + ImageIO.write(this, "png", outputStream) + return outputStream.toByteArray() + } +} \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/util/file/ConfigManager.kt b/src/main/kotlin/cn/rtast/fancybot/util/file/ConfigManager.kt index 6c5973d..3b62954 100644 --- a/src/main/kotlin/cn/rtast/fancybot/util/file/ConfigManager.kt +++ b/src/main/kotlin/cn/rtast/fancybot/util/file/ConfigManager.kt @@ -18,4 +18,5 @@ class ConfigManager : JsonFileHandler("config.json") { val wsType get() = this.read().wsType val listeningGroups get() = this.read().listeningGroups val qweatherKey get() = this.read().qweatherKey + val githubKey get() = this.read().githubKey } \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/util/str/StringFormatter.kt b/src/main/kotlin/cn/rtast/fancybot/util/str/StringFormatter.kt new file mode 100644 index 0000000..cb01690 --- /dev/null +++ b/src/main/kotlin/cn/rtast/fancybot/util/str/StringFormatter.kt @@ -0,0 +1,24 @@ +/* + * Copyright © 2024 RTAkland + * Author: RTAkland + * Date: 2024/9/12 + */ + + +package cn.rtast.fancybot.util.str + +import java.awt.Graphics2D + +fun setTruncat(origin: String, g2d: Graphics2D, maxWidth: Int = 500): String { + val fontMetrics = g2d.fontMetrics + val textWidth = fontMetrics.stringWidth(origin) + return if (textWidth > maxWidth) { + val ellipsisWidth = fontMetrics.stringWidth("...") + val width = maxWidth - ellipsisWidth + var endIndex = origin.length + while (fontMetrics.stringWidth(origin.substring(0, endIndex)) > width && endIndex > 0) { + endIndex-- + } + origin.substring(0, endIndex) + "..." + } else origin +} diff --git a/src/main/resources/config.json b/src/main/resources/config.json index 011b8a2..164fab3 100644 --- a/src/main/resources/config.json +++ b/src/main/resources/config.json @@ -9,5 +9,5 @@ 1919810 ], "qweatherKey": "114514", - "qweatherPlan": "Free" + "githubKey": "1919810" } \ No newline at end of file diff --git a/src/main/resources/github/favicon.png b/src/main/resources/github/favicon.png new file mode 100644 index 0000000..93ac1a0 Binary files /dev/null and b/src/main/resources/github/favicon.png differ diff --git a/src/main/resources/github/fork.png b/src/main/resources/github/fork.png new file mode 100644 index 0000000..351c7ba Binary files /dev/null and b/src/main/resources/github/fork.png differ diff --git a/src/main/resources/github/issue.png b/src/main/resources/github/issue.png new file mode 100644 index 0000000..e413b47 Binary files /dev/null and b/src/main/resources/github/issue.png differ diff --git a/src/main/resources/github/star.png b/src/main/resources/github/star.png new file mode 100644 index 0000000..af3bb96 Binary files /dev/null and b/src/main/resources/github/star.png differ