├── .gitignore ├── HttpRequests └── RequestTest.http ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main ├── kotlin ├── FriendMessageListener.kt ├── GroupMessageListener.kt ├── Molly.kt ├── MollyConfig.kt ├── MollyData.kt ├── Service.kt ├── model │ ├── MollyApiService.kt │ ├── MollyJson.kt │ └── Reply.kt └── utils │ ├── Convert.kt │ ├── KtorOkHttp.kt │ └── OkHttp.kt └── resources └── META-INF └── services └── net.mamoe.mirai.console.plugin.jvm.JvmPlugin /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | 120 | # Local Test Launch point 121 | src/test/kotlin/RunTerminal.kt 122 | -------------------------------------------------------------------------------- /HttpRequests/RequestTest.http: -------------------------------------------------------------------------------- 1 | POST https://i.mly.app/reply 2 | Content-Type: application/json;charset=UTF-8 3 | Api-Key: 00506c2611c695b71d46a410133e1335 4 | Api-Secret: laolittle114514 5 | 6 | { 7 | "content": "真是太厉害了", 8 | "type": 2, 9 | "from": 2652386228, 10 | "fromName": "tsu dzu ki", 11 | "to": 652934370, 12 | "toName": "萝卜子Test" 13 | } 14 | 15 | ### -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Molly-Mirai 2 | 3 | 4 | kotlin编写的简单的接入茉莉云聊天机器人,基于mirai-console的插件 5 | 6 | ### 使用方法 7 | 8 | 将插件放入plugins目录下,并启动一次mirai-console后关闭,然后在./config/Molly/MollyConfig.yml 中添加Api-Key和Api-Secret 9 | 10 | (可选) 添加机器人昵称,消息内包含机器人昵称就会回复 ( 建议将昵称保持与茉莉云昵称一致 ) 11 | 12 | 再次启动mirai-console,在群聊中@ 机器人即可 13 | 14 | (使用优惠码`laolittle`购买茉莉云会员即可有8折优惠) 15 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | val kotlinVersion = "1.8.0" 3 | kotlin("jvm") version kotlinVersion 4 | kotlin("plugin.serialization") version kotlinVersion 5 | 6 | id("net.mamoe.mirai-console") version "2.13.4" 7 | } 8 | 9 | group = "org.laolittle.plugin.molly" 10 | version = "1.4.0" 11 | 12 | repositories { 13 | maven("https://maven.aliyun.com/repository/central") 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | val ktorVer = "2.2.2" 19 | implementation("io.ktor:ktor-client:$ktorVer") 20 | implementation("io.ktor:ktor-client-core:$ktorVer") 21 | implementation("io.ktor:ktor-client-json:$ktorVer") 22 | implementation("io.ktor:ktor-client-okhttp:$ktorVer") 23 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaoLittle/Molly/3dfd4211e192fb4fd035222db4e745a4e5a8cc6f/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.4.2-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 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Molly" -------------------------------------------------------------------------------- /src/main/kotlin/FriendMessageListener.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import net.mamoe.mirai.event.GlobalEventChannel 5 | import net.mamoe.mirai.event.subscribeFriendMessages 6 | import net.mamoe.mirai.message.data.content 7 | import org.laolittle.plugin.molly.MollyConfig.dontReply 8 | import org.laolittle.plugin.molly.MollyConfig.enablePrivateChatReply 9 | import org.laolittle.plugin.molly.model.MollyApiService.request 10 | import org.laolittle.plugin.molly.model.Reply.reply 11 | 12 | @ExperimentalSerializationApi 13 | object FriendMessageListener : Service() { 14 | override suspend fun main() { 15 | GlobalEventChannel.parentScope(Molly).context(Molly.coroutineContext).filter { enablePrivateChatReply } 16 | .subscribeFriendMessages { 17 | always { 18 | if (subject.id == bot.id) return@always 19 | dontReply.forEach { dontNode -> if (message.content.contains(Regex(dontNode))) return@always } 20 | val mollyReply = request( 21 | message = it, 22 | userId = sender.id, 23 | userName = senderName, 24 | groupName = null, 25 | groupId = null, 26 | false 27 | ) 28 | reply(this@FriendMessageListener, mollyReply) 29 | } 30 | } 31 | /* finding(Regex("聊天")) { 32 | subject.sendMessage("在呢") 33 | var loop = true 34 | while (loop) 35 | whileSelectMessages { 36 | default { 37 | request( 38 | message = it, 39 | userId = sender.id, 40 | userName = senderName, 41 | groupName = null, 42 | groupId = null, 43 | false 44 | ) 45 | reply(this@FriendMessageListener, mollyReply) 46 | false 47 | } 48 | startsWith("不聊了") { 49 | subject.sendMessage("好吧") 50 | loop = false 51 | false 52 | } 53 | timeout(30_000) { 54 | subject.sendMessage("不聊了么?那我走了") 55 | loop = false 56 | false 57 | } 58 | } 59 | } 60 | } 61 | } */ 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/kotlin/GroupMessageListener.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import net.mamoe.mirai.event.GlobalEventChannel 5 | import net.mamoe.mirai.event.events.GroupMessageEvent 6 | import net.mamoe.mirai.event.subscribeGroupMessages 7 | import net.mamoe.mirai.message.data.content 8 | import org.laolittle.plugin.molly.MollyConfig.name 9 | import org.laolittle.plugin.molly.model.MollyApiService.containsMember 10 | import org.laolittle.plugin.molly.model.Reply.groupLoopReply 11 | 12 | @ExperimentalSerializationApi 13 | object GroupMessageListener : Service() { 14 | override suspend fun main() { 15 | GlobalEventChannel.parentScope(Molly).context(Molly.coroutineContext).filterIsInstance() 16 | .filter { !containsMember(it.sender.id) }.subscribeGroupMessages { 17 | finding(Regex(name)) { 18 | groupLoopReply(this@GroupMessageListener, message.content.replace(" ", "")) 19 | } 20 | atBot { 21 | val msg = it 22 | .replace("@${bot.id}", "") 23 | .replace(" ", "") 24 | groupLoopReply(this@GroupMessageListener, msg) 25 | } 26 | /*"mollydebug" { 27 | subject.sendMessage("start") 28 | whileSelectMessages { 29 | "stop" { 30 | subject.sendMessage("stopped") 31 | false 32 | } 33 | default { 34 | request( 35 | message = it, 36 | userId = sender.id, 37 | userName = senderName, 38 | groupName = group.name, 39 | groupId = group.id, 40 | true 41 | ) 42 | val mollyReplyTempo = mollyReply 43 | subject.sendMessage(mollyReplyTempo.toString()) 44 | for (i in mollyReplyTempo.keys) 45 | if (mollyReplyTempo[i]?.typed == 1) { 46 | val random = (100..3000).random().toLong() 47 | delay(random) 48 | subject.sendMessage(mollyReplyTempo[i]?.content.toString()) 49 | } else { 50 | val url = "https://files.molicloud.com/" + mollyReplyTempo[i]?.content 51 | subject.sendImage(mollyFile(url)) 52 | } 53 | mollyReply = linkedMapOf() 54 | true 55 | } 56 | } 57 | } 58 | 59 | */ 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/kotlin/Molly.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription 5 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin 6 | import net.mamoe.mirai.utils.info 7 | import org.laolittle.plugin.molly.MollyConfig.api_key 8 | import org.laolittle.plugin.molly.MollyConfig.api_secret 9 | 10 | @ExperimentalSerializationApi 11 | object Molly : KotlinPlugin( 12 | JvmPluginDescription( 13 | id = "org.laolittle.plugin.molly.Molly", 14 | version = "1.4.0", 15 | name = "Molly" 16 | ) 17 | ) { 18 | override fun onEnable() { 19 | MollyConfig.reload() 20 | MollyData.reload() 21 | logger.info { "茉莉云插件加载完毕" } 22 | if (api_key == "" || api_secret == "") 23 | logger.info { "请修改配置文件添加ApiKey和ApiSecret,配置文件位于./config/Molly/MollyConfig.yml" } 24 | else { 25 | GroupMessageListener.start() 26 | FriendMessageListener.start() 27 | } 28 | 29 | } 30 | 31 | override fun onDisable() { 32 | logger.info { "已卸载" } 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/MollyConfig.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly 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 MollyConfig : AutoSavePluginConfig("MollyConfig") { 8 | @ValueDescription("在此填入你的Api-Key") 9 | val api_key by value("") 10 | 11 | @ValueDescription("在此填入你的Api-Secret") 12 | val api_secret by value("") 13 | 14 | @ValueDescription("你的机器人昵称") 15 | val name by value("茉莉") 16 | 17 | @ValueDescription("群聊呼叫机器人后持续回复的次数,默认为0") 18 | val replyTimes by value(0) 19 | 20 | @ValueDescription("是否引用回复") 21 | val doQuoteReply by value(false) 22 | 23 | @ValueDescription("控制台显示服务器返回的数据") 24 | val doPrintResultsOnConsole by value(false) 25 | 26 | @ValueDescription( 27 | """ 28 | 私聊不触发聊天关键字 29 | 主要是防止私聊进行功能调用的时候触发此聊天 30 | 消息中包含就不回复 31 | 支持正则 32 | """ 33 | ) 34 | val dontReply by value(setOf("/", "色图")) 35 | 36 | @ValueDescription("机器人被呼叫但是消息没有任何内容的回应") 37 | val defaultReply by value(setOf("?", "怎么", "怎么了", "什么?", "在", "嗯?")) 38 | 39 | @ValueDescription("机器人被呼叫,消息没有任何内容且发送人一直没有说话的回应") 40 | val timeoutReply by value(setOf("没事我就溜了", "emmmmm", "......", "溜了", "?")) 41 | 42 | @ValueDescription("是否开启私聊") 43 | val enablePrivateChatReply by value(true) 44 | 45 | @ValueDescription( 46 | """ 47 | 万金油回复模式设定 48 | DEFAULT: 使用茉莉云官方的万金油 49 | LOCAL: 使用本地的万金油 50 | OFF: 不开启万金油回复 51 | """ 52 | ) 53 | val unknownReplyBehavior by value(UnknownReply.DEFAULT) 54 | 55 | enum class UnknownReply { 56 | DEFAULT, 57 | LOCAL, 58 | OFF 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/MollyData.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly 2 | 3 | import net.mamoe.mirai.console.data.ReadOnlyPluginData 4 | import net.mamoe.mirai.console.data.ValueDescription 5 | import net.mamoe.mirai.console.data.value 6 | 7 | object MollyData : ReadOnlyPluginData("MollyData") { 8 | @ValueDescription("自定义万金油回复,重复语句会被覆盖") 9 | val customUnknownReply by value(setOf("我不理解", "嗯?")) 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/Service.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly 2 | 3 | import kotlinx.coroutines.* 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlin.coroutines.CoroutineContext 6 | 7 | @ExperimentalSerializationApi 8 | abstract class Service(ctx: CoroutineContext? = null) : CoroutineScope { 9 | 10 | final override val coroutineContext: CoroutineContext 11 | get() = SupervisorJob(Molly.coroutineContext.job) 12 | 13 | init { 14 | if (ctx != null) { 15 | coroutineContext.plus(ctx) 16 | } 17 | } 18 | 19 | protected abstract suspend fun main() 20 | 21 | fun start(): Job = this.launch(context = this.coroutineContext) { main() } 22 | 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/model/MollyApiService.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly.model 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.json.* 5 | import net.mamoe.mirai.utils.error 6 | import net.mamoe.mirai.utils.info 7 | import org.laolittle.plugin.molly.Molly 8 | import org.laolittle.plugin.molly.MollyConfig 9 | import org.laolittle.plugin.molly.MollyConfig.UnknownReply.* 10 | import org.laolittle.plugin.molly.MollyData.customUnknownReply 11 | import org.laolittle.plugin.molly.utils.KtorOkHttp.post 12 | import java.security.cert.X509Certificate 13 | import java.util.concurrent.ConcurrentHashMap 14 | import javax.net.ssl.* 15 | 16 | object MollyApiService { 17 | val inActMember = ConcurrentHashMap() 18 | 19 | fun addMember(id: Long): Boolean { 20 | return inActMember.put(id, Unit) == null 21 | } 22 | 23 | fun removeMember(id: Long): Boolean { 24 | return inActMember.remove(id) != null 25 | } 26 | 27 | fun containsMember(id: Long): Boolean { 28 | return inActMember.contains(id) 29 | } 30 | 31 | @ExperimentalSerializationApi 32 | suspend fun request( 33 | message: String, 34 | userId: Long, 35 | userName: String, 36 | groupName: String?, 37 | groupId: Long?, 38 | inGroup: Boolean 39 | ): List { 40 | val mollyUrl = "https://api.mlyai.com/reply" 41 | 42 | val jsonRequest = buildJsonObject { 43 | put("content", message) 44 | put("type", if (!inGroup) 1 else 2) 45 | put("from", userId) 46 | put("fromName", userName) 47 | put("to", groupId) 48 | put("toName", groupName) 49 | } 50 | 51 | useInsecureSSL() // 忽略SSL证书 52 | val json = jsonRequest.toString().post(mollyUrl) 53 | if (MollyConfig.doPrintResultsOnConsole) 54 | Molly.logger.info { "服务器返回数据: $json" } 55 | return runCatching { 56 | val mollyData: MollyData = Json.decodeFromJsonElement(json) 57 | val replyData = if (mollyData.plugin == null) { 58 | when (MollyConfig.unknownReplyBehavior) { 59 | DEFAULT -> mollyData 60 | LOCAL -> { 61 | mollyData.copy(data = buildJsonArray { 62 | addJsonObject { 63 | val nullStr: String? = null 64 | put("content", customUnknownReply.random()) 65 | put("typed", 1) 66 | put("remark", nullStr) 67 | } 68 | }) 69 | } 70 | 71 | OFF -> mollyData.copy(data = buildJsonArray { }) 72 | } 73 | } else mollyData 74 | decode(replyData.data) 75 | }.onFailure { 76 | val mollyError: MollyError = Json.decodeFromJsonElement(json) 77 | hasError(mollyError) 78 | when (mollyError.code) { 79 | "C1001" -> return decode(buildJsonArray { 80 | addJsonObject { 81 | val nullStr: String? = null 82 | put("content", mollyError.message) 83 | put("typed", 5) 84 | put("remark", nullStr) 85 | } 86 | }) 87 | } 88 | }.getOrElse { throw Exception("解析错误! $json") } 89 | } 90 | 91 | @ExperimentalSerializationApi 92 | private fun hasError(mollyError: MollyError) { 93 | Molly.logger.error { 94 | """ 95 | 回复发生错误! 96 | 错误代码: ${mollyError.code} 97 | 错误信息: ${mollyError.message} 98 | """.trimIndent() 99 | } 100 | } 101 | 102 | @ExperimentalSerializationApi 103 | private fun decode(msgData: JsonArray): List { 104 | val mollyReply = mutableListOf() 105 | for (json in msgData) { 106 | mollyReply.add(Json.decodeFromJsonElement(json)) 107 | } 108 | return mollyReply 109 | } 110 | 111 | private fun useInsecureSSL() { 112 | val trustAllCerts = arrayOf(object : X509TrustManager { 113 | override fun getAcceptedIssuers(): Array? = null 114 | override fun checkClientTrusted(chain: Array, authType: String) = Unit 115 | override fun checkServerTrusted(chain: Array, authType: String) = Unit 116 | }) 117 | 118 | val sc = SSLContext.getInstance("SSL") 119 | sc.init(null, trustAllCerts, java.security.SecureRandom()) 120 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory) 121 | 122 | val allHostsValid = HostnameVerifier { _, _ -> true } 123 | 124 | HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid) 125 | } 126 | } -------------------------------------------------------------------------------- /src/main/kotlin/model/MollyJson.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.Json 6 | import kotlinx.serialization.json.JsonArray 7 | 8 | /** 9 | * Molly机器人返回的正常数据 10 | * @param plugin 使用的Molly插件 11 | * @param data Molly机器人返回的消息[JsonArray] 12 | * */ 13 | @Serializable 14 | data class MollyData( 15 | @SerialName("plugin") val plugin: String?, 16 | @SerialName("data") val data: JsonArray 17 | ) 18 | 19 | /** 20 | * Molly机器人返回的[Json]数据内的[JsonArray],其为回复消息的主要数据 21 | * @param content Molly机器人返回的消息字符串 22 | * @param typed Molly机器人返回的消息类型 1:文本,2:图片,3:文档,4:音频,9:其它文件 23 | * @param remark 表示附件上传时的原文件名 24 | * */ 25 | @Serializable 26 | data class MollyReply( 27 | @SerialName("content") val content: String, 28 | @SerialName("typed") val typed: Int, 29 | @SerialName("remark") val remark: String? 30 | ) 31 | 32 | /** 33 | * 当尝试接收消息失败时Molly机器人返回的错误信息 34 | * @param code Molly机器人返回的错误代码 35 | * @param message Molly机器人返回的错误信息 36 | * */ 37 | 38 | @Serializable 39 | data class MollyError( 40 | @SerialName("code") val code: String, 41 | @SerialName("message") val message: String 42 | ) 43 | 44 | internal val Json = Json { 45 | prettyPrint = true 46 | ignoreUnknownKeys = true 47 | isLenient = true 48 | allowStructuredMapKeys = true 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/model/Reply.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly.model 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.withTimeout 6 | import kotlinx.serialization.ExperimentalSerializationApi 7 | import net.mamoe.mirai.contact.AudioSupported 8 | import net.mamoe.mirai.event.events.GroupMessageEvent 9 | import net.mamoe.mirai.event.events.MessageEvent 10 | import net.mamoe.mirai.event.globalEventChannel 11 | import net.mamoe.mirai.event.nextEvent 12 | import net.mamoe.mirai.message.data.MessageChainBuilder 13 | import net.mamoe.mirai.message.data.MessageSource.Key.quote 14 | import net.mamoe.mirai.message.data.content 15 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource 16 | import org.laolittle.plugin.molly.Molly 17 | import org.laolittle.plugin.molly.MollyConfig.defaultReply 18 | import org.laolittle.plugin.molly.MollyConfig.doQuoteReply 19 | import org.laolittle.plugin.molly.MollyConfig.name 20 | import org.laolittle.plugin.molly.MollyConfig.replyTimes 21 | import org.laolittle.plugin.molly.MollyConfig.timeoutReply 22 | import org.laolittle.plugin.molly.model.MollyApiService.addMember 23 | import org.laolittle.plugin.molly.model.MollyApiService.removeMember 24 | import org.laolittle.plugin.molly.model.MollyApiService.request 25 | import org.laolittle.plugin.molly.utils.KtorOkHttp.getFile 26 | import org.laolittle.plugin.molly.utils.conversation 27 | 28 | object Reply { 29 | /** 30 | * 接收消息并发送请求到Molly机器人 31 | * */ 32 | @ExperimentalSerializationApi 33 | suspend fun GroupMessageEvent.reply(ctx: CoroutineScope, msg: String) { 34 | conversation(ctx) { 35 | val mollyReply = request( 36 | message = msg, 37 | userId = sender.id, 38 | userName = senderName, 39 | groupName = group.name, 40 | groupId = group.id, 41 | true 42 | ) 43 | reply(ctx, mollyReply, doQuoteReply) 44 | } 45 | } 46 | 47 | suspend fun MessageEvent.reply( 48 | ctx: CoroutineScope, 49 | mollyReplyTempo: List, 50 | quoteReply: Boolean = false 51 | ) { 52 | conversation(ctx) { 53 | mollyReplyTempo.forEach { receive -> 54 | val send = MessageChainBuilder() 55 | when (receive.typed) { 56 | 1 -> { 57 | send.add(receive.content) 58 | 59 | val random = (100..3000).random().toLong() 60 | delay(random) 61 | } 62 | 63 | 2 -> { 64 | val url = "https://files.molicloud.com/" + receive.content 65 | 66 | getFile(url).use { 67 | it.toExternalResource().use { ex -> 68 | send.add(subject.uploadImage(ex)) 69 | } 70 | } 71 | } 72 | 73 | 4 -> { 74 | val receiver = subject as AudioSupported 75 | val url = "https://files.molicloud.com/" + receive.content 76 | getFile(url).use { 77 | it.toExternalResource().use { ex -> 78 | send.add(receiver.uploadAudio(ex)) 79 | } 80 | } 81 | } 82 | 83 | 5 -> { 84 | send.add(receive.content) 85 | 86 | val random = (100..3000).random().toLong() 87 | delay(random) 88 | throw IllegalAccessException("回复次数超限") 89 | } 90 | 91 | else -> { 92 | send.add("https://files.molicloud.com/${receive.content}") 93 | } 94 | } 95 | 96 | if (quoteReply) send.add(message.quote()) 97 | 98 | subject.sendMessage(send.build()) 99 | } 100 | } 101 | } 102 | 103 | @ExperimentalSerializationApi 104 | suspend fun GroupMessageEvent.groupLoopReply(ctx: CoroutineScope, msg: String) { 105 | conversation(ctx) { 106 | addMember(sender.id) 107 | val proMsg = msg.replace("@${bot.id}", name) 108 | runCatching { 109 | if (proMsg == "") { 110 | subject.sendMessage(defaultReply.random()) 111 | if (replyTimes == 0) waitReply(ctx, 1) else waitReply(ctx, 0) 112 | for (i in 1 until replyTimes) { 113 | if (waitReply(ctx, i)) break 114 | } 115 | } else { 116 | reply(ctx, proMsg) 117 | if (proMsg == name) waitReply(ctx, 0) 118 | else waitReply(ctx, 1) 119 | for (i in 1 until replyTimes) 120 | if (waitReply(ctx, i)) break 121 | } 122 | } 123 | removeMember(sender.id) 124 | } 125 | } 126 | 127 | /** 128 | * 挂起当前协程并监听消息 129 | * 130 | * @return 是否超时 131 | * */ 132 | @ExperimentalSerializationApi 133 | suspend fun GroupMessageEvent.waitReply(ctx: CoroutineScope, i: Int): Boolean { 134 | val groupId = group.id 135 | val senderId = sender.id 136 | 137 | var isTimeout = false 138 | conversation(ctx) { 139 | isTimeout = runCatching { 140 | val next = withTimeout(10_000) { 141 | Molly.globalEventChannel().nextEvent { 142 | group.id == groupId && sender.id == senderId 143 | } 144 | } 145 | next.reply(ctx, next.message.content.replace("@${bot.id}", name).replace(" ", "")) 146 | false 147 | }.onFailure { 148 | if ((i == 0) && (replyTimes > 0)) { 149 | subject.sendMessage(timeoutReply.random()) 150 | } 151 | }.getOrDefault(true) 152 | /* whileSelectMessages { 153 | default { 154 | reply(ctx, it.replace("@${bot.id}", name)) 155 | false 156 | } 157 | timeout(10_000) { 158 | if ((i == 1) && (replyTimes > 0)) { 159 | subject.sendMessage(timeoutReply.random()) 160 | } 161 | isTimeout = false 162 | false 163 | } 164 | } */ 165 | } 166 | return isTimeout 167 | } 168 | } -------------------------------------------------------------------------------- /src/main/kotlin/utils/Convert.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly.utils 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.withContext 5 | 6 | class ConversationBuilder( 7 | val conversationBlock: suspend ConversationBuilder.() -> Unit 8 | ) { 9 | suspend operator fun invoke() = conversationBlock() 10 | } 11 | 12 | suspend fun conversation( 13 | scope: CoroutineScope, 14 | block: suspend ConversationBuilder.() -> Unit 15 | ): ConversationBuilder { 16 | suspend fun execute() = ConversationBuilder( 17 | conversationBlock = block 18 | ).also { it() } 19 | 20 | return withContext(scope.coroutineContext + scope.coroutineContext) { execute() } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/utils/KtorOkHttp.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly.utils 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.call.* 5 | import io.ktor.client.engine.okhttp.* 6 | import io.ktor.client.request.* 7 | import io.ktor.client.statement.* 8 | import kotlinx.serialization.json.JsonElement 9 | import org.laolittle.plugin.molly.MollyConfig.api_key 10 | import org.laolittle.plugin.molly.MollyConfig.api_secret 11 | import org.laolittle.plugin.molly.model.Json 12 | import java.io.InputStream 13 | 14 | object KtorOkHttp { 15 | private val client = HttpClient(OkHttp) 16 | 17 | suspend fun getFile(url: String): InputStream { 18 | return client.get(url).body() 19 | } 20 | 21 | suspend fun String.post(url: String): JsonElement { 22 | val responseData = client.post(url) client@{ 23 | setBody(this@post) 24 | header("Content-Type", "application/json;charset=utf-8") 25 | header("Api-Key", api_key) 26 | header("Api-Secret", api_secret) 27 | } 28 | return Json.parseToJsonElement(responseData.bodyAsText()) 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/utils/OkHttp.kt: -------------------------------------------------------------------------------- 1 | package org.laolittle.plugin.molly.utils 2 | 3 | /* 4 | object OkHttp { 5 | 6 | private var client: OkHttpClient = OkHttpClient().newBuilder().connectTimeout(Duration.ofMillis(8_000)).build() 7 | 8 | fun getFile(url: String): InputStream { 9 | val request = Request.Builder().url(url) 10 | .get().build() 11 | val responseBody = client.newCall(request).execute().body 12 | if (responseBody != null){ 13 | return responseBody.byteStream() 14 | } else { 15 | throw Exception("响应为空") 16 | } 17 | } 18 | 19 | fun String.post(url: String): JsonElement { 20 | val media = "application/json;charset=utf-8" 21 | val request = Request.Builder().url(url) 22 | .header("Content-Type", media) 23 | .header("Api-Key", api_key) 24 | .header("Api-Secret", api_secret) 25 | .post(this.toRequestBody(media.toMediaTypeOrNull())).build() 26 | val responseBody = client.newCall(request).execute().body 27 | if (responseBody != null){ 28 | return responseBody.use { Json.parseToJsonElement(it.string()) } 29 | } else { 30 | throw Exception("响应为空") 31 | } 32 | } 33 | }*/ -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin: -------------------------------------------------------------------------------- 1 | org.laolittle.plugin.molly.Molly --------------------------------------------------------------------------------