├── .gitignore ├── .run └── Run Mirai Console.run.xml ├── LICENSE ├── README.md ├── WINDOWS_MARK ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── com │ ├── dancecube │ ├── api │ │ ├── LvRatioHistory.java │ │ ├── Machine.java │ │ ├── PhoneLoginBuilder.java │ │ └── PlayerMusic.java │ ├── image │ │ ├── LastPlayImage.java │ │ ├── NewUserRatioImage.java │ │ ├── UserInfoImage.java │ │ └── UserRatioImage.java │ ├── info │ │ ├── AccountInfo.java │ │ ├── InfoStatus.java │ │ ├── ReplyItem.java │ │ └── UserInfo.java │ ├── music │ │ ├── CoverUtil.java │ │ ├── GoodsMusic.java │ │ ├── Music.java │ │ ├── MusicUtil.java │ │ └── OfficialMusic.java │ ├── ratio │ │ ├── AccGrade.java │ │ ├── RankMusicInfo.java │ │ ├── RatioCalculator.java │ │ ├── RecentMusicInfo.java │ │ └── RecordedMusicInfo.java │ └── token │ │ ├── Token.java │ │ └── TokenBuilder.java │ ├── mirai │ ├── MiraiBot.java │ ├── command │ │ ├── AbstractCommand.java │ │ ├── AllCommands.java │ │ ├── ArgsCommand.java │ │ ├── ArgsCommandBuilder.java │ │ ├── DeclaredCommand.java │ │ ├── MsgHandleable.java │ │ ├── RegexCommand.java │ │ ├── RegexCommandBuilder.java │ │ ├── ReplyCommand.java │ │ ├── ReplyCommandBuilder.java │ │ └── Scope.java │ ├── config │ │ ├── AbstractConfig.java │ │ └── UserConfigUtils.java │ ├── event │ │ ├── MainHandler.java │ │ └── PlainTextHandler.java │ └── task │ │ ├── RefreshTokenJob.java │ │ └── SchedulerTask.java │ └── tools │ ├── DanceCubeRequestCrypto.java │ ├── HttpUtil.java │ ├── JsonUtil.java │ └── image │ ├── ImageDrawer.java │ ├── ImageEffect.java │ └── TextEffect.java └── resources ├── META-INF └── services │ └── net.mamoe.mirai.console.plugin.jvm.JvmPlugin └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Personal 2 | DcConfig/ 3 | 4 | # User-specific stuff 5 | .idea/ 6 | 7 | *.iml 8 | *.ipr 9 | *.iws 10 | 11 | # IntelliJ 12 | out/ 13 | # mpeltonen/sbt-idea plugin 14 | .idea_modules/ 15 | 16 | # JIRA plugin 17 | atlassian-ide-plugin.xml 18 | 19 | # Compiled class file 20 | *.class 21 | 22 | # Log file 23 | *.log 24 | 25 | # BlueJ files 26 | *.ctxt 27 | 28 | # Package Files # 29 | *.jar 30 | *.war 31 | *.nar 32 | *.ear 33 | *.zip 34 | *.tar.gz 35 | *.rar 36 | 37 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 38 | hs_err_pid* 39 | 40 | *~ 41 | 42 | # temporary files which can be created if a process still has a handle open of a deleted file 43 | .fuse_hidden* 44 | 45 | # KDE directory preferences 46 | .directory 47 | 48 | # Linux trash folder which might appear on any partition or disk 49 | .Trash-* 50 | 51 | # .nfs files are created when an open file is removed but is still being accessed 52 | .nfs* 53 | 54 | # General 55 | .DS_Store 56 | .AppleDouble 57 | .LSOverride 58 | 59 | # Icon must end with two \r 60 | Icon 61 | 62 | # Thumbnails 63 | ._* 64 | 65 | # Files that might appear in the root of a volume 66 | .DocumentRevisions-V100 67 | .fseventsd 68 | .Spotlight-V100 69 | .TemporaryItems 70 | .Trashes 71 | .VolumeIcon.icns 72 | .com.apple.timemachine.donotpresent 73 | 74 | # Directories potentially created on remote AFP share 75 | .AppleDB 76 | .AppleDesktop 77 | Network Trash Folder 78 | Temporary Items 79 | .apdisk 80 | 81 | # Windows thumbnail cache files 82 | Thumbs.db 83 | Thumbs.db:encryptable 84 | ehthumbs.db 85 | ehthumbs_vista.db 86 | 87 | # Dump file 88 | *.stackdump 89 | 90 | # Folder config file 91 | [Dd]esktop.ini 92 | 93 | # Recycle Bin used on file shares 94 | $RECYCLE.BIN/ 95 | 96 | # Windows Installer files 97 | *.cab 98 | *.msi 99 | *.msix 100 | *.msm 101 | *.msp 102 | 103 | # Windows shortcuts 104 | *.lnk 105 | 106 | .gradle 107 | build/ 108 | 109 | # Ignore Gradle GUI config 110 | gradle-app.setting 111 | 112 | # Cache of project 113 | .gradletasknamecache 114 | 115 | **/build/ 116 | 117 | # Common working directory 118 | run/ 119 | 120 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 121 | !gradle-wrapper.jar 122 | 123 | 124 | # Local Test Launch point 125 | src/test/kotlin/RunTerminal.kt 126 | 127 | # Mirai console files with direct bootstrap 128 | /config 129 | /data 130 | /plugins 131 | /bots 132 | 133 | # Local Test Launch Point working directory 134 | /debug-sandbox 135 | /.lingma 136 | -------------------------------------------------------------------------------- /.run/Run Mirai Console.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DanceCubeBot 2 | 3 | > ***前路漫漫亦灿灿*** 4 | > 5 | > ***愿我们终将在更好的未来重逢!*** 6 | > 7 | > 8 | > 编辑于 2025.5.10 9 | 10 | 这是一个基于**Mirai & Java**的舞立方机器人 11 | 目前只测试在**舞小铃**的账号上, 12 | ~~如果你看到了这个机器人,就说明它的框架是这个b写的~~ 13 | 14 | *给个**star**或许我会很开心🥰* 15 | 16 | ## 功能介绍 17 | 18 | 用户可用功能: 19 | 20 | - 手机号或扫码至登录机器人 21 | - 查看个人信息(图片,战力,排行,金币,积分等等) 22 | - 查看战力分析(图片,b15/r15,单曲详情等等) 23 | - 查看战力截图(模仿舞立方秀绘制) 24 | - 舞立方机台二维码登录(发送给机器人二维码)~~至少不用微信扫码了~~ 25 | - 查找地区在线/离线的舞立方(包括舞立方秀) 26 | - 自动批量兑换自制谱兑换码 27 | - Token每日自动更新 28 | - **...更多请查看舞小铃主页使用文档** 29 | 30 | --- 31 | 管理员可用功能: 32 | 33 | - 读取/写入 Tokens 34 | - 设置默认 Token 35 | - 查看个人 Token 36 | - 强制刷新 Token 37 | 38 | > ~~一些咕咕咕还没做的功能~~: 39 | > 40 | > - 新版战力分析 41 | > - 今日运势 42 | > - 谱师功能 43 | > 44 | > 或许等到2077年才能做出来💦💦 45 | 46 | --- 47 | > **以下内容更新于2024.7.19,过时内容如需细节请联系开发者** 48 | ## 搭建指南 49 | 50 | 如果你只是插件使用者,只要配置好文件就行了 51 | 52 | 53 | ### 文件配置 54 | 55 | ***前情提要:不难的其实,就是第一次有一点点的麻烦了...*** 56 | 57 | ***当然,后续~~可能~~会优化*** 58 | 59 | --- 60 | 61 | 首先要在**与 mcl文件夹 并列**的目录下 62 | 创建一个文件夹 `DcConfig`放入如下文件,使用如下文件结构(**注意`DcConfig`在`mcl`外面**) 63 | 64 | ``` 65 | *当然如果你没有主动配置文件夹,插件也会自己生成 66 | - root 67 | - mcl 68 | - mcl 69 | - plugins 70 | - ... 71 | - DcConfig (见下一代码块) 72 | - Images 73 | - ... 74 | ``` 75 | 76 | --- 77 | 这是DcConfig的结构(tree生成的,看不懂怪ms去) 78 | 79 | ```text 80 | DcConfig 81 | │ ApiKeys.yml 82 | │ OfficialMusicIds.json 83 | │ TokenIds.json 84 | │ UserTokens.json 85 | │ 86 | └─Images 87 | │ 88 | ├─Cover 89 | │ │ default.png 90 | │ │ 91 | │ ├─CustomImage 92 | │ │ 1011.jpg 93 | │ │ 1023.jpg 94 | │ │ 1026.jpg ... 95 | │ │ default.png 96 | │ │ 97 | │ └─OfficialImage 98 | │ 101.jpg 99 | │ 106.jpg 100 | │ 115.jpg ... 101 | │ default.png 102 | │ 103 | ├─UserRatioImage 104 | │ A.png 105 | │ B.png 106 | │ Background1.png 107 | │ C.png 108 | │ Card1.png 109 | │ Card2.png 110 | │ Card3.png 111 | │ D.png 112 | │ result.png 113 | │ S.png 114 | │ SS.png 115 | │ SSS.png 116 | │ 117 | └─UserInfoImage 118 | Background1.png 119 | Background2.png 120 | ``` 121 | 122 | ~~其实是我不会写Mirai配置文件,才把文件夹放在外面的~~ 123 | 124 | --- 125 | 126 | 以下是相关文件作用 127 | 128 | | 文件 | 类型 | 功能 | 要求 | 129 | |------------------------------|---------|--------------|------------| 130 | | `Images` | **文件夹** | 存放素材图片文件 | **手动配置** | 131 | | `UserTokens.json` | 文件 | 用于保存用户令牌 | **无需手动配置** | 132 | | `TokenIds.json` | 文件 | 用于获取二维码登录 | **手动配置** | 133 | | `ApiKeys.yml` | 文件 | 用于API令牌 | **手动配置** | 134 | | `UserCommands.json` | 文件 | 用于保存用户信息触发指令 | **无需手动配置** | 135 | 136 | #### TokenIds 137 | 138 | 用于登录时获取二维码,需要在 [舞立方制谱网站](https://danceweb.shenghuayule.com/MusicMaker/#/) 上 139 | 抓包找到一个名为`Token`的POST请求,然后多复制几个负载中的`client_id`,写入`DcConfig`里面 140 | 141 | 类似于:`client_id: yyQ6*****N4WUUQ8` 的 142 | 143 | 以**json**格式写入文件如下(星号是我加的) 144 | 145 | ```json 146 | [ 147 | "yyQ6VxqMeIL2hceWzZ******81Ru8pIE", 148 | "yyQ6VxqMeILLsdi*****SnddhlyVGcNa", 149 | "yyQ6VxqMeILneEzfVyXPFVCZo****oH3", 150 | "yyQ6VxqMeIL2h**********xNf/hHSzH", 151 | "yyQ6Vxq******zVmQuHtNAU******xmR" 152 | ] 153 | ``` 154 | 155 | 可能你会发现不管开几个标签都是一样的,可以尝试先**登录**一个二维码,再打开另一个标签页 156 | 157 | #### ApiKeys 158 | 159 | 用于**二维码识别**和**地名转经纬度** 160 | 161 | *本项目使用的是[**腾讯SDK**](https://cloud.tencent.com/)和[**高德地图**](https://lbs.amap.com/) 162 | 的API,每月限度充足且**免费**,所以请自行申请API令牌* 163 | 164 | ~~所以别偷我的Key了!!~~ 165 | 166 | --- 167 | 168 | **当然,如果有别的需求或者使用其它第三方平台SDK,请自己修改源码** 169 | 170 | ```yaml 171 | # 腾讯OCR SDK密钥 172 | tencentScannerKeys: 173 | secretId: AKIDK****TBnFXeibIm********* 174 | secretKey: HLCrQoyzrZ8Z1************ 175 | # 高德地图定位 SDK密钥 176 | gaodeMapKeys: 177 | apiKey: b1bbd99c****1172************** 178 | ``` 179 | 180 | ~~唯独这个是YML文件,因为我觉得这个最像配置文件~~ 181 | 182 | #### Images 183 | 184 | 配置文件中已含有背景图片 185 | 186 | 如果想自定义模板,需要修改`Image`类的源码 187 | 你也可以进入[即时设计](https://js.design/f/M5a8Zp)中获取本图片模板,自行设计 188 | 189 | ### 开发帮助 190 | 191 | 看不懂?翻翻源码就知道了! 192 | 193 | #### 指令功能 194 | 195 | 本机器人支持**正则指令**和**参数指令**两种指令触发 196 | 197 | 所有的指令存放在 `AllCommands` 类中,具体在**声明指令**后, 198 | 会在调用 `init()` 后,被自动保存到如下两个属性中 199 | 200 | ```java 201 | public class AllCommands { 202 | public static HashSet regexCommands = new HashSet<>(); //所有正则指令 203 | public static HashSet argsCommands = new HashSet<>(); //所有参数指令 204 | 205 | // your commands... 206 | } 207 | ``` 208 | 209 | #### 指令声明 210 | 211 | 你需要使用 `@DeclaredCommand("name")` 来声明一个`public static final`指令对象, 212 | 没有 `@DeclaredCommand("name")` 的对象不会被保存, 213 | 参数为指令名,没有实际用途,仅便于开发者,使用具体见以下实例 214 | 215 | #### 正则指令 216 | 217 | 你可以通过 `RegexCommandBuilder` 链式调用来创建一个 `RegexCommand` 对象,例如: 218 | 219 | ```java 220 | public class AllCommands { 221 | 222 | @DeclaredCommand("舞立方自制谱兑换") //指令声明 223 | public static final RegexCommand gainMusicByCode = new RegexCommandBuilder() 224 | .regex("[a-zA-Z0-9]{15}", false) 225 | .onCall(Scope.USER, (event, contact, qq, args) -> { 226 | Token token = loginDetect(contact, qq); 227 | if(token==null) return; 228 | 229 | // type your code here 230 | 231 | }).build(); 232 | } 233 | ``` 234 | 235 | --- 236 | `regex(String regex, boolean lineOnly)` 237 | 正则匹配方式,`regex`为正则表达式字符串, `lineOnly`为是否仅匹配单行,当为`true`时会默认加上 `^...$` 238 | 行匹配标识,默认为`true` 239 | 240 | `onCall(Scope scope, MsgHandleable (lambda) )` 241 | 调用指令,`scope`为作用域,`lambda`为调用指令实现体,你需要传入`(event, contact, qq, args) -> {}`,其中`args`无需实现。 242 | 243 | `build()` 244 | 构建指令,返回一个`RegexCommand`对象 245 | 246 | #### 参数指令 247 | 248 | 类似于**正则指令**,你需要使用`ArgsCommandBuilder`来创建一个`ArgsCommand`对象 249 | 250 | ```java 251 | public class AllCommands { 252 | @DeclaredCommand("查找舞立方机台") 253 | public static final ArgsCommand msgMachineList = new ArgsCommandBuilder() 254 | .prefix("查找舞立方", "查找机台", "舞立方") 255 | .form(ArgsCommand.CHAR) 256 | .onCall(Scope.GROUP, (event, contact, qq, args) -> { 257 | if(args==null) return; 258 | 259 | // type your code here... 260 | 261 | }).build(); 262 | } 263 | ``` 264 | 265 | --- 266 | `prefix(String... name)` 267 | 用于声明一个参数指令的前缀,仅当消息触发前缀后才会匹配参数 268 | 269 | `form(Pattern... patterns)` 270 | 声明参数的格式,建议使用`ArgsCommand`类提供的模板: 271 | 272 | ```java 273 | public class ArgsCommand extends AbstractCommand { 274 | // 数字 275 | public static final Pattern NUMBER = Pattern.compile("\\d+"); 276 | // 字母+数字 277 | public static final Pattern WORD = Pattern.compile("[0-9a-zA-z]+"); 278 | // 非空字符 279 | public static final Pattern CHAR = Pattern.compile("\\S+"); 280 | } 281 | ``` 282 | 283 | `onCall(Scope scope, MsgHandleable (lambda) )` 284 | 和参数指令类似,但是获取参数的值需要使用到`args`来获取(需要做非`null`判定) 285 | 286 | #### 作用域 287 | 288 | ```java 289 | public enum Scope { 290 | GLOBAL, // 全局指令 291 | USER, // 仅用户 292 | GROUP, //仅群聊 293 | ADMIN, //仅管理员(大铃) 294 | } 295 | ``` 296 | 297 | 作用域用于对不同的聊天环境触发不同的`onCall()`功能 298 | 299 | ## 一些提醒 300 | 301 | 如果真的有人需要搭建,以下是一些注意事项: 302 | 303 | - 请不要高频http请求 304 | - 请遵循开源协议 305 | - 本项目和[**广州市胜骅动漫科技有限公司**](https://arccer.com/#/home)无关 306 | 307 | ## 鸣谢 308 | - **感谢 艾鲁Bot 的API提供** ~~呜呜呜,我的寄鲁😭~~ 309 | - **感谢各个开发者的测试与帮助** 310 | 311 | -------------------------------------------------------------------------------- /WINDOWS_MARK: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LingerJAB/DanceCubeBot/dcb04a04bb8033405d1bce105ea8c75c36e7b241/WINDOWS_MARK -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.7.21' 3 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.21' 4 | id 'net.mamoe.mirai-console' version '2.16.0' 5 | } 6 | 7 | 8 | group = 'com.mirai' 9 | version = '0.1.0' 10 | 11 | mirai { 12 | jvmTarget = JavaVersion.VERSION_17 13 | } 14 | repositories { 15 | maven { url 'https://maven.aliyun.com/repository/public' } 16 | mavenCentral() 17 | } 18 | dependencies { 19 | implementation 'com.google.zxing:javase:3.5.1' 20 | implementation 'com.squareup.okhttp3:okhttp:4.12.0' 21 | implementation 'com.google.code.gson:gson:2.10.1' 22 | implementation 'net.coobird:thumbnailator:0.4.20' 23 | implementation 'com.tencentcloudapi:tencentcloud-sdk-java-ocr:3.1.764' 24 | implementation 'org.yaml:snakeyaml:2.0' 25 | implementation 'org.junit.jupiter:junit-jupiter:5.9.2' 26 | implementation 'junit:junit:4.13.2' 27 | implementation 'org.quartz-scheduler:quartz:2.3.2' 28 | implementation 'io.github.humbleui:skija-windows-x64:0.116.2' 29 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LingerJAB/DanceCubeBot/dcb04a04bb8033405d1bce105ea8c75c36e7b241/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu May 08 13:15:27 GMT+08:00 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-7.5.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' regexCommand could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the regexCommand line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = "DanceCubeBot" 9 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/api/LvRatioHistory.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.api; 2 | 3 | import com.dancecube.token.Token; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonParser; 7 | import com.tools.HttpUtil; 8 | import okhttp3.Response; 9 | 10 | import java.io.IOException; 11 | import java.text.ParseException; 12 | import java.text.SimpleDateFormat; 13 | import java.util.ArrayList; 14 | import java.util.Calendar; 15 | import java.util.Date; 16 | import java.util.Map; 17 | 18 | /** 19 | * 历史战力 20 | */ 21 | public class LvRatioHistory { 22 | private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 23 | private final Calendar calendar; 24 | 25 | public Calendar getCalendar() { 26 | return calendar; 27 | } 28 | 29 | public int getRatio() { 30 | return ratio; 31 | } 32 | 33 | private int ratio; 34 | 35 | public LvRatioHistory(Calendar calendar, int ratio) { 36 | this.calendar = calendar; 37 | this.ratio = ratio; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | 43 | return "LvRatioHistory{" + 44 | "calendar=" + format.format(calendar.getTime()) + 45 | ", ratio=" + ratio + 46 | '}'; 47 | } 48 | 49 | public static ArrayList get(Token token) { 50 | ArrayList ratioList = new ArrayList<>(); 51 | 52 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/User/GetLvRatioHistory?userId=" + token.getUserId(), 53 | Map.of("Authorization", token.getBearerToken()))) { 54 | 55 | if(response!=null && response.body()!=null) { 56 | String json = response.body().string(); 57 | 58 | JsonElement parseString = JsonParser.parseString(json); 59 | if(!parseString.isJsonArray()) { 60 | return new ArrayList<>(); 61 | } 62 | for(JsonElement element : parseString.getAsJsonArray()) { 63 | JsonObject object = element.getAsJsonObject(); 64 | try { 65 | Calendar instance = Calendar.getInstance(); 66 | Date date = format.parse(object.get("LogTime").getAsString()); 67 | instance.setTime(date); 68 | int ratio = object.get("LvRatio").getAsInt(); 69 | ratioList.add(new LvRatioHistory(instance, ratio)); 70 | } catch(ParseException e) { 71 | return new ArrayList<>(); 72 | } 73 | } 74 | } 75 | } catch(IOException e) { 76 | throw new RuntimeException(e); 77 | } 78 | return ratioList; 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/com/dancecube/api/Machine.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.api; 2 | 3 | import com.dancecube.token.Token; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParser; 6 | import com.google.gson.reflect.TypeToken; 7 | import com.tools.HttpUtil; 8 | import okhttp3.Response; 9 | 10 | import java.io.IOException; 11 | import java.lang.reflect.Type; 12 | import java.net.URLEncoder; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | public class Machine { 19 | private String placeName; 20 | private String address; 21 | private boolean show; 22 | private boolean online; 23 | 24 | 25 | public static List getMachineList(String lng, String lat) { 26 | try { 27 | Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/OAuth/GetMachineListByLocation?lng=" + lng + "&lat=" + lat); 28 | Type type = new TypeToken>() { 29 | }.getType(); 30 | String json = null; 31 | ArrayList machineList = new ArrayList<>(); 32 | if(response!=null && response.body()!=null) { 33 | json = response.body().string(); 34 | response.close(); 35 | } 36 | JsonParser.parseString(json).getAsJsonArray().forEach(element -> { 37 | Machine machine = new Machine(); 38 | JsonObject object = element.getAsJsonObject(); 39 | machine.placeName = object.get("PlaceName").getAsString(); 40 | machine.address = object.get("Address").getAsString(); 41 | machine.online = object.get("Online").getAsBoolean(); 42 | machine.show = object.get("MachineType").getAsInt()==1; 43 | machineList.add(machine); 44 | }); 45 | return machineList; 46 | } catch(IOException e) { 47 | e.printStackTrace(); 48 | } 49 | return null; 50 | } 51 | 52 | public static List getMachineList(String region) { 53 | if(region==null || region.isBlank()) return null; 54 | String json = HttpUtil.getLocationInfo(region); 55 | if(json==null) return null; 56 | String result; 57 | try { 58 | result = JsonParser.parseString(json).getAsJsonObject().get("geocodes") 59 | .getAsJsonArray().get(0).getAsJsonObject() 60 | .get("location").getAsString(); 61 | } catch(NullPointerException e) { 62 | return List.of(); 63 | } 64 | String[] location = result.split(","); 65 | return getMachineList(location[0], location[1]); 66 | } 67 | 68 | public boolean isOnline() { 69 | return online; 70 | } 71 | 72 | public String getAddress() { 73 | return address; 74 | } 75 | 76 | public String getPlaceName() { 77 | return placeName; 78 | } 79 | 80 | public boolean isShow() { 81 | return show; 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return "Machine{" + 87 | "PlaceName='" + placeName + '\'' + 88 | ", Address='" + address + '\'' + 89 | ", Online=" + online + 90 | '}'; 91 | } 92 | 93 | public static Response qrLogin(Token token, String qrUrl) { 94 | String url = "https://dancedemo.shenghuayule.com/Dance/api/Machine/AppLogin?qrCode=" 95 | + URLEncoder.encode(qrUrl, StandardCharsets.UTF_8); 96 | return HttpUtil.httpApi(url 97 | , Map.of("Authorization", token.getBearerToken())); 98 | } 99 | // 100 | // @Test 101 | // public void test() { 102 | // System.out.println(getMachineList("六安")); 103 | // } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/api/PhoneLoginBuilder.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.api; 2 | 3 | import com.dancecube.token.Token; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParser; 6 | import com.tools.HttpUtil; 7 | import okhttp3.Response; 8 | 9 | import java.io.ByteArrayInputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.util.Base64; 13 | import java.util.Map; 14 | 15 | public class PhoneLoginBuilder { 16 | private final String phoneNumber; 17 | 18 | public PhoneLoginBuilder(String phoneNumber) { 19 | this.phoneNumber = phoneNumber; 20 | } 21 | 22 | /** 23 | * 获取图形验证码 24 | * 25 | * @return 图片输入流 26 | */ 27 | public InputStream getGraphCode() { 28 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/Common/GetGraphCode?phone=" + phoneNumber)) { 29 | if(response != null && response.code() == 200) { 30 | String base64 = ""; 31 | if(response.body() != null) { 32 | base64 = response.body().string(); 33 | base64 = base64.substring(1, base64.length() - 1); 34 | } 35 | byte[] bytes = Base64.getDecoder().decode(base64); 36 | return new ByteArrayInputStream(bytes); 37 | } 38 | } catch(IOException e) { 39 | return null; 40 | } 41 | return null; 42 | } 43 | 44 | 45 | /** 46 | * 获取短信验证码 47 | * 48 | * @param graphCode 图形验证码 49 | * @return 是否成功发送 50 | */ 51 | public boolean getSMSCode(String graphCode) { 52 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/Common/GetSMSCode?phone=" + phoneNumber + "&graphCode=" + graphCode)) { 53 | return (response != null && response.code() == 200); 54 | } 55 | } 56 | 57 | /** 58 | * 手机号登录 59 | * 60 | * @param smsCode 短信验证码 61 | * @return Token令牌 62 | */ 63 | public Token login(String smsCode) { 64 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/token", 65 | Map.of("content-type", "application/x-www-form-urlencoded"), 66 | Map.of("client_type", "phone", "grant_type", "client_credentials", "client_id", phoneNumber, "client_secret", smsCode) 67 | )) { 68 | if(response.body() != null && response.code() == 200) { 69 | JsonObject json = JsonParser.parseString(response.body().string()).getAsJsonObject(); 70 | return new Token(json.get("userId").getAsInt(), 71 | json.get("access_token").getAsString(), 72 | json.get("refresh_token").getAsString(), 73 | System.currentTimeMillis()); 74 | } 75 | } catch(IOException e) { 76 | return null; 77 | } 78 | return null; 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/com/dancecube/api/PlayerMusic.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.api; 2 | 3 | import com.dancecube.token.Token; 4 | import com.tools.HttpUtil; 5 | import okhttp3.Response; 6 | 7 | import java.util.Map; 8 | 9 | public class PlayerMusic { 10 | 11 | public static Response gainMusicByCode(Token token, String code) { 12 | return HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/MusicData/GainMusicByCode?code=" + code, 13 | Map.of("Authorization", token.getBearerToken()), 14 | null); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/image/LastPlayImage.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.image; 2 | 3 | import com.dancecube.info.UserInfo; 4 | import com.dancecube.music.CoverUtil; 5 | import com.dancecube.ratio.AccGrade; 6 | import com.dancecube.ratio.RatioCalculator; 7 | import com.dancecube.ratio.RecentMusicInfo; 8 | import com.dancecube.token.Token; 9 | import io.github.humbleui.skija.*; 10 | import io.github.humbleui.types.Rect; 11 | 12 | import java.io.ByteArrayInputStream; 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.net.MalformedURLException; 17 | import java.net.URL; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.nio.file.StandardOpenOption; 21 | import java.util.List; 22 | 23 | import static com.mirai.config.AbstractConfig.configPath; 24 | 25 | public class LastPlayImage { 26 | public static final String path = configPath + "Images/LastPlayImage/"; 27 | public static final String savePath = configPath + "Images/result.png"; 28 | 29 | public static final Image LV_A; 30 | public static final Image LV_B; 31 | public static final Image LV_C; 32 | public static final Image LV_D; 33 | public static final Image LV_S; 34 | public static final Image LV_S_AP; 35 | public static final Image LV_FC; 36 | public static final Image LV_AP; 37 | public static final Image NEW_RECORD; 38 | public static final Image BACKGROUND; 39 | public static final Image AVATAR_BOX; 40 | public static final Image COVER_BOX; 41 | public static final Image SONG_BOX; 42 | 43 | public static final Typeface scoreFace; 44 | public static final Typeface titleFace; 45 | 46 | static { 47 | try { 48 | LV_A = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "A.png"))); 49 | LV_B = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "B.png"))); 50 | LV_C = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "C.png"))); 51 | LV_D = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "D.png"))); 52 | LV_S = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "S.png"))); 53 | LV_S_AP = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "S_AP.png"))); 54 | LV_FC = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "FC.png"))); 55 | LV_AP = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "AP.png"))); 56 | 57 | NEW_RECORD = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "newRec.png"))); 58 | BACKGROUND = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "background.png"))); 59 | AVATAR_BOX = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "avatarBox.png"))); 60 | COVER_BOX = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "coverBox.png"))); 61 | SONG_BOX = Image.makeDeferredFromEncodedBytes(Files.readAllBytes(Path.of(path + "songBanner.png"))); 62 | 63 | scoreFace = Typeface.makeFromFile(configPath + "Fonts/PangMenZhengDaoBiaoTiTi.ttf"); 64 | titleFace = Typeface.makeFromFile(configPath + "Fonts/SourceHanSans-Bold.otf"); 65 | } catch(IOException e) { 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | 70 | /** 71 | * 生成游玩成绩图片 72 | * 73 | * @param token 用户Token 74 | * @param info 游玩记录 75 | * @return 成绩图片 76 | */ 77 | public static InputStream generate(Token token, RecentMusicInfo info) { 78 | UserInfo user = UserInfo.get(token); 79 | 80 | Image backgroundImage = BACKGROUND; 81 | 82 | // 创建以图片为基础的 Surface 83 | Surface surface = Surface.makeRaster(ImageInfo.makeN32( 84 | backgroundImage.getWidth(), 85 | backgroundImage.getHeight(), 86 | ColorAlphaType.UNPREMUL 87 | )); 88 | Canvas canvas = surface.getCanvas(); 89 | canvas.drawImage(backgroundImage, 0, 0); 90 | Font scoreFont = new Font(scoreFace, 95); 91 | Font titleFont = new Font(titleFace, 50); 92 | Paint plainPaint = new Paint().setAntiAlias(true); // 公共,节约内存(我讨厌new Paint() 93 | 94 | // 绘制封面,曲名,单曲战力 95 | Rect coverRect = Rect.makeXYWH(41, 202, 127, 127); 96 | Rect coverboxRect = Rect.makeXYWH(30, 191, 149, 149); 97 | byte[] coverBytes = CoverUtil.getCoverBytesOrDefault(info.getId()); 98 | canvas.drawImageRect(Image.makeDeferredFromEncodedBytes(coverBytes), coverRect); 99 | canvas.drawImageRect(COVER_BOX, coverboxRect); 100 | 101 | canvas.drawString(info.getName(), 175, 247, titleFont, plainPaint.setColor(0xFFFFFFFF)); 102 | canvas.drawString("Rating: " + info.getRatioInt(), 175 + 10, 300, titleFont.setSize(30), plainPaint.setColor(0xFFFFFFFF)); 103 | 104 | // 绘制头像昵称,战队,积分,战力,日期 105 | Rect avatarRect = Rect.makeXYWH(612 + 3, 880 + 3, 120, 120); 106 | URL avatarUrl = null; 107 | try { 108 | avatarUrl = new URL(user.getHeadimgURL()); 109 | try(InputStream is = avatarUrl.openStream()) { 110 | byte[] avatarBytes = is.readAllBytes(); 111 | canvas.drawImageRect(Image.makeDeferredFromEncodedBytes(avatarBytes), avatarRect); 112 | } catch(Exception e) { 113 | e.printStackTrace(); 114 | } 115 | } catch(MalformedURLException e) { 116 | throw new RuntimeException(e); 117 | } 118 | 119 | Rect boxRect = Rect.makeXYWH(612, 880, 128, 128); 120 | canvas.drawImageRect(AVATAR_BOX, boxRect); 121 | titleFont.setSize(25); 122 | canvas.drawString(user.getUserName(), 857, 903, titleFont, plainPaint.setColor(0xFFDBDC64)); 123 | canvas.drawString(user.getTeamName(), 857, 903 + 40, titleFont, plainPaint.setColor(0xFF81C8DA)); 124 | canvas.drawString(String.valueOf(user.getMusicScore()), 857 + 20, 903 + 80, titleFont, plainPaint.setColor(0xFFD5BC53)); 125 | canvas.drawString(String.valueOf(user.getLvRatio()), 857 + 20, 903 + 120, titleFont, plainPaint.setColor(0xFFE93B63)); 126 | scoreFont.setSize(26); 127 | drawTextGlow(canvas, reformatDate(info.getRecordTime()), 610, 1055, scoreFont, 0xFF8C57F1, 3); 128 | canvas.drawString(reformatDate(info.getRecordTime()), 610, 1055, scoreFont.setSize(26), plainPaint.setColor(0xFFFFFFFF).setAntiAlias(true)); 129 | 130 | 131 | // 绘制难度等级 132 | drawLevel(canvas, info.getLevelType(), info.getLevel(), 445, 304, scoreFont); 133 | 134 | // 绘制精确度 135 | drawAccuracy(canvas, info.getAccuracy(), 160, 463, 0xFFFAE425, scoreFont); 136 | 137 | // 绘制评级 138 | drawAccGrade(canvas, info.getAccGrade(), 15, 477); 139 | 140 | // 绘制 FC/AP 141 | if(info.isFullCombo()) { 142 | canvas.drawImage(LV_FC, 700 + 5, 727); 143 | if(!info.isAllPerfect()) { 144 | canvas.drawImage(LV_AP, 840 + 9, 727); 145 | } 146 | } 147 | 148 | 149 | // Todo Font放到内存 150 | // 绘制判断结果:MaxCombo Perfect Great Good Miss Score 151 | scoreFont.setSkewX(-0.1f).setSize(40); 152 | drawJudgments(canvas, spacedIntFrom(info.getCombo()), 285, 773, 0xFFFAE425, scoreFont); 153 | drawJudgments(canvas, spacedIntFrom(info.getPerfect()), 285, 773 + 45, 0xFFEE58FB, scoreFont); 154 | drawJudgments(canvas, spacedIntFrom(info.getGreat()), 285, 773 + 45 * 2, 0xFF82FA2D, scoreFont); 155 | drawJudgments(canvas, spacedIntFrom(info.getGood()), 285, 773 + 45 * 3, 0xFF1DC9FA, scoreFont); 156 | drawJudgments(canvas, spacedIntFrom(info.getMiss()), 285, 773 + 45 * 4, 0xFFF0442D, scoreFont); 157 | scoreFont.setSkewX(0f).setSize(45); 158 | drawJudgments(canvas, String.valueOf(info.getScore()), 280, 998 + 18, 0xFF7DD6E6, scoreFont); 159 | 160 | Image resultImage = surface.makeImageSnapshot(); 161 | surface.close(); 162 | return new ByteArrayInputStream(EncoderPNG.encode(resultImage).getBytes()); 163 | } 164 | 165 | public static void main(String[] args) throws IOException { 166 | Token token = new Token(939088, "xvp1mVzCtOYJ4dQ-l9ujOWDNwU4pFkoXsnrw3gIZwukJNo61wDNlhadDTBcLm5wYWezhVnm_zAGXabtplhGATWa2-pDdiSQ_-HLiozBnLX81drC3vkIEisTGfH2jbh2V7h2icD2hOjGC0pSEPYWA4Miv_l59_k3J8DUdV8n9PE5kI6A5_dUQ2rXXL7PWHdJPlYQGcFKUz9Q56ctai5u761p1gs6s4D3pVllbNJiG3_OhKxGB7M9GiDVcPBhCzd88ZnMddYVmgTtXW3vhrmME7mbtfM0lmP0WVQRmA0cdZwIWXd1JeyDH0d186syuuch2qIM3TTdz1FgBXbCpsU_ZpSUFdWapFyQNQXrgb5kUAEv3o2MHGQ2hzxAXZA9RdnNqb"); 167 | 168 | Path path = new File(savePath).toPath(); 169 | List allRecentList = RatioCalculator.getAllRecentList(token.getBearerToken()); 170 | 171 | byte[] bytes = generate(token, allRecentList.get(9)).readAllBytes(); 172 | Files.write(path, bytes, StandardOpenOption.WRITE); 173 | System.out.println("Done!"); 174 | } 175 | // Todo 绘制新记录 176 | 177 | private static void drawNewRec(Canvas canvas, RecentMusicInfo info, float x, float y) { 178 | 179 | } 180 | 181 | // 我讨厌封装 182 | private static String spacedIntFrom(int i) { 183 | if(0 <= i & i <= 9) return String.valueOf(i); 184 | return String.valueOf(i).replaceAll("", " ").trim(); 185 | } 186 | 187 | private static String reformatDate(String ori) { 188 | return ori.replace('-', '.').substring(0, 16); 189 | } 190 | 191 | private static void drawLevel(Canvas canvas, int levelType, int level, float x, float y, Font font) { 192 | font.setSize(40); 193 | int textColor = switch(levelType) { 194 | case 101 -> 0xFF19BCFD; 195 | case 102 -> 0xFF5EC31E; 196 | case 103 -> 0xFFB988FA; 197 | case 104 -> 0xFFFD9EA8; 198 | case 105 -> 0xFFFDC067; 199 | default -> 0x00; 200 | }; 201 | String text = switch(levelType) { 202 | case 101 -> "基础"; 203 | case 102 -> "进阶"; 204 | case 103 -> "专家"; 205 | case 104 -> "大师"; 206 | case 105 -> "传奇"; 207 | default -> ""; 208 | }; 209 | drawTextStroke(canvas, text, x, y, font, 0xFFFFFFFF, textColor, 8); 210 | drawTextGlow(canvas, text, x, y, font, 0x77000000, 1f); 211 | 212 | font.setSize(20); 213 | drawTextStroke(canvas, "LV.", x + 86, y, font, 0xFFFFFFFF, 0xFF473581, 8); 214 | font.setSize(50); 215 | drawTextStroke(canvas, String.valueOf(level), x + 121, y, font, 0xFFFFFFFF, 0xFF473581, 8); 216 | } 217 | 218 | private static void drawJudgments(Canvas canvas, String score, float x, float y, int color, Font font) { 219 | drawTextGlow(canvas, score, x, y, font, color, 8f); 220 | drawTextGradient(canvas, score, x, y, font, color, 0xFFFFFFFF); 221 | } 222 | 223 | private static void drawAccuracy(Canvas canvas, float accuracy, float x, float y, int color, Font font) { 224 | font.setSize(95); 225 | if(accuracy == 100f) x -= 10; 226 | String text = "%.2f".formatted(accuracy); 227 | drawTextGlow(canvas, text, x, y, font, color, 5f); 228 | drawTextGradient(canvas, text, x, y, font, color, 0xFFFDF5AB); 229 | 230 | float width = font.measureTextWidth(text); 231 | font.setSize(72f); 232 | drawTextGlow(canvas, "%", x + width + 6, y, font, color, 5f); 233 | drawTextGradient(canvas, "%", x + width + 6, y, font, color, 0xFFFDF5AB); 234 | } 235 | 236 | private static void drawTextStroke(Canvas canvas, String text, float x, float y, Font font, int color, int strokeColor, int strokeWidth) { 237 | Paint strokePaint = new Paint().setColor(strokeColor); 238 | strokePaint.setStroke(true).setStrokeJoin(PaintStrokeJoin.ROUND); // 圆角描边 239 | strokePaint.setStrokeWidth(strokeWidth); 240 | strokePaint.setAntiAlias(true); 241 | 242 | canvas.drawString(text, x, y, font, strokePaint); 243 | Paint fillPaint = new Paint().setColor(color); 244 | fillPaint.setAntiAlias(true); 245 | 246 | canvas.drawString(text, x, y, font, fillPaint); 247 | } 248 | 249 | private static void drawTextGlow(Canvas canvas, String text, float x, float y, Font font, int glowColor, float blurRadius) { 250 | Paint glowPaint = new Paint() 251 | .setColor(glowColor) 252 | .setMaskFilter(MaskFilter.makeBlur(FilterBlurMode.OUTER, blurRadius)); 253 | canvas.drawString(text, x, y, font, glowPaint); 254 | } 255 | 256 | private static void drawTextGradient(Canvas canvas, String text, float x, float y, Font font, int topColor, int bottomColor) { 257 | // 获取字体真实高度(使用 metrics) 258 | FontMetrics metrics = font.getMetrics(); 259 | float ascent = Math.abs(metrics.getAscent()); // ascent 通常为负数 260 | float descent = metrics.getDescent(); 261 | float textHeight = ascent + descent; 262 | 263 | Paint gradientPaint = new Paint().setShader( 264 | Shader.makeLinearGradient( 265 | x, y - ascent, // 渐变从文字顶部(注意:y 是 baseline,所以顶部是 y - ascent) 266 | x, y, // 到文字中心 267 | new int[]{topColor, bottomColor}, 268 | null, 269 | GradientStyle.DEFAULT 270 | ) 271 | ); 272 | canvas.drawString(text, x, y, font, gradientPaint); 273 | } 274 | 275 | private static void drawAccGrade(Canvas canvas, AccGrade grade, float x, float y) { 276 | if(grade.getMinAcc() <= AccGrade.S.getMinAcc()) { 277 | canvas.drawImage(getAccGradeImage(grade), x + 100, y); 278 | return; 279 | } 280 | // 反正至少要画一个 281 | canvas.drawImage(getAccGradeImage(grade), x, y); 282 | canvas.drawImage(getAccGradeImage(grade), x + 117, y); 283 | if(grade.getMinAcc() >= AccGrade.SSS.getMinAcc()) { 284 | canvas.drawImage(getAccGradeImage(grade), x + 234, y); 285 | } 286 | } 287 | 288 | private static Image getAccGradeImage(AccGrade grade) { 289 | return switch(grade) { 290 | case SSS_AP -> LV_S_AP; 291 | case S, SS, SSS -> LV_S; 292 | case A -> LV_A; 293 | case B -> LV_B; 294 | case C -> LV_C; 295 | default -> LV_D; 296 | }; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/image/NewUserRatioImage.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.image; 2 | 3 | import com.dancecube.token.Token; 4 | 5 | import javax.imageio.ImageIO; 6 | import java.awt.image.BufferedImage; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | import static com.mirai.config.AbstractConfig.configPath; 12 | 13 | public class NewUserRatioImage { 14 | public static final BufferedImage CARD_1; 15 | public static final BufferedImage CARD_2; 16 | public static final BufferedImage CARD_3; 17 | public static final BufferedImage CARD_4; 18 | public static final BufferedImage CARD_5; 19 | 20 | public static final BufferedImage LV_SSS_AP; 21 | public static final BufferedImage LV_SSS; 22 | public static final BufferedImage LV_SS; 23 | public static final BufferedImage LV_S; 24 | public static final BufferedImage LV_A; 25 | public static final BufferedImage LV_B; 26 | public static final BufferedImage LV_C; 27 | public static final BufferedImage LV_D; 28 | 29 | public static String path = configPath + "Images/NewUserRatioImage/"; 30 | 31 | static { 32 | try { 33 | // 素材缓存到内存 34 | CARD_1 = ImageIO.read(new File(path + "Card1.png")); 35 | CARD_2 = ImageIO.read(new File(path + "Card2.png")); 36 | CARD_3 = ImageIO.read(new File(path + "Card3.png")); 37 | CARD_4 = ImageIO.read(new File(path + "Card4.png")); 38 | CARD_5 = ImageIO.read(new File(path + "Card5.png")); 39 | LV_SSS_AP = ImageIO.read(new File(path + "SSS_AP.png")); 40 | LV_SSS = ImageIO.read(new File(path + "SSS.png")); 41 | LV_SS = ImageIO.read(new File(path + "SS.png")); 42 | LV_S = ImageIO.read(new File(path + "S.png")); 43 | LV_A = ImageIO.read(new File(path + "A.png")); 44 | LV_B = ImageIO.read(new File(path + "B.png")); 45 | LV_C = ImageIO.read(new File(path + "C.png")); 46 | LV_D = ImageIO.read(new File(path + "D.png")); 47 | } catch(IOException e) { 48 | throw new RuntimeException(e); 49 | } 50 | 51 | } 52 | 53 | 54 | public static InputStream generate(Token token) { 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/image/UserInfoImage.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.image; 2 | 3 | 4 | import com.dancecube.info.AccountInfo; 5 | import com.dancecube.info.InfoStatus; 6 | import com.dancecube.info.ReplyItem; 7 | import com.dancecube.info.UserInfo; 8 | import com.dancecube.token.Token; 9 | import com.tools.image.ImageDrawer; 10 | import com.tools.image.TextEffect; 11 | import org.junit.Test; 12 | 13 | import java.awt.*; 14 | import java.io.InputStream; 15 | import java.util.concurrent.ExecutionException; 16 | import java.util.concurrent.Future; 17 | 18 | import static com.mirai.command.AllCommands.scheduler; 19 | import static com.mirai.config.AbstractConfig.configPath; 20 | import static com.mirai.config.AbstractConfig.itIsAReeeeaaaalWindowsMark; 21 | 22 | 23 | public class UserInfoImage { 24 | /** 25 | * 生成个人信息图,可能是用户查询别人账号 26 | * 27 | * @param token 发起查看信息的用户 28 | * @param id 需要查看的信息目标 29 | */ 30 | public static InputStream generate(Token token, int id) { 31 | //todo 随机背景 32 | String bgPath = "file:" + configPath + "Images/UserInfoImage/Background2.png"; 33 | 34 | UserInfo userInfo = UserInfo.get(token, id); 35 | // 不存在查询的id 36 | if(userInfo.getStatus()==InfoStatus.NONEXISTENT) return null; 37 | 38 | //不存在用户 39 | if(userInfo.getHeadimgURL()==null) return null; 40 | 41 | ImageDrawer drawer = new ImageDrawer(bgPath); 42 | drawer.setAntiAliasing(); 43 | drawer.drawImage(ImageDrawer.read(userInfo.getHeadimgURL()), 120, 150, 137, 137); 44 | 45 | if(!userInfo.getHeadimgBoxPath().equals("")) // 头像框校验 46 | drawer.drawImage(ImageDrawer.read(userInfo.getHeadimgBoxPath()), 74, 104, 230, 230); 47 | if(!userInfo.getTitleUrl().equals("")) // 头衔校验 48 | drawer.drawImage(ImageDrawer.read(userInfo.getTitleUrl()), 108, 300, 161, 68); 49 | 50 | Font font = new Font("得意黑", Font.PLAIN, 36); 51 | Font font2 = new Font("得意黑", Font.PLAIN, 20); 52 | TextEffect effect = new TextEffect().setMaxWidth(235).setSpaceHeight(0); 53 | drawer.font(font); 54 | //信息开放 55 | if(userInfo.getStatus()!=InfoStatus.PRIVATE) { 56 | String gold = "不可见"; 57 | String playedTimes = "不可见"; 58 | if(token.getUserId()==id) { 59 | ReplyItem replyItem; 60 | AccountInfo accountInfo; 61 | 62 | // 异步获取个人信息 63 | if(itIsAReeeeaaaalWindowsMark()) { 64 | accountInfo = AccountInfo.get(token); 65 | replyItem = ReplyItem.get(token); 66 | } else { 67 | try { 68 | Future replyItemFuture = scheduler.async(() -> ReplyItem.get(token)); 69 | Future accountInfoFuture = scheduler.async(() -> AccountInfo.get(token)); 70 | replyItem = replyItemFuture.get(); 71 | accountInfo = accountInfoFuture.get(); 72 | } catch(ExecutionException | InterruptedException e) { 73 | accountInfo = AccountInfo.get(token); 74 | replyItem = ReplyItem.get(token); 75 | } 76 | } 77 | 78 | gold = String.valueOf(accountInfo.getGold()); 79 | playedTimes = String.valueOf(replyItem.getPlayedTimes()); 80 | 81 | 82 | } 83 | drawer.drawText("%s\n\n战队:%s\n战力:%d\n金币:%s" 84 | .formatted(userInfo.getUserName(), 85 | userInfo.getTeamName().equals("") ? "无" : userInfo.getTeamName(), 86 | userInfo.getLvRatio(), 87 | gold), 293, 137, effect) 88 | .drawText("积分:%s\n全连率:%.2f%%\n全国排名:%d\n游玩次数:%s" 89 | .formatted(userInfo.getMusicScore(), 90 | (float) userInfo.getComboPercent() / 100, 91 | userInfo.getRankNation(), 92 | playedTimes), 106, 472, effect) 93 | .font(font2) 94 | .drawText("ID:" + userInfo.getUserID(), 293, 170); 95 | } else { //屏蔽 96 | drawer.drawText("%s\n\n地区:%s\n战力:%d" 97 | .formatted(userInfo.getUserName(), 98 | userInfo.getCityName().equals("") ? "无" : userInfo.getCityName(), 99 | userInfo.getLvRatio()), 293, 137, effect) 100 | .drawText("该账号已设置隐私", 106, 472) 101 | .font(font2) 102 | .drawText("ID:" + userInfo.getUserID(), 293, 170); 103 | 104 | } 105 | drawer.dispose(); 106 | return drawer.getImageStream("PNG"); 107 | } 108 | 109 | @Test 110 | public void test() { 111 | Token token = new Token(939800, "Tbp4C0QAJeIsqNcJx7psKoYAxFNsD0qH68qutYCZod8ybPiRoEJ05RZhHzy4LPQDtw3tJvKYqSkCpnEd-qrg-c7MMY7DwQecXF3-uuU-6qDd7zIQ7IpfTHbcVHvN_st9XnVCyt9op0b6CYFY3nTvNH1F4aidP5M-P-MXes3-TIH80YHN8zHgua_XjgFWfi0loubYS0KW9APsB0POsoaBmeJz-85ZxnqlOdzUkW7cb9vGPzgQvP7adZPa6igEfynpx1YXTthssnhGyjKdMSQnKkR2Zhmx4zdbwo9N1eTDoAv0ZuNZ9-29gSirqGHbwRS-GXPnXG4mGLvdMuRWY1OKuLk1HWvV-AsceOuMvZX9vin0BcxGDKmK4axbU8kRQkx-"); 112 | String path = "C:\\Users\\Lin\\IdeaProjects\\DanceCubeBot\\DcConfig\\Images\\result.png"; 113 | ImageDrawer.write(generate(token, 939088), path); 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/image/UserRatioImage.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.image; 2 | 3 | import com.dancecube.api.LvRatioHistory; 4 | import com.dancecube.info.UserInfo; 5 | import com.dancecube.music.CoverUtil; 6 | import com.dancecube.ratio.AccGrade; 7 | import com.dancecube.ratio.RankMusicInfo; 8 | import com.dancecube.ratio.RatioCalculator; 9 | import com.dancecube.ratio.RecentMusicInfo; 10 | import com.dancecube.token.Token; 11 | import com.tools.image.ImageDrawer; 12 | import com.tools.image.ImageEffect; 13 | import com.tools.image.TextEffect; 14 | import net.coobird.thumbnailator.Thumbnails; 15 | import org.junit.Test; 16 | 17 | import javax.imageio.ImageIO; 18 | import java.awt.*; 19 | import java.awt.image.BufferedImage; 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.net.URL; 24 | import java.text.DateFormat; 25 | import java.util.Calendar; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.concurrent.*; 29 | 30 | import static com.mirai.config.AbstractConfig.configPath; 31 | import static com.mirai.config.AbstractConfig.itIsAReeeeaaaalWindowsMark; 32 | 33 | public class UserRatioImage { 34 | public static final BufferedImage CARD_1; //低级 35 | public static final BufferedImage CARD_2; //中级 36 | public static final BufferedImage CARD_3; //高级 37 | 38 | public static final BufferedImage LV_SSS; 39 | public static final BufferedImage LV_SS; 40 | public static final BufferedImage LV_S; 41 | public static final BufferedImage LV_A; 42 | public static final BufferedImage LV_B; 43 | public static final BufferedImage LV_C; 44 | public static final BufferedImage LV_D; 45 | public static String path = configPath + "Images/UserRatioImage/"; 46 | 47 | 48 | static { 49 | try { 50 | // 素材缓存到内存 51 | CARD_1 = ImageIO.read(new File(path + "Card1.png")); 52 | CARD_2 = ImageIO.read(new File(path + "Card2.png")); 53 | CARD_3 = ImageIO.read(new File(path + "Card3.png")); 54 | LV_SSS = ImageIO.read(new File(path + "SSS.png")); 55 | LV_SS = ImageIO.read(new File(path + "SS.png")); 56 | LV_S = ImageIO.read(new File(path + "S.png")); 57 | LV_A = ImageIO.read(new File(path + "A.png")); 58 | LV_B = ImageIO.read(new File(path + "B.png")); 59 | LV_C = ImageIO.read(new File(path + "C.png")); 60 | LV_D = ImageIO.read(new File(path + "D.png")); 61 | } catch(IOException e) { 62 | throw new RuntimeException(e); 63 | } 64 | 65 | } 66 | 67 | public static InputStream generate(Token token) { 68 | // 个人信息 69 | UserInfo info; 70 | List ratioList; 71 | // if(!itIsAReeeeaaaalWindowsMark()) { 72 | info = UserInfo.get(token); 73 | ratioList = LvRatioHistory.get(token); 74 | // } else { 75 | // CompletableFuture userInfoFuture = CompletableFuture.supplyAsync(() -> UserInfo.get(token)); 76 | // CompletableFuture> ratioFuture = CompletableFuture.supplyAsync(() -> LvRatioHistory.get(token)); 77 | // try { 78 | // info = userInfoFuture.get(); 79 | // ratioList = ratioFuture.get(); 80 | // } catch(ExecutionException | InterruptedException e) { 81 | // info = UserInfo.get(token); 82 | // ratioList = LvRatioHistory.get(token); 83 | // } 84 | // } 85 | 86 | ImageDrawer drawer; 87 | try { 88 | // 为什么这里放个finalInfo? 89 | final UserInfo finalInfo = info; 90 | if(info==null) { 91 | System.err.println("# 战力分析时个人信息获取失败"); 92 | } 93 | 94 | // 获取背景图片 95 | CompletableFuture backgroundImgFuture = CompletableFuture.supplyAsync(() -> { 96 | try { 97 | // TODO 背景图片写到常量区(内存) 98 | return ImageIO.read(new File(path + "Background1.png")); 99 | } catch(IOException e) { 100 | throw new RuntimeException(e); 101 | } 102 | }); 103 | 104 | // 个人信息 头像/头像框/头衔 异步获取 105 | CompletableFuture avatarFuture = CompletableFuture.supplyAsync(() -> { 106 | try { 107 | if(finalInfo.getHeadimgURL().isEmpty()) return null; 108 | return ImageIO.read(new URL(finalInfo.getHeadimgURL())); 109 | } catch(IOException e) { 110 | return null; 111 | } 112 | }); 113 | 114 | CompletableFuture boxFuture = CompletableFuture.supplyAsync(() -> { 115 | try { 116 | if(finalInfo.getHeadimgBoxPath().isEmpty()) return null; 117 | return ImageIO.read(new URL(finalInfo.getHeadimgBoxPath())); 118 | } catch(IOException e) { 119 | return null; 120 | } 121 | }); 122 | 123 | CompletableFuture titleFuture = CompletableFuture.supplyAsync(() -> { 124 | if(finalInfo.getTitleUrl().isEmpty()) return null; 125 | try { 126 | return ImageIO.read(new URL(finalInfo.getTitleUrl())); 127 | } catch(IOException e) { 128 | return null; 129 | } 130 | }); 131 | 132 | //异步阻塞完绘制战力图 133 | drawer = new ImageDrawer(backgroundImgFuture.get()); 134 | drawer.setAntiAliasing(); // 抗锯齿 135 | 136 | CompletableFuture.allOf(avatarFuture, boxFuture, titleFuture).join(); 137 | if(avatarFuture.get()!=null) drawer.drawImage(avatarFuture.get(), 34, 180, 174, 174); 138 | if(boxFuture.get()!=null) drawer.drawImage(boxFuture.get(), -24, 122, 290, 290); 139 | if(titleFuture.get()!=null) drawer.drawImage(titleFuture.get(), 28, 373, 186, 79); 140 | } catch(InterruptedException | ExecutionException e) { 141 | throw new RuntimeException(e); 142 | } 143 | 144 | int lvRatio = info.getLvRatio(); 145 | String userInfoText = """ 146 | %s 147 | 148 | 战队:%s 149 | 排名:%d 150 | 战力:%d""".formatted(info.getUserName(), info.getTeamName(), info.getRankNation(), lvRatio); 151 | Font infoFont = new Font("得意黑", Font.PLAIN, 45); 152 | Font idFont = new Font("得意黑", Font.PLAIN, 30); 153 | drawer.color(Color.BLACK).font(idFont).drawText("ID: " + token.getUserId(), 245, 200) 154 | .font(infoFont).drawText(userInfoText, 245, 160, new TextEffect().setMaxWidth(230).setSpaceHeight(0)); 155 | 156 | 157 | // 异步获取两个列表 158 | List allRankList; 159 | List allRecentList; 160 | if(itIsAReeeeaaaalWindowsMark()) { // Windows下执行异步 161 | CompletableFuture> rankMusicFuture = CompletableFuture.supplyAsync(() -> RatioCalculator.getAllRankList(token.getBearerToken())); 162 | CompletableFuture> recentMusicFuture = CompletableFuture.supplyAsync(() -> RatioCalculator.getAllRecentList(token.getBearerToken())); 163 | try { 164 | allRankList = rankMusicFuture.get(); 165 | allRecentList = recentMusicFuture.get(); 166 | } catch(ExecutionException | InterruptedException e) { 167 | allRankList = RatioCalculator.getAllRankList(token.getBearerToken()); 168 | allRecentList = RatioCalculator.getAllRecentList(token.getBearerToken()); 169 | } 170 | } else { // 171 | allRankList = RatioCalculator.getAllRankList(token.getBearerToken()); 172 | allRecentList = RatioCalculator.getAllRecentList(token.getBearerToken()); 173 | } 174 | List rank15List = RatioCalculator.getSubRank15List(allRankList, true); 175 | List recent15List = RatioCalculator.getSubRecent15List(allRecentList, false); 176 | 177 | // TODO 这里要改 178 | boolean stopDownloading = false; 179 | if(stopDownloading) { 180 | //准备自制谱封面id下载列表 181 | HashSet waitingCoversSet = new HashSet<>(); 182 | for(RankMusicInfo value : rank15List) { 183 | if(CoverUtil.isCoverAbsent(value.getId())) waitingCoversSet.add(value.getId()); 184 | } 185 | for(RecentMusicInfo value : recent15List) { 186 | if(CoverUtil.isCoverAbsent(value.getId())) waitingCoversSet.add(value.getId()); 187 | } 188 | 189 | //多线程下载不存在的封面 190 | ExecutorService threadPool = Executors.newCachedThreadPool(); 191 | CountDownLatch latch = new CountDownLatch(waitingCoversSet.size()); 192 | waitingCoversSet.forEach(id -> threadPool.submit(() -> { 193 | CoverUtil.downloadCover(id); 194 | latch.countDown(); 195 | })); 196 | threadPool.shutdown(); 197 | try { 198 | latch.await(); 199 | } catch(InterruptedException e) { 200 | throw new RuntimeException(e); 201 | } 202 | } 203 | 204 | 205 | // B15绘制 206 | int index = 0; 207 | int dx = 395, dy = 180; //x y延伸长度 208 | Font titleFont = new Font("Microsoft YaHei UI", Font.BOLD, 32); 209 | Font scoreFont = new Font("庞门正道标题体", Font.PLAIN, 52); 210 | Font comboMissAccFont = new Font("庞门正道标题体", Font.PLAIN, 15); 211 | Font levelFont = new Font("庞门正道标题体", Font.PLAIN, 23); 212 | 213 | out: 214 | for(int row = 0; row<5; row++) { //列 215 | for(int col = 0; col<3; col++, index++) { //行 216 | if(index>=rank15List.size()) break out; 217 | 218 | int dx2 = col * dx; 219 | int dy2 = row * dy; 220 | RankMusicInfo musicInfo = rank15List.get(index); 221 | BufferedImage cover = CoverUtil.getCoverOrDefault(musicInfo.getId()); 222 | BufferedImage card = getCardImage(musicInfo.getDifficulty()); 223 | BufferedImage grade = getGradeImage(musicInfo.getAccGrade()); 224 | int fix = switch(musicInfo.getAccGrade()) { 225 | case SSS, C -> 0; 226 | case SS -> -17; 227 | case S -> -6; 228 | default -> 5;// case A B D 229 | }; 230 | ImageEffect effect = new ImageEffect().setArc(35); 231 | 232 | // 战力 >xxxx(+/- xx) 233 | String diff = musicInfo.getRatioInt() > lvRatio 234 | ? "+" + (musicInfo.getRatioInt() - lvRatio) 235 | : String.valueOf(musicInfo.getRatioInt() - lvRatio); 236 | drawer.drawImage(cover, 16 + dx2, 621 + dy2, 130, 158, effect) 237 | .drawImage(card, 15 + dx2, 620 + dy2) 238 | .drawImage(grade, 285 + fix + dx2, 715 + dy2) 239 | .font(titleFont, Color.BLACK) 240 | .drawText(musicInfo.getName(), 160 + dx2, 624 + dy2, new TextEffect().setMaxWidth(220)) 241 | .font(scoreFont).drawText(String.valueOf(musicInfo.getScore()), 160 + dx2, 646 + dy2) 242 | .font(comboMissAccFont) 243 | .drawText("%d\n%d\n%.2f%%".formatted(musicInfo.getCombo(), musicInfo.getMiss(), musicInfo.getAccuracy()), 230 + dx2, 725 + dy2, 244 | new TextEffect().setSpaceHeight(1)) 245 | .drawText("> %d (%s)".formatted(musicInfo.getRatioInt(), diff), 163 + dx2, 702 + dy2) 246 | .font(levelFont, Color.WHITE) 247 | .drawText(String.valueOf(musicInfo.getLevel()), 17 + dx2, 747 + dy2); 248 | } 249 | } 250 | 251 | // R15绘制 252 | index = 0; 253 | a: 254 | for(int row = 0; row<5; row++) { //列 255 | for(int col = 0; col<3; col++, index++) { //行 256 | int dx2 = col * dx; 257 | int dy2 = row * dy + 1065; 258 | RecentMusicInfo musicInfo; 259 | 260 | if(index>=recent15List.size()) break a; 261 | musicInfo = recent15List.get(index); 262 | BufferedImage cover = CoverUtil.getCoverOrDefault(musicInfo.getId()); 263 | BufferedImage card = getCardImage(musicInfo.getDifficulty()); 264 | BufferedImage grade = getGradeImage(musicInfo.getAccGrade()); 265 | int fix = switch(musicInfo.getAccGrade()) { 266 | case SSS, C -> 0; 267 | case SS -> -17; 268 | case S -> -6; 269 | default -> 5;// case A B D 270 | }; 271 | ImageEffect effect = new ImageEffect().setArc(35); 272 | String diff = musicInfo.getRatioInt()>lvRatio ? 273 | "+" + (musicInfo.getRatioInt() - lvRatio) : String.valueOf(musicInfo.getRatioInt() - lvRatio); 274 | 275 | drawer.drawImage(cover, 16 + dx2, 621 + dy2, 130, 158, effect) //y+1065 276 | .drawImage(card, 15 + dx2, 620 + dy2).drawImage(grade, 285 + fix + dx2, 715 + dy2) 277 | .font(titleFont, Color.BLACK).drawText(musicInfo.getName(), 160 + dx2, 624 + dy2, new TextEffect().setMaxWidth(220)).font(scoreFont) 278 | .drawText(String.valueOf(musicInfo.getScore()), 160 + dx2, 646 + dy2) 279 | .font(comboMissAccFont) 280 | .drawText("%d\n%d\n%.2f%%".formatted(musicInfo.getCombo(), musicInfo.getMiss(), musicInfo.getAccuracy()), 281 | 230 + dx2, 725 + dy2, 282 | new TextEffect().setSpaceHeight(1)) 283 | .drawText("> %d (%s)".formatted(musicInfo.getRatioInt(), diff), 163 + dx2, 702 + dy2) 284 | .font(levelFont, Color.WHITE) 285 | .drawText(String.valueOf(musicInfo.getLevel()), 17 + dx2, 747 + dy2); 286 | } 287 | } 288 | 289 | // 战力概况 290 | LvRatioHistory lvRatioHistory; 291 | if(!ratioList.isEmpty()) { 292 | lvRatioHistory = ratioList.get(ratioList.size() - (ratioList.size()>1 ? 2 : 1)); 293 | } else { 294 | lvRatioHistory = new LvRatioHistory(DateFormat.getDateInstance().getCalendar(), info.getLvRatio()); 295 | } 296 | float avg1 = RatioCalculator.average(rank15List); 297 | float avg2 = RatioCalculator.average(recent15List); 298 | float allAvg = (avg1 + avg2) / 2; 299 | Calendar calendar = lvRatioHistory.getCalendar(); 300 | String extraInfoText = """ 301 | 上次战力:%d (%d月%d日) 302 | B-15 战力:%.4f 303 | R-15 战力:%.4f 304 | 平均战力:%.5f 305 | """.formatted(lvRatioHistory.getRatio(), 306 | calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH), 307 | avg1, avg2, allAvg) + getRatioComment(lvRatio); 308 | drawer.font(infoFont).color(Color.BLACK).drawText(extraInfoText, 720, 160, new TextEffect().setSpaceHeight(-6)); 309 | drawer.dispose(); 310 | return drawer.getImageStream("png"); 311 | } 312 | 313 | private static BufferedImage getGradeImage(AccGrade grade) { 314 | return switch(grade) { 315 | case SSS -> LV_SSS; 316 | case SS -> LV_SS; 317 | case S -> LV_S; 318 | case A -> LV_A; 319 | case B -> LV_B; 320 | case C -> LV_C; 321 | default -> LV_D; 322 | }; 323 | } 324 | 325 | private static BufferedImage getCardImage(int difficulty) { 326 | return switch(difficulty) { 327 | case 0, -1 -> CARD_1; //-1为秀谱 328 | case 1 -> CARD_2; 329 | case 2 -> CARD_3; 330 | default -> CARD_1; 331 | }; 332 | } 333 | 334 | @Test 335 | public void test() throws IOException { 336 | System.out.println("Running..."); 337 | Token token = new Token(939088, 338 | "Tbp4C0QAJeIsqNcJx7psKoYAxFNsD0qH68qutYCZod8ybPiRoEJ05RZhHzy4LPQDtw3tJvKYqSkCpnEd-qrg-c7MMY7DwQecXF3-uuU-6qDd7zIQ7IpfTHbcVHvN_st9XnVCyt9op0b6CYFY3nTvNH1F4aidP5M-P-MXes3-TIH80YHN8zHgua_XjgFWfi0loubYS0KW9APsB0POsoaBmeJz-85ZxnqlOdzUkW7cb9vGPzgQvP7adZPa6igEfynpx1YXTthssnhGyjKdMSQnKkR2Zhmx4zdbwo9N1eTDoAv0ZuNZ9-29gSirqGHbwRS-GXPnXG4mGLvdMuRWY1OKuLk1HWvV-AsceOuMvZX9vin0BcxGDKmK4axbU8kRQkx-"); 339 | String path = "C:\\Users\\Lin\\IdeaProjects\\DanceCubeBot\\DcConfig\\Images\\result.jpg"; 340 | 341 | InputStream image = generate(token); 342 | Thumbnails.of(image) 343 | .scale(1) 344 | .outputFormat("jpg") 345 | .toFile(path); 346 | // ImageDrawer.write(ImageDrawer.convertPngToJpg(ImageDrawer.read(image),0.5f), path); 347 | // ImageDrawer.write(ImageDrawer.read(image), path); 348 | System.out.println("Done!"); 349 | } 350 | 351 | public static float deltaSeconds(long mills) { 352 | return (float) (System.currentTimeMillis() - mills) / 1000; 353 | } 354 | 355 | //常量放在这里我有病( 356 | public static final String[] RATIO_COMMENTS = { 357 | // 连1145的战力都没有 358 | " \"你已初步了解这款游戏了\n 继续练习吧~\"", //..1000 359 | " \"你已经适应10级的歌曲了\n 继续练习吧~\"", //1000..1300 360 | " \"你正在对线14级的歌了\n 继续加油~\"", //1300..1500 361 | " \"你即将迈入大佬的行列\n 加油加油!\"",//1500..1800 362 | " \"恭喜突破1800守门员\n 正式成为大佬啦!\"",//1800..1900 363 | " \"你即将成神\n 请继续和1819对线\"",//1900..2000 364 | " \"你已步入神的行列\n 快快杀19吧~\"",//2000..2080 365 | //卧槽,外星人?! 366 | " \"你已经成为外星人\n 正在薄纱一切歌曲\""//2080..2100 367 | }; 368 | 369 | public static String getRatioComment(int ratio) { 370 | String comment; 371 | if(ratio<1000) comment = RATIO_COMMENTS[0]; 372 | else if(ratio<1300) comment = RATIO_COMMENTS[1]; 373 | else if(ratio<1500) comment = RATIO_COMMENTS[2]; 374 | else if(ratio<1800) comment = RATIO_COMMENTS[3]; 375 | else if(ratio<1900) comment = RATIO_COMMENTS[4]; 376 | else if(ratio<2000) comment = RATIO_COMMENTS[5]; 377 | else if(ratio<2080) comment = RATIO_COMMENTS[6]; 378 | else if(ratio<2100) comment = RATIO_COMMENTS[7]; 379 | else comment = ""; 380 | return comment; 381 | } 382 | } 383 | 384 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/info/AccountInfo.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.info; 2 | 3 | import com.dancecube.token.Token; 4 | import com.google.gson.FieldNamingPolicy; 5 | import com.google.gson.Gson; 6 | import com.google.gson.GsonBuilder; 7 | import com.tools.HttpUtil; 8 | import okhttp3.Call; 9 | import okhttp3.Response; 10 | 11 | import java.io.IOException; 12 | import java.util.Map; 13 | 14 | public class AccountInfo { 15 | private int userID; 16 | 17 | public int getUserID() { 18 | return userID; 19 | } 20 | 21 | public int getGold() { 22 | return gold; 23 | } 24 | 25 | private int gold; 26 | 27 | public static AccountInfo get(Token token) { 28 | String accountInfoJson; 29 | Call call = HttpUtil.httpApiCall("https://dancedemo.shenghuayule.com/Dance/api/User/GetAccountInfo?userId=" + token.getUserId(), Map.of("Authorization", token.getBearerToken())); 30 | 31 | try(Response response = call.execute()) { 32 | // Response response = call.execute(); 33 | accountInfoJson = response.body().string(); 34 | } catch(IOException e) { 35 | throw new RuntimeException(e); 36 | } 37 | 38 | Gson gson = new GsonBuilder() 39 | .serializeNulls() 40 | .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE) 41 | .create(); 42 | 43 | return gson.fromJson(accountInfoJson, AccountInfo.class); 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return "AccountInfo{" + 49 | "userID=" + userID + 50 | ", gold=" + gold + 51 | '}'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/info/InfoStatus.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.info; 2 | 3 | public enum InfoStatus { 4 | OPEN, //开放 5 | PRIVATE, //保密 6 | NONEXISTENT //不存在 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/info/ReplyItem.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.info; 2 | 3 | import com.dancecube.token.Token; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonParser; 7 | import com.tools.HttpUtil; 8 | import okhttp3.Call; 9 | import okhttp3.Response; 10 | 11 | import java.io.IOException; 12 | import java.util.Map; 13 | 14 | public class ReplyItem { 15 | private float victoryRates; //对战胜率 16 | private float teamVictoryRates; //战队赛胜率 17 | private String playedAge; //舞龄 18 | private int danLevel; //段位 19 | private int playedTimes; //游玩次数 20 | private int passedSongs; //游玩次数 21 | private int addedCoins; //增加金币 22 | 23 | public static ReplyItem get(Token token) { 24 | ReplyItem replyItem = new ReplyItem(); 25 | String itemsJson; 26 | Call call = HttpUtil.httpApiCall("https://dancedemo.shenghuayule.com/Dance/api/ReplyTextItem/GetAllList?machineId=0", Map.of("Authorization", token.getBearerToken())); 27 | try { 28 | try(Response response = call.execute()) { 29 | itemsJson = response.body().string(); 30 | } 31 | } catch(IOException e) { 32 | throw new RuntimeException(e); 33 | } 34 | 35 | for(JsonElement element : JsonParser.parseString(itemsJson).getAsJsonArray()) { 36 | JsonObject jsonObject = element.getAsJsonObject(); 37 | int type = jsonObject.get("ItemType").getAsInt(); 38 | String content = jsonObject.get("Content").getAsString(); 39 | switch(type) { 40 | case 13 -> 41 | replyItem.victoryRates = content.contains("无") ? 0 : Float.parseFloat(content.replace('%', ' ')); 42 | case 9 -> 43 | replyItem.teamVictoryRates = content.contains("无") ? 0 : Float.parseFloat(content.replace('%', ' ')); 44 | case 3 -> replyItem.playedAge = content; 45 | case 7 -> replyItem.danLevel = content.contains("无") ? 0 : Integer.parseInt(content); 46 | case 5 -> replyItem.playedTimes = Integer.parseInt(content); 47 | case 6 -> replyItem.passedSongs = Integer.parseInt(content); 48 | case 10 -> replyItem.addedCoins = Integer.parseInt(content); 49 | } 50 | } 51 | return replyItem; 52 | } 53 | 54 | 55 | public float getVictoryRates() { 56 | return victoryRates; 57 | } 58 | 59 | public float getTeamVictoryRates() { 60 | return teamVictoryRates; 61 | } 62 | 63 | public String getPlayedAge() { 64 | return playedAge; 65 | } 66 | 67 | public int getDanLevel() { 68 | return danLevel; 69 | } 70 | 71 | public int getPlayedTimes() { 72 | return playedTimes; 73 | } 74 | 75 | public int getPassedSongs() { 76 | return passedSongs; 77 | } 78 | 79 | public int getAddedCoins() { 80 | return addedCoins; 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | return "ReplyItem{" + 86 | "victoryRates=" + victoryRates + 87 | ", teamVictoryRates=" + teamVictoryRates + 88 | ", playedAge='" + playedAge + '\'' + 89 | ", danLevel=" + danLevel + 90 | ", playedTimes=" + playedTimes + 91 | ", passedSongs=" + passedSongs + 92 | ", addedCoins=" + addedCoins + 93 | '}'; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/info/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.info; 2 | 3 | import com.dancecube.token.Token; 4 | import com.google.gson.*; 5 | import com.tools.HttpUtil; 6 | import okhttp3.Call; 7 | import okhttp3.OkHttpClient; 8 | import okhttp3.Request; 9 | import okhttp3.Response; 10 | 11 | import java.io.IOException; 12 | import java.util.Map; 13 | 14 | public class UserInfo { 15 | private int userID; //用户ID 16 | private int musicScore; //积分 17 | private int lvRatio; //战力 18 | private int rankNation; //全国排名 19 | private int comboPercent; //全连率(518为5.18%) 20 | private int sex; //性别(1男 2女) 21 | private String userName; //用户名 22 | private String headimgURL; //头像URL 23 | private String phone; //手机号 24 | private String cityName; //城市名 25 | private String teamName; //战队名 26 | private String titleUrl; //头衔 27 | private String headimgBoxPath; //头像框 28 | private InfoStatus status = InfoStatus.OPEN; 29 | 30 | 31 | public int getUserID() { 32 | return userID; 33 | } 34 | 35 | public int getLvRatio() { 36 | return lvRatio; 37 | } 38 | 39 | public String getHeadimgURL() { 40 | return headimgURL; 41 | } 42 | 43 | public String getUserName() { 44 | return userName; 45 | } 46 | 47 | public int getSex() { 48 | return sex; 49 | } 50 | 51 | public String getPhone() { 52 | return phone; 53 | } 54 | 55 | public String getCityName() { 56 | return cityName; 57 | } 58 | 59 | public int getMusicScore() { 60 | return musicScore; 61 | } 62 | 63 | public int getRankNation() { 64 | return rankNation; 65 | } 66 | 67 | public int getComboPercent() { 68 | return comboPercent; 69 | } 70 | 71 | public String getTeamName() { 72 | return teamName==null ? "" : teamName; 73 | } 74 | 75 | public String getTitleUrl() { 76 | return titleUrl==null || titleUrl.length()<5 ? "" : titleUrl; 77 | } 78 | 79 | public String getHeadimgBoxPath() { 80 | return headimgBoxPath==null ? "" : headimgBoxPath; 81 | } 82 | 83 | public InfoStatus getStatus() { 84 | return status; 85 | } 86 | 87 | public void setStatus(InfoStatus status) { 88 | this.status = status; 89 | } 90 | 91 | 92 | public UserInfo() { 93 | } 94 | 95 | /** 96 | * 不推荐的构造函数,仅取代 null 97 | */ 98 | public static UserInfo getNull() { 99 | UserInfo userInfo = new UserInfo(); 100 | userInfo.userID = -1; 101 | userInfo.status = InfoStatus.NONEXISTENT; 102 | return userInfo; 103 | } 104 | 105 | public static UserInfo get(Token token) { 106 | String userInfoJson = ""; 107 | OkHttpClient client = new OkHttpClient(); 108 | Request request = new Request.Builder() 109 | .url("https://dancedemo.shenghuayule.com/Dance/api/User/GetInfo?userId=" + token.getUserId()) 110 | .addHeader("Authorization", token.getBearerToken()) 111 | .get().build(); 112 | Call call = client.newCall(request); 113 | try(Response response = call.execute()) { 114 | 115 | if(response.body()!=null) { 116 | userInfoJson = response.body().string(); 117 | } 118 | } catch(IOException e) { 119 | e.printStackTrace(); 120 | } 121 | 122 | Gson gson = new GsonBuilder() 123 | .serializeNulls() 124 | .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE) 125 | .create(); 126 | 127 | return gson.fromJson(userInfoJson, UserInfo.class); 128 | } 129 | 130 | public static UserInfo get(Token token, int id) { 131 | String userInfoJson = ""; 132 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/User/GetInfo?userId=" + id, 133 | Map.of("Authorization", token.getBearerToken()))) { 134 | if(response!=null && response.body()!=null) 135 | userInfoJson = response.body().string(); 136 | } catch(IOException e) { 137 | e.printStackTrace(); 138 | } 139 | UserInfo userInfo = getNull(); 140 | 141 | //状态判断 142 | switch(statusOf(userInfoJson)) { 143 | case PRIVATE -> { 144 | String url = "https://dancedemo.shenghuayule.com/Dance/api/Common/Search?keyword=" + id + "&type=0&page=1&pagesize=1"; 145 | try(Response response = HttpUtil.httpApi(url, 146 | Map.of("Authorization", token.getBearerToken()))) { 147 | if(response!=null && response.body()!=null) 148 | userInfoJson = response.body().string(); 149 | } catch(IOException e) { 150 | e.printStackTrace(); 151 | } 152 | JsonArray jsonArray = JsonParser.parseString(userInfoJson).getAsJsonObject().get("List").getAsJsonArray(); 153 | if(!jsonArray.isEmpty()) { 154 | JsonObject jsonObject = jsonArray.get(0).getAsJsonObject(); 155 | userInfo.userID = id; 156 | userInfo.userName = jsonObject.get("Name").getAsString(); 157 | userInfo.headimgURL = jsonObject.get("HeadimgURL").getAsString(); 158 | userInfo.lvRatio = jsonObject.get("LvRatio").getAsInt(); 159 | userInfo.cityName = jsonObject.get("Region").getAsString(); 160 | } 161 | userInfo.setStatus(InfoStatus.PRIVATE); 162 | return userInfo; 163 | } 164 | case NONEXISTENT -> { 165 | userInfo = new UserInfo(); 166 | userInfo.setStatus(InfoStatus.NONEXISTENT); 167 | return userInfo; 168 | } 169 | } 170 | 171 | //构造 172 | Gson gson = new GsonBuilder() 173 | .serializeNulls() 174 | .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE) 175 | .create(); 176 | 177 | userInfo = gson.fromJson(userInfoJson, UserInfo.class); 178 | return userInfo; 179 | } 180 | 181 | @Override 182 | public String toString() { 183 | return "UserInfo{userID=%d, musicScore=%d, lvRatio=%d, rankNation=%d, comboPercent=%d, sex=%d, userName='%s', headimgURL='%s', phone='%s', cityName='%s', teamName='%s', titleUrl='%s', headimgBoxPath='%s'}".formatted(userID, musicScore, lvRatio, rankNation, comboPercent, sex, userName, headimgURL, phone, cityName, teamName, titleUrl, headimgBoxPath); 184 | } 185 | 186 | private static InfoStatus statusOf(String message) { 187 | if(message.contains("账号不存在")) return InfoStatus.NONEXISTENT; 188 | if(message.contains("已设置保密")) return InfoStatus.PRIVATE; 189 | return InfoStatus.OPEN; 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/music/CoverUtil.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.music; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import javax.imageio.ImageIO; 6 | import java.awt.image.BufferedImage; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.net.URL; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | 13 | import static com.mirai.config.AbstractConfig.configPath; 14 | 15 | public class CoverUtil { 16 | public static String officialImgPath = configPath + "Images/Cover/OfficialImage/"; 17 | public static String customImgPath = configPath + "Images/Cover/CustomImage/"; 18 | public static String coverImgPath = configPath + "Images/Cover/"; 19 | 20 | static { 21 | new File(officialImgPath).mkdirs(); 22 | new File(customImgPath).mkdirs(); 23 | } 24 | 25 | ///不保证存在 26 | private static String getImgPath(int id) { 27 | if(id == 0) return coverImgPath + "default.jpg"; 28 | return (MusicUtil.isOfficial(id) ? officialImgPath : customImgPath) + id + ".jpg"; 29 | } 30 | 31 | /** 32 | * Absent adj.缺席的,没有的 33 | */ 34 | public static boolean isCoverAbsent(int id) { 35 | return !new File(getImgPath(id)).exists(); 36 | } 37 | 38 | public static void downloadCover(int id) { 39 | Music music = MusicUtil.getMusic(id); 40 | BufferedImage image; 41 | File imgFile; 42 | try { 43 | image = ImageIO.read(new URL(music.getCoverUrl())); 44 | imgFile = new File(getImgPath(id)); 45 | ImageIO.write(image, "JPG", imgFile); 46 | } catch(IOException e) { 47 | throw new RuntimeException(id + "的id 封面url 无效"); 48 | } 49 | } 50 | 51 | @Nullable 52 | public static BufferedImage getCoverOrDefault(int id) { 53 | if(isCoverAbsent(id)) id = 0; 54 | try { 55 | return ImageIO.read(new File(getImgPath(id))); 56 | } catch(IOException e) { 57 | return null; 58 | } 59 | } 60 | 61 | public static byte[] getCoverBytesOrDefault(int id) { 62 | if(isCoverAbsent(id)) id = 0; 63 | try { 64 | return Files.readAllBytes(Path.of(getImgPath(id))); 65 | } catch(IOException e) { 66 | return null; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/music/GoodsMusic.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.music; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import okhttp3.OkHttpClient; 5 | import okhttp3.Request; 6 | import okhttp3.Response; 7 | import okhttp3.ResponseBody; 8 | 9 | import java.io.File; 10 | import java.io.FileOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.util.Objects; 14 | 15 | @Deprecated 16 | class GoodsMusic { 17 | @SerializedName("MusicID") 18 | private final int id; 19 | @SerializedName("GoodsName") 20 | private final String name; 21 | @SerializedName("PicPath") 22 | private final String coverUrl; 23 | 24 | public GoodsMusic(int id, String name, String coverUrl) { 25 | this.id = id; 26 | this.name = name; 27 | this.coverUrl = coverUrl; 28 | } 29 | 30 | public int getId() { 31 | return id; 32 | } 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | public String getCoverUrl() { 39 | return coverUrl; 40 | } 41 | 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if(this==o) return true; 46 | if(o==null || getClass()!=o.getClass()) return false; 47 | 48 | GoodsMusic music = (GoodsMusic) o; 49 | 50 | if(id!=music.id) return false; 51 | return Objects.equals(name, music.name); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | int result = id; 57 | result = 31 * result + (name!=null ? name.hashCode() : 0); 58 | return result; 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "GoodsMusic{" + 64 | "id=" + id + 65 | ", name='" + name + '\'' + 66 | ", coverUrl='" + coverUrl + '\'' + 67 | '}'; 68 | } 69 | 70 | 71 | public static void saveGoodsImg(OkHttpClient client, GoodsMusic music) { 72 | String name = music.getName(); 73 | int id = music.getId(); 74 | String url = music.getCoverUrl(); 75 | 76 | try { 77 | Response response = client.newCall(new Request.Builder().url(url).build()).execute(); 78 | 79 | File file = new File("C:\\Users\\Lin\\IdeaProjects\\DanceCubeBot\\src\\goodsImg\\" + id + ".jpg"); 80 | if(file.exists()) { 81 | System.out.println("#" + id + " " + name + " 已存在,未保存"); 82 | } else { 83 | ResponseBody responseBody = response.body(); 84 | InputStream inputStream = responseBody.byteStream(); 85 | FileOutputStream outputStream = new FileOutputStream(file); 86 | outputStream.write(inputStream.readAllBytes()); 87 | responseBody.close(); 88 | outputStream.close(); 89 | System.out.println("#" + id + " " + name + " 已保存"); 90 | } 91 | } catch(IOException e) { 92 | e.printStackTrace(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/music/Music.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.music; 2 | 3 | // 无视官方铺面/自制铺面获取封面 4 | public class Music { 5 | private final String name; 6 | private final int id; 7 | private final String coverUrl; 8 | 9 | public Music(String name, int id, String coverUrl) { 10 | this.name = name; 11 | this.id = id; 12 | this.coverUrl = coverUrl; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public String getCoverUrl() { 20 | return coverUrl; 21 | } 22 | 23 | public int getId() { 24 | return id; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/music/MusicUtil.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.music; 2 | 3 | import com.google.gson.*; 4 | import com.google.gson.reflect.TypeToken; 5 | import com.mirai.config.AbstractConfig; 6 | import com.tools.HttpUtil; 7 | import okhttp3.OkHttpClient; 8 | import okhttp3.Request; 9 | import okhttp3.Response; 10 | import okhttp3.ResponseBody; 11 | 12 | import java.io.FileOutputStream; 13 | import java.io.IOException; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.util.HashSet; 17 | import java.util.Map; 18 | 19 | public class MusicUtil { 20 | public static HashSet OFFICIAL_IDS; 21 | 22 | private static final String path = AbstractConfig.configPath + "OfficialMusicIds.json"; 23 | 24 | static { 25 | // 启动默认读取本地OFFICIAL_ID 26 | updateIdsFromFile(path); 27 | } 28 | 29 | public static boolean isOfficial(int id) { 30 | return OFFICIAL_IDS.contains(id); 31 | } 32 | 33 | public static boolean updateIdsFromFile(String path) { 34 | try { 35 | String idsJson = Files.readString(Path.of(path)); 36 | OFFICIAL_IDS = new Gson().fromJson(idsJson, new TypeToken<>() { 37 | }); 38 | return true; 39 | } catch(IOException e) { 40 | e.printStackTrace(); 41 | return false; 42 | } 43 | } 44 | 45 | public static boolean updateIdsFromAPI(String path) { 46 | String json = ""; 47 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/Music/GetMusicList", 48 | Map.of("getAdvanced", "true", "getNotDisplay", "false", "category", "0"))) { 49 | if(response!=null && response.body()!=null) { 50 | json = response.body().string(); 51 | } 52 | } catch(IOException e) { 53 | e.printStackTrace(); 54 | } 55 | if(OFFICIAL_IDS==null) OFFICIAL_IDS = new HashSet<>(); 56 | JsonParser.parseString(json).getAsJsonArray().forEach(obj -> { 57 | OFFICIAL_IDS.add(obj.getAsJsonObject().get("MusicID").getAsInt()); 58 | try { 59 | FileOutputStream outputStream = new FileOutputStream(path); 60 | Gson gson = new GsonBuilder().setPrettyPrinting().create(); 61 | outputStream.write(gson.toJson(OFFICIAL_IDS).getBytes()); 62 | outputStream.close(); 63 | } catch(IOException e) { 64 | throw new RuntimeException(e); 65 | } 66 | }); 67 | return true; 68 | } 69 | 70 | 71 | public static Music getMusic(int id) { 72 | String name; 73 | String coverUrl = ""; 74 | String json = ""; 75 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/MusicData/GetInfo?musicId=" + id)) { 76 | if(response!=null && response.body()!=null) { 77 | json = response.body().string(); 78 | } 79 | } catch(IOException e) { 80 | throw new RuntimeException(e); 81 | } 82 | 83 | JsonObject MusicJsonObject = JsonParser.parseString(json).getAsJsonObject(); 84 | name = MusicJsonObject.get("Name").getAsString(); 85 | for(JsonElement element : MusicJsonObject.get("MusicFileList").getAsJsonArray()) { 86 | JsonObject obj = element.getAsJsonObject(); 87 | if("背景图片".equals(obj.get("FileTypeText").getAsString())) { 88 | coverUrl = obj.get("Url").getAsString(); 89 | } 90 | } 91 | 92 | return new Music(name, id, coverUrl); 93 | } 94 | 95 | // 刷新OFFICIAL_IDS与下载图片 96 | public static void main(String[] args) throws Exception { 97 | Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/MusicData/GetInfo?musicId=" + 6179); 98 | System.out.println(response.body().string()); 99 | System.out.println("\n一共" + OFFICIAL_IDS.size() + "项"); 100 | 101 | for(Integer id : OFFICIAL_IDS) { 102 | if(CoverUtil.isCoverAbsent(id)) { 103 | System.out.println("id" + id); 104 | CoverUtil.downloadCover(id); 105 | System.out.println("id=" + id + " is done!"); 106 | } 107 | } 108 | System.out.println("\n# All right!"); 109 | } 110 | 111 | 112 | //用于遍历获取 113 | @Deprecated 114 | private static HashSet getListMusicSet(int index, int page, int size) { 115 | String url = "https://dancedemo.shenghuayule.com/Dance/api/User/GetMusicRankingNew?musicIndex=%d&pagesize=%d&page=%d".formatted(index, size, page); 116 | 117 | OkHttpClient client = new OkHttpClient(); 118 | Request request = new Request.Builder().url(url).get().build(); 119 | String musicJsons = ""; 120 | try { 121 | Response response = client.newCall(request).execute(); 122 | ResponseBody body = response.body(); 123 | musicJsons = body.string(); 124 | body.close(); 125 | } catch(IOException e) { 126 | e.printStackTrace(); 127 | } 128 | 129 | HashSet musicHashSet = new HashSet<>(); 130 | JsonObject object = JsonParser.parseString(musicJsons).getAsJsonObject(); 131 | int num = 0; 132 | for(JsonElement element : object.get("List").getAsJsonArray()) { 133 | Gson gson = new Gson(); 134 | OfficialMusic music = gson.fromJson(element.getAsJsonObject(), OfficialMusic.class); 135 | System.out.println(++num + ": " + music); 136 | musicHashSet.add(music); 137 | } 138 | return musicHashSet; 139 | } 140 | } -------------------------------------------------------------------------------- /src/main/java/com/dancecube/music/OfficialMusic.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.music; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.Objects; 6 | 7 | class OfficialMusic { 8 | @SerializedName("MusicID") 9 | private final int id; 10 | @SerializedName("Name") 11 | private final String name; 12 | @SerializedName("CoverUrl") 13 | private final String coverUrl; 14 | // private final ArrayList levels; 15 | 16 | public OfficialMusic(int id, String name, String coverUrl) { 17 | this.id = id; 18 | this.name = name; 19 | this.coverUrl = coverUrl; 20 | // this.levels = levels; 21 | } 22 | 23 | public int getId() { 24 | return id; 25 | } 26 | 27 | public String getName() { 28 | return name; 29 | } 30 | 31 | public String getCoverUrl() { 32 | return coverUrl; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object o) { 37 | if(this==o) return true; 38 | if(o==null || getClass()!=o.getClass()) return false; 39 | 40 | OfficialMusic music = (OfficialMusic) o; 41 | 42 | if(id!=music.id) return false; 43 | return Objects.equals(name, music.name); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | int result = id; 49 | result = 31 * result + (name!=null ? name.hashCode() : 0); 50 | return result; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return "OfficialMusic{" + 56 | "id=" + id + 57 | ", name='" + name + '\'' + 58 | ", coverUrl='" + coverUrl + '\'' + 59 | '}'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/ratio/AccGrade.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.ratio; 2 | 3 | /** 4 | * 舞立方成绩精确度评级 5 | */ 6 | public enum AccGrade { 7 | SSS_AP(100), 8 | SSS(98), 9 | SS(95), 10 | S(90), 11 | A(80), 12 | B(70), 13 | C(60), 14 | D(0); 15 | 16 | private final float minAcc; 17 | 18 | AccGrade(int minAcc) { 19 | this.minAcc = minAcc; 20 | } 21 | 22 | public float getMinAcc() { 23 | return minAcc; 24 | } 25 | 26 | /** 27 | * 通过精确度获取精度评级 28 | * 29 | * @param acc 精确度 30 | * @return 精度评级 31 | */ 32 | public static AccGrade get(float acc) { 33 | for(AccGrade grade : AccGrade.values()) { 34 | if(acc>=grade.getMinAcc()) { 35 | return grade; 36 | } 37 | } 38 | return AccGrade.D; 39 | } 40 | 41 | /** 42 | * 仅测试用,获取随机精度评级 43 | * 44 | * @return 随机的AccGrade 45 | */ 46 | public static AccGrade getRandom() { 47 | return values()[(int) (Math.random() * values().length)]; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/java/com/dancecube/ratio/RankMusicInfo.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.ratio; 2 | 3 | 4 | import com.google.gson.JsonObject; 5 | 6 | /** 7 | * RankMusic的一个其中难度/谱面记录 8 | */ 9 | public class RankMusicInfo extends RecordedMusicInfo { 10 | private final boolean isOfficial; 11 | private final int ranking; 12 | 13 | public int getRanking() { 14 | return ranking; 15 | } 16 | 17 | 18 | /** 19 | * 通过ItemRankList的列表分别创建对象,需要在创建时解析好源JSON并传递id name ownerType!!! 20 | */ 21 | public RankMusicInfo(int id, String name, int ownerType, JsonObject details) { 22 | super( 23 | id, name, details.get("MusicLevOld").getAsInt(), 24 | details.get("MusicRank").getAsInt(), 25 | details.get("MusicLev").getAsInt(), 26 | details.get("PlayerPercent").getAsFloat() / 100, 27 | details.get("PlayerScore").getAsInt(), 28 | details.get("ComboCount").getAsInt(), 29 | details.get("PlayerMiss").getAsInt() 30 | ); 31 | ranking = details.get("MusicRanking").getAsInt(); 32 | isOfficial = ownerType==1; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "RankMusicInfo{" + 38 | "difficulty=" + getDifficulty() + 39 | ", level=" + getLevel() + 40 | ", acc=" + getAccuracy() + 41 | ", ratio=" + getRatio() + 42 | '}'; 43 | } 44 | 45 | @Override 46 | public boolean isOfficial() { 47 | return isOfficial; 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/ratio/RatioCalculator.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.ratio; 2 | 3 | import com.dancecube.info.UserInfo; 4 | import com.dancecube.token.Token; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import com.google.gson.JsonParser; 8 | import com.tools.HttpUtil; 9 | import okhttp3.OkHttpClient; 10 | import okhttp3.Request; 11 | import okhttp3.Response; 12 | import okhttp3.ResponseBody; 13 | 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | 20 | public class RatioCalculator { 21 | 22 | public static void main(String[] args) { 23 | 24 | //Input Your DanceCubeBase Bearer Token Here 25 | String auth = "bearer " + "tLB-nyd8xVnXMpn2TObtpUZAoB_of9WhB9Sye7jrLuVldr_8JfV73qQvQ1-i-hAs2DPm83U4_LVqh-j7M4jZeULkaLvros29EKcMlpuPd76pBFScElsWd8LS07K2NmFBWwjtkmSxs7lhmSeWk1W0wZb7qVyZQiw-oPwLa_6kq2UngZxY2pGrr3SOJw3nuc58DaCexkJ_Hz6bZRC-Mfzhj4e59n-nr-7JN2A5t2U9znVdmDlfN1mrVauoGxdW-R29QhqYp-78hTDisUhogStCi9K7VHRdt1AoC5I9fUSpU9ZrXzJiUzMJTumw0dQ8hSAPGycxUDaDqIXViWqs78-zSw6giMQauJgI-feTSdDJkp3M86xw4qVHCPTeeMNKtPM8"; 26 | 27 | //The parameter officialOnly given true will ignore non-official music (including fan-made charts) 28 | List allRankList = getAllRankList(auth); 29 | List allRecentList = getAllRecentList(auth); 30 | List rank15List = new ArrayList<>(getSubRank15List(allRankList, true)); 31 | List recent15List = new ArrayList<>(getSubRecent15List(allRecentList, false)); 32 | 33 | //Best 15 34 | System.out.println("#The Best 15 PlayerMusic"); 35 | for(int i = 0, rank15ListSize = rank15List.size(); i float average(List multiInfo) { 63 | float sum = 0; 64 | for(RecordedMusicInfo info : multiInfo) { 65 | sum = info.getRatio() + sum; 66 | } 67 | return sum / multiInfo.size(); 68 | } 69 | 70 | private static List getCategoryRankList(String json) { 71 | List list = JsonParser.parseString(json).getAsJsonArray().asList(); 72 | List singleRankMusicList = new ArrayList<>(); 73 | list.forEach(element -> { 74 | JsonObject object = element.getAsJsonObject(); 75 | object.get("ItemRankList").getAsJsonArray().forEach(info -> 76 | singleRankMusicList.add(new RankMusicInfo( 77 | object.get("MusicID").getAsInt(), 78 | object.get("Name").getAsString(), 79 | object.get("OwnerType").getAsInt(), 80 | info.getAsJsonObject())) 81 | ); 82 | }); 83 | return singleRankMusicList; 84 | } 85 | 86 | public static List getAllRankList(String auth) { 87 | OkHttpClient client = new OkHttpClient(); 88 | String url = "https://dancedemo.shenghuayule.com/Dance/api/User/GetMyRankNew?musicIndex="; 89 | String json = ""; 90 | ArrayList musicInfos = new ArrayList<>(); 91 | 92 | for(int i = 2; i<=6; i++) { // 2 3 4 5 6 国语 粤语 韩语 欧美 其它 93 | Request request = new Request.Builder().url(url + i).get().addHeader("Authorization", auth).build(); 94 | try { 95 | try(Response response = client.newCall(request).execute()) { 96 | ResponseBody body = response.body(); 97 | if(body!=null) json = body.string(); 98 | } 99 | } catch(IOException e) { 100 | throw new RuntimeException(e); 101 | } 102 | musicInfos.addAll(getCategoryRankList(json)); 103 | } 104 | return musicInfos; 105 | } 106 | 107 | public static List getAllRecentList(String auth) { 108 | String url = "https://dancedemo.shenghuayule.com/Dance/api/User/GetLastPlay"; 109 | String json = ""; 110 | 111 | ResponseBody body = null; 112 | try(Response response = HttpUtil.httpApi(url, Map.of("Authorization", auth))) { 113 | if(response!=null && response.body()!=null) body = response.body(); 114 | if(body!=null) json = body.string(); 115 | } catch(IOException e) { 116 | throw new RuntimeException(e); 117 | } 118 | 119 | ArrayList musicInfoList = new ArrayList<>(); 120 | for(JsonElement element : JsonParser.parseString(json).getAsJsonArray()) { 121 | RecentMusicInfo musicInfo = new RecentMusicInfo(element.getAsJsonObject()); 122 | musicInfoList.add(musicInfo); 123 | } 124 | return musicInfoList; 125 | } 126 | 127 | /** 128 | * 判断是否满足算入战力要求 129 | */ 130 | public static boolean isRatioValid(RecordedMusicInfo musicInfo) { 131 | return musicInfo.level>0 && musicInfo.level<20 && musicInfo.isOfficial(); 132 | } 133 | 134 | 135 | /** 136 | * 获取B15 (可能小于15) 137 | * 138 | * @param musicInfoList 源Best列表 139 | * @param ratioValidOnly 仅可计入战力模式 140 | * @return b15 141 | */ 142 | public static List getSubRank15List(List musicInfoList, boolean ratioValidOnly) { 143 | musicInfoList.sort((o1, o2) -> Float.compare(o2.getRatio(), o1.getRatio())); 144 | List subRankList = new ArrayList<>(); 145 | if(ratioValidOnly) { 146 | int count = 0; // 用于跟踪添加到subRankList中的项数 147 | for(int i = 0; i getSubRecent15List(List musicInfoList, boolean officialOnly) { 171 | List subRencentList = new ArrayList<>(); 172 | if(officialOnly) { 173 | int count = 0; 174 | for(int i = 0; i*不严谨判定:{@code available}表示是否可用访问API,但不能判定{@code refresh_token}是否可再次刷新。 71 | * 但事实上,{@code refresh_token}的{@code expire_in}并不准确,应该尽可能的刷新。 72 | *

