├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.MD ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image ├── img1.png ├── img2.png └── img3.png ├── settings.gradle ├── src ├── main │ ├── kotlin │ │ ├── XXYan.kt │ │ ├── YanConfig.kt │ │ ├── YanData.kt │ │ ├── YanEntity.kt │ │ ├── commands │ │ │ ├── YanConfigCommands.kt │ │ │ └── YanQueryCommands.kt │ │ └── core │ │ │ ├── MessagePainter.kt │ │ │ └── data │ │ │ ├── Sender.kt │ │ │ └── ShowYanTask.kt │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ └── net.mamoe.mirai.console.plugin.jvm.JvmPlugin │ │ ├── notFoundImage.jpg │ │ └── sarasa-ui-tc-regular.ttf └── test │ ├── java │ └── com │ │ └── github │ │ └── SimpleGeometricImageProvider.java │ └── kotlin │ ├── MessagePainterTest.kt │ └── test.kt └── version.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | name: Gradle Automation Build 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | java: [11, 13] 19 | steps: 20 | - 21 | uses: actions/checkout@v2 22 | - 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: ${{ matrix.java }} 26 | - 27 | name: 读取当前版本号 28 | id: version 29 | uses: ashley-taylor/read-json-property-action@v1.0 30 | with: 31 | path: ./version.json 32 | property: version 33 | - 34 | name: 读取当前信息 35 | id: description 36 | uses: ashley-taylor/read-json-property-action@v1.0 37 | with: 38 | path: ./version.json 39 | property: description 40 | 41 | # add cache to improve workflow execution time 42 | - 43 | name: Cache .gradle/caches 44 | uses: actions/cache@v1 45 | with: 46 | path: ~/.gradle/caches 47 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} 48 | restore-keys: ${{ runner.os }}-gradle- 49 | - 50 | name: Cache .gradle/wrapper 51 | uses: actions/cache@v1 52 | with: 53 | path: ~/.gradle/wrapper 54 | key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/*.gradle') }} 55 | restore-keys: ${{ runner.os }}-gradle-wrapper- 56 | - 57 | name: Grant execute permission for gradlew 58 | run: chmod +x gradlew 59 | - 60 | name: Build Plugin 61 | run: ./gradlew buildPlugin 62 | - 63 | name: Build LegacyPlugin 64 | run: ./gradlew buildPluginLegacy 65 | - 66 | name: Create GitHub release 67 | uses: marvinpinto/action-automatic-releases@latest 68 | with: 69 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 70 | automatic_release_tag: "${{steps.version.outputs.value}}" 71 | title: ${{ env.ReleaseVersion }} 72 | prerelease: false 73 | files: | 74 | build/mirai/* 75 | 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | testOutput.png 3 | 4 | .idea/ 5 | 6 | *.iml 7 | *.ipr 8 | *.iws 9 | 10 | # IntelliJ 11 | out/ 12 | # mpeltonen/sbt-idea plugin 13 | .idea_modules/ 14 | 15 | # JIRA plugin 16 | atlassian-ide-plugin.xml 17 | 18 | # Compiled class file 19 | *.class 20 | 21 | # Log file 22 | *.log 23 | 24 | # BlueJ files 25 | *.ctxt 26 | 27 | # Package Files # 28 | *.jar 29 | *.war 30 | *.nar 31 | *.ear 32 | *.zip 33 | *.tar.gz 34 | *.rar 35 | 36 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 37 | hs_err_pid* 38 | 39 | *~ 40 | 41 | # temporary files which can be created if a process still has a handle open of a deleted file 42 | .fuse_hidden* 43 | 44 | # KDE directory preferences 45 | .directory 46 | 47 | # Linux trash folder which might appear on any partition or disk 48 | .Trash-* 49 | 50 | # .nfs files are created when an open file is removed but is still being accessed 51 | .nfs* 52 | 53 | # General 54 | .DS_Store 55 | .AppleDouble 56 | .LSOverride 57 | 58 | # Icon must end with two \r 59 | Icon 60 | 61 | # Thumbnails 62 | ._* 63 | 64 | # Files that might appear in the root of a volume 65 | .DocumentRevisions-V100 66 | .fseventsd 67 | .Spotlight-V100 68 | .TemporaryItems 69 | .Trashes 70 | .VolumeIcon.icns 71 | .com.apple.timemachine.donotpresent 72 | 73 | # Directories potentially created on remote AFP share 74 | .AppleDB 75 | .AppleDesktop 76 | Network Trash Folder 77 | Temporary Items 78 | .apdisk 79 | 80 | # Windows thumbnail cache files 81 | Thumbs.db 82 | Thumbs.db:encryptable 83 | ehthumbs.db 84 | ehthumbs_vista.db 85 | 86 | # Dump file 87 | *.stackdump 88 | 89 | # Folder config file 90 | [Dd]esktop.ini 91 | 92 | # Recycle Bin used on file shares 93 | $RECYCLE.BIN/ 94 | 95 | # Windows Installer files 96 | *.cab 97 | *.msi 98 | *.msix 99 | *.msm 100 | *.msp 101 | 102 | # Windows shortcuts 103 | *.lnk 104 | 105 | .gradle 106 | build/ 107 | 108 | # Ignore Gradle GUI config 109 | gradle-app.setting 110 | 111 | # Cache of project 112 | .gradletasknamecache 113 | 114 | **/build/ 115 | 116 | # Common working directory 117 | run/ 118 | 119 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 120 | !gradle-wrapper.jar 121 | 122 | 123 | # Local Test Launch point 124 | src/test/kotlin/RunTerminal.kt 125 | 126 | # Mirai console files with direct bootstrap 127 | /config 128 | /data 129 | /plugins 130 | /bots 131 | 132 | # Local Test Launch Point working directory 133 | /debug-sandbox 134 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## XXYan 2 | 3 | ### 一个用于记录群成员说过的话的插件 4 | 5 | 你是否为群成员满口骚话却无法有效记录而困扰? 6 | 那XXYan便是为你而准备的一个插件。 7 | 8 | 注:本插件需要使用[转发类插件](https://github.com/project-mirai/chat-command)。 9 | 10 | 欢迎issue,更欢迎pr(只要你可以看懂我的垃圾代码的话) 11 | 12 | ## 玩法 13 | 14 | 调用指令`/makeYan at `或`/makeYan id `来注册一个对他人说话的监听 15 | ![img1](/image/img1.png) 16 | 之后再群内发送`name [content]`时便就会返回他人**自监听开始**的某一句话 17 | ![img2](/image/img2.png) 18 | 而`content`参数可以用来指定搜索的内容 19 | ![img3](/image/img3.png) 20 | 21 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.6.10' 3 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10' 4 | 5 | id 'net.mamoe.mirai-console' version '2.11.0' 6 | } 7 | mirai{} 8 | 9 | group = 'com.github' 10 | version = '0.0.9' 11 | 12 | repositories { 13 | maven { url 'https://maven.aliyun.com/repository/public' } 14 | mavenCentral() 15 | } 16 | 17 | 18 | dependencies{ 19 | implementation 'org.xerial:sqlite-jdbc:3.36.0.3' 20 | implementation 'org.ktorm:ktorm-core:3.5.0' 21 | testImplementation "junit:junit:4.11" 22 | } 23 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myot233/XXYan/cf7dfbf3c1d75d92b0601011bb5b7acbc9b5538c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /image/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myot233/XXYan/cf7dfbf3c1d75d92b0601011bb5b7acbc9b5538c/image/img1.png -------------------------------------------------------------------------------- /image/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myot233/XXYan/cf7dfbf3c1d75d92b0601011bb5b7acbc9b5538c/image/img2.png -------------------------------------------------------------------------------- /image/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myot233/XXYan/cf7dfbf3c1d75d92b0601011bb5b7acbc9b5538c/image/img3.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "XXYan" -------------------------------------------------------------------------------- /src/main/kotlin/XXYan.kt: -------------------------------------------------------------------------------- 1 | package com.github 2 | 3 | import com.github.commands.YanConfigCommands 4 | import com.github.commands.YanQueryCommands 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import com.github.core.MessagePainter 8 | import com.github.core.data.Sender 9 | import com.github.core.data.ShowYanTask 10 | import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register 11 | import net.mamoe.mirai.console.permission.PermissionId 12 | import net.mamoe.mirai.console.permission.PermissionService 13 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription 14 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin 15 | import net.mamoe.mirai.contact.Group 16 | import net.mamoe.mirai.contact.nameCardOrNick 17 | import net.mamoe.mirai.event.ListeningStatus 18 | import net.mamoe.mirai.event.events.GroupMessageEvent 19 | import net.mamoe.mirai.event.globalEventChannel 20 | import net.mamoe.mirai.message.code.MiraiCode.deserializeMiraiCode 21 | import net.mamoe.mirai.message.data.* 22 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 23 | import org.ktorm.entity.add 24 | import org.ktorm.entity.toList 25 | import java.io.ByteArrayOutputStream 26 | import java.util.concurrent.ThreadLocalRandom 27 | import javax.imageio.ImageIO 28 | import kotlin.random.asKotlinRandom 29 | import kotlin.streams.toList 30 | 31 | 32 | object XXYan : KotlinPlugin(JvmPluginDescription( 33 | id = "com.github.XXYan", 34 | name = "XXYan", 35 | version = "0.0.4", 36 | ) { 37 | author("gsycl2004") 38 | info("""to record someone's word""") 39 | }) { 40 | private val notFoundImage by lazy { 41 | val inputStream = getResourceAsStream("notFoundImage.jpg") 42 | if (inputStream != null) { 43 | ImageIO.read(inputStream) 44 | } else { 45 | throw UnsupportedOperationException("notFoundImage初始化失败") 46 | } 47 | } 48 | 49 | val permission by lazy { 50 | PermissionService.INSTANCE.register(PermissionId("yan", "command"), "yans", permissionAll) 51 | } 52 | 53 | val permissionAdmin by lazy { 54 | PermissionService.INSTANCE.register(PermissionId("yan", "su"), "yans", permissionAll) 55 | } 56 | 57 | private val permissionAll by lazy { 58 | PermissionService.INSTANCE.register(PermissionId("yan", "*"), "yans") 59 | } 60 | 61 | private fun String.judgeRegex(name: String): Boolean { 62 | val regex = "^$name *(.*)".toRegex() 63 | return regex.containsMatchIn(this) 64 | } 65 | 66 | private fun String.matchList(name: String): List { 67 | val regex = "($name) *(.*)".toRegex() 68 | return regex.find(this)!!.groupValues.toList() 69 | } 70 | 71 | override fun onEnable() { 72 | Class.forName("org.sqlite.JDBC") 73 | YanConfigCommands.register() 74 | YanQueryCommands.register() 75 | YanConfig.reload() 76 | globalEventChannel().subscribe { it -> 77 | if (YanConfig.cares.keys.firstOrNull { this.message.contentToString().judgeRegex(it)} != null) { 78 | it.handleAsShowYan() 79 | } else if (sender.id in YanConfig.cares.values && this.message.contentToString() != "") { 80 | it.handleAsHistory() 81 | } 82 | return@subscribe ListeningStatus.LISTENING 83 | } 84 | 85 | } 86 | 87 | private fun GroupMessageEvent.handleAsHistory() { 88 | val drawText = message.serializeToDrawText(group) 89 | if (drawText != "") { 90 | val sequence = YanData.getSequence(sender.id) 91 | sequence.add(YanEntity { 92 | name = senderName 93 | head = sender.avatarUrl 94 | this.yan = message.serializeToMiraiCode() 95 | this.yanCode = message.serializeToYanCode() 96 | this.title = if (sender.specialTitle != "") sender.specialTitle else YanConfig.defaultTitle 97 | }) 98 | } 99 | } 100 | 101 | private suspend fun GroupMessageEvent.handleAsShowYan() { 102 | println("${this.sender.nameCardOrNick} from ${this.group.name} has requested") 103 | val args = this.message.contentToString() 104 | .matchList(YanConfig.cares.keys.first { this.message.contentToString().judgeRegex(it) }) 105 | val searchId = args[1]; 106 | val searchYanCode = if (args.size < 3) { 107 | null 108 | } else { 109 | args[2].lowercase() 110 | } 111 | val yanList = YanData.getSequence(YanConfig.cares[searchId]!!).toList() 112 | val yan: YanEntity? = if (searchYanCode == null) { 113 | yanList.random(ThreadLocalRandom.current().asKotlinRandom()) 114 | } else yanList.filter { 115 | //it.yanCode != "" && 116 | it.yanCode.lowercase().contains(searchYanCode) 117 | 118 | }.randomOrNull(ThreadLocalRandom.current().asKotlinRandom()) 119 | val showYanTask = if (yan != null) { 120 | val chain = yan.yan.deserializeMiraiCode() 121 | ShowYanTask( 122 | Sender( 123 | YanConfig.NameMap[YanConfig.cares[searchId]] ?: yan.name, 124 | { MessagePainter.downloadAvatar(yan.head) }, 125 | 1, 126 | yan.title, 127 | "red" 128 | ), 129 | group, 130 | chain 131 | ) 132 | } else { 133 | ShowYanTask( 134 | Sender( 135 | "错误警告", 136 | { notFoundImage!! }, 137 | 1, 138 | "警告", 139 | "red" 140 | ), 141 | group, 142 | messageChainOf(PlainText(YanConfig.missText)) 143 | ) 144 | 145 | } 146 | 147 | 148 | try { 149 | val image = MessagePainter.paintMessage( 150 | showYanTask 151 | ) 152 | 153 | val byteStream = ByteArrayOutputStream() 154 | withContext(Dispatchers.IO) { 155 | ImageIO.write(image, "png", byteStream) 156 | } 157 | byteStream.toByteArray().toExternalResource("png").use { 158 | val miraiImage = group.uploadImage(it) 159 | this.group.sendMessage(miraiImage) 160 | } 161 | } catch (ex: Exception) { 162 | logger.error(ex) 163 | this.group.sendMessage(YanConfig.failedText) 164 | } 165 | } 166 | 167 | /** 168 | * DrawText: 为绘制图片设计的格式,某些SingleMessage类型会被忽略 169 | */ 170 | fun MessageChain.serializeToDrawText(group: Group?): String { 171 | val text = this.joinToString("") { 172 | when (it) { 173 | is At -> it.getDisplay(group) 174 | is PlainText -> it.contentToString() 175 | else -> "" 176 | } 177 | } 178 | return text 179 | } 180 | 181 | /** 182 | * YanCode: 为yan检索设计的格式,某些SingleMessage类型会被忽略 183 | */ 184 | fun MessageChain.serializeToYanCode(): String { 185 | return this.stream() 186 | .map { 187 | when (it) { 188 | is PlainText -> it.contentToString() 189 | is Image -> it.contentToString() 190 | is Face -> it.contentToString() 191 | else -> "" 192 | } 193 | } 194 | .toList() 195 | .joinToString() 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/kotlin/YanConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github 2 | 3 | import net.mamoe.mirai.console.data.AutoSavePluginConfig 4 | import net.mamoe.mirai.console.data.ValueDescription 5 | import net.mamoe.mirai.console.data.value 6 | 7 | object YanConfig:AutoSavePluginConfig("config") { 8 | @ValueDescription("默认称号") 9 | val defaultTitle:String by value("小萝莉") 10 | @ValueDescription("这里用来指定搜索不到的内容") 11 | val missText by value("搜索失败,请重试") 12 | @ValueDescription("这里用来指定搜索失败后回复的内容") 13 | val failedText by value("遇到未知错误,生成yan失败") 14 | @ValueDescription("给某个用户指定一个固定昵称") 15 | val NameMap:MutableMap by value() 16 | @ValueDescription("这里可以指定字体") 17 | val font by value("sarasa-ui-tc-regular.ttf") 18 | @ValueDescription("一个用于指定指令对应触发的列表") 19 | val cares:MutableMap by value(mutableMapOf("样例yan" to 1234567L)) 20 | 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/YanData.kt: -------------------------------------------------------------------------------- 1 | package com.github 2 | 3 | import com.github.XXYan.serializeToYanCode 4 | import net.mamoe.mirai.message.code.MiraiCode.deserializeMiraiCode 5 | import org.ktorm.database.Database 6 | import org.ktorm.dsl.delete 7 | import org.ktorm.dsl.eq 8 | import org.ktorm.dsl.update 9 | import org.ktorm.entity.EntitySequence 10 | import org.ktorm.entity.forEach 11 | import org.ktorm.entity.sequenceOf 12 | import org.ktorm.schema.Table 13 | import org.ktorm.schema.text 14 | import java.sql.SQLException 15 | 16 | 17 | class YanData(id: Long) : Table(id.toString()) { 18 | 19 | 20 | val name = text("name").bindTo { it.name } 21 | val head = text("head").bindTo { it.head } 22 | val yan = text("yan").bindTo { it.yan } 23 | val title = text("title").bindTo { it.title } 24 | val yanCode = text("yanCode").bindTo { it.yanCode } 25 | 26 | companion object { 27 | private val database = Database.connect("jdbc:sqlite:file:${XXYan.resolveDataFile("yan.db")}") 28 | 29 | private fun YanData.tryAlterColumn(columnName: String, type: String) { 30 | try { 31 | database.useConnection { 32 | val sql = 33 | """ 34 | ALTER TABLE "${this@tryAlterColumn.tableName}" ADD COLUMN "$columnName" $type 35 | """.trimIndent() 36 | it.createStatement().execute(sql) 37 | } 38 | XXYan.logger.info("alterColumn done for ${this@tryAlterColumn.tableName}.$columnName") 39 | } catch (e: SQLException) { 40 | // alterColumn fail for ${this@tryAlterColumn.tableName}.$columnName, Most likely it already exists 41 | // do nothing 42 | } 43 | } 44 | 45 | private fun YanData.createTableIfNotExist() { 46 | database.useConnection { 47 | it.createStatement().execute( 48 | """ 49 | CREATE TABLE IF NOT EXISTs "${this@createTableIfNotExist.tableName}"( 50 | name TEXT, 51 | head TEXT, 52 | yan TEXT, 53 | title TEXT, 54 | yanCode Text 55 | ) 56 | """.trimIndent() 57 | ) 58 | } 59 | } 60 | 61 | fun updateDataVersion(id: Long): String { 62 | val table = YanData(id) 63 | table.createTableIfNotExist() 64 | table.tryAlterColumn(table.title.name, table.title.sqlType.typeName) 65 | table.tryAlterColumn(table.yanCode.name, table.yanCode.sqlType.typeName) 66 | var countNewYanCode = 0 67 | var countEmptyYanCode = 0 68 | database.sequenceOf(table).forEach { yanEntity -> 69 | if (yanEntity.yanCode == "") { 70 | val targetYanText = yanEntity.yan 71 | val newYanCode = yanEntity.yan.deserializeMiraiCode().serializeToYanCode() 72 | if (newYanCode != "") { 73 | database.update(table) { 74 | set(table.yanCode, newYanCode) 75 | where { 76 | it.yan eq targetYanText 77 | } 78 | } 79 | countNewYanCode++ 80 | } else { 81 | database.delete(table) { 82 | it.yan eq targetYanText 83 | } 84 | countEmptyYanCode++ 85 | } 86 | } 87 | } 88 | 89 | val message = "updateDataVersion done for ${table.tableName}, countNewYanCode = $countNewYanCode, countEmptyYanCode = $countEmptyYanCode" 90 | XXYan.logger.info(message) 91 | return message 92 | } 93 | 94 | fun getSequence(id: Long): EntitySequence { 95 | val table = YanData(id) 96 | updateDataVersion(id) 97 | table.createTableIfNotExist() 98 | return database.sequenceOf(table) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/kotlin/YanEntity.kt: -------------------------------------------------------------------------------- 1 | package com.github 2 | 3 | import org.ktorm.entity.Entity 4 | 5 | interface YanEntity: Entity { 6 | var name:String 7 | var head:String 8 | var yan:String 9 | var yanCode:String 10 | var title:String 11 | 12 | companion object: Entity.Factory() { 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/YanConfigCommands.kt: -------------------------------------------------------------------------------- 1 | package com.github.commands 2 | 3 | import com.github.XXYan 4 | import com.github.YanConfig 5 | import net.mamoe.mirai.console.command.CommandSender 6 | import net.mamoe.mirai.console.command.CommandSenderOnMessage 7 | import net.mamoe.mirai.console.command.CompositeCommand 8 | import net.mamoe.mirai.contact.Member 9 | import net.mamoe.mirai.event.events.MessageEvent 10 | 11 | object YanConfigCommands : CompositeCommand( 12 | XXYan, 13 | "makeYan", 14 | "SetYan", 15 | parentPermission = XXYan.permissionAdmin 16 | 17 | ) { 18 | 19 | @SubCommand("at") 20 | suspend fun CommandSenderOnMessage.byAt(name: String, member: Member) { 21 | byId(name, member.id) 22 | } 23 | 24 | @SubCommand("id") 25 | suspend fun CommandSender.byId(name: String, userId: Long) { 26 | YanConfig.cares[name] = userId 27 | this.sendMessage("已成功添加${name} -> $userId") 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/commands/YanQueryCommands.kt: -------------------------------------------------------------------------------- 1 | package com.github.commands 2 | 3 | import com.github.XXYan 4 | import com.github.YanConfig 5 | import com.github.YanData 6 | import net.mamoe.mirai.console.command.CommandSender 7 | import net.mamoe.mirai.console.command.CompositeCommand 8 | 9 | object YanQueryCommands : CompositeCommand( 10 | XXYan, 11 | "yan", 12 | parentPermission = XXYan.permission 13 | ) { 14 | @SubCommand 15 | suspend fun CommandSender.length(userId: Long) { 16 | val sequence = YanData.getSequence(userId) 17 | this.sendMessage("目前该用户的yan数量为:${sequence.rowSet.size()}") 18 | } 19 | 20 | @SubCommand 21 | suspend fun CommandSender.unsetYan(name: String) { 22 | val value = YanConfig.cares.remove(name) 23 | this.sendMessage("已成功移除${name} -> $value") 24 | } 25 | 26 | @SubCommand 27 | suspend fun CommandSender.updateAllDataVersion() { 28 | val messages = YanConfig.cares.values.map { 29 | YanData.updateDataVersion(it) 30 | }.toList() 31 | this.sendMessage("操作成功: $messages") 32 | } 33 | 34 | @SubCommand 35 | suspend fun CommandSender.stars() { 36 | val sequence = YanConfig.cares 37 | val ids = sequence.values.toSet() 38 | this.sendMessage("目前关注的人有:\n${ids.joinToString("\n")}") 39 | } 40 | 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/kotlin/core/MessagePainter.kt: -------------------------------------------------------------------------------- 1 | package com.github.core 2 | 3 | import com.github.XXYan 4 | import com.github.XXYan.serializeToDrawText 5 | import com.github.YanConfig 6 | import com.github.core.data.ShowYanTask 7 | import net.mamoe.mirai.message.data.Image 8 | import net.mamoe.mirai.message.data.Image.Key.queryUrl 9 | import java.awt.* 10 | import java.awt.geom.Ellipse2D 11 | import java.awt.geom.RoundRectangle2D 12 | import java.awt.image.BufferedImage 13 | import java.io.File 14 | import java.net.URL 15 | import java.nio.file.Files 16 | import java.util.* 17 | import javax.imageio.ImageIO 18 | import kotlin.io.path.Path 19 | 20 | 21 | object MessagePainter { 22 | private val standardFont: Font = if (YanConfig.font == "sarasa-ui-tc-regular.ttf") { 23 | Font.createFont(Font.TRUETYPE_FONT, XXYan.getResourceAsStream("sarasa-ui-tc-regular.ttf")) 24 | }else{ 25 | Font.createFont(Font.TRUETYPE_FONT,File(YanConfig.font)) 26 | } 27 | 28 | suspend fun paintMessage(yan: ShowYanTask): BufferedImage { 29 | 30 | var image = BufferedImage(880, 230, BufferedImage.TYPE_4BYTE_ABGR) 31 | fillColor(image) 32 | drawAvatar(yan, image) 33 | val length = drawTitle(image, yan) 34 | val tempg2d = image.createGraphics() 35 | tempg2d.color = Color(164, 169, 179) 36 | tempg2d.font = standardFont.deriveFont(30f) 37 | val width = tempg2d.fontMetrics.stringWidth(yan.sender.name) + length + 10 + 15 38 | image = extImage(image, newWidth = width) 39 | 40 | var g2d = image.createGraphics() 41 | g2d.font = standardFont.deriveFont(40f) 42 | 43 | g2d.applyAntialias() 44 | if (yan.message.contains(net.mamoe.mirai.message.data.Image)) { 45 | val url = yan.message[Image]!!.queryUrl() 46 | val userImage = downloadAvatar(url) 47 | 48 | println("paintMessage for $url") 49 | image = 50 | extImage( 51 | image, 52 | newWidth = listOf(width, 165 * 2 + userImage.width + 10).maxOf { it }, 53 | newHeight = 85 * 2 + userImage.height 54 | ) 55 | g2d = image.createGraphics() 56 | g2d.clip = RoundRectangle2D.Double( 57 | 165.toDouble(), 58 | 85.toDouble(), 59 | userImage.width.toDouble(), 60 | userImage.height.toDouble(), 61 | 25.toDouble(), 62 | 25.toDouble() 63 | 64 | ) 65 | g2d.drawImage(userImage, 165, 85, userImage.width, userImage.height, null) 66 | } else { 67 | val contentIm = drawContent(yan) 68 | val bubble = drawBubble(contentIm) 69 | image = 70 | extImage(image, 84 + bubble.height + 50, newWidth = listOf(width, bubble.width + 167 + 40).maxOf { it }) 71 | g2d = image.createGraphics() 72 | 73 | g2d.applyAntialias() 74 | g2d.drawImage(bubble, 167, 84, null) 75 | } 76 | drawName(image, yan, length) 77 | return image 78 | 79 | } 80 | 81 | private fun drawBubble(contentIm: BufferedImage): BufferedImage { 82 | val image = BufferedImage(contentIm.width + 60, contentIm.height + 45, BufferedImage.TYPE_4BYTE_ABGR) 83 | val g2d = image.createGraphics() 84 | g2d.fillRoundRect(0, 0, image.width, image.height, 30, 30) 85 | g2d.applyAntialias() 86 | g2d.drawImage(contentIm, 30, 18, contentIm.width, contentIm.height, null) 87 | return image 88 | } 89 | 90 | private fun drawContent(yan: ShowYanTask): BufferedImage { 91 | val drawText = yan.message.serializeToDrawText(yan.group) 92 | val drawTextLines = drawText.split("\n").toMutableList() 93 | var image = BufferedImage(680, 70, BufferedImage.TYPE_4BYTE_ABGR) 94 | var cg2d: Graphics2D = image.createGraphics() 95 | cg2d.font = standardFont.deriveFont(40f) 96 | fun split(text: String, fontMetrics: FontMetrics, max: Int): List { 97 | val list = Vector() 98 | var num = 1 99 | if (fontMetrics.stringWidth(text.subSequence(0, text.length) as String) <= max) { 100 | list.add(text) 101 | return list 102 | } 103 | while (true) { 104 | if (fontMetrics.stringWidth(text.subSequence(0, num) as String) <= max) { 105 | num += 1 106 | } else { 107 | num -= 1 108 | break 109 | } 110 | } 111 | list.add(text.subSequence(0, num) as String) 112 | list.addAll(split(text.subSequence(num, text.length) as String, fontMetrics, max)) 113 | 114 | return list 115 | } 116 | 117 | val iterator = drawTextLines.iterator() 118 | val new = ArrayList() 119 | iterator.forEach { it -> 120 | println("drawContent iterate for $it") 121 | new += split(it, fontMetrics = cg2d.fontMetrics, 680) 122 | } 123 | image = extImage(image, (cg2d.fontMetrics.height + 5) * new.size, Color(0, 0, 0, 0)) 124 | cg2d = image.createGraphics() 125 | cg2d.font = standardFont.deriveFont(40f) 126 | cg2d.color = Color(50, 50, 50) 127 | cg2d.applyAntialias() 128 | new.forEachIndexed { index, s -> 129 | cg2d.drawString(s, 0, cg2d.fontMetrics.height + index * (cg2d.fontMetrics.height + 5)) 130 | } 131 | 132 | return image.getSubimage(0, 0, new.maxOf { cg2d.fontMetrics.stringWidth(it) }, image.height) 133 | } 134 | 135 | 136 | private fun extImage( 137 | image: BufferedImage, 138 | newHeight: Int = image.height, 139 | backgroundColor: Color = Color(235, 238, 247), 140 | newWidth: Int = image.width, 141 | ): BufferedImage { 142 | if (newHeight < image.height) return image 143 | if (newWidth < image.height) return image 144 | val im = BufferedImage(newWidth, newHeight, BufferedImage.TYPE_4BYTE_ABGR) 145 | fillColor(im, backgroundColor) 146 | val g2d = im.createGraphics() 147 | g2d.drawImage(image, 0, 0, image.width, image.height, null) 148 | return im 149 | } 150 | 151 | 152 | private fun drawName(image: BufferedImage, yan: ShowYanTask, length: Int): Int { 153 | val g2d = image.createGraphics() 154 | g2d.applyAntialias() 155 | g2d.color = Color(164, 169, 179) 156 | g2d.font = standardFont.deriveFont(30f) 157 | g2d.drawString(yan.sender.name, length + 10, 55) 158 | return g2d.fontMetrics.stringWidth(yan.sender.name) + length + 10 + 5 159 | } 160 | 161 | 162 | private fun drawTitle(image: BufferedImage, yan: ShowYanTask): Int { 163 | val g2d = image.createGraphics() 164 | g2d.applyAntialias() 165 | g2d.font = standardFont.deriveFont(25f).deriveFont(Font.BOLD) 166 | g2d.color = Color(163, 187, 240) 167 | val wordLength = g2d.fontMetrics.stringWidth(yan.sender.title) 168 | g2d.fillRoundRect(170, 25, wordLength + 25, 40, 15, 15) 169 | g2d.color = Color.WHITE 170 | g2d.drawString(yan.sender.title, 185, 55) 171 | return 170 + wordLength + 25 172 | } 173 | 174 | private fun drawAvatar(yan: ShowYanTask, image: BufferedImage) { 175 | val avatar = yan.sender.avatarProvider.invoke().circleAvatar() 176 | val g2d = image.createGraphics() 177 | g2d.applyAntialias() 178 | g2d.drawImage(avatar, 25, 20, 110, 110, null) 179 | } 180 | 181 | fun downloadAvatar(url: String): BufferedImage { 182 | return ImageIO.read(URL(url)) 183 | } 184 | 185 | 186 | private fun fillColor(image: BufferedImage, color: Color = Color(235, 238, 247)) { 187 | val g2d = image.createGraphics() 188 | g2d.color = color 189 | g2d.fill(Rectangle(0, 0, image.width, image.height)) 190 | } 191 | } 192 | 193 | fun Graphics2D.applyAntialias() { 194 | val renderingHints = RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) 195 | renderingHints[RenderingHints.KEY_RENDERING] = RenderingHints.VALUE_RENDER_QUALITY 196 | this.setRenderingHints(renderingHints) 197 | } 198 | 199 | fun Graphics2D.default() { 200 | this.color 201 | } 202 | 203 | fun BufferedImage.testImage() { 204 | 205 | val path = Files.createTempFile(Path("temp\\"), "", ".png") 206 | ImageIO.write(this, "png", File(path.toString())) 207 | Runtime.getRuntime().exec("explorer .\\$path") 208 | } 209 | 210 | fun BufferedImage.circleAvatar(): BufferedImage { 211 | 212 | var avatarImage: BufferedImage = this 213 | avatarImage = run { 214 | val type = avatarImage.colorModel.transparency 215 | val width = avatarImage.width 216 | val height = avatarImage.height 217 | val renderingHints = RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) 218 | renderingHints[RenderingHints.KEY_RENDERING] = RenderingHints.VALUE_RENDER_QUALITY 219 | val img = BufferedImage(avatarImage.width, avatarImage.width, type) 220 | val graphics2d = img.createGraphics() 221 | graphics2d.setRenderingHints(renderingHints) 222 | graphics2d.drawImage(avatarImage, 0, 0, avatarImage.width, avatarImage.width, 0, 0, width, height, null) 223 | graphics2d.dispose() 224 | img 225 | } 226 | val width = avatarImage.width 227 | val formatAvatarImage = BufferedImage(width, width, BufferedImage.TYPE_4BYTE_ABGR) 228 | var graphics = formatAvatarImage.createGraphics() 229 | graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) 230 | val border = 0 231 | val shape = Ellipse2D.Double( 232 | border.toDouble(), border.toDouble(), 233 | (width - border * 2).toDouble(), (width - border * 2).toDouble() 234 | ) 235 | graphics.clip = shape 236 | graphics.drawImage(avatarImage, border, border, width - border * 2, width - border * 2, null) 237 | graphics.dispose() 238 | graphics = formatAvatarImage.createGraphics() 239 | graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) 240 | val border1 = 3 241 | val s: Stroke = BasicStroke(5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND) 242 | graphics.stroke = s 243 | graphics.color = Color.WHITE 244 | 245 | graphics.dispose() 246 | return formatAvatarImage 247 | } -------------------------------------------------------------------------------- /src/main/kotlin/core/data/Sender.kt: -------------------------------------------------------------------------------- 1 | package com.github.core.data 2 | 3 | import java.awt.image.BufferedImage 4 | 5 | 6 | data class Sender( 7 | val name:String, 8 | val avatarProvider:(() -> BufferedImage), 9 | val id:Long, 10 | val title:String, 11 | val titleColor:String, 12 | ) -------------------------------------------------------------------------------- /src/main/kotlin/core/data/ShowYanTask.kt: -------------------------------------------------------------------------------- 1 | package com.github.core.data 2 | 3 | import net.mamoe.mirai.contact.Group 4 | import net.mamoe.mirai.message.data.MessageChain 5 | 6 | 7 | data class ShowYanTask( 8 | val sender: Sender, 9 | val group: Group?, 10 | val message: MessageChain 11 | ) -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin: -------------------------------------------------------------------------------- 1 | com.github.XXYan -------------------------------------------------------------------------------- /src/main/resources/notFoundImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myot233/XXYan/cf7dfbf3c1d75d92b0601011bb5b7acbc9b5538c/src/main/resources/notFoundImage.jpg -------------------------------------------------------------------------------- /src/main/resources/sarasa-ui-tc-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myot233/XXYan/cf7dfbf3c1d75d92b0601011bb5b7acbc9b5538c/src/main/resources/sarasa-ui-tc-regular.ttf -------------------------------------------------------------------------------- /src/test/java/com/github/SimpleGeometricImageProvider.java: -------------------------------------------------------------------------------- 1 | package com.github; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | public class SimpleGeometricImageProvider { 7 | public static BufferedImage apply(String key) { 8 | try { 9 | String[] args = key.split(" "); 10 | String type = args[0]; 11 | if (type.equals("矩形")) { 12 | String[] sizeParts = args[1].split(","); 13 | int width = Integer.parseInt(sizeParts[0]); 14 | int height = Integer.parseInt(sizeParts[1]); 15 | 16 | String[] colorParts = args[2].split(","); 17 | String colorType = colorParts[0]; 18 | Paint paint; 19 | if (colorType.contains("渐变")) { 20 | Color colorFrom = new Color(Integer.parseInt(colorParts[1]), 21 | Integer.parseInt(colorParts[2]), 22 | Integer.parseInt(colorParts[3]), 23 | Integer.parseInt(colorParts[4])); 24 | Color colorTo = new Color(Integer.parseInt(colorParts[5]), 25 | Integer.parseInt(colorParts[6]), 26 | Integer.parseInt(colorParts[7]), 27 | Integer.parseInt(colorParts[8])); 28 | if (colorType.equals("上下渐变")) { 29 | paint = new GradientPaint((int)(width / 2), 0, colorFrom, (int)(width / 2), height, colorTo); 30 | } else if (colorType.equals("左右渐变")) { 31 | paint = new GradientPaint(0, (int)(height / 2), colorFrom, width, (int)(height / 2), colorTo); 32 | } else { 33 | paint = new GradientPaint((int)(width / 2), 0, colorFrom, (int)(width / 2), height, colorTo); 34 | } 35 | } else { 36 | paint = new Color(Integer.parseInt(colorParts[0]), 37 | Integer.parseInt(colorParts[1]), 38 | Integer.parseInt(colorParts[2]), 39 | Integer.parseInt(colorParts[3])); 40 | } 41 | 42 | BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); 43 | Graphics2D graphics = result.createGraphics(); 44 | graphics.setPaint(paint); 45 | graphics.fillRect(0, 0, result.getWidth(), result.getHeight()); 46 | return result; 47 | } 48 | } catch (Exception e) { 49 | throw new RuntimeException(String.format("处理key=%s时异常", key), e); 50 | } 51 | throw new RuntimeException(String.format("无法处理key=%s", key)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/kotlin/MessagePainterTest.kt: -------------------------------------------------------------------------------- 1 | package com.github 2 | 3 | import com.github.core.MessagePainter 4 | import com.github.core.data.Sender 5 | import com.github.core.data.ShowYanTask 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.runBlocking 8 | import kotlinx.coroutines.withContext 9 | import net.mamoe.mirai.message.data.MessageChainBuilder 10 | import net.mamoe.mirai.message.data.PlainText 11 | import org.junit.Test 12 | import java.io.* 13 | import javax.imageio.ImageIO 14 | 15 | 16 | class MessagePainterTest { 17 | 18 | @Test 19 | fun testMultiLines() { 20 | runBlocking { test( 21 | """ 22 | foo 23 | bar 24 | yan 25 | """.trimIndent()) } 26 | } 27 | 28 | @Test 29 | fun testLongLine() { 30 | runBlocking { test( 31 | """ 32 | fooooooooooooooooooooooooooooooooooooooooooooooooEOF 33 | """.trimIndent()) } 34 | } 35 | 36 | suspend fun test(text: String) { 37 | val avatarProvider = {SimpleGeometricImageProvider.apply("矩形 50,50 上下渐变,3,74,144,255,10,151,223,255")} 38 | val messageChain = MessageChainBuilder() 39 | .append(PlainText(text)) 40 | .build() 41 | val image = MessagePainter.paintMessage( 42 | ShowYanTask( 43 | Sender( 44 | "testName", 45 | avatarProvider, 46 | 1, 47 | "testTitle", 48 | "red" 49 | ), 50 | group = null, 51 | messageChain 52 | ) 53 | ) 54 | 55 | val byteStream = ByteArrayOutputStream() 56 | withContext(Dispatchers.IO) { 57 | ImageIO.write(image, "png", byteStream) 58 | } 59 | copyInputStreamToFile(ByteArrayInputStream(byteStream.toByteArray()), File("testOutput.png")) 60 | } 61 | } 62 | 63 | fun copyInputStreamToFile(inputStream: InputStream, file: File?) { 64 | // append = false 65 | try { 66 | FileOutputStream(file, false).use { outputStream -> 67 | var read: Int 68 | val bytes = ByteArray(DEFAULT_BUFFER_SIZE) 69 | while (inputStream.read(bytes).also { read = it } != -1) { 70 | outputStream.write(bytes, 0, read) 71 | } 72 | } 73 | } catch (ex: Exception) { 74 | ex.printStackTrace() 75 | } 76 | } -------------------------------------------------------------------------------- /src/test/kotlin/test.kt: -------------------------------------------------------------------------------- 1 | package com.github 2 | 3 | import net.mamoe.mirai.console.MiraiConsole 4 | import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.enable 5 | import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.load 6 | import net.mamoe.mirai.console.terminal.MiraiConsoleTerminalLoader.startAsDaemon 7 | 8 | 9 | suspend fun main() { 10 | startAsDaemon() 11 | 12 | XXYan.load() 13 | XXYan.enable() 14 | 15 | MiraiConsole.job.join() 16 | } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":"0.0.9", 3 | "description": "n" 4 | } 5 | --------------------------------------------------------------------------------