320 |
321 |
322 |
323 |
324 |
325 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BotPlugin
2 | 一个运行于[Mirai Console](https://github.com/mamoe/mirai-console)的插件
3 | A plugin for [Mirai Console](https://github.com/mamoe/mirai-console) with image sender,pixiv querying and more.
4 | ## 功能
5 | * 发送本地图库的圖片(请手动下载赫图,另提供指令sendImage一次发送多张)(发送带有'图'和'来'的消息或者色图来图片(不知道这图我也没办法,你可以用图片ID发出去))
6 | * 发送在线的Json源图片
7 | * 发送在线的Pixiv的圖片(允许设置代理,说得好像不设置代理谁进得去呢?)
8 | * 被At时复读,并且把At的对象换成对方(此外还支持颠倒消息顺序,随机旋转消息图片)
9 | * 在群聊中更新某些配置(带有用户控制(废话))
10 | * 设置群聊黑名单(注意settings和blacklist指令全局可用,黑名单群settings指令只回復manager)
11 | * 禁言,取消禁言,加群,退群提示(注意:有臭味
12 | * PetPet图片生成(群聊被戳一戳时,请务必为账号使用ANDROID_PHONE协议以便正常使用)
13 | * 插件管理员执行Javascript脚本(可用变量:bot,event,dlg,dlg请查看ScriptMethods.kt获取具体用法)
14 | * 随机复读
15 | * 没了
16 | ## 特色
17 | * 单线程撤回发出的本地图片(防止请求过于频繁被服务器拒绝)
18 | * CommandDispatcher(?蜜汁操作,指令派发)
19 | **在本仓库的Releases中可以直接下载编译好的jar文件! 如果不是需要研究,你可以直接下载**
20 | ## 简单使用
21 | - 下载Release里发布的jar.
22 | - [启动Mirai-Console](https://github.com/mamoe/mirai-console/blob/master/docs/Run.md)
23 | - 复制jar到Console工作目录下的plugins里
24 | - 见下方'开始使用'栏目
25 | ### 当前项目环境:
26 | * Mirai 2.6.5
27 | * Kotlin 1.5.10
28 | * OpenJdk 14
29 | * Intellij IDEA 2021.1.2
30 | **注意! 在编译本项目之前,请留意删除gradle.properties中的网络代理设置**
31 | ## 开始使用
32 | 建议使用 Java 11或者更高版本的Java
33 | 先带着插件运行一次Mirai Console,然后停止
34 | 在`mirai文件夹/config/RosemoeBotPlugin/PluginConfig.yml`中把
35 | ```yml
36 | managers: []
37 | ```
38 | 修改为
39 | ```yml
40 | managers:
41 | - 你的QQ
42 | ```
43 | 当然也可以添加多个Manager,比如
44 | ```yml
45 | managers:
46 | - 114514
47 | - 1919810
48 | - 12345678
49 | ```
50 | 然后重新运行Mirai Console
51 | ## 群聊指令表
52 | 非常建议您先读完下面的指令表再使用
53 | ### 设置部分
54 | 只有Plugin Manager(不是Bot Manager)才能使用这些指令!
55 | #### 全局设置
56 | ```Bash
57 | /settings set recallDelay <时间>
58 | /settings set recallInterval <时间>
59 | /settings set prefix <指令前缀,默认'/'>
60 | /settings set repeatFactor <概率的小数>
61 | /settings reload
62 | /settings reloadq
63 | /settings get recallDelay
64 | /settings get recallInterval
65 | /settings get prefix
66 | /settings get repeatFactor
67 | /settings enable <功能>
68 | /settings disable <功能>
69 | ```
70 | reloadBase只刷新配置不重新建立图片索引,算是轻重载
71 | #### 图片源设置
72 | 图片源保存在image_sources.yml中
73 | ```Bash
74 | 添加一个路径:
75 | /sources path <名称> <路径>
76 |
77 | 添加一个在线Json图源
78 | /sources json <名称> <网址> <数据路径>
79 | 对应的网址要返回一个Json文本,其中通过数据路径可以到达url元素
80 | 比如返回下面这段Json:
81 | {"code":1,"msg":"ok","data":"http:\/\/test.xxx.com\/large\/a15b4afegy1fmvjv7pshlj21hc0u0e0s.jpg"}
82 | 需要设置的数据路径是 data
83 | 对于下面这段Json:
84 | {"code":0,"msg":"","quota":8,"quota_min_ttl":7029,"count":1,"data":[{"pid":61732396,"p":0,"uid":946272,"title":"カンナ","author":"Aile\/エル","url":"https:\/\/i.pixiv.cat\/img-original\/img\/2017\/03\/04\/00\/00\/01\/61732396_p0.png","r18":false,"width":583,"height":650,"tags":["カンナカムイ(小林さんちのメイドラゴン)","康娜卡姆依(小林家的龙女仆)","カンナ","康娜","カンナカムイ","康娜卡姆依","小林さんちのメイドラゴン","小林家的龙女仆","尻神様","尻神样","竜娘","龙娘","マジやばくね","that's wicked","高品質パンツ","高品质内裤","魅惑のふともも","魅惑的大腿"]}]}
85 | 需要设置的数据路径是 data\0\url
86 |
87 | 删除一个源
88 | /sources remove <名称>
89 |
90 | 刷新源列表
91 | 对源进行操作时不会立即生效,使用settings reload会导致设置文件被覆盖
92 | 可以使用这个方法来在修改图源列表后刷新图源
93 | /sources refresh
94 | ```
95 | 另外,你也可以使用脚本手动完成获取图片的逻辑。至于如何配置使用,请研究ImageSource.kt(懒得写指令了23333)
96 | #### 功能名称表
97 | ```Kotlin
98 | val allowedModuleName = listOf(
99 | "ImageSender",
100 | "Pixiv",
101 | "BatchImg",
102 | "AtReply",
103 | "Welcome",
104 | "MuteTip",
105 | "ReverseAtReply",
106 | "ReverseAtReplyImage",
107 | "PetPet",
108 | "Repeat",
109 | "Help"
110 | )
111 | ```
112 | ### Pixiv
113 | ```Bash
114 | 查看画作信息和预览图
115 | /pixiv illust <画作ID>
116 | 查看指定画作的指定P的大图
117 | /pixiv illust <画作ID> <图片索引>
118 | ```
119 | R18画作将不会发送图片
120 | ### 本地图片
121 | ```Bash
122 | /sendImage <图片数目>
123 | ```
124 | ### Ping
125 | ```Bash
126 | /ping 后面和操作系统一样
127 | ```
128 | ### IP获取
129 | ```Bash
130 | /ipList <网址>
131 | /ipList4 <网址>
132 | /ipList6 <网址>
133 | ```
134 | ### 黑名单
135 | ```Bash
136 | /blocklist add <群号或this>
137 | /blocklist remove <群号或this>
138 | /blocklist list <页码,可选,每页15个>
139 | ```
140 | ## Console指令表
141 | 咕了,还没写呢
142 | ## 设置Pixiv代理
143 | * `proxyEnabled` 配置是否启用代理
144 | * `proxyType` 配置代理类型,必须是socks或者http其中一种,填写其它默认socks
145 | * `proxyAddress` 配置代理地址,如`127.0.0.1`
146 | * `proxyPort` 配置代理端口,如`1080`
147 | 这些代理设置仅用于Pixiv网路连接,不作它用
148 | 实现上使用了Java的Proxy类
149 | ## 示例PluginConfig.yml
150 | ```yml
151 | managers:
152 | - 1145141919
153 | - 8101145141
154 | states:
155 | ImageSender: true
156 | commandPrefix: '/'
157 | darkListGroups:
158 | - 122344646
159 | - 464544644
160 | imageRecallDelay: 30000
161 | recallMinPeriod: 180
162 | maxImageRequestCount: 16
163 | proxyEnabled: true
164 | proxyType: socks
165 | proxyAddress: 127.0.0.1
166 | proxyPort: 1080
167 | allowR18ImageInPixiv: false
168 | repeatFactor: 0.05
169 | ```
170 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 |
20 | plugins {
21 | val kotlinVersion = "1.7.10"
22 | kotlin("jvm") version kotlinVersion
23 | kotlin("plugin.serialization") version kotlinVersion
24 | java
25 | id("net.mamoe.mirai-console") version "2.13.0"
26 | }
27 |
28 | group = "io.github.Rosemoe"
29 | version = "2.4.2"
30 |
31 | repositories {
32 | mavenLocal()
33 | jcenter()
34 | mavenCentral()
35 | maven("https://dl.bintray.com/kotlin/kotlin-eap")
36 | }
37 |
38 | dependencies {
39 | compileOnly("net.mamoe:mirai-core-all:2.13.0")
40 | compileOnly(kotlin("stdlib-jdk8"))
41 | implementation("org.json:org.json:2.0")
42 | implementation(files("libs/rhino-1.7.13.jar"))
43 | testImplementation(kotlin("stdlib-jdk8"))
44 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # RosemoeBotPlugin
3 | # Copyright (C) 2020-2021 Rosemoe
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as published
7 | # by the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 | #
18 |
19 | kotlin.code.style=official
20 | systemProp.http.proxyHost=localhost
21 | systemProp.http.proxyPort=8888
22 | systemProp.https.proxyHost=localhost
23 | systemProp.https.proxyPort=8888
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rosemoe/BotPlugin/d4bf56f6799d0b2c3f5230528d444e7d77a302fb/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.5-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # RosemoeBotPlugin
5 | # Copyright (C) 2020-2021 Rosemoe
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published
9 | # by the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this program. If not, see .
19 | #
20 |
21 | ##############################################################################
22 | ##
23 | ## Gradle start up script for UN*X
24 | ##
25 | ##############################################################################
26 |
27 | # Attempt to set APP_HOME
28 | # Resolve links: $0 may be a link
29 | PRG="$0"
30 | # Need this for relative symlinks.
31 | while [ -h "$PRG" ] ; do
32 | ls=`ls -ld "$PRG"`
33 | link=`expr "$ls" : '.*-> \(.*\)$'`
34 | if expr "$link" : '/.*' > /dev/null; then
35 | PRG="$link"
36 | else
37 | PRG=`dirname "$PRG"`"/$link"
38 | fi
39 | done
40 | SAVED="`pwd`"
41 | cd "`dirname \"$PRG\"`/" >/dev/null
42 | APP_HOME="`pwd -P`"
43 | cd "$SAVED" >/dev/null
44 |
45 | APP_NAME="Gradle"
46 | APP_BASE_NAME=`basename "$0"`
47 |
48 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
49 | DEFAULT_JVM_OPTS='"-Xmx64m"'
50 |
51 | # Use the maximum available, or set MAX_FD != -1 to use that value.
52 | MAX_FD="maximum"
53 |
54 | warn () {
55 | echo "$*"
56 | }
57 |
58 | die () {
59 | echo
60 | echo "$*"
61 | echo
62 | exit 1
63 | }
64 |
65 | # OS specific support (must be 'true' or 'false').
66 | cygwin=false
67 | msys=false
68 | darwin=false
69 | nonstop=false
70 | case "`uname`" in
71 | CYGWIN* )
72 | cygwin=true
73 | ;;
74 | Darwin* )
75 | darwin=true
76 | ;;
77 | MINGW* )
78 | msys=true
79 | ;;
80 | NONSTOP* )
81 | nonstop=true
82 | ;;
83 | esac
84 |
85 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
86 |
87 | # Determine the Java command to use to start the JVM.
88 | if [ -n "$JAVA_HOME" ] ; then
89 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
90 | # IBM's JDK on AIX uses strange locations for the executables
91 | JAVACMD="$JAVA_HOME/jre/sh/java"
92 | else
93 | JAVACMD="$JAVA_HOME/bin/java"
94 | fi
95 | if [ ! -x "$JAVACMD" ] ; then
96 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
97 |
98 | Please set the JAVA_HOME variable in your environment to match the
99 | location of your Java installation."
100 | fi
101 | else
102 | JAVACMD="java"
103 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
104 |
105 | Please set the JAVA_HOME variable in your environment to match the
106 | location of your Java installation."
107 | fi
108 |
109 | # Increase the maximum file descriptors if we can.
110 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
111 | MAX_FD_LIMIT=`ulimit -H -n`
112 | if [ $? -eq 0 ] ; then
113 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
114 | MAX_FD="$MAX_FD_LIMIT"
115 | fi
116 | ulimit -n $MAX_FD
117 | if [ $? -ne 0 ] ; then
118 | warn "Could not set maximum file descriptor limit: $MAX_FD"
119 | fi
120 | else
121 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
122 | fi
123 | fi
124 |
125 | # For Darwin, add options to specify how the application appears in the dock
126 | if $darwin; then
127 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
128 | fi
129 |
130 | # For Cygwin, switch paths to Windows format before running java
131 | if $cygwin ; then
132 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
133 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
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=$((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 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
186 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
187 | cd "$(dirname "$0")"
188 | fi
189 |
190 | exec "$JAVACMD" "$@"
191 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS="-Xmx64m"
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/libs/rhino-1.7.13.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rosemoe/BotPlugin/d4bf56f6799d0b2c3f5230528d444e7d77a302fb/libs/rhino-1.7.13.jar
--------------------------------------------------------------------------------
/out/production/resources/strings.properties:
--------------------------------------------------------------------------------
1 | #
2 | # RosemoeBotPlugin
3 | # Copyright (C) 2020-2021 Rosemoe
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as published
7 | # by the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 | #
18 |
19 | settings.pluginName=BotPlugin设置
20 | settings.managers=管理员控制
21 | settings.moduleStates=功能开关
22 | settings.blacklist=黑名单
23 | settings.proxy=代理设置
24 | settings.other=杂项
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | mavenLocal()
4 | gradlePluginPortal()
5 | jcenter()
6 | maven("https://dl.bintray.com/kotlin/kotlin-eap")
7 | }
8 | }
9 |
10 | rootProject.name = "RosemoeBotPlugin"
11 |
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/ImageSourceConfig.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import net.mamoe.mirai.console.data.AutoSavePluginConfig
22 | import net.mamoe.mirai.console.data.value
23 |
24 | object ImageSourceConfig : AutoSavePluginConfig("images_storages") {
25 |
26 | var sources : MutableList by value()
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/ImageStorage.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import kotlinx.serialization.*
22 | import kotlinx.serialization.json.Json
23 | import kotlinx.serialization.modules.SerializersModule
24 | import kotlinx.serialization.modules.polymorphic
25 | import kotlinx.serialization.modules.subclass
26 | import net.mamoe.mirai.utils.ExternalResource
27 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
28 | import org.json.JSONArray
29 | import org.json.JSONObject
30 | import org.mozilla.javascript.Context
31 | import org.mozilla.javascript.ScriptableObject
32 | import java.io.File
33 | import java.lang.Exception
34 | import java.lang.IllegalArgumentException
35 | import java.net.URL
36 | import java.util.concurrent.locks.ReentrantReadWriteLock
37 | import kotlin.random.Random
38 |
39 | private val JsonSerialize = Json {
40 | ignoreUnknownKeys = true
41 | classDiscriminator = "type"
42 | serializersModule = SerializersModule {
43 | polymorphic(ImageStorage::class) {
44 | subclass(LocalImageStorage::class)
45 | subclass(OnlineJsonImageStorage::class)
46 | subclass(ScriptProxyImageStorage::class)
47 | }
48 | }
49 | }
50 |
51 | fun ImageStorage.serializeStorage() : String {
52 | return JsonSerialize.encodeToString(this)
53 | }
54 |
55 | fun deserializeStorage(data: String) : ImageStorage {
56 | return JsonSerialize.decodeFromString(data)
57 | }
58 |
59 | /**
60 | * Image Storage for sending images
61 | */
62 | @Serializable
63 | sealed class ImageStorage {
64 |
65 | @Required
66 | var storageName: String = "unnamed"
67 |
68 | /**
69 | * Get a random image from the source
70 | */
71 | open fun obtainImage(): ExternalResource? {
72 | return null
73 | }
74 |
75 | /**
76 | * initialize
77 | */
78 | open fun init() {
79 |
80 | }
81 |
82 | }
83 |
84 | @Serializable
85 | @SerialName("local")
86 | class LocalImageStorage(private val path: String) : ImageStorage() {
87 |
88 | @Transient
89 | private val file = File(path)
90 | @Transient
91 | private val images = ArrayList()
92 | @Transient
93 | private val lock = ReentrantReadWriteLock()
94 | @Transient
95 | private val random = Random(System.nanoTime() + System.currentTimeMillis())
96 |
97 | init {
98 | check(file.exists()) { "File does not exist" }
99 | }
100 |
101 | private fun File.isImageFile(): Boolean {
102 | val lowerCase = name.lowercase()
103 | return lowerCase.endsWith(".jpeg") || lowerCase.endsWith(".jpg") || lowerCase.endsWith(".png") || lowerCase.endsWith(".bmp") || lowerCase.endsWith(
104 | ".webp"
105 | )
106 | }
107 |
108 | override fun init() {
109 | lock.lockWrite()
110 | try {
111 | images.clear()
112 | fun search(file: File) {
113 | if (file.isFile && file.isImageFile()) {
114 | images.add(file)
115 | } else {
116 | file.listFiles()?.forEach {
117 | search(it)
118 | }
119 | }
120 | }
121 | search(file)
122 | } finally {
123 | lock.unlockWrite()
124 | }
125 | }
126 |
127 | override fun obtainImage(): ExternalResource? {
128 | lock.lockRead()
129 | try {
130 | if (images.isEmpty()) {
131 | return null
132 | }
133 | return images[random.nextInt(0, images.size)].toExternalResource()
134 | } finally {
135 | lock.unlockRead()
136 | }
137 | }
138 |
139 | }
140 |
141 | @Serializable
142 | @SerialName("json")
143 | class OnlineJsonImageStorage(private val requestUrl: String, private val jsonPathForUrl: String) : ImageStorage() {
144 |
145 | @Transient
146 | private val pathList = jsonPathForUrl.split("\\")
147 |
148 | override fun obtainImage(): ExternalResource {
149 | var obj: Any = JSONObject(getWebpageSource(requestUrl))
150 | for (name in pathList) {
151 | if (obj is JSONObject) {
152 | obj = obj.get(name)
153 | } else if (obj is JSONArray) {
154 | obj = obj.get(name.toInt())
155 | } else if (obj is String) {
156 | break
157 | } else {
158 | throw IllegalArgumentException("Unable to extract content from $obj")
159 | }
160 | }
161 | var url = obj.toString()
162 | if (url.startsWith("//")) {
163 | url = "https:$url"
164 | }
165 | return URL(url).openConnection().getInputStream().toExternalResource()
166 | }
167 |
168 | override fun init() {
169 | // Empty
170 | }
171 |
172 | }
173 |
174 | @Serializable
175 | @SerialName("proxy")
176 | class ScriptProxyImageStorage(private val script: String) : ImageStorage() {
177 |
178 | override fun obtainImage(): ExternalResource? {
179 | val context = Context.enter()
180 | try {
181 | val script = context.compileString(script, "", 1, null)
182 | val scope = context.initStandardObjects()
183 | fun setJsObject(name: String, value: Any?) {
184 | val jsObj = Context.javaToJS(value, scope)
185 | ScriptableObject.putProperty(scope, name, jsObj)
186 | }
187 |
188 | val result = script.exec(context, scope)
189 | if (result == null) {
190 | logWarning("ImageStorage named '${storageName}' attempted to execute its script but got null result")
191 | return null
192 | } else if (result is ExternalResource) {
193 | return result
194 | } else if (result is String) {
195 | return URL(result).openConnection().getInputStream().toExternalResource()
196 | }
197 | logWarning("ImageStorage named '${storageName}' attempted to execute its script but got unexpected result type ${result.javaClass.name}")
198 | return null
199 | } catch (e: Exception) {
200 | logError("ImageStorage named '${storageName}' failed to execute its script", e)
201 | } finally {
202 | Context.exit()
203 | }
204 | return null
205 | }
206 |
207 | override fun init() {
208 | // Empty
209 | }
210 |
211 | }
212 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/MessageStates.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import kotlinx.coroutines.CoroutineScope
22 | import kotlinx.coroutines.Job
23 | import kotlinx.coroutines.delay
24 | import kotlinx.coroutines.launch
25 | import net.mamoe.mirai.event.events.GroupMessageEvent
26 | import java.util.concurrent.ConcurrentHashMap
27 | import java.util.concurrent.locks.ReentrantReadWriteLock
28 |
29 | class MessageStates {
30 |
31 | private val lock = ReentrantReadWriteLock()
32 |
33 | private val mapGroup = ConcurrentHashMap()
34 |
35 | class GroupMessages {
36 |
37 | private val savedSigns = HashMap()
38 |
39 | fun handleForSign(sign: Long): Boolean {
40 | synchronized(this) {
41 | if (savedSigns.containsKey(sign)) {
42 | return false
43 | }
44 | savedSigns.put(sign, true)
45 | return true
46 | }
47 | }
48 |
49 | fun clear() {
50 | synchronized(this) {
51 | savedSigns.clear()
52 | }
53 | }
54 |
55 | }
56 |
57 | fun GroupMessageEvent.getSign(): Long {
58 | return sender.id.shl(29).or(source.time.toLong())
59 | }
60 |
61 | fun handle(event: GroupMessageEvent): Boolean {
62 | lock.lockRead()
63 | try {
64 | val groupMessages = mapGroup.computeIfAbsent(event.group.id) {
65 | GroupMessages()
66 | }
67 | return groupMessages.handleForSign(event.getSign())
68 | } finally {
69 | lock.unlockRead()
70 | }
71 | }
72 |
73 | fun clear() {
74 | lock.lockWrite()
75 | try {
76 | mapGroup.forEachValue(2) {
77 | it.clear()
78 | }
79 | } finally {
80 | lock.unlockWrite()
81 | }
82 | }
83 |
84 | fun launchClearer(scope: CoroutineScope): Job {
85 | return scope.launch {
86 | while (true) {
87 | clear()
88 | delay(30 * 1000) //30 seconds
89 | }
90 | }
91 | }
92 |
93 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/RosemoePlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import io.github.rosemoe.miraiPlugin.command.CommandPermissionChecker
22 | import io.github.rosemoe.miraiPlugin.command.Command
23 | import io.github.rosemoe.miraiPlugin.command.CommandDispatcher
24 | import io.github.rosemoe.miraiPlugin.command.MsgEvent
25 | import io.github.rosemoe.miraiPlugin.commands.*
26 | import io.github.rosemoe.miraiPlugin.commands.Setu.initializeImageList
27 | import io.github.rosemoe.miraiPlugin.commands.Setu.sendImageForEvent
28 | import io.github.rosemoe.miraiPlugin.modules.handleGpMessageForRepeating
29 | import io.github.rosemoe.miraiPlugin.utils.startRecallManager
30 | import kotlinx.coroutines.launch
31 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
32 | import net.mamoe.mirai.console.plugin.jvm.reloadPluginConfig
33 | import net.mamoe.mirai.contact.Group
34 | import net.mamoe.mirai.contact.MemberPermission
35 | import net.mamoe.mirai.contact.User
36 | import net.mamoe.mirai.contact.nameCardOrNick
37 | import net.mamoe.mirai.event.EventHandler
38 | import net.mamoe.mirai.event.ListenerHost
39 | import net.mamoe.mirai.event.events.*
40 | import net.mamoe.mirai.event.globalEventChannel
41 | import net.mamoe.mirai.message.MessageReceipt
42 | import net.mamoe.mirai.message.data.At
43 | import net.mamoe.mirai.message.data.Message
44 | import net.mamoe.mirai.message.data.PlainText
45 | import net.mamoe.mirai.message.data.messageChainOf
46 |
47 | object RosemoePlugin : ListenerHost, KotlinPlugin(
48 | net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription(
49 | id = "io.github.rosemoe.miraiPlugin.RosemoePlugin",
50 | version = pluginVersion
51 | ) {
52 | name("RosemoeBotPlugin")
53 | author("Rosemoe")
54 | }
55 | ) , CommandPermissionChecker {
56 | /**
57 | * Constant fields
58 | */
59 | private val IMAGE_REQUEST = arrayOf("来", "图")
60 |
61 | /**
62 | * Runtime fields
63 | */
64 | internal val config = RosemoePluginConfig
65 |
66 | internal val dispatcher = CommandDispatcher(this, coroutineContext)
67 | private val msgs = MessageStates()
68 |
69 | override fun onEnable() {
70 | super.onEnable()
71 | /*try {
72 | logger.verbose("正在检查插件更新...")
73 | val source = getWebpageSource("https://github.com/Rosemoe/BotPlugin/releases/latest")
74 | } catch (e: Exception) {
75 | logger.warning("检查插件更新失败", e)
76 | }*/
77 | initOrReloadConfig()
78 | globalEventChannel(this.coroutineContext).registerListenerHost(this)
79 | registerCommands()
80 | startRecallManager()
81 | msgs.launchClearer(this)
82 | }
83 |
84 | private fun registerCommands() {
85 | dispatcher.register(Blocklist, Help, Pixiv, Settings, Setu, Sources)
86 | }
87 |
88 | private fun isManagementCommand(command: Command) : Boolean {
89 | return command == Blocklist || command == Sources || command == Settings
90 | }
91 |
92 | override fun shouldRunCommand(user: User, command: Command, group: Long): Boolean {
93 | if (group != 0L && isDarklistGroup(group)) {
94 | return config.managers.contains(user.id) && isManagementCommand(command)
95 | } else {
96 | return true
97 | }
98 | }
99 |
100 | @EventHandler
101 | @Suppress("unused")
102 | fun onFriendMessage(event: FriendMessageEvent) {
103 | try {
104 | // Dispatch message
105 | if (isModuleEnabled("ImageSender") && (event.message.containsTexts(IMAGE_REQUEST) || event.message.containsImage(
106 | "B407F708-A2C6-A506-3420-98DF7CAC4A57"
107 | ))
108 | ) {
109 | sendImageForEvent(MsgEvent(event))
110 | }
111 | dispatcher.dispatch(event)
112 | } catch (e: Throwable) {
113 | pluginLaunch {
114 | event.sender.sendMessage(getExceptionInfo(e))
115 | }
116 | logger.error(e)
117 | }
118 | }
119 |
120 | @EventHandler
121 | @Suppress("unused")
122 | fun onGroupTempMessage(event: GroupTempMessageEvent) {
123 | try {
124 | // Dispatch message
125 | if (isModuleEnabled("ImageSender") && (event.message.containsTexts(IMAGE_REQUEST) || event.message.containsImage(
126 | "B407F708-A2C6-A506-3420-98DF7CAC4A57"
127 | ))
128 | ) {
129 | sendImageForEvent(MsgEvent(event))
130 | }
131 | dispatcher.dispatch(event)
132 | } catch (e: Throwable) {
133 | pluginLaunch {
134 | event.sender.sendMessage(getExceptionInfo(e))
135 | }
136 | logger.error(e)
137 | }
138 | }
139 |
140 | @EventHandler
141 | @Suppress("unused")
142 | fun onGroupMessage(event: GroupMessageEvent) {
143 | if (msgs.handle(event)) {
144 | try {
145 | // Dispatch message
146 | if (isModuleEnabled("ImageSender") && (event.message.containsTexts(IMAGE_REQUEST) || event.message.containsImage(
147 | "B407F708-A2C6-A506-3420-98DF7CAC4A57"
148 | ))
149 | ) {
150 | sendImageForEvent(MsgEvent(event))
151 | }
152 | randomRepeat(event)
153 | handleAtReply(event)
154 | handleGpMessageForRepeating(event)
155 | dispatcher.dispatch(event)
156 | } catch (e: Throwable) {
157 | event.sendAsync(getExceptionInfo(e))
158 | logger.error(e)
159 | }
160 | }
161 | }
162 |
163 | fun isDarklistGroup(event: GroupEvent): Boolean {
164 | return isDarklistGroup(event.group.id)
165 | }
166 |
167 | private fun isDarklistGroup(id: Long): Boolean {
168 | return config.darkListGroups.contains(id)
169 | }
170 |
171 | @EventHandler
172 | @Suppress("unused")
173 | suspend fun onMemberMute(event: MemberMuteEvent) {
174 | if (isDarklistGroup(event)) {
175 | return
176 | }
177 | if (!isModuleEnabled("MuteTip")) {
178 | return
179 | }
180 | event.group.sendMessage(
181 | messageChainOf(
182 | At(event.member), PlainText(
183 | " 喝下了${
184 | if (event.operator?.permission == MemberPermission.OWNER) "群主" else "管理"
185 | }的红茶,睡了过去"
186 | )
187 | )
188 | )
189 | }
190 |
191 | @EventHandler
192 | @Suppress("unused")
193 | suspend fun onMemberUnmute(event: MemberUnmuteEvent) {
194 | if (isDarklistGroup(event)) {
195 | return
196 | }
197 | if (!isModuleEnabled("MuteTip")) {
198 | return
199 | }
200 | event.group.sendMessage(
201 | messageChainOf(
202 | At(event.member), PlainText(" 被先辈叫醒了")
203 | )
204 | )
205 | }
206 |
207 | private fun processFormat(format: String, event: GroupMemberEvent) : String {
208 | return format.replace("\$nick", event.user.nameCardOrNick).replace("\$id", event.user.id.toString())
209 | }
210 |
211 | @EventHandler
212 | @Suppress("unused")
213 | suspend fun onMemberJoin(event: MemberJoinEvent) {
214 | if (isDarklistGroup(event)) {
215 | return
216 | }
217 | if (!isModuleEnabled("Welcome")) {
218 | return
219 | }
220 | event.group.sendMessage(processFormat(config.msgOnJoinFormat, event))
221 | }
222 |
223 | @EventHandler
224 | @Suppress("unused")
225 | suspend fun onMemberLeave(event: MemberLeaveEvent) {
226 | if (isDarklistGroup(event)) {
227 | return
228 | }
229 | if (!isModuleEnabled("Welcome")) {
230 | return
231 | }
232 | event.group.sendMessage(processFormat(config.msgOnLeaveFormat, event))
233 | }
234 |
235 | @EventHandler
236 | @Suppress("unused")
237 | suspend fun onMemberNudge(event: NudgeEvent) {
238 | if (event.subject is Group) {
239 | val group = event.subject as Group
240 | if (isDarklistGroup(group.id) || !isModuleEnabled("PetPet")) {
241 | return
242 | }
243 | val url = event.target.avatarUrl.replace("s=640", "s=100")
244 | generateGifAndSend(url, group, event.target.id)
245 | }
246 |
247 | }
248 |
249 | internal fun isModuleEnabled(name: String): Boolean {
250 | return config.states.getOrDefault(name, true)
251 | }
252 |
253 | internal fun initOrReloadConfig() {
254 | reloadBaseConfig()
255 | initializeImageList()
256 | }
257 |
258 | internal fun reloadBaseConfig() {
259 | reloadPluginConfig(config)
260 | reloadPluginConfig(ImageSourceConfig)
261 | dispatcher.prefix = if (config.commandPrefix.isBlank()) "/" else config.commandPrefix
262 | applyProxySettings()
263 | }
264 |
265 | suspend fun GroupMessageEvent.sendBack(reply: Message): MessageReceipt {
266 | return group.sendMessage(reply)
267 | }
268 |
269 | suspend fun GroupMessageEvent.sendBack(reply: String): MessageReceipt {
270 | return group.sendMessage(reply)
271 | }
272 |
273 | fun GroupMessageEvent.sendAsync(reply: Message) {
274 | pluginLaunch {
275 | group.sendMessage(reply)
276 | }
277 | }
278 |
279 | fun GroupMessageEvent.sendAsync(reply: String) {
280 | pluginLaunch {
281 | group.sendMessage(reply)
282 | }
283 | }
284 |
285 | fun pluginLaunch(action: suspend () -> Unit) {
286 | RosemoePlugin.launch(coroutineContext) {
287 | action()
288 | }
289 | }
290 |
291 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/RosemoePluginConfig.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import net.mamoe.mirai.console.data.AutoSavePluginConfig
22 | import net.mamoe.mirai.console.data.value
23 |
24 | /**
25 | * Config for [RosemoePlugin]
26 | */
27 | object RosemoePluginConfig : AutoSavePluginConfig("PluginConfig") {
28 |
29 | /**
30 | * Top-level users for User Access Control
31 | */
32 | var managers: MutableList by value()
33 |
34 | /**
35 | * Switches for modules and other functions
36 | */
37 | var states: MutableMap by value()
38 |
39 | /**
40 | * Prefix in group for commands outside
41 | */
42 | var commandPrefix by value("/")
43 |
44 | /**
45 | * List of ignored groups ids
46 | */
47 | var darkListGroups: MutableList by value()
48 |
49 | /**
50 | * Delay for image recalling
51 | */
52 | var imageRecallDelay by value(60000L)
53 |
54 | /**
55 | * Min interval for recalling a message
56 | */
57 | var recallMinPeriod by value(180L)
58 |
59 | /**
60 | * Max Image count for batch image sender (In one request)
61 | */
62 | var maxImageRequestCount by value(16)
63 |
64 | var proxyEnabled by value(false)
65 |
66 | var proxyType by value("http")
67 |
68 | var proxyAddress by value("127.0.0.1")
69 |
70 | var proxyPort by value(1080)
71 |
72 | var allowR18ImageInPixiv by value(false)
73 |
74 | var repeatFactor by value(0.05)
75 |
76 | var msgOnJoinFormat by value("欢迎\$nick加入本群~")
77 |
78 | var msgOnLeaveFormat by value("\$nick (\$id) 离开了我们...")
79 |
80 | var repeatDetectGroups: MutableList by value()
81 |
82 | var messageRecordSize by value(12)
83 |
84 | var messageDuplicateLimit by value(6)
85 |
86 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/Version.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | const val pluginVersion = "2.4.2"
22 | const val pluginBuildTime = "2021/7/3 13:50"
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/command/Command.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.command
20 |
21 | /**
22 | * Implements actions of a command
23 | */
24 | abstract class Command(val description: CommandDescription)
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/command/CommandDispatcher.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.command
20 |
21 | import io.github.rosemoe.miraiPlugin.RosemoePlugin
22 | import kotlinx.coroutines.CoroutineScope
23 | import kotlinx.coroutines.launch
24 | import net.mamoe.mirai.event.events.*
25 | import net.mamoe.mirai.message.data.Message
26 | import net.mamoe.mirai.message.data.MessageChain
27 | import net.mamoe.mirai.message.data.MessageSource
28 | import java.util.HashMap
29 | import kotlin.coroutines.CoroutineContext
30 | import kotlin.reflect.KClass
31 | import kotlin.reflect.KFunction
32 | import kotlin.reflect.KVisibility
33 | import kotlin.reflect.full.callSuspend
34 | import kotlin.reflect.full.memberFunctions
35 | import kotlin.reflect.jvm.jvmErasure
36 |
37 | class CommandDispatcher constructor(
38 | private val permissionChecker: CommandPermissionChecker,
39 | override val coroutineContext: CoroutineContext
40 | ) : CoroutineScope {
41 |
42 | var prefix = "/"
43 |
44 | private val commands: MutableList = mutableListOf()
45 | private val commandTree: MutableMap = mutableMapOf()
46 | private val commandFallback = mutableMapOf>()
47 |
48 | fun register(vararg commands: Command) {
49 | commands.forEach {
50 | register(command = it)
51 | }
52 | }
53 |
54 | fun register(command: Command) {
55 | if (!commands.contains(command)) {
56 | commands.add(command)
57 | }
58 | val node = Node()
59 | //println("Reg: $command")
60 | command::class.memberFunctions.forEach { func ->
61 | func.targetAnnotation()?.let {
62 | if ((func.visibility == null || func.visibility == KVisibility.PUBLIC) && func.parameters.size == 2 && func.parameterClassAt(
63 | 1
64 | ).qualifiedName == "io.github.rosemoe.miraiPlugin.command.MsgEvent"
65 | ) {
66 | //println("Register method")
67 | if (it.path == "") {
68 | commandFallback[command] = func
69 | } else {
70 | register(node, it.path, func)
71 | }
72 | } else {
73 | //println("Func: ${func} ${func.parameters.size} ${if(func.parameters.size > 1) func.parameterClassAt(1) else null}")
74 | }
75 | }
76 | }
77 | commandTree[command] = node
78 | }
79 |
80 | fun dispatch(event: MessageEvent) {
81 | var text = StringBuilder().also {
82 | toString(event.message, it)
83 | }.toString()
84 | if (text.startsWith(prefix)) {
85 | text = text.substring(prefix.length)
86 | var regionStart = getNextWordStart(0, text)
87 | var regionEnd = getWordEnd(regionStart, text)
88 | var state = 0
89 | var node: Node? = null
90 | val optionStates = OptionStates()
91 | var fallback: KFunction<*>? = null
92 | lateinit var pendingOption: String
93 | lateinit var options: Array
94 | lateinit var command: Command
95 | while (regionStart < regionEnd) {
96 | val name = text.substring(regionStart, regionEnd)
97 | when (state) {
98 | 0 -> {
99 | //println("State0: ${name}, ${node}")
100 | // State 0: Find matching command
101 | commands.forEach {
102 | // Check name and permissions
103 | if (it.description.name.contains(name) && permissionCheck(
104 | event,
105 | it.description.permission,
106 | it
107 | )
108 | ) {
109 | node = commandTree[it]!!
110 | options = it.description.options
111 | fallback = commandFallback[it]
112 | command = it
113 | //println("Match: ${it}, ${fallback}")
114 | return@forEach
115 | }
116 | }
117 | if (node == null) {
118 | // No matching command
119 | return
120 | }
121 | state = 1
122 | }
123 | 1 -> {
124 | // State 1: Collect options
125 | if (name.startsWith("-")) {
126 | val optionName = name.substring(1)
127 | var notFound = true
128 | options.forEach {
129 | if (it.name == optionName) {
130 | // Option matches
131 | if (it.hasArgument) {
132 | // Switch to get option argument
133 | pendingOption = it.name
134 | state = 3
135 | } else {
136 | // Just set flag
137 | optionStates.optionStates[optionName] = ""
138 | }
139 | notFound = false
140 | return@forEach
141 | }
142 | }
143 | // No option matches this name, switch to command types
144 | if (notFound) {
145 | state = 2
146 | continue
147 | }
148 | } else {
149 | // Not a valid option statement, switch to command types
150 | state = 2
151 | continue
152 | }
153 | }
154 | 2 -> {
155 | // State 2: Start search branches
156 | //println("State2: ${name}, ${node} ${node?.action}")
157 | val next = node!!.children[name]
158 | //println("Next: ${next}")
159 | if (next == null) {
160 | // Terminal node
161 | launch(coroutineContext) {
162 | //println("Call1: ${if (node!!.action == null) fallback else node!!.action}")
163 | (if (node!!.action == null) fallback else node!!.action)?.callSuspend(
164 | command,
165 | MsgEvent(
166 | event,
167 | text.substring(regionStart),
168 | optionStates
169 | )
170 | )
171 | }
172 | break
173 | } else {
174 | node = next
175 | }
176 | }
177 | 3 -> {
178 | // State 3: Seek for option argument
179 | optionStates.optionStates[pendingOption] = name
180 | state = 1
181 | }
182 | }
183 | regionStart = getNextWordStart(regionEnd, text)
184 | regionEnd = getWordEnd(regionStart, text)
185 | }
186 | if ((state == 2 || state == 1) && regionStart >= regionEnd) {
187 | launch(coroutineContext) {
188 | //println("Call2: ${if (node!!.action == null) fallback else node!!.action}")
189 | (if (node!!.action == null) fallback else node!!.action)?.callSuspend(
190 | command,
191 | MsgEvent(
192 | event,
193 | "",
194 | optionStates
195 | )
196 | )
197 | }
198 | }
199 | }
200 | }
201 |
202 | private fun permissionCheck(event: MessageEvent, checker: Permission, command: Command): Boolean {
203 | return checker.isGranted(
204 | when (event) {
205 | is GroupMessageEvent -> SessionType.GROUP
206 | is GroupTempMessageEvent -> SessionType.TEMP
207 | is FriendMessageEvent -> SessionType.FRIEND
208 | else -> SessionType.UNKNOWN
209 | },
210 | if (event is GroupMessageEvent) event.sender.permission else null,
211 | RosemoePlugin.config.managers.contains(event.sender.id)
212 | ) &&
213 | permissionChecker.shouldRunCommand(
214 | event.sender,
215 | command,
216 | if (event is GroupMessageEvent) event.group.id else 0
217 | )
218 |
219 | }
220 |
221 | private fun dfsRegister(
222 | depth: Int,
223 | aliasListMap: Array>,
224 | node: Node,
225 | action: KFunction<*>
226 | ) {
227 | for (element in aliasListMap[depth]) {
228 | var next = node.children[element]
229 | if (next == null) {
230 | next = Node()
231 | node.children[element] = next
232 | }
233 | if (depth + 1 == aliasListMap.size) {
234 | next.action = action
235 | } else {
236 | dfsRegister(depth + 1, aliasListMap, next, action)
237 | }
238 | }
239 | }
240 |
241 | private fun register(root: Node, path: String, action: KFunction<*>) {
242 | val subs: Array = path.split("/").toTypedArray()
243 | val aliasForEach = Array(subs.size) { index ->
244 | subs[index].split(",").toTypedArray()
245 | }
246 | dfsRegister(0, aliasForEach, root, action)
247 | }
248 |
249 | private fun KFunction.parameterClassAt(index: Int): KClass<*> {
250 | return parameters[index].type.jvmErasure
251 | }
252 |
253 | private fun KFunction.targetAnnotation(): CommandTriggerPath? {
254 | annotations.forEach {
255 | if (it is CommandTriggerPath) {
256 | return it
257 | }
258 | }
259 | return null
260 | }
261 |
262 | private class Node {
263 | var children: MutableMap = HashMap()
264 | var action: KFunction<*>? = null
265 | }
266 |
267 | private fun getNextWordStart(i: Int, msg: String): Int {
268 | var idx = i
269 | while (idx < msg.length && Character.isWhitespace(msg[idx])) {
270 | idx++
271 | }
272 | return idx
273 | }
274 |
275 | private fun getWordEnd(i: Int, msg: String): Int {
276 | var idx = i
277 | while (idx < msg.length && !Character.isWhitespace(msg[idx])) {
278 | idx++
279 | }
280 | return idx
281 | }
282 |
283 | private fun toString(msg: Message, sb: StringBuilder) {
284 | if (msg is MessageSource) {
285 | return
286 | }
287 | if (msg is MessageChain) {
288 | for (subMsg in msg) {
289 | toString(subMsg, sb)
290 | }
291 | } else {
292 | sb.append(msg.toString())
293 | }
294 | }
295 |
296 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/command/CommandPermissionChecker.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.command
20 |
21 | import net.mamoe.mirai.contact.User
22 |
23 | interface CommandPermissionChecker {
24 |
25 | fun shouldRunCommand(user: User, command: Command, group: Long) : Boolean
26 |
27 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/command/CommandTriggerPath.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.command
20 |
21 | /**
22 | * Specifies this method handles which type of argument sequence
23 | */
24 | annotation class CommandTriggerPath(val path: String)
25 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/command/MsgEvent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.command
20 |
21 | import io.github.rosemoe.miraiPlugin.RosemoePlugin
22 | import net.mamoe.mirai.Bot
23 | import net.mamoe.mirai.contact.Contact
24 | import net.mamoe.mirai.contact.Group
25 | import net.mamoe.mirai.contact.User
26 | import net.mamoe.mirai.event.events.FriendMessageEvent
27 | import net.mamoe.mirai.event.events.GroupMessageEvent
28 | import net.mamoe.mirai.event.events.GroupTempMessageEvent
29 | import net.mamoe.mirai.event.events.MessageEvent
30 | import net.mamoe.mirai.message.MessageReceipt
31 | import net.mamoe.mirai.message.data.*
32 | import net.mamoe.mirai.utils.ExternalResource
33 | import java.lang.IllegalArgumentException
34 |
35 | class MsgEvent constructor(val event: MessageEvent, val restContent: String = "", val optionStates: OptionStates = OptionStates()) {
36 |
37 | val sender: User
38 | get() = event.sender
39 |
40 | val source: MessageSource
41 | get() = event.source
42 |
43 | val message: MessageChain
44 | get() = event.message
45 |
46 | val bot: Bot
47 | get() = event.bot
48 |
49 | val subject: Contact
50 | get() {
51 | if (event is GroupMessageEvent) {
52 | return event.group
53 | } else {
54 | return sender
55 | }
56 | }
57 |
58 | fun group() : Group {
59 | if (event is GroupMessageEvent) {
60 | return event.group
61 | } else {
62 | throw IllegalArgumentException("非群聊环境无法获取群号")
63 | }
64 | }
65 |
66 | fun groupOrNull() : Group? {
67 | if (event is GroupMessageEvent) {
68 | return event.group
69 | } else {
70 | return null
71 | }
72 | }
73 |
74 | fun groupId() : Long {
75 | if (event is GroupMessageEvent) {
76 | return event.group.id
77 | } else {
78 | throw IllegalArgumentException("非群聊环境无法获取群号")
79 | }
80 | }
81 |
82 | suspend fun uploadImage(res: ExternalResource) : Image {
83 | return subject.uploadImage(res)
84 | }
85 |
86 | suspend fun send(reply: Message): MessageReceipt<*> {
87 | return when(event) {
88 | is GroupMessageEvent -> event.group.sendMessage(reply)
89 | is FriendMessageEvent -> event.friend.sendMessage(reply)
90 | is GroupTempMessageEvent -> event.group.sendMessage(reply)
91 | else -> throw IllegalArgumentException("Failed to send back for event object: $event")
92 | }
93 | }
94 |
95 | suspend fun send(reply: String): MessageReceipt<*> = send(PlainText(reply))
96 |
97 | fun sendAsync(reply: Message) {
98 | RosemoePlugin.pluginLaunch {
99 | send(reply)
100 | }
101 | }
102 |
103 | fun sendAsync(reply: String) {
104 | RosemoePlugin.pluginLaunch {
105 | send(reply)
106 | }
107 | }
108 |
109 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/command/OptionStates.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.command
20 |
21 | class OptionStates {
22 |
23 | val optionStates: MutableMap = mutableMapOf()
24 |
25 | fun isOptionSet(name: String) : Boolean {
26 | return optionStates[name] != null
27 | }
28 |
29 | fun getOption(name: String) : String? {
30 | return optionStates[name]
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/command/description.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.command
20 |
21 | import net.mamoe.mirai.contact.MemberPermission
22 |
23 | /**
24 | * Describes a command
25 | * @param name The names of command
26 | * @param options The options of command
27 | */
28 | class CommandDescription constructor(val name: Array, val permission: Permission, val options: Array
= arrayOf())
29 |
30 | /**
31 | * Describes an option of command
32 | * @param name The unique name of the option, such as -XX:useConcurrentGC
33 | * @param hasArgument Whether there is an argument for the option
34 | */
35 | class Option constructor(val name: String, val hasArgument: Boolean)
36 |
37 | /**
38 | * Describes the permission of a command
39 | */
40 | interface Permission {
41 | /**
42 | * @param permission this will be given only in group
43 | */
44 | fun isGranted(source: SessionType, permission: MemberPermission?, isPluginManager: Boolean): Boolean
45 |
46 | operator fun plus(another: Permission) : Permission {
47 | val list = mutableListOf()
48 | if (this is CompoundPermission) {
49 | list.addAll(this.subPermissions)
50 | } else {
51 | list.add(this)
52 | }
53 | if (another is CompoundPermission) {
54 | list.addAll(another.subPermissions)
55 | } else {
56 | list.add(another)
57 | }
58 | return CompoundPermission(list)
59 | }
60 | }
61 |
62 | class Permissions constructor(
63 | private val group: Boolean,
64 | private val permission: MemberPermission,
65 | private val pluginManager: Boolean = false,
66 | private val friend: Boolean = false,
67 | private val temp: Boolean = false,
68 | ) : Permission {
69 | override fun isGranted(source: SessionType, permission: MemberPermission?, isPluginManager: Boolean): Boolean {
70 | // Deny for plugin permission issue
71 | if (pluginManager && !isPluginManager) {
72 | return false
73 | }
74 | // Check type and group permission
75 | return when (source) {
76 | SessionType.GROUP ->
77 | group && (permission!!.level >= this.permission.level)
78 | SessionType.FRIEND ->
79 | friend
80 | SessionType.TEMP ->
81 | temp
82 | SessionType.UNKNOWN->
83 | false
84 | }
85 | }
86 |
87 | fun withManager() : Permissions {
88 | return Permissions(group, permission, true, friend, temp)
89 | }
90 |
91 | companion object {
92 | val GROUP = Permissions(true, MemberPermission.MEMBER)
93 | val GROUP_ADMIN = Permissions(true, MemberPermission.ADMINISTRATOR)
94 | val GROUP_OWNER = Permissions(true, MemberPermission.OWNER)
95 | val FRIEND = Permissions(false, MemberPermission.MEMBER, false,true)
96 | val TEMP = Permissions(false, MemberPermission.MEMBER, false, false, true)
97 | }
98 | }
99 |
100 | class CompoundPermission constructor(val subPermissions: List) : Permission {
101 |
102 | override fun isGranted(source: SessionType, permission: MemberPermission?, isPluginManager: Boolean): Boolean {
103 | subPermissions.forEach {
104 | if (it.isGranted(source, permission, isPluginManager)) {
105 | return true
106 | }
107 | }
108 | return false
109 | }
110 |
111 | }
112 |
113 | enum class SessionType {
114 | GROUP,
115 | FRIEND,
116 | TEMP,
117 | UNKNOWN
118 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/commands/Blocklist.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.commands
20 |
21 | import io.github.rosemoe.miraiPlugin.*
22 | import io.github.rosemoe.miraiPlugin.command.*
23 | import kotlin.math.ceil
24 |
25 | @Suppress("unused")
26 | object Blocklist: Command(
27 | CommandDescription(
28 | arrayOf("blocklist"), Permissions.GROUP.withManager() + Permissions.FRIEND.withManager() + Permissions.TEMP.withManager())
29 | ) {
30 |
31 | @CommandTriggerPath("add")
32 | fun addToDarkList(event: MsgEvent) {
33 | val restContent = event.restContent
34 | val target = restContent.trim()
35 | blocklistLock.lockWrite()
36 | try {
37 | val groupId = if (target.contentEquals("this")) event.groupId() else try {
38 | target.toLong()
39 | } catch (e: NumberFormatException) {
40 | event.sendAsync("错误的参数:${restContent}")
41 | -1
42 | }
43 | if (groupId != -1L) {
44 | if (RosemoePlugin.config.darkListGroups.contains(groupId)) {
45 | event.sendAsync("该群已经在黑名单了")
46 | } else {
47 | RosemoePlugin.config.darkListGroups.add(groupId)
48 | event.sendAsync("添加 $groupId 到黑名单成功")
49 | }
50 | }
51 | } finally {
52 | blocklistLock.unlockWrite()
53 | }
54 | }
55 |
56 | @CommandTriggerPath("remove")
57 | fun removeDarkListGroup(event: MsgEvent) {
58 | val restContent = event.restContent
59 | val target = restContent.trim()
60 | blocklistLock.lockWrite()
61 | try {
62 | val groupId = if (target.contentEquals("this")) event.groupId() else try {
63 | target.toLong()
64 | } catch (e: NumberFormatException) {
65 | event.sendAsync("错误的参数:${restContent}")
66 | -1
67 | }
68 | if (groupId != -1L) {
69 | if (!RosemoePlugin.config.darkListGroups.contains(groupId)) {
70 | event.sendAsync("该群不在黑名单")
71 | } else {
72 | RosemoePlugin.config.darkListGroups.removeAt(RosemoePlugin.config.darkListGroups.indexOf(groupId))
73 | event.sendAsync("移除 $groupId 的黑名单成功")
74 | }
75 | }
76 | } finally {
77 | blocklistLock.unlockWrite()
78 | }
79 | }
80 |
81 | @CommandTriggerPath("list")
82 | fun listDarklistGroups(event: MsgEvent) {
83 | val restContent = event.restContent
84 | blocklistLock.lockRead()
85 | try {
86 | val groups = RosemoePlugin.config.darkListGroups
87 | val pageCount = ceil(groups.size / ITEM_COUNT_EACH_PAGE.toDouble()).toInt()
88 | val targetPage = if (restContent.isBlank()) 1 else {
89 | try {
90 | restContent.trim().toInt()
91 | } catch (e: NumberFormatException) {
92 | 1
93 | }
94 | }
95 | if (targetPage < 1 || targetPage > pageCount) {
96 | event.sendAsync("页码错误:最大页数是 $pageCount, 最小页数是 1")
97 | return
98 | }
99 | val msg = StringBuilder("黑名单(当前页$targetPage/共${pageCount + 1}页)")
100 | for (index in ((targetPage - 1) * ITEM_COUNT_EACH_PAGE) until Integer.min(
101 | groups.size,
102 | targetPage * ITEM_COUNT_EACH_PAGE - 1
103 | )) {
104 | msg.append("\n").append(index + 1).append(":")
105 | val group = event.bot.getGroup(groups[index])
106 | if (group == null) {
107 | msg.append(groups[index]).append(" (查询名称失败)")
108 | } else {
109 | msg.append(group.name).append(" (").append(groups[index]).append(")")
110 | }
111 | }
112 | event.sendAsync(msg.toString())
113 | } finally {
114 | blocklistLock.unlockRead()
115 | }
116 | }
117 |
118 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/commands/Help.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.commands
20 |
21 | import io.github.rosemoe.miraiPlugin.RosemoePlugin
22 | import io.github.rosemoe.miraiPlugin.command.*
23 | import io.github.rosemoe.miraiPlugin.pluginBuildTime
24 | import io.github.rosemoe.miraiPlugin.pluginVersion
25 |
26 | @Suppress("unused")
27 | object Help : Command(
28 | CommandDescription(
29 | arrayOf("help"),
30 | Permissions.FRIEND + Permissions.GROUP
31 | )
32 | ) {
33 |
34 | private val items: MutableMap = mutableMapOf()
35 |
36 | init {
37 | init()
38 | }
39 |
40 | private fun registerHelpItem(path: String, content: String) {
41 | items[path] = content
42 | }
43 |
44 | @CommandTriggerPath("")
45 | fun seekHelp(event: MsgEvent) {
46 | if (!RosemoePlugin.isModuleEnabled("Help")) {
47 | return
48 | }
49 | val content = items[event.restContent.trim().replace(Regex("[\b\t\n]+"), "/")]
50 | if (content == null) {
51 | event.sendAsync("找不到相关帮助(((")
52 | } else {
53 | event.sendAsync(content)
54 | }
55 | }
56 |
57 | fun init() {
58 | registerHelpItem(
59 | "", """
60 | BotPlugin - 帮助
61 | 指令表:
62 | settings - 设置
63 | sendImage - 请求多张图片
64 | pixiv - Pixiv相关功能
65 | blacklist - 控制群聊黑名单
66 | 发送' help <下级指令1> <下级指令2> ... ' 可以获取它们的用法
67 | 比如:
68 | help settings set
69 | 关于:
70 | BotPlugin(https://github.com/Rosemoe/BotPlugin)(版本 $pluginVersion 编译时间 $pluginBuildTime),由 Rosemoe 创建
71 | 运行于Mirai Console(https://github.com/mamoe/mirai-console)
72 | """.trimIndent()
73 | )
74 |
75 | registerHelpItem(
76 | "settings", """
77 | 帮助:settings:调整或获取机器人的设置
78 | 该指令下有如下子项目:
79 | - 含子命令的子命令:
80 | enable
81 | disable
82 | set
83 | get
84 | - 直接指令:
85 | reload - 重载插件配置
86 | reloadBase - 重载插件配置,但是不刷新图片索引
87 | """.trimIndent()
88 | )
89 |
90 | registerHelpItem(
91 | "settings/enable", """
92 | 帮助:settings enable:启用指定的功能
93 | 用法:settings enable <功能名称>
94 | 可用的功能名称:
95 | AtReply - 被at时回復
96 | BatchImg - 请求多张图片,必须开启ImageSender
97 | ImageSender - 发送本地图库图片
98 | MuteTip - 禁言/解禁提示
99 | Pixiv - 获取Pixiv相关信息
100 | ReverseAtReply - 翻转at回復内容
101 | ReverseAtReplyImage - 随机旋转at回復的图片
102 | Welcome - 入群退群提示
103 | """.trimIndent()
104 | )
105 |
106 | registerHelpItem(
107 | "settings/disable", """
108 | 帮助:settings disable:禁用指定的功能
109 | 用法:settings disable <功能名称>
110 | 可用的功能名称:
111 | AtReply - 被at时回復
112 | BatchImg - 请求多张图片,必须开启ImageSender
113 | ImageSender - 发送本地图库图片
114 | IpList - 获取网站IP表
115 | MuteTip - 禁言/解禁提示
116 | Ping - 运行系统Ping指令
117 | Pixiv - 获取Pixiv相关信息
118 | ReverseAtReply - 翻转at回復内容
119 | ReverseAtReplyImage - 随机旋转at回復的图片
120 | Welcome - 入群退群提示
121 | """.trimIndent()
122 | )
123 |
124 | registerHelpItem(
125 | "settings/set", """
126 | 帮助:settings set:设置一些配置
127 | 用法:settings set <配置名称> <配置内容>
128 | 配置说明:
129 | Prefix - 指令前缀,默认为'/'
130 | RecallDelay - 发图之后撤回的间隔,0为不撤回,default为默认,单位为毫秒
131 | RecallInterval - 撤回消息的最小操作间隔时间,0或default都为默认值,单位为毫秒.本项请勿随意修改
132 | ImagePathList - 图片路径表,可以有多个路径,用';'分割
133 | """.trimIndent()
134 | )
135 |
136 | registerHelpItem(
137 | "settings/get", """
138 | 帮助:settings get:获取配置内容
139 | 用法:settings get <配置名称>
140 | 配置说明:
141 | Prefix - 指令前缀
142 | RecallDelay - 发图之后撤回的间隔,单位为毫秒
143 | RecallInterval - 撤回消息的最小操作间隔时间,单位为毫秒
144 | ImagePathList - 图片路径表,可以有多个路径,用';'分割
145 | """.trimIndent()
146 | )
147 |
148 | registerHelpItem(
149 | "settings/reload", """
150 | 帮助:settings reload
151 | 重载配置,所有配置都会被刷新
152 | """.trimIndent()
153 | )
154 |
155 | registerHelpItem(
156 | "settings/reloadBase", """
157 | 帮助:settings reload
158 | 重载配置
159 | 但是不刷新图片索引
160 | 对图片路径的修改不会生效,其它设置会被即时更新
161 | """.trimIndent()
162 | )
163 |
164 | registerHelpItem(
165 | "ping", """
166 | 帮助:ping
167 | 用法:ping <网址>
168 | 说明: 运行操作系统的ping指令. 一些风险指令已经被屏蔽.
169 | 直接发送ping可以查看系统ping的帮助
170 | """.trimIndent()
171 | )
172 |
173 | registerHelpItem(
174 | "sendImage", """
175 | 帮助:sendImage
176 | 用法:sendImage <图片数目>
177 | 说明:发送指定数量的本地图片
178 | """.trimIndent()
179 | )
180 |
181 | registerHelpItem(
182 | "pixiv", """
183 | 帮助:pixiv
184 | - 子命令
185 | illust - 插图相关
186 | """.trimIndent()
187 | )
188 |
189 | registerHelpItem(
190 | "pixiv/illust", """
191 | 帮助:pixiv illust
192 | [1]pixiv illust <画作ID>
193 | 获取画作概览信息,和预览图片(小图)
194 | [2]pixiv illust <画作ID> <插图索引>
195 | 发送指定的画作中的指定图片的大图
196 | """.trimIndent()
197 | )
198 |
199 | registerHelpItem(
200 | "blacklist", """
201 | 帮助:darklist
202 | - add <群号或this>
203 | 添加黑名单
204 | - remove <群号或this>
205 | 移除黑名单
206 | - list [页码]
207 | 查看黑名单列表
208 | 页码可不填
209 | """.trimIndent()
210 | )
211 | }
212 |
213 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/commands/Pixiv.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.commands
20 |
21 | import io.github.rosemoe.miraiPlugin.RosemoePlugin
22 | import io.github.rosemoe.miraiPlugin.command.*
23 | import io.github.rosemoe.miraiPlugin.getArtworkImage
24 | import io.github.rosemoe.miraiPlugin.getArtworkInformation
25 | import io.github.rosemoe.miraiPlugin.toLong
26 |
27 | object Pixiv : Command(
28 | CommandDescription(
29 | arrayOf("pixiv", "p"),
30 | Permissions.FRIEND + Permissions.GROUP
31 | )
32 | ) {
33 |
34 | @CommandTriggerPath("illust,i")
35 | suspend fun fetchIllustration(event: MsgEvent) {
36 | if (!RosemoePlugin.isModuleEnabled("Pixiv")) {
37 | return
38 | }
39 | val args = event.restContent.trim().split(Regex("[ \\n\\t]+")).toMutableList()
40 | for ((index, arg) in args.withIndex()) {
41 | args[index] = arg.trim()
42 | }
43 | RosemoePlugin.pluginLaunch {
44 | when (args.size) {
45 | 1 -> {
46 | val artworkId = args[0].toLong(-1)
47 | if (artworkId != -1L) {
48 | try {
49 | val s = getArtworkInformation(event.subject, artworkId)
50 | event.send(s)
51 | } catch (e: Throwable) {
52 | event.send("Pixiv画作信息(ID = $artworkId):\n获取失败.可能是画作不存在或网路问题")
53 | RosemoePlugin.logger.warning("Pixiv failure", e)
54 | }
55 | } else {
56 | event.send("数字格式错误:${args[0]}")
57 | }
58 | }
59 | 2 -> {
60 | val artworkId = args[0].toLong(-1)
61 | val index = args[1].toLong(-1).toInt()
62 | if (artworkId != -1L && index != -1) {
63 | try {
64 | val s = getArtworkImage(event.subject, artworkId, index)
65 | event.send(s)
66 | } catch (e: Throwable) {
67 | event.send("Pixiv图片(ID = $artworkId, Index = $index):\n获取失败.可能是图片不存在或网路问题")
68 | RosemoePlugin.logger.warning("Pixiv failure", e)
69 | }
70 | } else {
71 | event.send("数字格式错误:${args[0]}")
72 | }
73 | }
74 | else -> {
75 | event.send("参数必须是1个或者2个")
76 | }
77 | }
78 | }
79 | }
80 |
81 |
82 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/commands/Settings.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.commands
20 |
21 | import io.github.rosemoe.miraiPlugin.*
22 | import io.github.rosemoe.miraiPlugin.command.*
23 | import io.github.rosemoe.miraiPlugin.modules.onDisableRepeatBreakingForGroup
24 | import io.github.rosemoe.miraiPlugin.utils.ScriptMethods
25 | import org.mozilla.javascript.Context
26 | import org.mozilla.javascript.ScriptableObject
27 | import java.util.concurrent.locks.ReentrantReadWriteLock
28 |
29 | val allowedModuleName = listOf(
30 | "ImageSender",
31 | "Pixiv",
32 | "BatchImg",
33 | "AtReply",
34 | "Welcome",
35 | "MuteTip",
36 | "ReverseAtReply",
37 | "ReverseAtReplyImage",
38 | "PetPet",
39 | "Repeat",
40 | "Help"
41 | )
42 |
43 | val blocklistLock = ReentrantReadWriteLock(true)
44 |
45 | const val ITEM_COUNT_EACH_PAGE = 15
46 |
47 | @Suppress("unused")
48 | object Settings : Command(
49 | CommandDescription(
50 | arrayOf("settings"),
51 | Permissions.GROUP.withManager() + Permissions.FRIEND.withManager() + Permissions.TEMP.withManager()
52 | )
53 | ) {
54 |
55 | @CommandTriggerPath("disable")
56 | fun disableModule(event: MsgEvent) {
57 | val rest = event.restContent
58 | event.sendAsync(if (RosemoePlugin.disableModule(rest.trim())) "成功禁用了:${rest.trim()}" else "禁用${rest.trim()}失败了")
59 | }
60 |
61 | @CommandTriggerPath("enable")
62 | fun enableModule(event: MsgEvent) {
63 | val rest = event.restContent
64 | event.sendAsync(if (RosemoePlugin.enableModule(rest.trim())) "成功启用了:${rest.trim()}" else "启用${rest.trim()}失败了")
65 | }
66 |
67 | @CommandTriggerPath("reload")
68 | fun reloadConfig(event: MsgEvent) {
69 | try {
70 | RosemoePlugin.initOrReloadConfig()
71 | event.sendAsync("重载成功!")
72 | } catch (e: Exception) {
73 | RosemoePlugin.logger.error("重载配置失败了!!!", e)
74 | event.sendAsync("重载失败:${getExceptionInfo(e)}")
75 | }
76 | }
77 |
78 | @CommandTriggerPath("reloadq")
79 | fun reloadBaseConfig(event: MsgEvent) {
80 | try {
81 | RosemoePlugin.reloadBaseConfig()
82 | event.sendAsync("轻重载成功!")
83 | } catch (e: Exception) {
84 | RosemoePlugin.logger.error("基础重载失败了!!!", e)
85 | event.sendAsync("轻重载失败:\n${getExceptionInfo(e)}")
86 | }
87 | }
88 |
89 | @CommandTriggerPath("set/prefix")
90 | fun setCommandPrefix(event: MsgEvent) {
91 | RosemoePlugin.config.commandPrefix = event.restContent.trim()
92 | RosemoePlugin.dispatcher.prefix = RosemoePlugin.config.commandPrefix
93 | event.sendAsync("当前的前缀是:${RosemoePlugin.dispatcher.prefix}")
94 | }
95 |
96 | @CommandTriggerPath("get/prefix")
97 | fun getCommandPrefix(event: MsgEvent) {
98 | event.sendAsync("当前的前缀是:${RosemoePlugin.dispatcher.prefix}")
99 | }
100 |
101 | @CommandTriggerPath("set/recallDelay")
102 | fun setRecallDelay(event: MsgEvent) {
103 | val time = event.restContent.toLong(60000)
104 | RosemoePlugin.config.imageRecallDelay = time
105 | event.sendAsync("recallDelay的值设置为$time")
106 | }
107 |
108 | @CommandTriggerPath("get/recallDelay")
109 | fun getRecallDelay(event: MsgEvent) {
110 | event.sendAsync("recallDelay的值为${RosemoePlugin.config.imageRecallDelay}")
111 | }
112 |
113 | @CommandTriggerPath("set/recallInterval")
114 | fun setRecallInterval(event: MsgEvent) {
115 | val time = event.restContent.toLong(60000)
116 | RosemoePlugin.config.recallMinPeriod = time
117 | event.sendAsync("recallInterval的值设置为$time")
118 | }
119 |
120 | @CommandTriggerPath("get/recallInterval")
121 | fun getRecallInterval(event: MsgEvent) {
122 | event.sendAsync("recallInterval的值为${RosemoePlugin.config.recallMinPeriod}")
123 | }
124 |
125 | @CommandTriggerPath("set/repeatFactor")
126 | fun setRepeatFactor(event: MsgEvent) {
127 | RosemoePlugin.config.repeatFactor = event.restContent.toDouble(0.01)
128 | event.sendAsync("repeatFactor的值设置为${RosemoePlugin.config.repeatFactor}")
129 | }
130 |
131 | @CommandTriggerPath("get/repeatFactor")
132 | fun getRepeatFactor(event: MsgEvent) {
133 | event.sendAsync("repeatFactor的值为${RosemoePlugin.config.repeatFactor}")
134 | }
135 |
136 | @CommandTriggerPath("exec")
137 | fun executeScript(event: MsgEvent) {
138 | val code = event.restContent
139 | val context = Context.enter()
140 | try {
141 | val script = context.compileString(code, "", 1, null)
142 | val scope = context.initStandardObjects()
143 | fun setJsObject(name: String, value: Any?) {
144 | val jsObj = Context.javaToJS(value, scope)
145 | ScriptableObject.putProperty(scope, name, jsObj)
146 | }
147 | setJsObject("event", event)
148 | setJsObject("sender", event.sender)
149 | setJsObject("group", event.groupOrNull())
150 | setJsObject("bot", event.bot)
151 | setJsObject("dlg", ScriptMethods(event))
152 | context.evaluateString(scope, "function print(msg) { dlg.send(msg + \"\"); }", "", 1, null)
153 | script.exec(context, scope)
154 | } finally {
155 | Context.exit()
156 | }
157 | }
158 |
159 | @CommandTriggerPath("set/joinMsg")
160 | fun setMsgOnJoin(event: MsgEvent) {
161 | RosemoePlugin.config.msgOnJoinFormat = event.restContent
162 | event.sendAsync("设置成功")
163 | }
164 |
165 | @CommandTriggerPath("get/joinMsg")
166 | fun getMsgOnJoin(event: MsgEvent) {
167 | event.sendAsync("入群提示的模板为:\n${RosemoePlugin.config.msgOnJoinFormat}")
168 | }
169 |
170 | @CommandTriggerPath("set/leaveMsg")
171 | fun setMsgOnLeave(event: MsgEvent) {
172 | RosemoePlugin.config.msgOnLeaveFormat = event.restContent
173 | event.sendAsync("设置成功")
174 | }
175 |
176 | @CommandTriggerPath("get/leaveMsg")
177 | fun getMsgOnLeave(event: MsgEvent) {
178 | event.sendAsync("退群提示的模板为:\n${RosemoePlugin.config.msgOnLeaveFormat}")
179 | }
180 |
181 | @CommandTriggerPath("rb/add")
182 | fun enableRepeatBreakingForGroup(event: MsgEvent) {
183 | val groupId = resolveGroupPresentation(event, event.restContent)
184 | if (groupId == 0L) {
185 | return
186 | }
187 | if (!RosemoePlugin.config.repeatDetectGroups.contains(groupId)) {
188 | RosemoePlugin.config.repeatDetectGroups.add(groupId)
189 | }
190 | event.sendAsync("添加成功")
191 | }
192 |
193 | @CommandTriggerPath("rb/remove")
194 | fun disableRepeatBreakingForGroup(event: MsgEvent) {
195 | val groupId = resolveGroupPresentation(event, event.restContent)
196 | if (groupId == 0L) {
197 | return
198 | }
199 | RosemoePlugin.config.repeatDetectGroups.remove(groupId)
200 | RosemoePlugin.onDisableRepeatBreakingForGroup(groupId)
201 | event.sendAsync("删除成功")
202 | }
203 |
204 | private fun resolveGroupPresentation(event: MsgEvent, text: String): Long {
205 | val groupPresentation = text.trim()
206 | return if (groupPresentation == "this") {
207 | event.groupId()
208 | } else {
209 | try {
210 | groupPresentation.toLong()
211 | } catch (e: NumberFormatException) {
212 | event.sendAsync("群号格式有误")
213 | 0
214 | }
215 | }
216 | }
217 |
218 | fun RosemoePlugin.disableModule(name: String): Boolean {
219 | if (checkModuleName(name)) {
220 | config.states[name] = false
221 | return true
222 | }
223 | return false
224 | }
225 |
226 | fun RosemoePlugin.enableModule(name: String): Boolean {
227 | if (checkModuleName(name)) {
228 | config.states[name] = true
229 | return true
230 | }
231 | return false
232 | }
233 |
234 | fun checkModuleName(name: String): Boolean {
235 | return allowedModuleName.contains(name)
236 | }
237 |
238 | }
239 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/commands/Setu.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.commands
20 |
21 | import io.github.rosemoe.miraiPlugin.*
22 | import io.github.rosemoe.miraiPlugin.command.*
23 | import io.github.rosemoe.miraiPlugin.utils.scheduleRecall
24 | import kotlinx.coroutines.runInterruptible
25 | import net.mamoe.mirai.message.data.At
26 | import net.mamoe.mirai.message.data.PlainText
27 | import net.mamoe.mirai.message.data.messageChainOf
28 | import net.mamoe.mirai.utils.ExternalResource
29 | import java.lang.Exception
30 | import java.util.concurrent.locks.ReentrantReadWriteLock
31 | import kotlin.math.max
32 |
33 | @Suppress("unused")
34 | object Setu : Command(
35 | CommandDescription(
36 | arrayOf("sendImage"),
37 | Permissions.FRIEND + Permissions.GROUP
38 | )
39 | ) {
40 |
41 | @CommandTriggerPath("")
42 | fun sendSetu(event: MsgEvent) {
43 | if (RosemoePlugin.isModuleEnabled("BatchImg") && RosemoePlugin.isModuleEnabled("ImageSender")) {
44 | try {
45 | val count = event.restContent.trim().toInt()
46 | if (count > RosemoePlugin.config.maxImageRequestCount) {
47 | event.sendAsync(
48 | if (event.groupOrNull() == null)
49 | messageChainOf(PlainText("请求数量太多啦! 一次最多请求${max(0, RosemoePlugin.config.maxImageRequestCount)}张图片哦"))
50 | else
51 | messageChainOf(
52 | At(event.sender),
53 | PlainText("请求数量太多啦! 一次最多请求${max(0, RosemoePlugin.config.maxImageRequestCount)}张图片哦")
54 | )
55 | )
56 | } else {
57 | for (i in 1..count) {
58 | RosemoePlugin.sendImageForEvent(event)
59 | }
60 | }
61 | } catch (exception: NumberFormatException) {
62 | event.sendAsync(messageChainOf(At(event.sender), PlainText("数字格式不对,请重试")))
63 | }
64 | }
65 | }
66 |
67 | private val imageStorages = ArrayList()
68 | private val lock = ReentrantReadWriteLock()
69 |
70 | internal fun RosemoePlugin.initializeImageList() {
71 | logger.verbose("正在加载图片源")
72 | lock.lockWrite()
73 | try {
74 | imageStorages.clear()
75 | ImageSourceConfig.sources.forEach {
76 | try {
77 | imageStorages.add(deserializeStorage(it).also {
78 | it.init()
79 | })
80 | } catch (e: Exception) {
81 | logger.error("加载图片源时发生错误", e)
82 | }
83 | }
84 | } finally {
85 | lock.unlockWrite()
86 | }
87 | logger.verbose("图片源加载完毕")
88 | }
89 |
90 | private fun randomImage(): ExternalResource? {
91 | if (imageStorages.isEmpty()) {
92 | return null
93 | }
94 | lock.lockRead()
95 | val imageStorage = try {
96 | imageStorages.random()
97 | } finally {
98 | lock.unlockRead()
99 | }
100 | return imageStorage.obtainImage()
101 | }
102 |
103 | internal fun RosemoePlugin.sendImageForEvent(event: MsgEvent) {
104 | val target = randomImage()
105 | if (target == null) {
106 | event.sendAsync(
107 | if (event.groupOrNull() == null)
108 | messageChainOf(PlainText("没有可用的图片QAQ"))
109 | else
110 | messageChainOf(
111 | At(event.sender),
112 | PlainText("没有可用的图片QAQ")
113 | )
114 | )
115 | } else {
116 | pluginLaunch {
117 | //val startTime = System.currentTimeMillis()
118 | val img = event.uploadImage(target)
119 | //logger.info("Uploaded image in ${System.currentTimeMillis() - startTime} ms")
120 | val receipt = event.send(
121 | if (event.groupOrNull() == null)
122 | messageChainOf(PlainText("这是宁要的图!\n"), img)
123 | else
124 | messageChainOf(
125 | At(event.sender),
126 | PlainText("这是宁要的图!\n"),
127 | img
128 | )
129 | )
130 | runInterruptible {
131 | target.close()
132 | }
133 | val delay = config.imageRecallDelay
134 | if (delay > 0) {
135 | scheduleRecall(receipt, delay)
136 | }
137 | }
138 | }
139 | }
140 |
141 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/commands/Sources.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.commands
20 |
21 | import io.github.rosemoe.miraiPlugin.*
22 | import io.github.rosemoe.miraiPlugin.command.*
23 | import io.github.rosemoe.miraiPlugin.commands.Setu.initializeImageList
24 |
25 | object Sources: Command(
26 | CommandDescription(
27 | arrayOf("sources"), Permissions.GROUP.withManager() + Permissions.FRIEND.withManager() + Permissions.TEMP.withManager())
28 | ) {
29 |
30 | @CommandTriggerPath("json")
31 | fun addJson(event: MsgEvent) {
32 | val array = event.restContent.trim().split(Regex("[\t\b\n\r ]+"))
33 | if (array.size == 3) {
34 | ImageSourceConfig.sources.add(
35 | OnlineJsonImageStorage(array[1], array[2]).also { it.storageName = array[0] }.serializeStorage()
36 | )
37 | event.sendAsync("已添加在线源")
38 | } else {
39 | event.sendAsync("参数个数只能是3个")
40 | }
41 | }
42 |
43 | @CommandTriggerPath("path")
44 | fun addPath(event: MsgEvent) {
45 | val array = event.restContent.trim().split(Regex("[\t\b\n\r ]+"))
46 | if (array.size == 2) {
47 | ImageSourceConfig.sources.add(
48 | LocalImageStorage(array[1]).also { it.storageName = array[0] }.serializeStorage()
49 | )
50 | event.sendAsync("已添加路径")
51 | } else {
52 | RosemoePlugin.logger.warning(array.toString())
53 | event.sendAsync("参数个数只能是2个")
54 | }
55 | }
56 |
57 | @CommandTriggerPath("remove")
58 | fun removeSource(event: MsgEvent) {
59 | ImageSourceConfig.sources.filter {
60 | deserializeStorage(it).storageName == event.restContent
61 | }.forEach {
62 | ImageSourceConfig.sources.remove(it)
63 | }
64 | event.sendAsync("执行成功")
65 | }
66 |
67 | @CommandTriggerPath("refresh")
68 | fun refresh(event: MsgEvent) {
69 | RosemoePlugin.initializeImageList()
70 | event.sendAsync("图片源已刷新")
71 | }
72 |
73 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/gifmaker/Color.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.gifmaker
20 |
21 | object Color {
22 | const val TRANSPARENT = 0
23 |
24 | /**
25 | * Return the red component of a color int. This is the same as saying
26 | * (color >> 16) & 0xFF
27 | */
28 | fun red(color: Int): Int {
29 | return color shr 16 and 0xFF
30 | }
31 |
32 | /**
33 | * Return the green component of a color int. This is the same as saying
34 | * (color >> 8) & 0xFF
35 | */
36 | fun green(color: Int): Int {
37 | return color shr 8 and 0xFF
38 | }
39 |
40 | /**
41 | * Return the blue component of a color int. This is the same as saying
42 | * color & 0xFF
43 | */
44 | fun blue(color: Int): Int {
45 | return color and 0xFF
46 | }
47 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/gifmaker/LZWEncoder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.gifmaker
20 |
21 | import java.io.IOException
22 | import java.io.OutputStream
23 |
24 | class LZWEncoder(private val imgW: Int, private val imgH: Int, private val pixAry: ByteArray, color_depth: Int) {
25 | private val initCodeSize: Int
26 | private var remaining = 0
27 | private var curPixel = 0
28 |
29 | // GIF Image compression - modified 'compress'
30 | //
31 | // Based on: compress.c - File compression ala IEEE Computer, June 1984.
32 | //
33 | // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
34 | // Jim McKie (decvax!mcvax!jim)
35 | // Steve Davies (decvax!vax135!petsd!peora!srd)
36 | // Ken Turkowski (decvax!decwrl!turtlevax!ken)
37 | // James A. Woods (decvax!ihnp4!ames!jaw)
38 | // Joe Orost (decvax!vax135!petsd!joe)
39 | var n_bits // number of bits/code
40 | = 0
41 | var maxbits = BITS // user settable max # bits/code
42 | var maxcode // maximum code, given n_bits
43 | = 0
44 | var maxmaxcode = 1 shl BITS // should NEVER generate this code
45 | var htab = IntArray(HSIZE)
46 | var codetab = IntArray(HSIZE)
47 | var hsize = HSIZE // for dynamic table sizing
48 | var free_ent = 0 // first unused entry
49 |
50 | // block compression parameters -- after all codes are used up,
51 | // and compression rate changes, start over.
52 | var clear_flg = false
53 |
54 | // Algorithm: use open addressing double hashing (no chaining) on the
55 | // prefix code / next character combination. We do a variant of Knuth's
56 | // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
57 | // secondary probe. Here, the modular division first probe is gives way
58 | // to a faster exclusive-or manipulation. Also do block compression with
59 | // an adaptive reset, whereby the code table is cleared when the compression
60 | // ratio decreases, but after the table fills. The variable-length output
61 | // codes are re-sized at this point, and a special CLEAR code is generated
62 | // for the decompressor. Late addition: construct the table according to
63 | // file size for noticeable speed improvement on small files. Please direct
64 | // questions about this implementation to ames!jaw.
65 | var g_init_bits = 0
66 | var ClearCode = 0
67 | var EOFCode = 0
68 |
69 | // output
70 | //
71 | // Output the given code.
72 | // Inputs:
73 | // code: A n_bits-bit integer. If == -1, then EOF. This assumes
74 | // that n_bits =< wordsize - 1.
75 | // Outputs:
76 | // Outputs code to the file.
77 | // Assumptions:
78 | // Chars are 8 bits long.
79 | // Algorithm:
80 | // Maintain a BITS character long buffer (so that 8 codes will
81 | // fit in it exactly). Use the VAX insv instruction to insert each
82 | // code in turn. When the buffer fills up empty it and start over.
83 | var cur_accum = 0
84 | var cur_bits = 0
85 | var masks = intArrayOf(
86 | 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF,
87 | 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
88 | )
89 |
90 | // Number of characters so far in this 'packet'
91 | var a_count = 0
92 |
93 | // Define the storage for the packet accumulator
94 | var accum = ByteArray(256)
95 |
96 | // Add a character to the end of the current packet, and if it is 254
97 | // characters, flush the packet to disk.
98 | @Throws(IOException::class)
99 | fun char_out(c: Byte, outs: OutputStream) {
100 | accum[a_count++] = c
101 | if (a_count >= 254) flush_char(outs)
102 | }
103 |
104 | // Clear out the hash table
105 | // table clear for block compress
106 | @Throws(IOException::class)
107 | fun cl_block(outs: OutputStream) {
108 | cl_hash(hsize)
109 | free_ent = ClearCode + 2
110 | clear_flg = true
111 | output(ClearCode, outs)
112 | }
113 |
114 | // reset code table
115 | fun cl_hash(hsize: Int) {
116 | for (i in 0 until hsize) htab[i] = -1
117 | }
118 |
119 | @Throws(IOException::class)
120 | fun compress(init_bits: Int, outs: OutputStream) {
121 | var fcode: Int
122 | var i /* = 0 */: Int
123 | var c: Int
124 | var ent: Int
125 | var disp: Int
126 | val hsize_reg: Int
127 | var hshift: Int
128 |
129 | // Set up the globals: g_init_bits - initial number of bits
130 | g_init_bits = init_bits
131 |
132 | // Set up the necessary values
133 | clear_flg = false
134 | n_bits = g_init_bits
135 | maxcode = MAXCODE(n_bits)
136 | ClearCode = 1 shl init_bits - 1
137 | EOFCode = ClearCode + 1
138 | free_ent = ClearCode + 2
139 | a_count = 0 // clear packet
140 | ent = nextPixel()
141 | hshift = 0
142 | fcode = hsize
143 | while (fcode < 65536) {
144 | ++hshift
145 | fcode *= 2
146 | }
147 | hshift = 8 - hshift // set hash code range bound
148 | hsize_reg = hsize
149 | cl_hash(hsize_reg) // clear hash table
150 | output(ClearCode, outs)
151 | outer_loop@ while (nextPixel().also { c = it } != EOF) {
152 | fcode = (c shl maxbits) + ent
153 | i = c shl hshift xor ent // xor hashing
154 | if (htab[i] == fcode) {
155 | ent = codetab[i]
156 | continue
157 | } else if (htab[i] >= 0) // non-empty slot
158 | {
159 | disp = hsize_reg - i // secondary hash (after G. Knott)
160 | if (i == 0) disp = 1
161 | do {
162 | if (disp.let { i -= it; i } < 0) i += hsize_reg
163 | if (htab[i] == fcode) {
164 | ent = codetab[i]
165 | continue@outer_loop
166 | }
167 | } while (htab[i] >= 0)
168 | }
169 | output(ent, outs)
170 | ent = c
171 | if (free_ent < maxmaxcode) {
172 | codetab[i] = free_ent++ // code -> hashtable
173 | htab[i] = fcode
174 | } else cl_block(outs)
175 | }
176 | // Put out the final code.
177 | output(ent, outs)
178 | output(EOFCode, outs)
179 | }
180 |
181 | // ----------------------------------------------------------------------------
182 | @Throws(IOException::class)
183 | fun encode(os: OutputStream) {
184 | os.write(initCodeSize) // write "initial code size" byte
185 | remaining = imgW * imgH // reset navigation variables
186 | curPixel = 0
187 | compress(initCodeSize + 1, os) // compress and write the pixel data
188 | os.write(0) // write block terminator
189 | }
190 |
191 | // Flush the packet to disk, and reset the accumulator
192 | @Throws(IOException::class)
193 | fun flush_char(outs: OutputStream) {
194 | if (a_count > 0) {
195 | outs.write(a_count)
196 | outs.write(accum, 0, a_count)
197 | a_count = 0
198 | }
199 | }
200 |
201 | fun MAXCODE(n_bits: Int): Int {
202 | return (1 shl n_bits) - 1
203 | }
204 |
205 | // ----------------------------------------------------------------------------
206 | // Return the next pixel from the image
207 | // ----------------------------------------------------------------------------
208 | private fun nextPixel(): Int {
209 | if (remaining == 0) return EOF
210 | --remaining
211 | val pix = pixAry[curPixel++]
212 | return pix.toInt() and 0xff
213 | }
214 |
215 | @Throws(IOException::class)
216 | fun output(code: Int, outs: OutputStream) {
217 | cur_accum = cur_accum and masks[cur_bits]
218 | cur_accum = if (cur_bits > 0) cur_accum or (code shl cur_bits) else code
219 | cur_bits += n_bits
220 | while (cur_bits >= 8) {
221 | char_out((cur_accum and 0xff).toByte(), outs)
222 | cur_accum = cur_accum shr 8
223 | cur_bits -= 8
224 | }
225 |
226 | // If the next entry is going to be too big for the code size,
227 | // then increase it, if possible.
228 | if (free_ent > maxcode || clear_flg) {
229 | if (clear_flg) {
230 | maxcode = MAXCODE(g_init_bits.also { n_bits = it })
231 | clear_flg = false
232 | } else {
233 | ++n_bits
234 | maxcode = if (n_bits == maxbits) maxmaxcode else MAXCODE(n_bits)
235 | }
236 | }
237 | if (code == EOFCode) {
238 | // At EOF, write the rest of the buffer.
239 | while (cur_bits > 0) {
240 | char_out((cur_accum and 0xff).toByte(), outs)
241 | cur_accum = cur_accum shr 8
242 | cur_bits -= 8
243 | }
244 | flush_char(outs)
245 | }
246 | }
247 |
248 | companion object {
249 | private const val EOF = -1
250 |
251 | // GIFCOMPR.C - GIF Image compression routines
252 | //
253 | // Lempel-Ziv compression based on 'compress'. GIF modifications by
254 | // David Rowley (mgardi@watdcsu.waterloo.edu)
255 | // General DEFINEs
256 | const val BITS = 12
257 | const val HSIZE = 5003 // 80% occupancy
258 | }
259 |
260 | // ----------------------------------------------------------------------------
261 | init {
262 | initCodeSize = Math.max(2, color_depth)
263 | }
264 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/gifmaker/NeuQuant.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.gifmaker
20 |
21 | /*
22 | * NeuQuant Neural-Net Quantization Algorithm
23 | * ------------------------------------------
24 | *
25 | * Copyright (c) 1994 Anthony Dekker
26 | *
27 | * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See
28 | * "Kohonen neural networks for optimal colour quantization" in "Network:
29 | * Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of
30 | * the algorithm.
31 | *
32 | * Any party obtaining a copy of these files from the author, directly or
33 | * indirectly, is granted, free of charge, a full and unrestricted irrevocable,
34 | * world-wide, paid up, royalty-free, nonexclusive right and license to deal in
35 | * this software and documentation files (the "Software"), including without
36 | * limitation the rights to use, copy, modify, merge, publish, distribute,
37 | * sublicense, and/or sell copies of the Software, and to permit persons who
38 | * receive copies from any such party to do so, with the only requirement being
39 | * that this copyright notice remain intact.
40 | */ // Ported to Java 12/00 K Weiner
41 | internal class NeuQuant(thepic: ByteArray, len: Int, sample: Int) {
42 | private var alphadec /* biased by 10 bits */ = 0
43 |
44 | /*
45 | * Types and Global Variables --------------------------
46 | */
47 | private var thepicture /* the input image itself */: ByteArray
48 | private var lengthcount /* lengthcount = H*W*3 */: Int
49 | private var samplefac /* sampling factor 1..30 */: Int
50 |
51 | // typedef int pixel[4]; /* BGRc */
52 | private var network /* the network itself - [netsize][4] */: Array
53 | private var netindex = IntArray(256)
54 |
55 | /* for network lookup - really 256 */
56 | private var bias = IntArray(netsize)
57 |
58 | /* bias and freq arrays for learning */
59 | private var freq = IntArray(netsize)
60 | private var radpower = IntArray(initrad)
61 | private fun colorMap(): ByteArray {
62 | val map = ByteArray(3 * netsize)
63 | val index = IntArray(netsize)
64 | for (i in 0 until netsize) index[network[i]!![3]] = i
65 | var k = 0
66 | for (i in 0 until netsize) {
67 | val j = index[i]
68 | map[k++] = network[j]!![0].toByte()
69 | map[k++] = network[j]!![1].toByte()
70 | map[k++] = network[j]!![2].toByte()
71 | }
72 | return map
73 | }
74 |
75 | /*
76 | * Insertion sort of network and building of netindex[0..255] (to do after
77 | * unbias)
78 | * -------------------------------------------------------------------------------
79 | */
80 | private fun inxbuild() {
81 | var j: Int
82 | var smallpos: Int
83 | var smallval: Int
84 | var p: IntArray?
85 | var q: IntArray?
86 | var previouscol: Int
87 | var startpos: Int
88 | previouscol = 0
89 | startpos = 0
90 | var i = 0
91 | while (i < netsize) {
92 | p = network[i]
93 | smallpos = i
94 | smallval = p!![1] /* index on g */
95 | /* find smallest in i..netsize-1 */j = i + 1
96 | while (j < netsize) {
97 | q = network[j]
98 | if (q!![1] < smallval) { /* index on g */
99 | smallpos = j
100 | smallval = q[1] /* index on g */
101 | }
102 | j++
103 | }
104 | q = network[smallpos]
105 | /* swap p (i) and q (smallpos) entries */if (i != smallpos) {
106 | j = q!![0]
107 | q[0] = p[0]
108 | p[0] = j
109 | j = q[1]
110 | q[1] = p[1]
111 | p[1] = j
112 | j = q[2]
113 | q[2] = p[2]
114 | p[2] = j
115 | j = q[3]
116 | q[3] = p[3]
117 | p[3] = j
118 | }
119 | /* smallval entry is now in position i */if (smallval != previouscol) {
120 | netindex[previouscol] = startpos + i shr 1
121 | j = previouscol + 1
122 | while (j < smallval) {
123 | netindex[j] = i
124 | j++
125 | }
126 | previouscol = smallval
127 | startpos = i
128 | }
129 | i++
130 | }
131 | netindex[previouscol] = startpos + maxnetpos shr 1
132 | j = previouscol + 1
133 | while (j < 256) {
134 | netindex[j] = maxnetpos /* really 256 */
135 | j++
136 | }
137 | }
138 |
139 | /*
140 | * Main Learning Loop ------------------
141 | */
142 | private fun learn() {
143 | var j: Int
144 | var b: Int
145 | var g: Int
146 | var r: Int
147 | var rad: Int
148 | var delta: Int
149 | if (lengthcount < minpicturebytes) samplefac = 1
150 | alphadec = 30 + (samplefac - 1) / 3
151 | val p = thepicture
152 | var pix = 0
153 | val lim = lengthcount
154 | val samplepixels = lengthcount / (3 * samplefac)
155 | delta = samplepixels / ncycles
156 | var alpha = initalpha
157 | var radius = initradius
158 | rad = radius shr radiusbiasshift
159 | if (rad <= 1) rad = 0
160 | var i = 0
161 | while (i < rad) {
162 | radpower[i] = alpha * ((rad * rad - i * i) * radbias / (rad * rad))
163 | i++
164 | }
165 |
166 | // fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad);
167 | val step = if (lengthcount < minpicturebytes) 3 else if (lengthcount % prime1 != 0) 3 * prime1 else {
168 | if (lengthcount % prime2 != 0) 3 * prime2 else {
169 | if (lengthcount % prime3 != 0) 3 * prime3 else 3 * prime4
170 | }
171 | }
172 | i = 0
173 | while (i < samplepixels) {
174 | b = (p[pix + 0].toInt() and 0xff) shl netbiasshift
175 | g = (p[pix + 1].toInt() and 0xff) shl netbiasshift
176 | r = (p[pix + 2].toInt() and 0xff) shl netbiasshift
177 | j = contest(b, g, r)
178 | altersingle(alpha, j, b, g, r)
179 | if (rad != 0) alterneigh(rad, j, b, g, r) /* alter neighbours */
180 | pix += step
181 | if (pix >= lim) pix -= lengthcount
182 | i++
183 | if (delta == 0) delta = 1
184 | if (i % delta == 0) {
185 | alpha -= alpha / alphadec
186 | radius -= radius / radiusdec
187 | rad = radius shr radiusbiasshift
188 | if (rad <= 1) rad = 0
189 | j = 0
190 | while (j < rad) {
191 | radpower[j] = alpha * ((rad * rad - j * j) * radbias / (rad * rad))
192 | j++
193 | }
194 | }
195 | }
196 | // fprintf(stderr,"finished 1D learning: final alpha=%f
197 | // !\n",((float)alpha)/initalpha);
198 | }
199 |
200 | /*
201 | * Search for BGR values 0..255 (after net is unbiased) and return colour
202 | * index
203 | * ----------------------------------------------------------------------------
204 | */
205 | fun map(b: Int, g: Int, r: Int): Int {
206 | var i: Int
207 | var j: Int
208 | var dist: Int
209 | var a: Int
210 | var bestd: Int
211 | var p: IntArray?
212 | var best: Int
213 | bestd = 1000 /* biggest possible dist is 256*3 */
214 | best = -1
215 | i = netindex[g] /* index on g */
216 | j = i - 1 /* start at netindex[g] and work outwards */
217 | while (i < netsize || j >= 0) {
218 | if (i < netsize) {
219 | p = network[i]
220 | dist = p!![1] - g /* inx key */
221 | if (dist >= bestd) i = netsize /* stop iter */ else {
222 | i++
223 | if (dist < 0) dist = -dist
224 | a = p[0] - b
225 | if (a < 0) a = -a
226 | dist += a
227 | if (dist < bestd) {
228 | a = p[2] - r
229 | if (a < 0) a = -a
230 | dist += a
231 | if (dist < bestd) {
232 | bestd = dist
233 | best = p[3]
234 | }
235 | }
236 | }
237 | }
238 | if (j >= 0) {
239 | p = network[j]
240 | dist = g - p!![1] /* inx key - reverse dif */
241 | if (dist >= bestd) j = -1 /* stop iter */ else {
242 | j--
243 | if (dist < 0) dist = -dist
244 | a = p[0] - b
245 | if (a < 0) a = -a
246 | dist += a
247 | if (dist < bestd) {
248 | a = p[2] - r
249 | if (a < 0) a = -a
250 | dist += a
251 | if (dist < bestd) {
252 | bestd = dist
253 | best = p[3]
254 | }
255 | }
256 | }
257 | }
258 | }
259 | return best
260 | }
261 |
262 | fun process(): ByteArray {
263 | learn()
264 | unbiasnet()
265 | inxbuild()
266 | return colorMap()
267 | }
268 |
269 | /*
270 | * Unbias network to give byte values 0..255 and record position i to prepare
271 | * for sort
272 | * -----------------------------------------------------------------------------------
273 | */
274 | private fun unbiasnet() {
275 | //var j: Int
276 | var i = 0
277 | while (i < netsize) {
278 | network[i]!![0] = network[i]!![0] shr netbiasshift
279 | network[i]!![1] = network[i]!![1] shr netbiasshift
280 | network[i]!![2] = network[i]!![2] shr netbiasshift
281 | network[i]!![3] = i /* record colour no */
282 | i++
283 | }
284 | }
285 |
286 | /*
287 | * Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in
288 | * radpower[|i-j|]
289 | * ---------------------------------------------------------------------------------
290 | */
291 | private fun alterneigh(rad: Int, i: Int, b: Int, g: Int, r: Int) {
292 | var lo: Int
293 | var hi: Int
294 | var a: Int
295 | var p: IntArray?
296 | lo = i - rad
297 | if (lo < -1) lo = -1
298 | hi = i + rad
299 | if (hi > netsize) hi = netsize
300 | var j = i + 1
301 | var k = i - 1
302 | var m = 1
303 | while (j < hi || k > lo) {
304 | a = radpower[m++]
305 | if (j < hi) {
306 | p = network[j++]
307 | try {
308 | p!![0] -= a * (p!![0] - b) / alpharadbias
309 | p[1] -= a * (p[1] - g) / alpharadbias
310 | p[2] -= a * (p[2] - r) / alpharadbias
311 | } catch (e: Exception) {
312 | } // prevents 1.3 miscompilation
313 | }
314 | if (k > lo) {
315 | p = network[k--]
316 | try {
317 | p!![0] -= a * (p!![0] - b) / alpharadbias
318 | p[1] -= a * (p[1] - g) / alpharadbias
319 | p[2] -= a * (p[2] - r) / alpharadbias
320 | } catch (e: Exception) {
321 | }
322 | }
323 | }
324 | }
325 |
326 | /*
327 | * Move neuron i towards biased (b,g,r) by factor alpha
328 | * ----------------------------------------------------
329 | */
330 | private fun altersingle(alpha: Int, i: Int, b: Int, g: Int, r: Int) {
331 |
332 | /* alter hit neuron */
333 | val n = network[i]
334 | n!![0] -= alpha * (n!![0] - b) / initalpha
335 | n[1] -= alpha * (n[1] - g) / initalpha
336 | n[2] -= alpha * (n[2] - r) / initalpha
337 | }
338 |
339 | /*
340 | * Search for biased BGR values ----------------------------
341 | */
342 | private fun contest(b: Int, g: Int, r: Int): Int {
343 |
344 | /* finds closest neuron (min dist) and updates freq */
345 | /* finds best neuron (min dist-bias) and returns position */
346 | /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */
347 | /* bias[i] = gamma*((1/netsize)-freq[i]) */
348 | var dist: Int
349 | var a: Int
350 | var biasdist: Int
351 | var betafreq: Int
352 | var bestpos: Int
353 | var bestbiaspos: Int
354 | var bestd: Int
355 | var bestbiasd: Int
356 | var n: IntArray?
357 | bestd = (1 shl 31).inv()
358 | bestbiasd = bestd
359 | bestpos = -1
360 | bestbiaspos = bestpos
361 | var i = 0
362 | while (i < netsize) {
363 | n = network[i]
364 | dist = n!![0] - b
365 | if (dist < 0) dist = -dist
366 | a = n[1] - g
367 | if (a < 0) a = -a
368 | dist += a
369 | a = n[2] - r
370 | if (a < 0) a = -a
371 | dist += a
372 | if (dist < bestd) {
373 | bestd = dist
374 | bestpos = i
375 | }
376 | biasdist = dist - (bias[i] shr intbiasshift - netbiasshift)
377 | if (biasdist < bestbiasd) {
378 | bestbiasd = biasdist
379 | bestbiaspos = i
380 | }
381 | betafreq = freq[i] shr betashift
382 | freq[i] -= betafreq
383 | bias[i] += betafreq shl gammashift
384 | i++
385 | }
386 | freq[bestpos] += beta
387 | bias[bestpos] -= betagamma
388 | return bestbiaspos
389 | }
390 |
391 | companion object {
392 | private const val netsize = 256 /* number of colours used */
393 |
394 | /* four primes near 500 - assume no image has a length so large */ /* that it is divisible by all four primes */
395 | private const val prime1 = 499
396 | private const val prime2 = 491
397 | private const val prime3 = 487
398 | private const val prime4 = 503
399 | private const val minpicturebytes = 3 * prime4
400 |
401 | /* minimum size for input image */ /*
402 | * Program Skeleton ---------------- [select samplefac in range 1..30] [read
403 | * image from input file] pic = (unsigned char*) malloc(3*width*height);
404 | * initnet(pic,3*width*height,samplefac); learn(); unbiasnet(); [write output
405 | * image header, using writecolourmap(f)] inxbuild(); write output image using
406 | * inxsearch(b,g,r)
407 | */
408 | /*
409 | * Network Definitions -------------------
410 | */
411 | private const val maxnetpos = netsize - 1
412 | private const val netbiasshift = 4 /* bias for colour values */
413 | private const val ncycles = 100 /* no. of learning cycles */
414 |
415 | /* defs for freq and bias */
416 | private const val intbiasshift = 16 /* bias for fractions */
417 | private const val intbias = 1 shl intbiasshift
418 | private const val gammashift = 10 /* gamma = 1024 */
419 | private const val gamma = 1 shl gammashift
420 | private const val betashift = 10
421 | private const val beta = intbias shr betashift /* beta = 1/1024 */
422 | private const val betagamma = intbias shl gammashift - betashift
423 |
424 | /* defs for decreasing radius factor */
425 | private const val initrad = netsize shr 3 /*
426 | * for 256 cols, radius
427 | * starts
428 | */
429 | private const val radiusbiasshift = 6 /* at 32.0 biased by 6 bits */
430 | private const val radiusbias = 1 shl radiusbiasshift
431 | private const val initradius = initrad * radiusbias /*
432 | * and
433 | * decreases
434 | * by a
435 | */
436 | private const val radiusdec = 30 /* factor of 1/30 each cycle */
437 |
438 | /* defs for decreasing alpha factor */
439 | private const val alphabiasshift = 10 /* alpha starts at 1.0 */
440 | private const val initalpha = 1 shl alphabiasshift
441 |
442 | /* radbias and alpharadbias used for radpower calculation */
443 | private const val radbiasshift = 8
444 | private const val radbias = 1 shl radbiasshift
445 | private const val alpharadbshift = alphabiasshift + radbiasshift
446 | private const val alpharadbias = 1 shl alpharadbshift
447 | }
448 |
449 | /* radpower for precomputation */ /*
450 | * Initialise network in range (0,0,0) to (255,255,255) and set parameters
451 | * -----------------------------------------------------------------------
452 | */
453 | init {
454 | var p: IntArray?
455 | thepicture = thepic
456 | lengthcount = len
457 | samplefac = sample
458 | network = arrayOfNulls(netsize)
459 | var i = 0
460 | while (i < netsize) {
461 | network[i] = IntArray(4)
462 | p = network[i]
463 | p!![2] = (i shl netbiasshift + 8) / netsize
464 | p!![1] = p!![2]
465 | p!![0] = p!![1]
466 | freq[i] = intbias / netsize /* 1/netsize */
467 | bias[i] = 0
468 | i++
469 | }
470 | }
471 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/modules/AtReply.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.runBlocking
23 | import kotlinx.coroutines.runInterruptible
24 | import net.mamoe.mirai.event.events.GroupMessageEvent
25 | import net.mamoe.mirai.message.data.*
26 | import net.mamoe.mirai.message.data.Image.Key.queryUrl
27 | import java.awt.image.BufferedImage
28 | import java.io.IOException
29 | import java.net.URL
30 | import java.util.*
31 | import java.util.concurrent.atomic.AtomicReference
32 | import javax.imageio.ImageIO
33 |
34 | private val random = Random()
35 |
36 | fun RosemoePlugin.handleAtReply(event: GroupMessageEvent) {
37 | val msg = event.message
38 |
39 | suspend fun sendBack() {
40 | var chain = messageChainOf()
41 | var replyQuote: QuoteReply? = null
42 | var quoteIndex = -2
43 | for (i in 0 until msg.size) {
44 | val sub = msg[i]
45 | if (sub is QuoteReply) {
46 | if (sub.source.fromId == event.bot.id) {
47 | replyQuote = QuoteReply(event.source)
48 | quoteIndex = i
49 | }
50 | }
51 | }
52 | for (i in 0 until msg.size) {
53 | val sub = msg[i]
54 | if (sub is MessageMetadata) {
55 | continue
56 | }
57 | if (sub is PlainText || sub is Face || sub is VipFace) {
58 | chain = chain.plus(sub)
59 | } else if (sub is AtAll) {
60 | chain = chain.plus("@全体成员 ")
61 | } else if (sub is At) {
62 | if (sub.target == event.bot.id) {
63 | if (i != quoteIndex - 1) {
64 | chain = chain.plus(At(event.sender))
65 | }
66 | } else {
67 | chain = chain.plus(sub)
68 | }
69 | } else if (sub is Image) {
70 | try {
71 | chain = if (isModuleEnabled("ReverseAtReplyImage")) {
72 | val file = runInterruptible(Dispatchers.IO) {
73 | val rotated = rotateImage(sub)
74 | makeImageCache(rotated)
75 | }
76 | chain.plus(event.group.uploadImageResource(file))
77 | } else {
78 | chain.plus(sub)
79 | }
80 | } catch (e: IOException) {
81 | chain = chain.plus(sub)
82 | e.printStackTrace()
83 | }
84 | }
85 | }
86 | if (isModuleEnabled("ReverseAtReply")) {
87 | val reversedChain = AtomicReference(messageChainOf())
88 | chain.forEach { element: SingleMessage ->
89 | if (element is PlainText) {
90 | reversedChain.set(PlainText(StringBuilder(element.content).reverse()) + reversedChain.get())
91 | } else {
92 | reversedChain.set(element + reversedChain.get())
93 | }
94 | }
95 | chain = reversedChain.get()
96 | }
97 | if (replyQuote != null) {
98 | chain = replyQuote.plus(chain)
99 | }
100 | event.group.sendMessage(chain)
101 | }
102 |
103 | if (isModuleEnabled("AtReply")) {
104 | msg.forEach { subMsg: Message? ->
105 | if (subMsg is At && subMsg.target == event.bot.id) {
106 | pluginLaunch { sendBack() }
107 | return
108 | }
109 | }
110 | }
111 | }
112 |
113 | @Throws(IOException::class)
114 | private fun rotateImage(image: Image): BufferedImage {
115 | return rotateImage(
116 | ImageIO.read(URL(runBlocking { image.queryUrl() }).openConnection().getInputStream()),
117 | random.nextDouble() > 0.5
118 | )
119 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/modules/PetPet.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import io.github.rosemoe.miraiPlugin.gifmaker.GifEncoder
22 | import kotlinx.coroutines.Dispatchers
23 | import kotlinx.coroutines.runInterruptible
24 | import net.mamoe.mirai.contact.Group
25 | import java.awt.Color
26 | import java.awt.image.BufferedImage
27 | import java.io.File
28 | import java.io.FileInputStream
29 | import java.io.FileOutputStream
30 | import java.io.IOException
31 | import java.net.HttpURLConnection
32 | import java.net.URL
33 | import javax.imageio.ImageIO
34 | import kotlin.math.max
35 |
36 | private const val OUT_SIZE = 112//hand size
37 | private const val MAX_FRAME = 5
38 |
39 | private const val squish = 1.25
40 | private const val scale = 0.875
41 | private const val spriteY = 20.0
42 | private const val duration = 16
43 |
44 | private val frameOffsets = listOf(
45 | mapOf("x" to 0, "y" to 0, "w" to 0, "h" to 0),
46 | mapOf("x" to -4, "y" to 12, "w" to 4, "h" to -12),
47 | mapOf("x" to -12, "y" to 18, "w" to 12, "h" to -18),
48 | mapOf("x" to -8, "y" to 12, "w" to 4, "h" to -12),
49 | mapOf("x" to -4, "y" to 0, "w" to 0, "h" to 0)
50 | )
51 |
52 | private val hands: Array by lazy {
53 | val handImage = ImageIO.read(
54 | getTargetImage(
55 | "https://benisland.neocities.org/petpet/img/sprite.png",
56 | "${RosemoePlugin.dataFolderPath}${File.separator}hand.png"
57 | )
58 | )
59 | Array(MAX_FRAME) {
60 | handImage.getSubimage(it * OUT_SIZE, 0, OUT_SIZE, OUT_SIZE)
61 | }
62 | }
63 |
64 | suspend fun RosemoePlugin.generateGifAndSend(url: String, group: Group, id: Long) {
65 | val outputFile = newFile("${userDirPath(id)}${File.separator}PetPet.gif")
66 | runInterruptible(Dispatchers.IO) {
67 | val head = ImageIO.read(FileInputStream(getUserHead(url, id)))
68 | val outputStream = FileOutputStream(outputFile)
69 | GifEncoder().run {
70 | delay = duration
71 | repeat = 0
72 | setTransparent(Color.TRANSLUCENT)
73 | start(outputStream)
74 | for (i in 0 until MAX_FRAME) {
75 | addFrame(generateFrame(head, i))
76 | }
77 | finish()
78 | }
79 | }
80 | group.sendMessage(group.uploadImageResource(outputFile))
81 | }
82 |
83 | operator fun Map.minus(x: K): V {
84 | return getValue(x)
85 | }
86 |
87 | private fun getSpriteFrame(i: Int): Map {
88 | val offset = frameOffsets[i]
89 | return mapOf(
90 | "dx" to ((offset - "x") * squish * 0.4).toInt(),
91 | "dy" to (spriteY + (offset - "y") * squish * 0.9).toInt(),
92 | "dw" to ((OUT_SIZE + (offset - "w") * squish) * scale).toInt(),
93 | "dh" to ((OUT_SIZE + (offset - "h") * squish) * scale).toInt()
94 | )
95 | }
96 |
97 | private fun generateFrame(head: BufferedImage, i: Int): BufferedImage {
98 | val cf = getSpriteFrame(i)
99 | val result = BufferedImage(OUT_SIZE, OUT_SIZE, BufferedImage.TYPE_INT_ARGB)
100 | result.createGraphics().apply {
101 | //color = Color.WHITE
102 | //drawRect(0, 0, OUT_SIZE, OUT_SIZE)
103 | //fillRect(0, 0, OUT_SIZE, OUT_SIZE)
104 | create().apply {
105 | translate(cf - "dx" + 15, cf - "dy" + 15)
106 | drawImage(head, 0, 0, ((cf - "dw") * 0.9).toInt(), ((cf - "dh") * 0.9).toInt(), null)
107 | }
108 | drawImage(hands[i], 0, max(0.0, ((cf - "dy") * 0.75 - max(0.0, spriteY) - 0.5)).toInt(), null, null)
109 | }
110 | return result
111 | }
112 |
113 | @Throws(IOException::class)
114 | private fun getUserHead(url: String, memberId: Long): File {
115 | return getTargetImage(
116 | url,
117 | "${userDirPath(memberId)}${File.separator}avatar.jpg",
118 | false
119 | )
120 | }
121 |
122 | @Throws(IOException::class)
123 | private fun getTargetImage(url: String, pathname: String, isUseCache: Boolean = true): File {
124 | val file = File(pathname)
125 | if (isUseCache && file.exists()) {
126 | return file
127 | }
128 | val connection = (URL(url).openConnection() as HttpURLConnection).apply {
129 | connectTimeout = 5000
130 | setRequestProperty(
131 | "User-Agent",
132 | "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0"
133 | )
134 | setRequestProperty("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3")
135 | setRequestProperty("Accept-Encoding", "utf-8")
136 | setRequestProperty("Connection", "keep-alive")
137 | connect()
138 | }
139 | if (!file.exists()) {
140 | file.parentFile.mkdirs()
141 | file.createNewFile()
142 | }
143 | val `is` = connection.inputStream
144 | val fos = FileOutputStream(file)
145 | val buffer = ByteArray(8192 * 2)
146 | var count: Int
147 | while (`is`.read(buffer).also { count = it } != -1) {
148 | fos.write(buffer, 0, count)
149 | }
150 | `is`.close()
151 | fos.flush()
152 | fos.close()
153 | connection.disconnect()
154 | return file
155 | }
156 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/modules/Pixiv.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import kotlinx.coroutines.runBlocking
22 | import net.mamoe.mirai.contact.Contact
23 | import net.mamoe.mirai.contact.Group
24 | import net.mamoe.mirai.message.data.*
25 | import org.json.JSONObject
26 | import java.io.*
27 | import java.net.InetSocketAddress
28 | import java.net.Proxy
29 | import java.net.URL
30 | import javax.net.ssl.HttpsURLConnection
31 |
32 | fun RosemoePlugin.applyProxySettings() {
33 | proxy = config.proxyEnabled
34 | proxyAddress = config.proxyAddress
35 | proxyPort = config.proxyPort
36 | proxyType = if (config.proxyType.lowercase().contentEquals("http")) Proxy.Type.HTTP else Proxy.Type.SOCKS
37 | }
38 |
39 |
40 |
41 | var proxy = false
42 | var proxyType = Proxy.Type.HTTP
43 | var proxyAddress = "127.0.0.1"
44 | var proxyPort = 1080
45 |
46 | @Throws(Throwable::class)
47 | fun getArtworkSource(artworkId: Long): StringBuilder {
48 | val connection: HttpsURLConnection = if (proxy) {
49 | val proxyInstance = Proxy(proxyType, InetSocketAddress(proxyAddress, proxyPort))
50 | URL("https://www.pixiv.net/artworks/$artworkId").openConnection(proxyInstance) as HttpsURLConnection
51 | } else {
52 | URL("https://www.pixiv.net/artworks/$artworkId").openConnection() as HttpsURLConnection
53 | }
54 | connection.connectTimeout = 5000
55 | connection.connect()
56 | val br = BufferedReader(
57 | InputStreamReader(
58 | connection.inputStream,
59 | if (connection.contentEncoding == null) "UTF8" else connection.contentEncoding
60 | )
61 | )
62 | val sb = StringBuilder()
63 | var line: String?
64 | while (br.readLine().also { line = it } != null) {
65 | sb.append(line)
66 | }
67 | br.close()
68 | connection.disconnect()
69 | return sb
70 | }
71 |
72 | private fun trimSourceToJson(source: StringBuilder) {
73 | val beginMarker = "<"
77 | val end = source.lastIndexOf(endMarker)
78 | source.setLength(end)
79 | }
80 |
81 | @Throws(Throwable::class)
82 | internal fun getArtworkInformation(gp: Contact, artworkId: Long): MessageChain {
83 | var sb: StringBuilder = getArtworkSource(artworkId)
84 | trimSourceToJson(sb)
85 | var artwork = JSONObject(sb.toString())
86 | artwork = artwork.getJSONObject("illust")
87 | artwork = artwork.getJSONObject(artworkId.toString())
88 | sb = StringBuilder()
89 | sb.append("Pixiv画作信息:\n")
90 | .append("插图ID:").append(artworkId).append('\n')
91 | .append("画作标题:").append(artwork.getString("illustTitle"))
92 | .append('\n') //.append("画作评论:\n").append(translateHtml(artwork.getString("illustComment"))).append('\n')
93 | .append("画师ID:").append(artwork.getString("userId")).append('\n')
94 | .append("画师:").append(artwork.getString("userName")).append('\n')
95 | .append("画作地址:").append("https://www.pixiv.net/artworks/").append(artworkId).append('\n')
96 | .append("原图地址:")
97 | val urls = artwork.getJSONObject("urls")
98 | sb.append(urls["original"].toString().replace("i.pximg.net", "i.pixiv.cat"))
99 | sb.append("\n标签:")
100 | val tags = artwork.getJSONObject("tags").getJSONArray("tags")
101 | var r18 = false
102 | if (!RosemoePlugin.config.allowR18ImageInPixiv) {
103 | for (i in 0 until tags.length()) {
104 | val tag = tags.getJSONObject(i)
105 | if (tag.getString("tag").lowercase().contains("r-18")) {
106 | r18 = true
107 | }
108 | sb.append(tag.getString("tag"))
109 | if (i + 1 != tag.length()) {
110 | sb.append(", ")
111 | }
112 | }
113 | }
114 | var msg= messageChainOf(PlainText(sb.toString()))
115 | if (r18) {
116 | msg = msg.plus("\n画作可能含有敏感内容,取消了图片的发送")
117 | } else {
118 | msg = msg.plus("\n")
119 | msg = msg.plus("预览图片:\n")
120 | val pageCount =
121 | artwork.getJSONObject("userIllusts").getJSONObject(artworkId.toString() + "").getInt("pageCount")
122 | val firstUrl = urls.getString("regular")
123 | for (index in 0 until pageCount) {
124 | var img: Image? = null
125 | try {
126 | img = getTargetImage(gp, firstUrl.replace("_p0", "_p$index"), artworkId, true)
127 | } catch (ex: IOException) {
128 | try {
129 | img = getTargetImage(gp, firstUrl.replace("_p0", "_p$index"), artworkId, false)
130 | } catch (ex1: IOException) {
131 | //ignored
132 | }
133 | }
134 | msg = if (img != null) {
135 | msg.plus(img)
136 | } else {
137 | msg.plus("\n第 ${index + 1} P获取失败")
138 | }
139 | }
140 | }
141 | return msg
142 | }
143 |
144 | @Throws(Throwable::class)
145 | fun getArtworkImage(gp: Contact, artworkId: Long, index: Int): Message {
146 | val sb: StringBuilder = getArtworkSource(artworkId)
147 | trimSourceToJson(sb)
148 | var artwork = JSONObject(sb.toString())
149 | artwork = artwork.getJSONObject("illust")
150 | artwork = artwork.getJSONObject(artworkId.toString())
151 | val urls = artwork.getJSONObject("urls")
152 | val originUrl = urls["original"].toString()
153 | val tags = artwork.getJSONObject("tags").getJSONArray("tags")
154 | var r18 = false
155 | if (!RosemoePlugin.config.allowR18ImageInPixiv) {
156 | for (i in 0 until tags.length()) {
157 | val tag = tags.getJSONObject(i)
158 | if (tag.getString("tag").lowercase().contains("r-18")) {
159 | r18 = true
160 | break
161 | }
162 | }
163 | }
164 | return if (r18) {
165 | PlainText("画作可能含有敏感内容,取消了图片的发送")
166 | } else {
167 | val pageCount =
168 | artwork.getJSONObject("userIllusts").getJSONObject(artworkId.toString() + "").getInt("pageCount")
169 | if (index < 0 || index >= pageCount) {
170 | PlainText("超出画作的插图数目")
171 | } else {
172 | var img: Image? = null
173 | try {
174 | img = getTargetImage(gp, originUrl.replace("_p0", "_p$index"), artworkId, true)
175 | } catch (ex: IOException) {
176 | try {
177 | img = getTargetImage(gp, originUrl.replace("_p0", "_p$index"), artworkId, false)
178 | } catch (ex1: IOException) {
179 | //ignored
180 | }
181 | }
182 | checkNotNull(img)
183 | img
184 | }
185 | }
186 | }
187 |
188 | @Throws(IOException::class)
189 | private fun getTargetImage(gp: Contact, url: String, artworkId: Long, proxy: Boolean): Image {
190 | val file = File("${cacheDirPath()}${File.separator}Pixiv${File.separator}${url.substring(url.lastIndexOf("/") + 1)}")
191 | val res : Image
192 | if (file.exists()) {
193 | runBlocking(RosemoePlugin.coroutineContext) {
194 | res = gp.uploadImageResource(file)
195 | }
196 | return res
197 | }
198 | val connection: HttpsURLConnection = if (proxy) {
199 | val proxyInstance = Proxy(proxyType, InetSocketAddress(proxyAddress, proxyPort))
200 | URL(url).openConnection(proxyInstance) as HttpsURLConnection
201 | } else {
202 | URL(url).openConnection() as HttpsURLConnection
203 | }
204 | connection.connectTimeout = 5000
205 | connection.setRequestProperty(
206 | "User-Agent",
207 | "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0"
208 | )
209 | connection.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3")
210 | connection.setRequestProperty("Accept-Encoding", "utf-8")
211 | connection.setRequestProperty("Connection", "keep-alive")
212 | connection.setRequestProperty(
213 | "Referer",
214 | "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=$artworkId"
215 | )
216 | connection.connect()
217 | val tmp = File(file.absolutePath + ".tmp")
218 | if (!tmp.exists()) {
219 | check(!(!(tmp.parentFile.exists() || tmp.parentFile.mkdirs()) || !tmp.createNewFile())) { "failed to create tmp file" }
220 | }
221 | val `is` = connection.inputStream
222 | val fos = FileOutputStream(tmp)
223 | val buffer = ByteArray(8192 * 2)
224 | var count: Int
225 | while (`is`.read(buffer).also { count = it } != -1) {
226 | fos.write(buffer, 0, count)
227 | }
228 | `is`.close()
229 | fos.flush()
230 | fos.close()
231 | connection.disconnect()
232 | if (tmp.renameTo(file)) {
233 | runBlocking (RosemoePlugin.coroutineContext) {
234 | res = gp.uploadImageResource(file)
235 | }
236 | } else {
237 | runBlocking (RosemoePlugin.coroutineContext) {
238 | res = gp.uploadImageResource(tmp)
239 | }
240 | tmp.delete()
241 | }
242 | return res
243 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/modules/Repeat.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import net.mamoe.mirai.event.events.GroupMessageEvent
22 | import net.mamoe.mirai.message.data.MessageSource
23 | import net.mamoe.mirai.message.data.toMessageChain
24 | import java.util.*
25 |
26 | private val random = Random()
27 |
28 | @Synchronized
29 | private fun nextDouble(): Double {
30 | return random.nextDouble()
31 | }
32 |
33 | fun RosemoePlugin.randomRepeat(event: GroupMessageEvent) {
34 | if (!isModuleEnabled("Repeat") || isDarklistGroup(event)) {
35 | return
36 | }
37 | if (nextDouble() < config.repeatFactor) {
38 | event.sendAsync(event.message.asSequence().filter { predicate -> !(predicate is MessageSource) }.toMessageChain())
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/modules/RepeatBreaking.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2022 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.modules
20 |
21 | import io.github.rosemoe.miraiPlugin.RosemoePlugin
22 | import net.mamoe.mirai.event.events.GroupMessageEvent
23 | import net.mamoe.mirai.message.data.*
24 | import net.mamoe.mirai.utils.ConcurrentHashMap
25 | import java.util.LinkedList
26 | import java.util.Queue
27 |
28 | private val messagePool = ConcurrentHashMap>()
29 |
30 | fun RosemoePlugin.onDisableRepeatBreakingForGroup(group: Long) {
31 | messagePool.remove(group)
32 | }
33 |
34 | fun RosemoePlugin.handleGpMessageForRepeating(event: GroupMessageEvent) {
35 | if (!config.repeatDetectGroups.contains(event.group.id)) {
36 | return
37 | }
38 | val groupMsgPool = messagePool.computeIfAbsent(event.group.id) { _ ->
39 | return@computeIfAbsent LinkedList()
40 | }
41 | if (config.messageRecordSize <= groupMsgPool.size) {
42 | groupMsgPool.poll()
43 | }
44 | groupMsgPool.offer(LocalMessageRecord(event.sender.id, event.message.contentToString()))
45 | var msgToDel: String? = null
46 | if (groupMsgPool.size >= config.messageDuplicateLimit) {
47 | // Check duplicated messages
48 | for (msg in groupMsgPool) {
49 | val filtered = groupMsgPool.filter { msg.messageText == it.messageText }
50 | if (filtered.size >= config.messageDuplicateLimit) {
51 | val luckyDog = filtered.random().senderId
52 | event.sendAsync(messageChainOf(PlainText("幸运复读群员:"), event.group[luckyDog]?.at() ?: PlainText("@$luckyDog")))
53 | msgToDel = msg.messageText
54 | break
55 | }
56 | }
57 | }
58 | msgToDel?.let {
59 | groupMsgPool.removeIf { it.messageText == msgToDel }
60 | }
61 | }
62 |
63 | private data class LocalMessageRecord(
64 | val senderId: Long,
65 | val messageText: String
66 | )
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/utils/PluginFiles.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import java.io.File
22 |
23 | fun cacheDirPath() : String {
24 | return "${RosemoePlugin.dataFolderPath}${File.separator}Cache"
25 | }
26 |
27 | fun userDirPath(id: Long) : String {
28 | return "${cacheDirPath()}${File.separator}Users${File.separator}$id"
29 | }
30 | /*
31 | fun userDir(id: Long) : File {
32 | return File(userDirPath(id)).also {
33 | it.mkdirs()
34 | }
35 | }*/
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/utils/PluginRecallWorker.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.utils
20 |
21 | import io.github.rosemoe.miraiPlugin.RosemoePlugin
22 | import kotlinx.coroutines.Dispatchers
23 | import kotlinx.coroutines.delay
24 | import kotlinx.coroutines.launch
25 | import kotlinx.coroutines.runInterruptible
26 | import net.mamoe.mirai.message.MessageReceipt
27 | import net.mamoe.mirai.message.data.MessageSource
28 | import java.util.concurrent.BlockingQueue
29 | import java.util.concurrent.LinkedBlockingQueue
30 | import java.util.concurrent.atomic.AtomicBoolean
31 |
32 | val taskQueue = LinkedBlockingQueue(8192 * 2)
33 |
34 | suspend fun BlockingQueue.awaitTake() : E = runInterruptible (Dispatchers.IO) { take() }
35 |
36 | suspend fun recall(target: MessageReceipt<*>) {
37 | target.recall()
38 | }
39 |
40 | fun resetRecallSign(source: MessageSource) {
41 | try {
42 | val internalClass = Class.forName("net.mamoe.mirai.internal.message.MessageSourceInternal")
43 | val method = internalClass.getDeclaredMethod("isRecalledOrPlanned")
44 | val value = method.invoke(source) as AtomicBoolean
45 | value.compareAndExchange(true, false)
46 | }catch (ignored: Exception) {
47 |
48 | }
49 | }
50 |
51 | /**
52 | * This is to work around the shit Tencent protocol
53 | * If we try to recall messages concurrently or too quickly, it will be denied
54 | * So we create this manager to recall messages in a single thread without concurrency but an extra interval time
55 | */
56 | fun RosemoePlugin.startRecallManager() {
57 | launch (coroutineContext) {
58 | logger.info("RecallManager START!")
59 | while (!Thread.interrupted()) {
60 | val req = taskQueue.awaitTake()
61 | if (req.expectedTime <= System.currentTimeMillis()) {
62 | try {
63 | recall(req.target)
64 | logger.info("Recalled a message")
65 | val interval = config.recallMinPeriod
66 | delay(if(interval <= 0L) 180L else interval)
67 | }catch (e: Exception) {
68 | logger.warning("Recall message failed")
69 | logger.verbose(e)
70 | if ((req.createTime + 120000) > System.currentTimeMillis()) {
71 | val source = req.target.source
72 | resetRecallSign(source)
73 | launch (Dispatchers.IO) { taskQueue.put(req) }
74 | delay(10000)
75 | continue
76 | }
77 | delay(10000)
78 | logger.warning("Un-recalled message is outdated: now: ${System.currentTimeMillis()} createTime: ${req.createTime}")
79 | }
80 | } else {
81 | runInterruptible (Dispatchers.IO) { taskQueue.put(req) }
82 | delay(20)
83 | }
84 | }
85 | }
86 | }
87 |
88 | fun scheduleRecall(receipt: MessageReceipt<*>, delay: Long) {
89 | taskQueue.put(Request(delay, receipt))
90 | }
91 |
92 | class Request constructor(delay: Long, receipt: MessageReceipt<*>) {
93 |
94 | val createTime = System.currentTimeMillis()
95 |
96 | val expectedTime = delay + createTime
97 |
98 | val target = receipt
99 |
100 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/utils/ScriptMethods.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin.utils
20 |
21 | import io.github.rosemoe.miraiPlugin.RosemoePlugin
22 | import io.github.rosemoe.miraiPlugin.command.MsgEvent
23 | import io.github.rosemoe.miraiPlugin.commands.Setu.sendImageForEvent
24 | import kotlinx.coroutines.CoroutineScope
25 | import kotlinx.coroutines.runBlocking
26 | import net.mamoe.mirai.contact.NormalMember
27 | import net.mamoe.mirai.contact.getMemberOrFail
28 | import net.mamoe.mirai.message.action.Nudge.Companion.sendNudge
29 | import net.mamoe.mirai.message.code.MiraiCode
30 | import net.mamoe.mirai.message.data.At
31 |
32 | @Suppress("unused")
33 | class ScriptMethods(val event : MsgEvent) {
34 |
35 | private fun runBlocking(scope: suspend CoroutineScope.() -> Unit) {
36 | runBlocking(RosemoePlugin.coroutineContext, scope)
37 | }
38 |
39 | fun send(content: String) {
40 | runBlocking {
41 | event.send(MiraiCode.deserializeMiraiCode(content, event.sender))
42 | }
43 | }
44 |
45 | fun mute(user: String, duration: Int) {
46 | runBlocking {
47 | member(user).mute(duration)
48 | }
49 | }
50 |
51 | fun unmute(user: String) {
52 | runBlocking {
53 | member(user).unmute()
54 | }
55 | }
56 |
57 | fun kick(user: String, msg: String) {
58 | runBlocking {
59 | member(user).kick(msg)
60 | }
61 | }
62 |
63 | fun nudge(user: String) {
64 | runBlocking {
65 | event.group().sendNudge(member(user).nudge())
66 | }
67 | }
68 |
69 | fun member(user: String): NormalMember {
70 | val msg = MiraiCode.deserializeMiraiCode(user, event.group())
71 | val target = msg.get(0)
72 | return if (target is At) {
73 | event.group().getMemberOrFail(target.target)
74 | } else {
75 | event.group().getMemberOrFail(user.toLong())
76 | }
77 | }
78 |
79 | fun muteAll() {
80 | event.group().settings.isMuteAll = true
81 | }
82 |
83 | fun unmuteAll() {
84 | event.group().settings.isMuteAll = false
85 | }
86 |
87 | fun find(keyword: String) : NormalMember? {
88 | event.group().members.forEach {
89 | if (it.nameCard.contains(keyword, true) || it.nick.contains(keyword, true)) {
90 | return it
91 | }
92 | }
93 | return null
94 | }
95 |
96 | fun findAll(keyword: String) : Array {
97 | val list = ArrayList()
98 | event.group().members.forEach {
99 | if (it.nameCard.contains(keyword, true) || it.nick.contains(keyword, true)) {
100 | list.add(it)
101 | }
102 | }
103 | return list.toArray(Array(0) {null})
104 | }
105 |
106 | fun gkd() {
107 | RosemoePlugin.sendImageForEvent(event)
108 | }
109 |
110 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/rosemoe/miraiPlugin/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * RosemoeBotPlugin
3 | * Copyright (C) 2020-2021 Rosemoe
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Affero General Public License as published
7 | * by the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Affero General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Affero General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package io.github.rosemoe.miraiPlugin
20 |
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.coroutineScope
23 | import kotlinx.coroutines.launch
24 | import net.mamoe.mirai.contact.Contact
25 | import net.mamoe.mirai.contact.Group
26 | import net.mamoe.mirai.message.data.Image
27 | import net.mamoe.mirai.message.data.MessageChain
28 | import net.mamoe.mirai.message.data.PlainText
29 | import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
30 | import java.awt.image.BufferedImage
31 | import java.io.BufferedReader
32 | import java.io.File
33 | import java.io.InputStreamReader
34 | import java.net.*
35 | import java.util.concurrent.locks.ReentrantReadWriteLock
36 | import javax.imageio.ImageIO
37 | import javax.net.ssl.HttpsURLConnection
38 |
39 | suspend fun Contact.uploadImageResource(file: File): Image {
40 | val resource = file.toExternalResource()
41 | try {
42 | return uploadImage(resource)
43 | } finally {
44 | coroutineScope {
45 | launch(Dispatchers.IO) {
46 | resource.close()
47 | }
48 | }
49 | }
50 | }
51 |
52 | fun MessageChain.containsImage(id: String): Boolean {
53 | for (element in this) {
54 | if (element is Image) {
55 | if (element.imageId.contains(id)) {
56 | return true
57 | }
58 | }
59 | }
60 | return false
61 | }
62 |
63 | fun MessageChain.containsTexts(patterns: Array): Boolean {
64 | val states = Array(patterns.size) {
65 | true
66 | }
67 | var matched = 0
68 | forEach { singleMsg ->
69 | if (singleMsg is PlainText) {
70 | for ((index, element) in patterns.withIndex()) {
71 | if (states[index] && singleMsg.content.contains(element, false)) {
72 | states[index] = false
73 | matched++
74 | }
75 | }
76 | }
77 | }
78 | return matched == patterns.size
79 | }
80 |
81 |
82 | fun rotateImage(src: java.awt.Image, type: Boolean): BufferedImage {
83 | return if (type) rotateImage90(src) else rotateImage180(src)
84 | }
85 |
86 | private fun rotateImage90(src: java.awt.Image): BufferedImage {
87 | val res = BufferedImage(src.getHeight(null), src.getWidth(null), BufferedImage.TYPE_INT_ARGB)
88 | res.accelerationPriority = 1f
89 | src.accelerationPriority = 1f
90 | res.createGraphics().apply {
91 | translate(src.getHeight(null) / 2.0, src.getWidth(null) / 2.0)
92 | rotate(Math.toRadians(90.0))
93 | translate(-src.getWidth(null) / 2.0, -src.getHeight(null) / 2.0)
94 | drawImage(src, null, null)
95 | }
96 | return res
97 | }
98 |
99 | private fun rotateImage180(src: java.awt.Image): BufferedImage {
100 | val res = BufferedImage(src.getWidth(null), src.getHeight(null), BufferedImage.TYPE_INT_ARGB)
101 | res.accelerationPriority = 1f
102 | src.accelerationPriority = 1f
103 | res.createGraphics().apply {
104 | translate(src.getWidth(null) / 2.0, src.getHeight(null) / 2.0)
105 | rotate(Math.toRadians(180.0))
106 | translate(-src.getWidth(null) / 2.0, -src.getHeight(null) / 2.0)
107 | drawImage(src, null, null)
108 | }
109 | return res
110 | }
111 |
112 | fun getExceptionInfo(e: Throwable): String {
113 | val info = StringBuilder()
114 | var it: Throwable? = e
115 | var first = true
116 | while (it != null) {
117 | if (!first) {
118 | info.append("\nCaused by:")
119 | }
120 | info.append(it.javaClass.name).append(':').append(it.message)
121 | first = false
122 | it = it.cause
123 | }
124 | return info.toString()
125 | }
126 |
127 | fun ReentrantReadWriteLock.lockRead() {
128 | readLock().lock()
129 | }
130 |
131 | fun ReentrantReadWriteLock.unlockRead() {
132 | readLock().unlock()
133 | }
134 |
135 | fun ReentrantReadWriteLock.lockWrite() {
136 | writeLock().lock()
137 | }
138 |
139 | fun ReentrantReadWriteLock.unlockWrite() {
140 | writeLock().unlock()
141 | }
142 |
143 | fun String.toLong(default: Long): Long {
144 | return try {
145 | toLong()
146 | } catch (e: NumberFormatException) {
147 | default
148 | }
149 | }
150 |
151 | fun String.toDouble(default: Double): Double {
152 | return try {
153 | toDouble()
154 | } catch (e: java.lang.NumberFormatException) {
155 | default
156 | }
157 | }
158 |
159 | fun makeImageCache(image: BufferedImage): File {
160 | val file = File.createTempFile(
161 | "buffered-",
162 | ".tmp",
163 | File("${cacheDirPath()}${File.separator}ImageTmp").also { if (!it.exists()) it.mkdirs() })
164 | ImageIO.write(image, "png", file)
165 | file.deleteOnExit()
166 | return file
167 | }
168 |
169 | /**
170 | * Create a new File object
171 | * As well as create it in file system if it does not exist
172 | */
173 | fun newFile(path: String): File {
174 | val file = File(path)
175 | if (!file.exists()) {
176 | file.parentFile.mkdirs()
177 | file.createNewFile()
178 | }
179 | return file
180 | }
181 |
182 | fun getWebpageSource(url: String): String {
183 | val connection: URLConnection = URL(url).openConnection()
184 | connection.connectTimeout = 5000
185 | connection.connect()
186 | val br = BufferedReader(
187 | InputStreamReader(
188 | connection.inputStream,
189 | if (connection.contentEncoding == null) "UTF-8" else connection.contentEncoding
190 | )
191 | )
192 | val sb = StringBuilder()
193 | var line: String?
194 | while (br.readLine().also { line = it } != null) {
195 | sb.append(line)
196 | }
197 | br.close()
198 | if (connection is HttpsURLConnection) {
199 | connection.disconnect()
200 | } else if (connection is HttpURLConnection) {
201 | connection.disconnect()
202 | }
203 | return sb.toString()
204 | }
205 |
206 | fun logWarning(message: String, e: Throwable? = null) {
207 | RosemoePlugin.logger.warning(message, e)
208 | }
209 |
210 | fun logError(message: String, e: Throwable? = null) {
211 | RosemoePlugin.logger.error(message, e)
212 | }
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin:
--------------------------------------------------------------------------------
1 | io.github.rosemoe.miraiPlugin.RosemoePlugin
--------------------------------------------------------------------------------
/src/main/resources/strings.properties:
--------------------------------------------------------------------------------
1 | #
2 | # RosemoeBotPlugin
3 | # Copyright (C) 2020-2021 Rosemoe
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as published
7 | # by the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 | #
18 |
19 | settings.pluginName=BotPlugin设置
20 | settings.managers=管理员控制
21 | settings.moduleStates=功能开关
22 | settings.blacklist=黑名单
23 | settings.proxy=代理设置
24 | settings.other=杂项
--------------------------------------------------------------------------------