73 | * 而对于能够即时刷新的定时程序来说,可以认为{@code available}等价于{@code refreshable}

74 | * 75 | * @return Token是否可用 76 | */ 77 | public boolean checkAvailable() { 78 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/Message/GetUnreadCount", 79 | Map.of("Authorization", getBearerToken()))) { 80 | available = response!=null && response.code()==200; 81 | } 82 | return !(!available | accessToken==null | refreshToken==null); 83 | // return available; 84 | } 85 | 86 | //如果是默认Token(公共token) 87 | @Deprecated 88 | public boolean isDefault() { 89 | return userId==0; 90 | } 91 | 92 | public boolean refresh() { 93 | if(!available) return false; 94 | 95 | try { 96 | Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/token", 97 | Map.of("content-type", "application/x-www-form-urlencoded"), 98 | Map.of("client_type", "qrcode", "grant_type", "refresh_token", "refresh_token", refreshToken)); 99 | JsonObject json; 100 | if(response!=null && response.body()!=null) { 101 | json = JsonParser.parseString(response.body().string()).getAsJsonObject(); 102 | response.close(); 103 | if(response.code()!=200) { 104 | available = false; 105 | throw new IOException("code:" + response.code() + " id:" + userId + " msg:" + response.message()); 106 | } else { 107 | accessToken = json.get("access_token").getAsString(); 108 | refreshToken = json.get("refresh_token").getAsString(); 109 | recTime = System.currentTimeMillis(); 110 | return true; 111 | } 112 | } 113 | } catch(IOException e) { 114 | System.out.println("# refreshTokenHttp执行bug辣!"); 115 | e.printStackTrace(); 116 | return false; 117 | } 118 | return false; 119 | } 120 | 121 | @Override 122 | public String toString() { 123 | return (""" 124 | { 125 | "userId"="%s", 126 | "accessToken"="%s", 127 | "refreshToken"="%s", 128 | "recTime"=%d 129 | "desc"="当前token时长为%.3f天。Token拥有账号的所有控制权,请务必保管好token以免泄露,你可以发送"退出登录"来删除当前会话token" 130 | } 131 | """) 132 | .formatted(userId, accessToken, refreshToken, recTime, (float) (System.currentTimeMillis() - recTime) / 86400_000); 133 | } 134 | 135 | public void forceAccessible() { 136 | this.available = true; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/dancecube/token/TokenBuilder.java: -------------------------------------------------------------------------------- 1 | package com.dancecube.token; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonParser; 7 | import com.google.gson.reflect.TypeToken; 8 | import com.mirai.task.RefreshTokenJob; 9 | import com.tools.HttpUtil; 10 | import okhttp3.Call; 11 | import okhttp3.Response; 12 | import org.junit.jupiter.api.Test; 13 | import org.quartz.*; 14 | import org.quartz.impl.StdSchedulerFactory; 15 | 16 | import java.io.*; 17 | import java.lang.reflect.Type; 18 | import java.net.URLEncoder; 19 | import java.nio.charset.Charset; 20 | import java.nio.charset.StandardCharsets; 21 | import java.nio.file.Files; 22 | import java.nio.file.StandardOpenOption; 23 | import java.util.ArrayList; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | import static com.mirai.config.AbstractConfig.configPath; 28 | 29 | public final class TokenBuilder { 30 | //公用 ids 31 | private static ArrayList ids = initIds(); 32 | 33 | //公用 pointer 34 | private static int pointer = 0; 35 | 36 | //临时 index 37 | private final int index; 38 | 39 | //临时个人 id,多线程不会阻塞 40 | private final String id; 41 | 42 | 43 | public static ArrayList initIds() { 44 | try { 45 | File tokenIdsFile = new File(configPath + "TokenIds.json"); 46 | if(!tokenIdsFile.exists()) { 47 | tokenIdsFile.createNewFile(); 48 | Files.writeString(tokenIdsFile.toPath(), "[]", StandardOpenOption.WRITE); 49 | } 50 | ArrayList strings = new ArrayList<>(); 51 | JsonParser.parseString(Files.readString(tokenIdsFile.toPath())).getAsJsonArray().forEach( 52 | element -> strings.add(element.getAsString()) 53 | ); 54 | 55 | if(strings.isEmpty()) throw new RuntimeException("# id缺失,请手动补充!"); 56 | 57 | System.out.printf("# id缓存成功,共%d项%n", strings.size()); 58 | return strings; 59 | } catch(FileNotFoundException e) { 60 | throw new RuntimeException("TokenIds.json 文件未找到,请重新配置"); 61 | } catch(IOException e) { 62 | throw new RuntimeException(e); 63 | } 64 | } 65 | 66 | public TokenBuilder() { 67 | this.index = pointer; 68 | this.id = getId(); 69 | } 70 | 71 | public static ArrayList updateIds() { 72 | TokenBuilder.ids = initIds(); 73 | return ids; 74 | } 75 | 76 | public static int getSize() { 77 | return ids.size(); 78 | } 79 | 80 | private static String getId() { 81 | //ID会一段时间释放,需要换用ID 82 | if(pointer>getSize() - 1) pointer = 0; 83 | return ids.get(pointer++); 84 | } 85 | 86 | public String getQrcodeUrl() { 87 | return getQrcodeUrl(id); 88 | } 89 | 90 | private String getQrcodeUrl(String id) { 91 | //对于含有'+' '/'等符号的TokenId会自动url编码 92 | id = URLEncoder.encode(id, StandardCharsets.UTF_8); 93 | 94 | String string = ""; 95 | try { 96 | Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/Common/GetQrCode?id=" + id); 97 | if(response!=null && response.body()!=null) { 98 | string = response.body().string(); 99 | response.close(); // 释放 100 | return JsonParser.parseString(string).getAsJsonObject().get("QrcodeUrl").getAsString(); 101 | } 102 | return ""; 103 | } catch(IOException e) { 104 | throw new RuntimeException(e); 105 | } catch(NullPointerException e) { 106 | System.out.println(string); 107 | ids.remove(index); 108 | System.out.println("# ID:" + id + " 不可用已删除,还剩下" + getSize() + "条"); 109 | throw new RuntimeException(e); 110 | } 111 | } 112 | 113 | public String getNewID() { 114 | try { 115 | Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/Common/GetQrCode"); 116 | 117 | String string; 118 | if(response!=null && response.body()!=null) { 119 | string = response.body().string(); 120 | if(response.code()==400) return null; 121 | response.close(); // 释放 122 | JsonObject jsonObject = JsonParser.parseString(string).getAsJsonObject(); 123 | System.out.println(jsonObject); 124 | return jsonObject.get("ID").getAsString(); 125 | } 126 | return ""; 127 | } catch(IOException e) { 128 | throw new RuntimeException(e); 129 | } 130 | } 131 | 132 | @Test 133 | public void testID() throws InterruptedException { 134 | ArrayList strings = new ArrayList<>(); 135 | for(int i = 0; i<100; i++) { 136 | String id = getNewID(); 137 | Thread.sleep(100); 138 | if(id.isBlank()) continue; 139 | strings.add("\"" + id + "\""); 140 | } 141 | System.out.println(strings); 142 | System.out.println("共 " + strings.size()); 143 | } 144 | 145 | public Token getToken() { 146 | return getToken(id); 147 | } 148 | 149 | public Token getToken(String id) { 150 | long curTime = System.currentTimeMillis(); 151 | Call call = HttpUtil.httpApiCall("https://dancedemo.shenghuayule.com/Dance/token", 152 | Map.of("content-type", "application/x-www-form-urlencoded"), 153 | Map.of("client_type", "qrcode", 154 | "grant_type", "client_credentials", 155 | "client_id", URLEncoder.encode(id, Charset.defaultCharset()))); 156 | Response response; 157 | //五分钟计时 158 | long wait = System.currentTimeMillis(); 159 | while(System.currentTimeMillis() - curTime<300_000) { // 5min超时 160 | if(System.currentTimeMillis() - wait<4000) continue; //等待4s时间(防止高频) 161 | wait = System.currentTimeMillis(); //重新赋值等待时间 162 | 163 | try { 164 | //call不能重复请求 165 | response = call.clone().execute(); 166 | //未登录为 400 登录为 200 167 | if(response.body()!=null && response.code()==200) { 168 | JsonObject json = JsonParser.parseString(response.body().string()).getAsJsonObject(); 169 | return new Token(json.get("userId").getAsInt(), 170 | json.get("access_token").getAsString(), 171 | json.get("refresh_token").getAsString(), 172 | curTime); 173 | } 174 | response.close(); // 关闭释放 175 | } catch(IOException e) { 176 | System.out.println("# TokenHttp执行bug辣!"); 177 | e.printStackTrace(); 178 | } 179 | } 180 | return null; 181 | } 182 | 183 | 184 | // HashMap写入json文件 不用数组是为了覆盖原key 185 | public static void tokensToFile(HashMap tokenMap, String filePath) { 186 | Type type = new TypeToken>() { 187 | }.getType(); 188 | String json = new GsonBuilder().setPrettyPrinting().create().toJson(tokenMap, type); 189 | 190 | try { 191 | FileOutputStream stream = new FileOutputStream(filePath); 192 | stream.write(json.getBytes(StandardCharsets.UTF_8)); 193 | stream.close(); 194 | } catch(IOException e) { 195 | System.out.println("# TokenToFile执行bug辣!"); 196 | throw new RuntimeException(e); 197 | } 198 | } 199 | 200 | // 读取json文件Token Map 201 | public static HashMap tokensFromFile(String filePath, boolean refreshing) { 202 | Type type = new TypeToken>() { 203 | }.getType(); 204 | HashMap userMap; 205 | try { 206 | userMap = new Gson().fromJson(new FileReader(filePath), type); 207 | if(userMap==null) { // 空文件判断 208 | return new HashMap<>(); 209 | } 210 | // 读取并refresh() 211 | if(refreshing) userMap.forEach((key, token) -> token.refresh()); 212 | } catch(FileNotFoundException e) { 213 | throw new RuntimeException(e); 214 | } 215 | return userMap; 216 | } 217 | 218 | public static HashMap tokensFromFile(String filePath) { 219 | return tokensFromFile(filePath, false); 220 | } 221 | 222 | 223 | @Test 224 | public void main() { 225 | TokenBuilder builder = new TokenBuilder(); 226 | System.out.println("url:" + builder.getQrcodeUrl()); 227 | Token token = builder.getToken(); 228 | System.out.println("your id:" + token.getUserId()); 229 | } 230 | 231 | @Deprecated 232 | public void autoRefreshToken() { 233 | Scheduler scheduler; 234 | try { 235 | scheduler = new StdSchedulerFactory().getScheduler(); 236 | } catch(SchedulerException e) { 237 | throw new RuntimeException(e); 238 | } 239 | JobDetail jobDetail = JobBuilder.newJob(RefreshTokenJob.class).build(); 240 | Trigger trigger = TriggerBuilder.newTrigger().startNow().withSchedule( 241 | SimpleScheduleBuilder.repeatSecondlyForever(3)) 242 | // .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(13, 36)) 243 | .build(); 244 | try { 245 | scheduler.scheduleJob(jobDetail, trigger); 246 | scheduler.start(); 247 | } catch(SchedulerException e) { 248 | throw new RuntimeException(e); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/MiraiBot.java: -------------------------------------------------------------------------------- 1 | package com.mirai; 2 | 3 | import com.dancecube.token.Token; 4 | import com.dancecube.token.TokenBuilder; 5 | import com.mirai.event.MainHandler; 6 | import com.mirai.task.SchedulerTask; 7 | import net.mamoe.mirai.console.extension.PluginComponentStorage; 8 | import net.mamoe.mirai.console.plugin.jvm.JavaPlugin; 9 | import net.mamoe.mirai.console.plugin.jvm.JavaPluginScheduler; 10 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription; 11 | import net.mamoe.mirai.event.Event; 12 | import net.mamoe.mirai.event.EventChannel; 13 | import net.mamoe.mirai.event.GlobalEventChannel; 14 | import net.mamoe.mirai.event.events.MessageEvent; 15 | import net.mamoe.mirai.event.events.NewFriendRequestEvent; 16 | import net.mamoe.mirai.event.events.NudgeEvent; 17 | import org.jetbrains.annotations.NotNull; 18 | 19 | import java.text.SimpleDateFormat; 20 | import java.util.*; 21 | import java.util.logging.Logger; 22 | 23 | import static com.mirai.config.AbstractConfig.configPath; 24 | import static com.mirai.config.AbstractConfig.userTokensMap; 25 | 26 | public final class MiraiBot extends JavaPlugin { 27 | public static final MiraiBot INSTANCE = new MiraiBot(); 28 | String path = configPath + "UserTokens.json"; 29 | 30 | private MiraiBot() { 31 | super(JvmPluginDescription.loadFromResource("plugin.yml", MiraiBot.class.getClassLoader())); 32 | } 33 | 34 | @Override 35 | public void onLoad(@NotNull PluginComponentStorage $this$onLoad) { 36 | super.onLoad($this$onLoad); 37 | } 38 | 39 | @Override 40 | public void onEnable() { 41 | getLogger().info("Plugin loaded!"); 42 | EventChannel channel = GlobalEventChannel.INSTANCE 43 | .parentScope(MiraiBot.INSTANCE) 44 | .context(this.getCoroutineContext()); 45 | 46 | // 输出加载Token 47 | onLoadToken(); 48 | 49 | // Token刷新器 50 | SchedulerTask.autoRefreshToken(); 51 | 52 | 53 | // 监听器 54 | channel.subscribeAlways(MessageEvent.class, MainHandler::eventCenter); 55 | channel.subscribeAlways(NudgeEvent.class, MainHandler::NudgeHandler); 56 | channel.subscribeAlways(NewFriendRequestEvent.class, MainHandler::addFriendHandler); 57 | 58 | } 59 | 60 | @Override 61 | public void onDisable() { 62 | // 保存Tokens 63 | TokenBuilder.tokensToFile(userTokensMap, configPath + "UserTokens.json"); 64 | System.out.printf("保存成功!共%d条%n", userTokensMap.size()); 65 | } 66 | 67 | @Deprecated 68 | public void refreshTokensTimer() { 69 | long period = 86400 * 500; //半天 70 | 71 | JavaPluginScheduler scheduler = MiraiBot.INSTANCE.getScheduler(); 72 | userTokensMap = TokenBuilder.tokensFromFile(path); 73 | 74 | TimerTask task = new TimerTask() { 75 | @Override 76 | public void run() { 77 | Token defaultToken = userTokensMap.get(0L); 78 | if(defaultToken==null) { 79 | userTokensMap.forEach((qq, token) -> 80 | scheduler.async(() -> { 81 | if(token.checkAvailable()) token.refresh(); 82 | })); 83 | } else { 84 | userTokensMap.forEach((qq, token) -> 85 | scheduler.async(() -> { 86 | // 默认token不为用户token 87 | if(token.checkAvailable() & 88 | !defaultToken.getAccessToken().equals(token.getAccessToken())) 89 | token.refresh(); 90 | })); 91 | 92 | } 93 | TokenBuilder.tokensToFile(userTokensMap, path); 94 | System.out.println(new SimpleDateFormat("MM-dd hh:mm:ss").format(new Date()) + ": 今日已刷新token"); 95 | } 96 | }; 97 | 98 | new Timer().schedule(task, 0, 86400); 99 | } 100 | 101 | public void onLoadToken() { 102 | StringBuilder sb = new StringBuilder(); 103 | // 导入Token 104 | userTokensMap = Objects.requireNonNullElse( 105 | TokenBuilder.tokensFromFile(configPath + "UserTokens.json"), 106 | new HashMap<>()); 107 | 108 | for(Map.Entry entry : userTokensMap.entrySet()) { 109 | Long qq = entry.getKey(); 110 | Token token = entry.getValue(); 111 | sb.append("\nqq: %d , id: %s;".formatted(qq, token.getUserId())); 112 | } 113 | Logger.getGlobal().info(("刷新加载成功!共%d条".formatted(userTokensMap.size()) + sb)); 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/AbstractCommand.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | import net.mamoe.mirai.contact.Contact; 4 | import net.mamoe.mirai.event.events.MessageEvent; 5 | 6 | import java.util.HashSet; 7 | 8 | public abstract class AbstractCommand { 9 | 10 | final HashSet scopes = new HashSet<>(); //作用范围 11 | MsgHandleable globalOnCall; //作用效果 12 | MsgHandleable userOnCall; 13 | MsgHandleable groupOnCall; 14 | MsgHandleable adminOnCall; 15 | 16 | 17 | 18 | 19 | public void onCall(Scope scope, MessageEvent event, Contact contact, long qq, String[] args) { 20 | 21 | //不同情况筛选 22 | if(scope==Scope.ADMIN) { 23 | adminOnCall.handle(event, contact, qq, args); 24 | } else if(scope==Scope.GROUP) { 25 | groupOnCall.handle(event, contact, qq, args); 26 | } else if(scope==Scope.USER) { 27 | userOnCall.handle(event, contact, qq, args); 28 | } else { 29 | globalOnCall.handle(event, contact, qq, args); 30 | } 31 | } 32 | 33 | protected void setGlobalOnCall(MsgHandleable onCall) { 34 | globalOnCall = onCall; 35 | userOnCall = onCall; 36 | groupOnCall = onCall; 37 | adminOnCall = onCall; 38 | } 39 | 40 | protected void setUserOnCall(MsgHandleable onCall) { 41 | this.userOnCall = onCall; 42 | } 43 | 44 | protected void setGroupOnCall(MsgHandleable onCall) { 45 | this.groupOnCall = onCall; 46 | } 47 | 48 | protected void setAdminOnCall(MsgHandleable onCall) { 49 | this.adminOnCall = onCall; 50 | } 51 | 52 | public HashSet getScopes() { 53 | return scopes; 54 | } 55 | 56 | protected final void addScope(Scope scope) { 57 | scopes.add(scope); 58 | // clearScopes(); 59 | } 60 | 61 | // void clearScopes() { 62 | // if(scopes.contains(Scope.GLOBAL) | ((Scope.values().length - scopes.size()==1) & !scopes.contains(Scope.GLOBAL))) { 63 | // scopes.clear(); 64 | // scopes.add(Scope.GLOBAL); 65 | // } 66 | // } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/ArgsCommand.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class ArgsCommand extends AbstractCommand { 6 | public static final Pattern NUMBER = Pattern.compile("\\d+"); 7 | public static final Pattern WORD = Pattern.compile("[0-9a-zA-z]+"); 8 | public static final Pattern CHAR = Pattern.compile("\\S+"); 9 | public static final Pattern ANY = Pattern.compile(".+"); 10 | 11 | private String[] prefix; 12 | private Pattern[] form; 13 | 14 | public String[] getPrefix() { 15 | return prefix; 16 | } 17 | 18 | protected void setPrefix(String[] prefix) { 19 | this.prefix = prefix; 20 | } 21 | 22 | protected void setForm(Pattern[] form) { 23 | this.form = form; 24 | } 25 | 26 | 27 | /** 28 | * 检查格式 29 | * 30 | * @param command 需要的指令 31 | * @param args 传递的参数 32 | * @return -1为成功,-2为无参数,否则为匹配错误的索引 33 | */ 34 | public static int checkError(ArgsCommand command, String[] args) { 35 | if(args == null) return -2; 36 | 37 | if(args.length command.setUserOnCall(onCall); 28 | case GROUP -> command.setGroupOnCall(onCall); 29 | case ADMIN -> command.setAdminOnCall(onCall); 30 | } 31 | } 32 | return this; 33 | } 34 | 35 | public ArgsCommand build() { 36 | return command; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/DeclaredCommand.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 用于注册指令到监听器, 10 | * 仅注解了 @DeclaredCommand 的 Command 对象才会被放入 Handler 以监听事件 11 | * 12 | * @author Lin 13 | */ 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.FIELD) 16 | public @interface DeclaredCommand { 17 | //指令名称或描述,不必要填写 18 | String value(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/MsgHandleable.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | 4 | import net.mamoe.mirai.contact.Contact; 5 | import net.mamoe.mirai.event.events.MessageEvent; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | /** 9 | * 消息处理接口 10 | */ 11 | @FunctionalInterface 12 | public interface MsgHandleable { 13 | /** 14 | * 类似于 Consumer 但是定义好了方法参数,通过Lambda表达式传值 15 | * 16 | * @param event 消息事件 17 | * @param contact 消息发送者 18 | * @param qq 消息发送者的QQ号 19 | * @param args 消息参数,传入{@code RegexCommand}对象或者没有参数时为null 20 | */ 21 | void handle(MessageEvent event, Contact contact, long qq, @Nullable String[] args); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/RegexCommand.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | // 原始指令 6 | public class RegexCommand extends AbstractCommand { 7 | private Pattern regex; //正则 8 | 9 | protected RegexCommand() { 10 | } 11 | 12 | protected void setRegex(Pattern regex) { 13 | this.regex = regex; 14 | } 15 | 16 | public Pattern getRegex() { 17 | return regex; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/RegexCommandBuilder.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | // 构造器 8 | public class RegexCommandBuilder { 9 | private final RegexCommand command = new RegexCommand(); 10 | 11 | public RegexCommandBuilder regex(@NotNull String regex) { 12 | return regex(regex, true); 13 | } 14 | 15 | public RegexCommandBuilder regex(String regex, boolean lineOnly) { 16 | if(regex.isBlank()) throw new RuntimeException("regex不能为空值"); 17 | command.setRegex(Pattern.compile(lineOnly ? "^" + regex + "$" : regex)); 18 | return this; 19 | } 20 | 21 | // TODO lineOnly -> ^(...|...)$ 22 | public RegexCommandBuilder multiStrings(String... strings) { 23 | if(strings.length<1) throw new RuntimeException("regex不能为空值"); 24 | StringBuilder builder = new StringBuilder(); 25 | for(String string : strings) { 26 | builder.append('|').append('^').append(string).append('$'); 27 | } 28 | builder.deleteCharAt(0); 29 | command.setRegex(Pattern.compile(builder.toString())); 30 | return this; 31 | } 32 | 33 | public RegexCommandBuilder onCall(Scope scope, @NotNull MsgHandleable onCall) { 34 | command.addScope(scope); 35 | if(scope==Scope.GLOBAL) { 36 | command.setGlobalOnCall(onCall); 37 | } else { 38 | switch(scope) { 39 | case USER -> command.setUserOnCall(onCall); 40 | case GROUP -> command.setGroupOnCall(onCall); 41 | case ADMIN -> command.setAdminOnCall(onCall); 42 | } 43 | } 44 | return this; 45 | } 46 | 47 | public RegexCommand build() { 48 | return command; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/ReplyCommand.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | public class ReplyCommand extends AbstractCommand { 4 | private String[] recvMessages; 5 | private String[] replyMessages; 6 | 7 | public void setReplyMessages(String[] replyMessages) { 8 | this.replyMessages = replyMessages; 9 | } 10 | 11 | public void setRecvMessages(String[] recvMessages) { 12 | this.recvMessages = recvMessages; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/ReplyCommandBuilder.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public class ReplyCommandBuilder { 6 | private final ReplyCommand command = new ReplyCommand(); 7 | 8 | public ReplyCommandBuilder replyMessages(String... strings) { 9 | command.setRecvMessages(strings); 10 | return this; 11 | } 12 | 13 | public ReplyCommandBuilder recvMessages(String... strings) { 14 | command.setRecvMessages(strings); 15 | return this; 16 | } 17 | 18 | public ReplyCommandBuilder onCall(Scope scope, @NotNull MsgHandleable onCall) { 19 | command.addScope(scope); 20 | if(scope==Scope.GLOBAL) { 21 | command.setGlobalOnCall(onCall); 22 | } else { 23 | switch(scope) { 24 | case USER -> command.setUserOnCall(onCall); 25 | case GROUP -> command.setGroupOnCall(onCall); 26 | case ADMIN -> command.setAdminOnCall(onCall); 27 | } 28 | } 29 | return this; 30 | } 31 | 32 | public ReplyCommand build() { 33 | return command; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/command/Scope.java: -------------------------------------------------------------------------------- 1 | package com.mirai.command; 2 | 3 | public enum Scope { 4 | GLOBAL, //全局 5 | USER, //私聊 6 | GROUP, //群聊 7 | ADMIN //机器人管理员 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/config/AbstractConfig.java: -------------------------------------------------------------------------------- 1 | package com.mirai.config; 2 | 3 | import com.dancecube.token.Token; 4 | import org.yaml.snakeyaml.Yaml; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.util.HashMap; 10 | import java.util.HashSet; 11 | import java.util.Map; 12 | 13 | public abstract class AbstractConfig { 14 | public static HashMap userTokensMap; 15 | public static HashSet logStatus = new HashSet<>(); 16 | public static String linuxRootPath; 17 | public static String windowsRootPath; 18 | private static final boolean windowsMark; 19 | public static String configPath; 20 | 21 | // api key 22 | public static String gaodeApiKey = ""; 23 | public static String tencentSecretId = ""; 24 | public static String tencentSecretKey = ""; 25 | 26 | static { 27 | 28 | windowsMark = new File("./WINDOWS_MARK").exists(); 29 | try { 30 | linuxRootPath = new File("..").getCanonicalPath(); 31 | windowsRootPath = new File(".").getCanonicalPath(); 32 | 33 | //在项目下创建 “WINDOWS_MARK” 文件,存在即使用Windows路径的配置,而Linux则不需要 34 | if(itIsAReeeeaaaalWindowsMark()) { 35 | configPath = windowsRootPath + "/DcConfig/"; 36 | } else { 37 | configPath = linuxRootPath + "/DcConfig/"; 38 | } 39 | new File(configPath).mkdirs(); 40 | } catch(IOException e) { 41 | e.printStackTrace(); 42 | } 43 | 44 | 45 | // Authorization错误时查看控制台ip白名单 46 | Map> map; 47 | try { 48 | File apiKeyYml = new File(configPath + "ApiKeys.yml"); 49 | if(!apiKeyYml.exists()) { 50 | apiKeyYml.getParentFile().mkdirs(); 51 | apiKeyYml.createNewFile(); 52 | } 53 | 54 | map = new Yaml().load(Files.readString(apiKeyYml.toPath())); 55 | } catch(IOException e) { 56 | throw new RuntimeException(e); 57 | } 58 | try { 59 | Map tencentScannerKeys = map.get("tencentScannerKeys"); 60 | Map gaodeMapKeys = map.get("gaodeMapKeys"); 61 | 62 | gaodeApiKey = gaodeMapKeys.get("apiKey"); 63 | tencentSecretId = tencentScannerKeys.get("secretId"); 64 | tencentSecretKey = tencentScannerKeys.get("secretKey"); 65 | } catch(NullPointerException e) { 66 | System.out.println("# ApiKey配置不完整!"); 67 | e.printStackTrace(); 68 | } 69 | } 70 | 71 | /** 72 | * 让我看看是不是Windows! 73 | * 74 | * @return 这是个Windows系统 75 | */ 76 | public static boolean itIsAReeeeaaaalWindowsMark() { 77 | return windowsMark; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/config/UserConfigUtils.java: -------------------------------------------------------------------------------- 1 | package com.mirai.config; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.reflect.TypeToken; 6 | 7 | import java.io.FileNotFoundException; 8 | import java.io.FileOutputStream; 9 | import java.io.FileReader; 10 | import java.io.IOException; 11 | import java.lang.reflect.Type; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.HashMap; 14 | import java.util.HashSet; 15 | 16 | public class UserConfigUtils extends AbstractConfig { 17 | public static HashMap> configsFromFile(String filePath) { 18 | Type type = new TypeToken>>() { 19 | }.getType(); 20 | HashMap> map = null; 21 | try { 22 | map = new Gson().fromJson(new FileReader(filePath), type); 23 | } catch(FileNotFoundException e) { 24 | e.printStackTrace(); 25 | } 26 | return map; 27 | } 28 | 29 | public static void configsToFile(HashMap> map, String filePath) { 30 | String json = new GsonBuilder().setPrettyPrinting().create().toJson(map); 31 | try { 32 | FileOutputStream stream = new FileOutputStream(filePath); 33 | stream.write(json.getBytes(StandardCharsets.UTF_8)); 34 | stream.close(); 35 | } catch(IOException e) { 36 | e.printStackTrace(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/event/MainHandler.java: -------------------------------------------------------------------------------- 1 | package com.mirai.event; 2 | 3 | import com.dancecube.token.Token; 4 | import com.dancecube.token.TokenBuilder; 5 | import net.mamoe.mirai.Bot; 6 | import net.mamoe.mirai.contact.Contact; 7 | import net.mamoe.mirai.contact.Friend; 8 | import net.mamoe.mirai.contact.friendgroup.FriendGroup; 9 | import net.mamoe.mirai.event.EventHandler; 10 | import net.mamoe.mirai.event.events.MessageEvent; 11 | import net.mamoe.mirai.event.events.NewFriendRequestEvent; 12 | import net.mamoe.mirai.event.events.NudgeEvent; 13 | import net.mamoe.mirai.message.data.At; 14 | import net.mamoe.mirai.message.data.MessageChain; 15 | import net.mamoe.mirai.message.data.PlainText; 16 | 17 | import static com.mirai.config.AbstractConfig.configPath; 18 | import static com.mirai.config.AbstractConfig.userTokensMap; 19 | 20 | // 不过滤通道 21 | public class MainHandler { 22 | 23 | @EventHandler 24 | public static void eventCenter(MessageEvent event) { 25 | MessageChain messageChain = event.getMessage(); 26 | if(messageChain.size() - 1==messageChain.stream() 27 | .filter(msg -> msg instanceof At | msg instanceof PlainText) 28 | .toList().size()) { 29 | PlainTextHandler.accept(event); 30 | } else return; 31 | 32 | String message = messageChain.contentToString(); 33 | long qq = event.getSender().getId(); // qq发送者id 而非群聊id 34 | Contact contact = event.getSubject(); 35 | 36 | // 文本消息检测 37 | switch(message) { 38 | case "#save" -> saveTokens(contact); 39 | case "#load" -> loadTokens(contact); 40 | case "#logout" -> logoutToken(contact); 41 | } 42 | } 43 | 44 | @EventHandler 45 | public static void NudgeHandler(NudgeEvent event) { 46 | if(event.getTarget() instanceof Bot) { 47 | event.getFrom().nudge().sendTo(event.getSubject()); 48 | } 49 | } 50 | 51 | @EventHandler 52 | public static void addFriendHandler(NewFriendRequestEvent event) { 53 | event.accept(); 54 | Friend friend = event.getBot().getFriend(event.getFromId()); 55 | if(friend != null) { 56 | friend.sendMessage("🥰呐~ 现在我们是好朋友啦!\n请到主页查看功能哦!"); 57 | FriendGroup friendGroup = event.getBot().getFriendGroups().get(0); 58 | if(friendGroup != null) { 59 | friendGroup.moveIn(friend); 60 | } 61 | } 62 | } 63 | 64 | 65 | /** 66 | * 保存Token到文件JSON 67 | * 68 | * @param contact 触发对象 69 | */ 70 | public static void saveTokens(Contact contact) { 71 | TokenBuilder.tokensToFile(userTokensMap, configPath + "UserTokens.json"); 72 | contact.sendMessage("保存成功!共%d条".formatted(userTokensMap.size())); 73 | } 74 | 75 | /** 76 | * 从文件JSON中加载Token 77 | * 78 | * @param contact 触发对象 79 | */ 80 | public static void loadTokens(Contact contact) { 81 | String path = configPath + "UserTokens.json"; 82 | userTokensMap = TokenBuilder.tokensFromFile(path, false); 83 | contact.sendMessage("不刷新加载成功!共%d条".formatted(userTokensMap.size())); 84 | } 85 | 86 | /** 87 | * 注销Token 88 | * 89 | * @param contact 触发对象 90 | */ 91 | public static void logoutToken(Contact contact) { 92 | long qq = contact.getId(); 93 | Token token = userTokensMap.get(qq); 94 | if(token == null) { 95 | contact.sendMessage("当前账号未登录到舞小铃!"); 96 | return; 97 | } 98 | userTokensMap.remove(qq); 99 | contact.sendMessage("id:%d 注销成功!".formatted(token.getUserId())); 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /src/main/java/com/mirai/event/PlainTextHandler.java: -------------------------------------------------------------------------------- 1 | package com.mirai.event; 2 | 3 | import com.mirai.command.*; 4 | import net.mamoe.mirai.contact.Contact; 5 | import net.mamoe.mirai.contact.Group; 6 | import net.mamoe.mirai.contact.User; 7 | import net.mamoe.mirai.event.events.MessageEvent; 8 | import net.mamoe.mirai.message.data.At; 9 | import net.mamoe.mirai.message.data.MessageChain; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Calendar; 14 | import java.util.HashSet; 15 | 16 | public class PlainTextHandler { 17 | 18 | public static HashSet adminsSet = new HashSet<>(); 19 | 20 | static { 21 | // 初始化AllCommands所有指令 22 | AllCommands.init(); 23 | adminsSet.add(2862125721L); 24 | } 25 | 26 | public static HashSet regexCommands = AllCommands.regexCommands; //所有正则指令 27 | public static HashSet argsCommands = AllCommands.argsCommands; //所有参数指令 28 | 29 | private static final int MAX_LENGTH = 0x7fff; //单次指令字符最大长度(用于过滤) 30 | 31 | 32 | /** 33 | * 事件处理 34 | * 35 | * @param messageEvent 消息事件 36 | */ 37 | public static void accept(MessageEvent messageEvent) { 38 | MessageChain messageChain = messageEvent.getMessage(); 39 | 40 | String message; 41 | 42 | if(messageChain.stream().anyMatch(m -> m instanceof At)) { //At 转 QQ 43 | StringBuilder builder = new StringBuilder(); 44 | messageChain.forEach(msg -> builder.append(" ").append(msg instanceof At ? (((At) msg).getTarget()) : msg.contentToString())); 45 | message = builder.toString(); 46 | } else { 47 | message = messageChain.contentToString(); 48 | } 49 | if(message.length()>MAX_LENGTH) return; 50 | 51 | 52 | // 执行正则指令 53 | for(RegexCommand regexCommand : regexCommands) {// 匹配作用域 54 | boolean find = regexCommand.getRegex().matcher(message).find(); 55 | if(find) { 56 | runCommand(messageEvent, regexCommand); 57 | } 58 | } 59 | 60 | ArrayList prefixAndArgs = new ArrayList<>(Arrays.asList(message.strip().split("\\s+"))); 61 | String msgPre = prefixAndArgs.remove(0); //前缀 62 | String[] args = prefixAndArgs.isEmpty() ? null : prefixAndArgs.toArray(new String[0]); //参数 奇奇怪怪的特性,这不是空数组! 63 | 64 | 65 | //执行参数指令 66 | for(ArgsCommand command : argsCommands) { 67 | String[] commandPrefixes = command.getPrefix(); 68 | if(Arrays.asList(commandPrefixes).contains(msgPre)) { 69 | if(ArgsCommand.checkError(command, args) < 0) { //args可能不存在,需要判空 70 | runCommand(messageEvent, command, args); 71 | } 72 | } 73 | } 74 | 75 | 76 | } 77 | 78 | private static final MsgHandleable MUTE = (event, contact, qq, args) -> contact.sendMessage("小铃困啦,白天再来玩吧,先晚安安啦~"); 79 | 80 | /** 81 | * 宵禁 82 | * 83 | * @return 是否在宵禁 84 | */ 85 | public static boolean isMutedNow() { 86 | // 获取当前时间 87 | int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); 88 | // 如果当前时间是 3 点到 4 点,则将 `autoMuted` 设置为 true 89 | return hour>=3 && hour<=4; 90 | } 91 | 92 | //含参指令 93 | private static void runCommand(MessageEvent messageEvent, AbstractCommand command, String[] args) { 94 | 95 | HashSet scopes = command.getScopes(); //作用域 96 | long qq = messageEvent.getSender().getId(); // qq不为contact.getId() 97 | Contact contact = messageEvent.getSubject(); //发送对象 98 | 99 | if(isMutedNow()) { 100 | MUTE.handle(messageEvent, contact, qq, args); 101 | if(!adminsSet.contains(qq)) return; 102 | } 103 | 104 | if(scopes.contains(Scope.GLOBAL)) 105 | command.onCall(Scope.GLOBAL, messageEvent, contact, qq, args); 106 | else if((scopes.contains(Scope.USER) & contact instanceof User)) 107 | command.onCall(Scope.USER, messageEvent, contact, qq, args); 108 | else if((scopes.contains(Scope.GROUP) & contact instanceof Group)) 109 | command.onCall(Scope.GROUP, messageEvent, contact, qq, args); 110 | else if(scopes.contains(Scope.ADMIN) & adminsSet.contains(qq)) 111 | command.onCall(Scope.ADMIN, messageEvent, contact, qq, args); 112 | } 113 | 114 | // 无参指令(其实就是给上面的runCommand传了个args=null) 115 | private static void runCommand(MessageEvent messageEvent, AbstractCommand command) { 116 | runCommand(messageEvent, command, null); 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/task/RefreshTokenJob.java: -------------------------------------------------------------------------------- 1 | package com.mirai.task; 2 | 3 | import com.dancecube.token.Token; 4 | import com.dancecube.token.TokenBuilder; 5 | import com.mirai.config.AbstractConfig; 6 | import org.quartz.Job; 7 | import org.quartz.JobExecutionContext; 8 | import org.quartz.JobExecutionException; 9 | 10 | import java.text.SimpleDateFormat; 11 | import java.util.Date; 12 | import java.util.HashMap; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | import static com.mirai.config.AbstractConfig.configPath; 16 | 17 | /* 18 | 为什么设计成JobDetail + Job,不直接使用Job? 19 | 20 | JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。 21 | 这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。 22 | 而JobDetail & Job 方式,Scheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以 规避并发访问 的问题 23 | */ 24 | 25 | /** 26 | * 刷新本地Token任务 27 | * 28 | * @author Lin 29 | */ 30 | public class RefreshTokenJob implements Job { 31 | private static final String dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); 32 | private static final HashMap userTokensMap = AbstractConfig.userTokensMap; 33 | 34 | public void execute(JobExecutionContext context) throws JobExecutionException { 35 | AtomicInteger validNum = new AtomicInteger(); 36 | userTokensMap.forEach( 37 | (qq, token) -> { 38 | if(token.refresh()) validNum.getAndIncrement(); 39 | } 40 | ); 41 | TokenBuilder.tokensToFile(userTokensMap, configPath + "UserTokens.json"); 42 | System.out.printf("#%s 读取共%d个token,有效token共%d个%n", dateFormat, userTokensMap.size(), validNum.get()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/mirai/task/SchedulerTask.java: -------------------------------------------------------------------------------- 1 | package com.mirai.task; 2 | 3 | import org.quartz.*; 4 | import org.quartz.impl.StdSchedulerFactory; 5 | 6 | public class SchedulerTask { 7 | 8 | public static void autoRefreshToken() { 9 | Scheduler scheduler; 10 | try { 11 | scheduler = new StdSchedulerFactory().getScheduler(); 12 | } catch(SchedulerException e) { 13 | throw new RuntimeException(e); 14 | } 15 | JobDetail jobDetail = JobBuilder.newJob(RefreshTokenJob.class).build(); 16 | Trigger trigger = TriggerBuilder.newTrigger().startNow() 17 | // .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(60)) 18 | .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 10)) 19 | .build(); 20 | try { 21 | scheduler.scheduleJob(jobDetail, trigger); 22 | scheduler.start(); 23 | } catch(SchedulerException e) { 24 | throw new RuntimeException(e); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/tools/DanceCubeRequestCrypto.java: -------------------------------------------------------------------------------- 1 | package com.tools; 2 | 3 | import com.google.gson.JsonObject; 4 | 5 | import javax.crypto.BadPaddingException; 6 | import javax.crypto.Cipher; 7 | import javax.crypto.IllegalBlockSizeException; 8 | import javax.crypto.NoSuchPaddingException; 9 | import javax.crypto.spec.IvParameterSpec; 10 | import javax.crypto.spec.SecretKeySpec; 11 | import java.nio.charset.StandardCharsets; 12 | import java.security.InvalidAlgorithmParameterException; 13 | import java.security.InvalidKeyException; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.util.Base64; 16 | import java.util.Map; 17 | 18 | public class DanceCubeRequestCrypto { 19 | public static final String KEY = "3339363237333738"; 20 | public static final String IV = "3339363237333738"; 21 | 22 | 23 | public static String dataBuild(String path, 24 | Map params, 25 | String method, 26 | JsonObject data, 27 | String contentType) { 28 | String paramsStr = convertMapToString(params); 29 | 30 | String result = "{\"path\":\"%s\",\"param\":\"%s\",\"data\":%s,\"method\":\"%s\",\"contentType\":\"%s\"}" 31 | .formatted(path, paramsStr, method, data.toString(), contentType); 32 | return result; 33 | 34 | } 35 | 36 | public static String decryptWithDesCbc(String cipherText, byte[] key, byte[] iv) { 37 | Cipher cipher = null; 38 | String result = null; 39 | try { 40 | cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); 41 | cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "DES"), new IvParameterSpec(iv)); 42 | 43 | byte[] ciphertext = Base64.getDecoder().decode(cipherText); 44 | byte[] plaintext = cipher.doFinal(ciphertext); 45 | result = new String(plaintext, StandardCharsets.UTF_8); 46 | } catch(NoSuchAlgorithmException | NoSuchPaddingException e) { 47 | throw new RuntimeException("No such algorithm or padding", e); 48 | } catch(InvalidKeyException | InvalidAlgorithmParameterException e) { 49 | throw new RuntimeException("Invalid key or iv", e); 50 | } catch(IllegalBlockSizeException | BadPaddingException e) { 51 | throw new RuntimeException("Illegal block size or bad padding in doFinal()", e); 52 | } 53 | return result; 54 | } 55 | 56 | public static String cryptoWithDesCbc(String plainText, byte[] key, byte[] iv) { 57 | Cipher cipher = null; 58 | String result = null; 59 | try { 60 | cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); 61 | cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "DES"), new IvParameterSpec(iv)); 62 | 63 | byte[] ciphertext = Base64.getDecoder().decode(plainText); 64 | byte[] plaintext = cipher.doFinal(ciphertext); 65 | result = new String(plaintext, StandardCharsets.UTF_8); 66 | } catch(NoSuchAlgorithmException | NoSuchPaddingException e) { 67 | throw new RuntimeException("No such algorithm or padding", e); 68 | } catch(InvalidKeyException | InvalidAlgorithmParameterException e) { 69 | throw new RuntimeException("Invalid key or iv", e); 70 | } catch(IllegalBlockSizeException | BadPaddingException e) { 71 | throw new RuntimeException("Illegal block size or bad padding in doFinal()", e); 72 | } 73 | return result; 74 | } 75 | 76 | private static byte[] hexToBytes(String hex) { 77 | int len = hex.length(); 78 | byte[] bytes = new byte[len / 2]; 79 | for(int i = 0; i < len; i += 2) { 80 | bytes[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) 81 | + Character.digit(hex.charAt(i + 1), 16)); 82 | } 83 | return bytes; 84 | } 85 | 86 | public static String convertMapToString(Map map) { 87 | StringBuilder result = new StringBuilder(); 88 | for(Map.Entry entry : map.entrySet()) { 89 | if(!result.isEmpty()) { 90 | result.append("&"); 91 | } 92 | result.append(entry.getKey()).append("=").append(entry.getValue()); 93 | } 94 | return result.toString(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/tools/HttpUtil.java: -------------------------------------------------------------------------------- 1 | package com.tools; 2 | 3 | import com.google.zxing.*; 4 | import com.google.zxing.client.j2se.BufferedImageLuminanceSource; 5 | import com.google.zxing.common.HybridBinarizer; 6 | import com.mirai.config.AbstractConfig; 7 | import com.tencentcloudapi.common.Credential; 8 | import com.tencentcloudapi.common.exception.TencentCloudSDKException; 9 | import com.tencentcloudapi.common.profile.ClientProfile; 10 | import com.tencentcloudapi.common.profile.HttpProfile; 11 | import com.tencentcloudapi.ocr.v20181119.OcrClient; 12 | import com.tencentcloudapi.ocr.v20181119.models.QrcodeOCRRequest; 13 | import com.tencentcloudapi.ocr.v20181119.models.QrcodeOCRResponse; 14 | import net.mamoe.mirai.contact.Contact; 15 | import net.mamoe.mirai.message.data.Image; 16 | import net.mamoe.mirai.utils.ExternalResource; 17 | import okhttp3.*; 18 | import org.jetbrains.annotations.NotNull; 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import javax.imageio.ImageIO; 22 | import java.awt.image.BufferedImage; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.net.URL; 26 | import java.net.URLConnection; 27 | import java.util.HashMap; 28 | import java.util.Map; 29 | 30 | 31 | public class HttpUtil { 32 | 33 | public static Image getImageFromURL(String strUrl, Contact contact) { 34 | Image image = null; 35 | try { 36 | URL url = new URL(strUrl); 37 | URLConnection uc = url.openConnection(); 38 | InputStream in = uc.getInputStream(); 39 | byte[] bytes = in.readAllBytes(); 40 | in.close(); 41 | ExternalResource ex = ExternalResource.create(bytes); 42 | image = ExternalResource.uploadAsImage(ex, contact); 43 | ex.close(); 44 | } catch(IOException e) { 45 | e.printStackTrace(); 46 | System.out.println("# getImageFromURL出bug啦!"); 47 | } 48 | return image; 49 | } 50 | 51 | public static Image getImageFromStream(InputStream inputStream, Contact contact) { 52 | try { 53 | return getImageFromBytes(inputStream.readAllBytes(), contact); 54 | } catch(IOException e) { 55 | throw new RuntimeException(e); 56 | } 57 | } 58 | 59 | public static Image getImageFromBytes(byte[] bytes, Contact contact) { 60 | Image image = null; 61 | try { 62 | ExternalResource ex = ExternalResource.create(bytes); 63 | image = ExternalResource.uploadAsImage(ex, contact); 64 | ex.close(); 65 | } catch(IOException e) { 66 | e.printStackTrace(); 67 | System.out.println("# getImageFromIS出bug啦!"); 68 | } 69 | return image; 70 | } 71 | 72 | 73 | /** 74 | * @param url 使用URL格式,即“ http:// ”和“ file:/// ” 75 | */ 76 | public static String qrDecodeZXing(String url) { 77 | BufferedImage bufferedImage = null; 78 | try { 79 | bufferedImage = ImageIO.read(new URL(url)); 80 | } catch(IOException e) { 81 | e.printStackTrace(); 82 | } 83 | LuminanceSource source = null; 84 | if(bufferedImage!=null) source = new BufferedImageLuminanceSource(bufferedImage); 85 | Binarizer binarizer = new HybridBinarizer(source); 86 | BinaryBitmap bitmap = new BinaryBitmap(binarizer); 87 | HashMap decodeHints = new HashMap<>(); 88 | decodeHints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); 89 | Result result = null; 90 | try { 91 | result = new MultiFormatReader().decode(bitmap, decodeHints); 92 | } catch(NotFoundException e) { 93 | e.printStackTrace(); 94 | } 95 | return result==null ? "" : result.getText(); 96 | } 97 | 98 | public static String qrDecodeTencent(String imgUrl) { 99 | String url = ""; 100 | try { 101 | Credential cred = new Credential(AbstractConfig.tencentSecretId, AbstractConfig.tencentSecretKey); 102 | // 实例化一个http选项,可选的,没有特殊需求可以跳过 103 | HttpProfile httpProfile = new HttpProfile(); 104 | httpProfile.setEndpoint("ocr.tencentcloudapi.com"); 105 | // 实例化一个client选项,可选的,没有特殊需求可以跳过 106 | ClientProfile clientProfile = new ClientProfile(); 107 | clientProfile.setHttpProfile(httpProfile); 108 | // 实例化要请求产品的client对象,clientProfile是可选的 109 | OcrClient client = new OcrClient(cred, "ap-shanghai", clientProfile); 110 | // 实例化一个请求对象,每个接口都会对应一个request对象 111 | QrcodeOCRRequest req = new QrcodeOCRRequest(); 112 | req.setImageUrl(imgUrl); 113 | // 返回的resp是一个QrcodeOCRResponse的实例,与请求对象对应 114 | QrcodeOCRResponse resp = client.QrcodeOCR(req); 115 | // 输出json格式的字符串回包 116 | url = resp.getCodeResults()[0].getUrl(); 117 | } catch(TencentCloudSDKException e) { 118 | e.printStackTrace(); 119 | } 120 | return url; 121 | } 122 | 123 | public static String getLocationInfo(String region) { 124 | Response response = httpApi("https://restapi.amap.com/v3/geocode/geo?address=" + region.strip() + "&output=json&key=" + AbstractConfig.gaodeApiKey); 125 | String result = ""; 126 | try { 127 | if(response!=null && response.body()!=null) { 128 | result = response.body().string(); 129 | response.close(); 130 | } 131 | } catch(IOException e) { 132 | e.printStackTrace(); 133 | } 134 | return result; 135 | } 136 | 137 | @Nullable // 用于获取HTTP API资源 138 | public static Response httpApi(String url) { 139 | Request request = new Request.Builder().url(url).get().build(); 140 | OkHttpClient client = new OkHttpClient(); 141 | 142 | try { 143 | return client.newCall(request).execute(); 144 | } catch(IOException e) { 145 | e.printStackTrace(); 146 | } 147 | return null; 148 | } 149 | 150 | @Nullable // GET 151 | public static Response httpApi(String url, @NotNull Map headersMap) { 152 | OkHttpClient client = new OkHttpClient(); 153 | Request request = new Request.Builder().url(url).get().headers(Headers.of(headersMap)).build(); 154 | 155 | try { 156 | return client.newCall(request).execute(); 157 | } catch(IOException e) { 158 | e.printStackTrace(); 159 | } 160 | return null; 161 | } 162 | 163 | /** 164 | * @param bodyMap MediaType 默认为 application/x-www-form-urlencoded, null时则为POST请求 165 | */ 166 | @Nullable // POST 167 | public static Response httpApi(String url, @NotNull Map headersMap, @Nullable Map bodyMap) { 168 | 169 | if(bodyMap==null) bodyMap = Map.of("", ""); 170 | String body = bodyOf(bodyMap); 171 | OkHttpClient client = new OkHttpClient(); 172 | MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded"); 173 | RequestBody requestBody = RequestBody.create(body, mediaType); 174 | Request request = new Request.Builder().url(url).headers(Headers.of(headersMap)).post(requestBody).build(); 175 | 176 | try { 177 | return client.newCall(request).execute(); 178 | } catch(IOException e) { 179 | e.printStackTrace(); 180 | } 181 | return null; 182 | } 183 | 184 | public static Response httpApiPut(String url, @NotNull Map headersMap, Map bodyMap) { 185 | String body = bodyOf(bodyMap); 186 | 187 | OkHttpClient client = new OkHttpClient(); 188 | MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded"); 189 | RequestBody requestBody = RequestBody.create(body, mediaType); 190 | 191 | Request request = new Request.Builder().url(url).headers(Headers.of(headersMap)).put(requestBody).build(); 192 | try { 193 | return client.newCall(request).execute(); 194 | } catch(IOException e) { 195 | throw new RuntimeException(e); 196 | } 197 | } 198 | 199 | 200 | public static Call httpApiCall(String url, @NotNull Map headersMap, Map bodyMap) { 201 | 202 | String body = bodyOf(bodyMap); 203 | 204 | OkHttpClient client = new OkHttpClient(); 205 | MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded"); 206 | RequestBody requestBody = RequestBody.create(body, mediaType); 207 | 208 | Request request = new Request.Builder().url(url).headers(Headers.of(headersMap)).post(requestBody).build(); 209 | return client.newCall(request); 210 | } 211 | 212 | public static Call httpApiCall(String url, @NotNull Map headersMap) { 213 | OkHttpClient client = new OkHttpClient(); 214 | Request request = new Request.Builder().url(url).headers(Headers.of(headersMap)).get().build(); 215 | return client.newCall(request); 216 | } 217 | 218 | @NotNull 219 | private static String bodyOf(Map bodyMap) { 220 | StringBuilder bodySb = new StringBuilder(); 221 | bodyMap.forEach((k, v) -> bodySb.append('&').append(k).append('=').append(v)); 222 | bodySb.deleteCharAt(0); 223 | return bodySb.toString(); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/main/java/com/tools/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package com.tools; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.Map; 8 | 9 | public class JsonUtil { 10 | 11 | @NotNull 12 | public static JsonObject mergeJson(JsonObject... jsonObjects) { 13 | JsonObject mergedObject = new JsonObject(); 14 | for(JsonObject json : jsonObjects) { 15 | for(Map.Entry entry : json.entrySet()) { 16 | mergedObject.add(entry.getKey(), entry.getValue()); 17 | } 18 | } 19 | return mergedObject; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/tools/image/ImageDrawer.java: -------------------------------------------------------------------------------- 1 | package com.tools.image; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import javax.imageio.IIOImage; 6 | import javax.imageio.ImageIO; 7 | import javax.imageio.ImageWriteParam; 8 | import javax.imageio.ImageWriter; 9 | import java.awt.*; 10 | import java.awt.image.BufferedImage; 11 | import java.io.*; 12 | import java.net.URL; 13 | import java.util.Iterator; 14 | 15 | public class ImageDrawer { 16 | private final BufferedImage originImage; 17 | private final Graphics2D graphics; 18 | 19 | public ImageDrawer(BufferedImage image) { 20 | originImage = image; 21 | graphics = originImage.createGraphics(); 22 | graphics.setColor(Color.BLACK); 23 | } 24 | 25 | public ImageDrawer(File file) { 26 | try { 27 | originImage = ImageIO.read(new FileInputStream(file)); 28 | graphics = originImage.createGraphics(); 29 | graphics.setColor(Color.BLACK); 30 | } catch(IOException e) { 31 | throw new RuntimeException(e); 32 | } 33 | } 34 | 35 | public ImageDrawer(String url) { 36 | try { 37 | originImage = ImageIO.read(new URL(url)); 38 | } catch(IOException e) { 39 | throw new RuntimeException(e); 40 | } 41 | graphics = originImage.createGraphics(); 42 | graphics.setColor(Color.BLACK); 43 | } 44 | 45 | public ImageDrawer color(Color color) { 46 | graphics.setColor(color); 47 | return this; 48 | } 49 | 50 | public ImageDrawer font(Font font) { 51 | graphics.setFont(font); 52 | return this; 53 | } 54 | 55 | public ImageDrawer paint(Paint paint) { 56 | graphics.setPaint(paint); 57 | return this; 58 | } 59 | 60 | public ImageDrawer clip(Shape clip) { 61 | graphics.setClip(clip); 62 | return this; 63 | } 64 | 65 | 66 | public ImageDrawer clip(int x, int y, int width, int height) { 67 | graphics.setClip(x, y, width, height); 68 | return this; 69 | } 70 | 71 | private BufferedImage makeBlur(BufferedImage srcImage, int radius) { 72 | 73 | if(radius<1) { 74 | return srcImage; 75 | } 76 | 77 | int w = srcImage.getWidth(); 78 | int h = srcImage.getHeight(); 79 | 80 | int[] pix = new int[w * h]; 81 | srcImage.getRGB(0, 0, w, h, pix, 0, w); 82 | 83 | int wm = w - 1; 84 | int hm = h - 1; 85 | int wh = w * h; 86 | int div = radius + radius + 1; 87 | 88 | int[] r = new int[wh]; 89 | int[] g = new int[wh]; 90 | int[] b = new int[wh]; 91 | int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; 92 | int[] vmin = new int[Math.max(w, h)]; 93 | 94 | int divsum = (div + 1) >> 1; 95 | divsum *= divsum; 96 | int[] dv = new int[256 * divsum]; 97 | for(i = 0; i<256 * divsum; i++) { 98 | dv[i] = (i / divsum); 99 | } 100 | 101 | yw = yi = 0; 102 | 103 | int[][] stack = new int[div][3]; 104 | int stackpointer; 105 | int stackstart; 106 | int[] sir; 107 | int rbs; 108 | int r1 = radius + 1; 109 | int routsum, goutsum, boutsum; 110 | int rinsum, ginsum, binsum; 111 | 112 | for(y = 0; y> 16; 118 | sir[1] = (p & 0x00ff00) >> 8; 119 | sir[2] = (p & 0x0000ff); 120 | rbs = r1 - Math.abs(i); 121 | rsum += sir[0] * rbs; 122 | gsum += sir[1] * rbs; 123 | bsum += sir[2] * rbs; 124 | if(i>0) { 125 | rinsum += sir[0]; 126 | ginsum += sir[1]; 127 | binsum += sir[2]; 128 | } else { 129 | routsum += sir[0]; 130 | goutsum += sir[1]; 131 | boutsum += sir[2]; 132 | } 133 | } 134 | stackpointer = radius; 135 | 136 | for(x = 0; x> 16; 159 | sir[1] = (p & 0x00ff00) >> 8; 160 | sir[2] = (p & 0x0000ff); 161 | 162 | rinsum += sir[0]; 163 | ginsum += sir[1]; 164 | binsum += sir[2]; 165 | 166 | rsum += rinsum; 167 | gsum += ginsum; 168 | bsum += binsum; 169 | 170 | stackpointer = (stackpointer + 1) % div; 171 | sir = stack[(stackpointer) % div]; 172 | 173 | routsum += sir[0]; 174 | goutsum += sir[1]; 175 | boutsum += sir[2]; 176 | 177 | rinsum -= sir[0]; 178 | ginsum -= sir[1]; 179 | binsum -= sir[2]; 180 | 181 | yi++; 182 | } 183 | yw += w; 184 | } 185 | for(x = 0; x0) { 204 | rinsum += sir[0]; 205 | ginsum += sir[1]; 206 | binsum += sir[2]; 207 | } else { 208 | routsum += sir[0]; 209 | goutsum += sir[1]; 210 | boutsum += sir[2]; 211 | } 212 | 213 | if(imaxWidth) { 445 | String ellipsis = "..."; 446 | int ellipsisWidth = metrics.stringWidth(ellipsis); 447 | 448 | while(textWidth + ellipsisWidth>maxWidth && !text.isEmpty()) { 449 | text = text.substring(0, text.length() - 1); 450 | textWidth = metrics.stringWidth(text); 451 | } 452 | text += ellipsis; 453 | } 454 | return text; 455 | } 456 | 457 | public static void printAvailableFonts() { 458 | // 获取系统所有可用字体名称 459 | GraphicsEnvironment e = GraphicsEnvironment.getLocalGraphicsEnvironment(); 460 | String[] fontName = e.getAvailableFontFamilyNames(); 461 | for(String s : fontName) { 462 | System.out.println(s); 463 | } 464 | } 465 | 466 | public static BufferedImage convertPngToJpg(BufferedImage pngImage, float quality) { 467 | BufferedImage rgbImage = new BufferedImage(pngImage.getWidth(), pngImage.getHeight(), BufferedImage.TYPE_INT_RGB); 468 | Graphics2D g = rgbImage.createGraphics(); 469 | g.drawImage(pngImage, 0, 0, null); 470 | g.dispose(); 471 | try(ByteArrayOutputStream os = new ByteArrayOutputStream()) { 472 | Iterator writers = ImageIO.getImageWritersByFormatName("jpg"); 473 | if(!writers.hasNext()) { 474 | throw new IllegalStateException("No writers found for JPEG format."); 475 | } 476 | ImageWriter writer = writers.next(); 477 | ImageWriteParam iwp = writer.getDefaultWriteParam(); 478 | 479 | // 设置压缩质量 480 | iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); 481 | iwp.setCompressionQuality(quality); 482 | 483 | writer.setOutput(ImageIO.createImageOutputStream(os)); 484 | IIOImage iioImage = new IIOImage(rgbImage, null, null); 485 | writer.write(null, iioImage, iwp); 486 | writer.dispose(); 487 | 488 | // 从字节数组中读取JPEG图像 489 | try(ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray())) { 490 | return ImageIO.read(is); 491 | } 492 | } catch(Exception e) { 493 | throw new RuntimeException("Error compressing image to JPEG format and reading it back.", e); 494 | } 495 | } 496 | } 497 | 498 | -------------------------------------------------------------------------------- /src/main/java/com/tools/image/ImageEffect.java: -------------------------------------------------------------------------------- 1 | package com.tools.image; 2 | 3 | public class ImageEffect { 4 | private int arcW = -1; 5 | private int arcH = -1; 6 | private int blur = -1; 7 | 8 | 9 | public ImageEffect setArc(int Arc) { 10 | this.arcW = Arc; 11 | this.arcH = Arc; 12 | return this; 13 | } 14 | 15 | public int getArcW() { 16 | return arcW; 17 | } 18 | 19 | public ImageEffect setArcW(int arcW) { 20 | this.arcW = arcW; 21 | return this; 22 | } 23 | 24 | public int getArcH() { 25 | return arcH; 26 | } 27 | 28 | public ImageEffect setArcH(int arcH) { 29 | this.arcH = arcH; 30 | return this; 31 | } 32 | 33 | public int getBlur() { 34 | return blur; 35 | } 36 | 37 | public ImageEffect setBlur(int blur) { 38 | this.blur = blur; 39 | return this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/tools/image/TextEffect.java: -------------------------------------------------------------------------------- 1 | package com.tools.image; 2 | 3 | public class TextEffect { 4 | private Integer maxWidth = null; 5 | private Integer spaceHeight = null; 6 | 7 | public TextEffect() { 8 | } 9 | 10 | public Integer getMaxWidth() { 11 | return maxWidth; 12 | } 13 | 14 | public TextEffect setMaxWidth(int maxWidth) { 15 | this.maxWidth = maxWidth; 16 | return this; 17 | } 18 | 19 | public Integer getSpaceHeight() { 20 | return spaceHeight; 21 | } 22 | 23 | public TextEffect setSpaceHeight(int spaceHeight) { 24 | this.spaceHeight = spaceHeight; 25 | return this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin: -------------------------------------------------------------------------------- 1 | com.mirai.MiraiBot -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | #file: noinspection YAMLSchemaValidation 2 | id: com.mirai.lin 3 | version: 0.1.0 4 | name: DanceCubeBot 5 | author: Lin 6 | dependencies: [ ] # 或者不写这行 7 | info: Just A DanceCubeBot --------------------------------------------------------------------------------