├── .gitignore ├── README.md ├── pom.xml └── src ├── main ├── java │ └── top │ │ └── mothership │ │ └── cabbage │ │ ├── annotation │ │ ├── GroupAuthorityControl.java │ │ └── UserAuthorityControl.java │ │ ├── aspect │ │ ├── ExceptionNoticeAspect.java │ │ ├── ParameterVerifyAspect.java │ │ ├── RoleControlAspect.java │ │ └── StatTimeConsAspect.java │ │ ├── constant │ │ ├── Overall.java │ │ ├── Tip.java │ │ └── pattern │ │ │ ├── CQCodePattern.java │ │ │ ├── MpCommandPattern.java │ │ │ ├── RegularPattern.java │ │ │ ├── SearchKeywordPattern.java │ │ │ └── WebPagePattern.java │ │ ├── controller │ │ ├── ApiController.java │ │ ├── CqController.java │ │ └── vo │ │ │ ├── ChartsVo.java │ │ │ └── PPChartVo.java │ │ ├── enums │ │ ├── CompressLevelEnum.java │ │ └── ParameterEnum.java │ │ ├── manager │ │ ├── ApiManager.java │ │ ├── DayLilyManager.java │ │ ├── OneBotManager.java │ │ ├── OsuApiV2Manager.java │ │ └── WebPageManager.java │ │ ├── mapper │ │ ├── RedisDAO.java │ │ ├── ResDAO.java │ │ ├── UserDAO.java │ │ └── UserInfoDAO.java │ │ ├── pojo │ │ ├── User.java │ │ ├── WebResponse.java │ │ ├── coolq │ │ │ ├── Argument.java │ │ │ ├── CqMsg.java │ │ │ ├── CqResponse.java │ │ │ ├── OneBotApiRequest.java │ │ │ ├── QQInfo.java │ │ │ └── RespData.java │ │ ├── elo │ │ │ ├── Elo.java │ │ │ ├── EloChange.java │ │ │ └── EloMatch.java │ │ └── osu │ │ │ ├── Beatmap.java │ │ │ ├── CalcResult.java │ │ │ ├── CalculateByBidRequest.java │ │ │ ├── ClientFile.java │ │ │ ├── Game.java │ │ │ ├── Lobby.java │ │ │ ├── Match.java │ │ │ ├── MpBeatmap.java │ │ │ ├── OppaiResult.java │ │ │ ├── OsuFile.java │ │ │ ├── OsuSearchResp.java │ │ │ ├── Remark.java │ │ │ ├── Replay.java │ │ │ ├── Score.java │ │ │ ├── ScoreResult.java │ │ │ ├── SearchParam.java │ │ │ ├── UserScore.java │ │ │ ├── Userinfo.java │ │ │ └── apiv2 │ │ │ ├── OAuthCredentials.java │ │ │ ├── request │ │ │ └── UserScoresRequest.java │ │ │ └── response │ │ │ ├── ApiV2Score.java │ │ │ └── TokenResponse.java │ │ ├── service │ │ ├── CqAdminServiceImpl.java │ │ ├── CqServiceImpl.java │ │ └── UserServiceImpl.java │ │ ├── task │ │ └── FixErrorBannedTasker.java │ │ ├── util │ │ ├── osu │ │ │ ├── DbFileUtil.java │ │ │ ├── Koohii.java │ │ │ ├── KoohiiLegacy.java │ │ │ ├── ReplayUtil.java │ │ │ ├── ScoreUtil.java │ │ │ ├── StringSimilarityUtil.java │ │ │ └── UserUtil.java │ │ ├── qq │ │ │ ├── ImgUtil.java │ │ │ ├── MsgQueue.java │ │ │ └── SmokeUtil.java │ │ └── web │ │ │ └── CaptchaUtil.java │ │ └── websocket │ │ ├── OneBotMessageHandler.java │ │ └── WebSocketConfig.java ├── res │ ├── font │ │ ├── Aller_Lt_MODFIED.ttf │ │ ├── Aller_Rg_MODFIED.ttf │ │ ├── Futura Std Medium.otf │ │ ├── Gayatri.ttf │ │ ├── PingFang Regular.ttf │ │ ├── Ubuntu-M.ttf │ │ ├── Ubuntu-R.ttf │ │ ├── msyh.ttc │ │ ├── tahoma.ttf │ │ └── tahomabd.ttf │ └── osu.sql ├── resources │ ├── freemarker │ │ └── mail.ftl │ ├── log4j2.xml │ ├── mybatis-config.xml │ └── spring │ │ ├── spring-db.xml │ │ └── spring-web.xml └── webapp │ └── WEB-INF │ └── web.xml └── test ├── SimpleTest.java └── test.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | /src/main/resources/cabbage.properties 4 | /src/main/resources/jdbc.properties 5 | src/main/webapp/WEB-INF/jsp/ 6 | target/ 7 | /cabbageWeb.iml 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 白菜。 2 | 3 | osu!游戏数据查询机器人。 4 | 5 | 提供: 6 | 7 | 查询本人游戏数据、与凌晨时对比游戏数据, 8 | 9 | 查询最佳成绩、当日更新最佳成绩, 10 | 11 | 最近游戏记录,谱面搜索,指定谱面成绩等功能。 12 | 13 | 14 | ## 特性 15 | 16 | 一个普通的使用三大框架的Java Web项目。 17 | 18 | 从osu!api和官网获取数据,并绘制图片以QQ消息的形式发送。 19 | 20 | 与osu! /酷Q /osusearch.com**提供的JSON API进行对接**; 21 | 22 | 使用一些简单的**正则表达式**处理用户输入、网页内容等; 23 | 24 | **实现了一个循环队列**,存储最近的群消息,用于复现其他人撤回的消息; 25 | 26 | 当日凌晨的数据(每次使用stat命令都要查询的热点数据)**使用Redis存储**,减轻数据库压力; 27 | 28 | 对外提供**RESTful**接口,供其他Bot开发者使用,免去用户重复在多个Bot出设置自己的osu!游戏id; 29 | 30 | 使用Spring框架相关功能,实现**权限控制、性能统计、异常通知、定时任务、邮件发送(已废弃)** 等。 31 | 32 | 33 | ## 鸣谢(不分先后) 34 | 35 | + [Imouto koko](https://osu.ppy.sh/u/7679162) 36 | 37 | 提供了消息队列实现思路,名片/BP/#1等大量图片元素的设计,提出了一批奠定基础的功能需求。 38 | 39 | + [Pata-Mon](https://osu.ppy.sh/u/6149313) 40 | 41 | 开发初期的赞助,发布前夕帮我指出设计思路中的致命错误。 42 | 43 | + osu!mp乐园5号群(群号:201872650)的各位群友以及管理层 44 | 45 | 测试、帮忙发掘bug,提出玩家管理需求。 46 | 47 | 广告:mp5群欢迎4500PP以下的osu!玩家加入,建议下限为2500PP(不严格)! 48 | 49 | + [Koohii](https://github.com/Francesco149/koohii) 项目 50 | 51 | 尽管作者不喜欢Java,但是依然用Java实现了oppai的全部功能,免去在Java中调用外部命令的繁琐。 52 | 53 | + [coolq-http-api](https://github.com/richardchien/coolq-http-api) 54 | 55 | 作者提供了完善的文档、稳定的更新,实现了酷Q和其他语言的打通,可以说是白菜依赖的核心。 56 | 57 | + [Sakura Miku](https://tieba.baidu.com/p/4399134680)皮肤,以及作者[鲤鱼](https://osu.ppy.sh/u/4642549) 58 | 59 | 白菜在制作之初没有考虑到后续的规模,因此直接使用了本人自用的皮肤。后来经人提醒才发现此皮肤修改自Sakura Miku,现本人已经得到作者的正式授权。 60 | 61 | 另:[自用魔改版皮肤](http://www.mothership.top/skin) 62 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | cabbageWeb 5 | cabbageWeb 6 | war 7 | 1.0-SNAPSHOT 8 | cabbageWeb Maven Webapp 9 | http://maven.apache.org 10 | 11 | 1.8 12 | 1.8 13 | 14 | 15 | 16 | jitpack.io 17 | https://jitpack.io 18 | 19 | 20 | 21 | 22 | com.google.guava 23 | guava 24 | 31.1-jre 25 | 26 | 27 | 28 | junit 29 | junit 30 | 4.12 31 | test 32 | 33 | 34 | 35 | org.jsoup 36 | jsoup 37 | 1.10.3 38 | 39 | 40 | 41 | com.google.code.gson 42 | gson 43 | 2.8.2 44 | 45 | 46 | 47 | com.github.javaplugs 48 | mybatis-gson 49 | 0.2 50 | 51 | 52 | 53 | org.apache.logging.log4j 54 | log4j-core 55 | 2.8.2 56 | 57 | 58 | 59 | 60 | org.mybatis 61 | mybatis 62 | 3.4.5 63 | 64 | 65 | 66 | mysql 67 | mysql-connector-java 68 | 8.0.28 69 | 70 | 71 | 72 | org.mybatis 73 | mybatis-spring 74 | 1.3.0 75 | 76 | 77 | 78 | com.alibaba 79 | druid 80 | 1.0.29 81 | 82 | 83 | 84 | com.twelvemonkeys.imageio 85 | imageio-jpeg 86 | 3.3.2 87 | 88 | 89 | 90 | javax.servlet 91 | javax.servlet-api 92 | 3.1.0 93 | 94 | 95 | 96 | javax.mail 97 | javax.mail-api 98 | 1.5.6 99 | 100 | 101 | 102 | 103 | org.springframework 104 | spring-context 105 | 4.3.18.RELEASE 106 | 107 | 108 | 109 | org.springframework 110 | spring-webmvc 111 | 4.3.18.RELEASE 112 | 113 | 114 | 115 | org.springframework 116 | spring-core 117 | 4.3.18.RELEASE 118 | 119 | 120 | 121 | 122 | org.springframework 123 | spring-beans 124 | 4.3.18.RELEASE 125 | 126 | 127 | 128 | org.springframework 129 | spring-web 130 | 4.3.18.RELEASE 131 | 132 | 133 | org.springframework 134 | spring-tx 135 | 4.3.18.RELEASE 136 | 137 | 138 | 139 | org.springframework 140 | spring-test 141 | 4.3.18.RELEASE 142 | 143 | 144 | org.springframework 145 | spring-jdbc 146 | 4.3.18.RELEASE 147 | 148 | 149 | org.springframework 150 | spring-context-support 151 | 4.3.18.RELEASE 152 | 153 | 154 | 155 | org.springframework 156 | spring-websocket 157 | 4.3.18.RELEASE 158 | 159 | 160 | org.springframework 161 | spring-messaging 162 | 4.3.18.RELEASE 163 | 164 | 165 | 166 | 167 | 168 | 169 | commons-io 170 | commons-io 171 | 2.4 172 | 173 | 174 | org.apache.commons 175 | commons-email 176 | 1.4 177 | 178 | 179 | 180 | com.github.jponge 181 | lzma-java 182 | 1.3 183 | 184 | 185 | 186 | 187 | 188 | org.aspectj 189 | aspectjrt 190 | 1.8.10 191 | 192 | 193 | 194 | org.aspectj 195 | aspectjweaver 196 | 1.8.10 197 | 198 | 199 | 200 | org.freemarker 201 | freemarker 202 | 2.3.26-incubating 203 | 204 | 205 | 206 | 207 | org.apache.httpcomponents 208 | httpclient 209 | 4.5.3 210 | 211 | 212 | com.squareup.okhttp3 213 | okhttp 214 | 3.11.0 215 | 216 | 217 | 218 | org.projectlombok 219 | lombok 220 | 1.16.18 221 | 222 | 223 | 224 | redis.clients 225 | jedis 226 | 2.9.0 227 | 228 | 229 | 230 | org.springframework.data 231 | spring-data-redis 232 | 1.8.3.RELEASE 233 | 234 | 235 | org.mybatis.generator 236 | mybatis-generator-core 237 | 1.3.6 238 | 239 | 240 | 241 | 242 | 243 | org.apache.maven.plugins 244 | maven-war-plugin 245 | 3.1.0 246 | 247 | WEB-INF/lib/*.jar 248 | 249 | 250 | 251 | org.mybatis.generator 252 | mybatis-generator-maven-plugin 253 | 1.3.6 254 | 255 | true 256 | true 257 | 258 | 259 | 260 | cabbageWeb 261 | 262 | 263 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/annotation/GroupAuthorityControl.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.annotation; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | 11 | /** 12 | * The interface Group role control. 13 | */ 14 | @Target({ElementType.METHOD}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @Component 17 | public @interface GroupAuthorityControl { 18 | /** 19 | * 禁止使用命令的群。 20 | * 21 | * @return the long [ ] 22 | */ 23 | long[] banned() default {0L}; 24 | 25 | long[] bannedDefault() default { 26 | //MP4后花园 27 | 112177148L, 28 | // MP2群 29 | 234219559L, 30 | // MP3群 31 | 210342787L, 32 | // MP5群 33 | 201872650L, 34 | // FK群 35 | 263668213L, 36 | // 测试群 37 | 693299572L 38 | }; 39 | 40 | /** 41 | * 允许使用命令的群。 42 | * 该参数优先级比banned高,如果注解里同时加了allowed和banned同一个群,allowed起效。 43 | * 如果allowed和banned不同的群,则banned没有意义。 44 | * (逻辑是 如果存在Allowed,先判断Allowed,如果包含则放行,不包含则直接拦截;不存在Allowed才判断Banned,如果包含则直接阻断;其他情况放行) 45 | * 46 | * @return the long [ ] 47 | */ 48 | long[] allowed() default {0L}; 49 | 50 | /** 51 | * 如果是,则该命令不允许在任何群使用。 52 | * (比使用默认值判断要简洁明了一点) 53 | * 54 | * @return the boolean 55 | */ 56 | boolean allBanned() default false; 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/annotation/UserAuthorityControl.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.annotation; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | 11 | @Target({ElementType.METHOD,ElementType.TYPE}) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Component 14 | public @interface UserAuthorityControl { 15 | /** 16 | * @author 瞿瀚盛 17 | * 在这里写管理员QQ,要加人的话得维护两个地方。。 18 | */ 19 | long[] value() default {}; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/aspect/ExceptionNoticeAspect.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.aspect; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.aspectj.lang.annotation.Around; 7 | import org.aspectj.lang.annotation.Aspect; 8 | import org.aspectj.lang.annotation.Pointcut; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.core.annotation.Order; 11 | import org.springframework.stereotype.Component; 12 | import top.mothership.cabbage.manager.OneBotManager; 13 | import top.mothership.cabbage.pojo.coolq.CqMsg; 14 | 15 | import java.time.LocalDateTime; 16 | import java.time.format.DateTimeFormatter; 17 | 18 | /** 19 | * The type Exception notice aspect. 20 | * 21 | * @author 瞿瀚盛 用于处理全局异常的类,一旦有异常发生就通过QQ消息通知我 22 | */ 23 | @Component 24 | @Aspect 25 | /** 26 | * 异常通知的优先级必须在拦截之后 27 | */ 28 | @Order(3) 29 | public class ExceptionNoticeAspect { 30 | @Autowired 31 | private OneBotManager oneBotManager; 32 | private Logger logger = LogManager.getLogger(this.getClass()); 33 | /** 34 | * 不知道放这对不对……总之是格式化时间用的 35 | */ 36 | private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy-MM-dd HH:mm:ss"); 37 | 38 | 39 | 40 | /** 41 | * 捕获并通知异常,这里不使用@AfterThrowing(我也忘记原因了,反正有坑) 42 | * 43 | * @param pjp 程序运行时的织入点 44 | * @return aop织入方法时处理的方法返回结果 45 | */ 46 | @Around(value = "aspectjMethod()") 47 | public Object exceptionNotice(ProceedingJoinPoint pjp) { 48 | Object result = null; 49 | 50 | try { 51 | result = pjp.proceed(); 52 | 53 | } catch (Throwable e) { 54 | logger.error("",e); 55 | String resp = formatter.format(LocalDateTime.now()) + 56 | "\n异常类型:" + e.toString() + "\n方法:" + pjp.getTarget().getClass() + "." 57 | + pjp.getSignature().getName() + "()\n方法入参:"; 58 | Object[] args = pjp.getArgs(); 59 | for (Object arg : args) { 60 | if (arg != null) { 61 | resp = resp.concat("\n" + arg); 62 | } 63 | } 64 | StackTraceElement[] stackTrace = e.getStackTrace(); 65 | for (StackTraceElement stackTraceElement : stackTrace) { 66 | // if(stackTraceElement.getClassName().contains("top.mothership")) { 67 | resp = resp.concat("\n at " + stackTraceElement); 68 | // }else if(stackTraceElement.getClassName().contains("java.")){ 69 | // resp = resp.concat("\n at " + stackTraceElement); 70 | // } else if(stackTraceElement.getClassName().contains("javax.")){ 71 | // resp = resp.concat("\n at " + stackTraceElement); 72 | // }else{ 73 | // if(!resp.endsWith("\n ……")) { 74 | // resp = resp.concat("\n ……"); 75 | // } 76 | // } 77 | } 78 | CqMsg cqMsg = new CqMsg(); 79 | cqMsg.setMessage(resp); 80 | cqMsg.setSelfId(1335734629L); 81 | cqMsg.setUserId(1335734657L); 82 | cqMsg.setMessageType("private"); 83 | if (oneBotManager != null) { 84 | oneBotManager.sendMsg(cqMsg); 85 | } 86 | 87 | 88 | } 89 | return result; 90 | } 91 | 92 | /** 93 | * 用于指定后续方法的异常捕捉范围 94 | */ 95 | @Pointcut("execution(* top.mothership.cabbage.*.*.*(..))") 96 | private void aspectjMethod() { 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/aspect/RoleControlAspect.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.aspect; 2 | 3 | import org.aspectj.lang.ProceedingJoinPoint; 4 | import org.aspectj.lang.annotation.Around; 5 | import org.aspectj.lang.annotation.Aspect; 6 | import org.aspectj.lang.annotation.Pointcut; 7 | import org.aspectj.lang.reflect.MethodSignature; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.core.annotation.Order; 10 | import org.springframework.stereotype.Component; 11 | import top.mothership.cabbage.annotation.GroupAuthorityControl; 12 | import top.mothership.cabbage.annotation.UserAuthorityControl; 13 | import top.mothership.cabbage.manager.OneBotManager; 14 | import top.mothership.cabbage.pojo.coolq.CqMsg; 15 | 16 | import java.lang.annotation.Annotation; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | import static top.mothership.cabbage.constant.Overall.ADMIN_LIST; 22 | 23 | @Component 24 | @Aspect 25 | /** 26 | * @author 瞿瀚盛 27 | * 用于处理权限控制的类,配合自定义注解 28 | */ 29 | @Order(1) 30 | public class RoleControlAspect { 31 | private final OneBotManager oneBotManager; 32 | private static final long[] ZERO = {0L}; 33 | 34 | /** 35 | * 在构造函数中注入发送QQ消息的工具类 36 | * 37 | * @param oneBotManager 用于发送QQ消息 38 | */ 39 | @Autowired 40 | public RoleControlAspect(OneBotManager oneBotManager) { 41 | this.oneBotManager = oneBotManager; 42 | 43 | } 44 | 45 | /** 46 | * 拦截service层所有方法中带AllowedUser注解的方法 47 | */ 48 | @Pointcut("execution(* top.mothership.cabbage.service.*.*(top.mothership.cabbage.pojo.coolq.CqMsg,..))") 49 | private void aspectjMethod() { 50 | } 51 | 52 | @Around("aspectjMethod() && args(cqMsg,..)") 53 | public Object roleControl(ProceedingJoinPoint pjp, CqMsg cqMsg) throws Throwable { 54 | //取出Class上的注解 55 | UserAuthorityControl userAuthorityControl = null; 56 | 57 | List allowedUser = new ArrayList<>(); 58 | 59 | Annotation[] a = pjp.getTarget().getClass().getAnnotations(); 60 | for (Annotation aList : a) { 61 | if (aList.annotationType().equals(UserAuthorityControl.class)) { 62 | userAuthorityControl = (UserAuthorityControl) a[1]; 63 | } 64 | } 65 | //如果Class上的注解不是null 66 | if (userAuthorityControl != null) { 67 | //处理没有特别指定用户的情况(手动实现默认值) 68 | if(userAuthorityControl.value().length == 0){ 69 | for (long l : ADMIN_LIST) { 70 | allowedUser.add(l); 71 | } 72 | } 73 | for (long l : userAuthorityControl.value()) { 74 | allowedUser.add(l); 75 | } 76 | } 77 | //同理 取出方法上的注解 78 | userAuthorityControl = pjp.getTarget().getClass().getMethod( 79 | pjp.getSignature().getName(), 80 | ((MethodSignature) pjp.getSignature()).getParameterTypes() 81 | ).getAnnotation(UserAuthorityControl.class); 82 | if (userAuthorityControl != null) { 83 | if(userAuthorityControl.value().length == 0){ 84 | for (long l : ADMIN_LIST) { 85 | allowedUser.add(l); 86 | } 87 | } 88 | for (long l : userAuthorityControl.value()) { 89 | allowedUser.add(l); 90 | } 91 | } 92 | //如果拿到了用户权限的注解,并且这个注解的值没有消息发送者的qq,并且是QQ消息(而不是事件或者邀请) 93 | if (allowedUser.size() > 0 && !allowedUser.contains(cqMsg.getUserId()) && "message".equals(cqMsg.getPostType())) { 94 | cqMsg.setMessage("[CQ:face,id=14]?"); 95 | oneBotManager.sendMsg(cqMsg); 96 | return null; 97 | } else { 98 | //如果通过了用户权限判别 99 | //对群权限控制注解进行判别 100 | if (!"group".equals(cqMsg.getMessageType())) { 101 | //如果不是群消息,不吃群权限控制 102 | return pjp.proceed(); 103 | } 104 | GroupAuthorityControl groupAuthorityControl = pjp.getTarget().getClass().getMethod( 105 | pjp.getSignature().getName(), 106 | ((MethodSignature) pjp.getSignature()).getParameterTypes() 107 | ).getAnnotation(GroupAuthorityControl.class); 108 | if (groupAuthorityControl != null) { 109 | //如果不允许任何群内使用 110 | if (groupAuthorityControl.allBanned()) { 111 | cqMsg.setMessage("本命令不允许任何群内使用。请私聊。"); 112 | oneBotManager.sendMsg(cqMsg); 113 | return null; 114 | } 115 | if (!Arrays.equals(groupAuthorityControl.allowed(), ZERO)) { 116 | //当Allowed不是只有一个0的数组的时候,只有群号符合才通过 117 | for (long l : groupAuthorityControl.allowed()) { 118 | if (cqMsg.getGroupId().equals(l)) { 119 | return pjp.proceed(); 120 | } 121 | } 122 | return null; 123 | } 124 | //如果Allowed是0,则判断Banned列表 125 | for (long l : groupAuthorityControl.banned()) { 126 | if (cqMsg.getGroupId().equals(l)) { 127 | cqMsg.setMessage("该群已停用本命令。"); 128 | oneBotManager.sendMsg(cqMsg); 129 | return null; 130 | } 131 | } 132 | for (long l : groupAuthorityControl.bannedDefault()) { 133 | if (cqMsg.getGroupId().equals(l)) { 134 | cqMsg.setMessage("该群已停用本命令。"); 135 | oneBotManager.sendMsg(cqMsg); 136 | return null; 137 | } 138 | } 139 | return pjp.proceed(); 140 | } else { 141 | return pjp.proceed(); 142 | } 143 | } 144 | 145 | 146 | } 147 | } 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/aspect/StatTimeConsAspect.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.aspect; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.aspectj.lang.annotation.Around; 7 | import org.aspectj.lang.annotation.Aspect; 8 | import org.aspectj.lang.annotation.Pointcut; 9 | import org.springframework.core.annotation.Order; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.time.Duration; 13 | import java.time.Instant; 14 | @Aspect 15 | @Component 16 | @Order(0) 17 | public class StatTimeConsAspect { 18 | /** 19 | * 打印方法执行耗费时间的日志工具 20 | */ 21 | private Logger logger = LogManager.getLogger(this.getClass()); 22 | @Pointcut("execution(* top.mothership.cabbage.service.*.*(..))") 23 | private void aspectjMethod() { 24 | } 25 | @Around(value = "aspectjMethod()") 26 | public Object statTimeCons(ProceedingJoinPoint pjp) throws Throwable { 27 | Instant s = Instant.now(); 28 | pjp.proceed(); 29 | logger.info(pjp.getTarget().getClass() + "." 30 | + pjp.getSignature().getName()+"处理完毕,共耗费" + Duration.between(s, Instant.now()).toMillis() + "ms。"); 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/constant/Overall.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.constant; 2 | 3 | import top.mothership.cabbage.enums.ParameterEnum; 4 | 5 | import java.util.ResourceBundle; 6 | 7 | /** 8 | * 全局常量 9 | * 10 | * @author QHS 11 | */ 12 | public class Overall { 13 | /** 14 | * 指定API Key等私密信息的配置文件 15 | */ 16 | public final static ResourceBundle CABBAGE_CONFIG = ResourceBundle.getBundle("cabbage"); 17 | 18 | public final static long[] ADMIN_LIST = {2307282906L,1335734657L,2643555740L,992931505L,735862173L,744309983L}; 19 | 20 | public final static String DEFAULT_ROLE = "creep"; 21 | 22 | public final static ParameterEnum[] EMPTY_PARAMETER_LIST = new ParameterEnum[]{}; 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/constant/Tip.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.constant; 2 | 3 | public class Tip { 4 | public static final String USER_NOT_BIND = "你没有绑定osu!id。请使用!setid 你的osuid 命令。"; 5 | public static final String USER_IS_BANNED = "……期待你回来的那一天。"; 6 | public static final String USER_GET_FAILED = "没有从osu!api获取到QQ为%d的用户绑定的uid为%d的玩家信息。"; 7 | public static final String BEATMAP_GET_FAILED = "没有从osu!api获取到bid为%d的谱面信息。"; 8 | public static final String USERNAME_GET_FAILED = "没有从osu!api获取到名为%s的玩家信息。"; 9 | public static final String USERID_GET_FAILED = "没有从osu!api获取到uid为%d的玩家信息。"; 10 | public static final String USER_ID_GET_FAILED_AND_NOT_USED = "没有从osu!api获取到uid为%d的玩家信息。"; 11 | public static final String FORMAT_ERROR = "“%s”不是合法的%s。我们不再支持文中掺杂命令。"; 12 | public static final String ARGUMENTS_LESS_THAN_PARAMETERS = "该命令的必须形参为:%s,提供的实参数量为%d,不满足最小形参数量。"; 13 | public static final String ARGUMENTS_MORE_THAN_PARAMETERS = "该命令的必须形参为:%s,可选形参为:%s,提供的实参数量为%d,超过最大形参数量。"; 14 | public static final String QUERY_BANCHO_BOT = "你们总是想查BanchoBot。" + 15 | "\n可是BanchoBot已经很累了,她不想被查。" + 16 | "\n她想念自己的小ppy,而不是被逼着查PP。" + 17 | "\n你有考虑过这些吗?没有!你只考虑过你自己。"; 18 | public static final String BEATMAP_NO_SCORE = "没有从osu!api获取到谱面%d在模式%s的排行榜。"; 19 | public static final String THIS_USER_BEATMAP_NO_SCORE = "没有从osu!api获取到玩家%s在谱面%d、模式%s的分数。"; 20 | public static final String NO_RECENT_RECORD = "玩家%s在模式%s最近没有游戏记录。"; 21 | public static final String NO_RECENT_RECORD_PASSED = "玩家%s在模式%s最近没有Pass的游戏记录。"; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/constant/pattern/CQCodePattern.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.constant.pattern; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class CQCodePattern { 6 | /** 7 | * 匹配出带图片的消息 8 | */ 9 | public final static Pattern MSG_WITH_IMAGE = Pattern.compile("\\[CQ:image,file=.+]"); 10 | /** 11 | * 匹配出艾特消息中的QQ号(At全体时候,中间是all,所以不指定为数字) 12 | */ 13 | public final static Pattern AT = Pattern.compile("\\[CQ:at,qq=(.*)]"); 14 | /** 15 | * 匹配出纯图片的消息 16 | */ 17 | public final static Pattern SINGLE_IMG = Pattern.compile("^\\[CQ:image,file=.+]$"); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/constant/pattern/MpCommandPattern.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.constant.pattern; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class MpCommandPattern { 6 | /** 7 | * MP系列命令,和sudo一样 8 | */ 9 | public final static Pattern MP_CMD_REGEX = Pattern.compile("[!!]mp ([^ ]*)[ ]?([^::]*)[:|:]?(.*)"); 10 | /** 11 | * 对所有服务器消息(不是banchobot发送的私聊消息)进行基本的筛选…… 12 | */ 13 | public final static Pattern IRC_SERVER_MSG = Pattern.compile(":cho.ppy.sh (\\d\\d\\d) (.+)"); 14 | /** 15 | * Banchobot发送的没有该用户的消息 16 | */ 17 | public final static Pattern ROOM_NOT_EXIST = Pattern.compile(":cho.ppy.sh 401 .+ #mp_(.+) :No such nick"); 18 | 19 | /** 20 | * Banchobot发送的 创建房间已经达到4个的消息 21 | */ 22 | public final static Pattern ROOM_LIMITED = Pattern.compile( 23 | ":BanchoBot!cho@ppy.sh PRIVMSG [.*] :You cannot create any more tournament matches. Please close any previous tournament matches you have open."); 24 | 25 | /** 26 | * 其他用户发送的私聊 27 | */ 28 | public final static Pattern IRC_PRIVATE_MSG = Pattern.compile(":(.+)!cho@ppy.sh PRIVMSG (.+) :(.+)"); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/constant/pattern/RegularPattern.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.constant.pattern; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * 正则表达式的常量类。 7 | * 8 | * @author QHS 9 | */ 10 | public class RegularPattern { 11 | /** 12 | * 匹配出带/不带数字的常规命令,简单的用空格分割命令字符串。 13 | * 当处理!statme类命令时:group(1)为statme,2和3为"" 14 | * !setid xxx类命令是:group(1)为setid,group(2)为xxx,3为"" 15 | */ 16 | public final static Pattern REG_CMD_REGEX = Pattern.compile("[!!]\\s*([^#:: ]*)\\s*(.*)"); 17 | /** 18 | * sleep命令专用正则,只有感叹号在全文开头时才匹配 19 | */ 20 | public final static Pattern SLEEP_REGEX = Pattern.compile("^[!!]\\s*([^#:: ]*)\\s*(.*)"); 21 | 22 | /** 23 | * 匹配出sudo命令 24 | */ 25 | public final static Pattern ADMIN_CMD_REGEX = Pattern.compile("[!!]\\s*sudo\\s+([^#:: ]*)\\s*(.*)"); 26 | /** 27 | * 复读禁言时抗干扰的匹配表达式 28 | */ 29 | public final static Pattern REPEAT_FILTER_REGEX = Pattern.compile("[^\\u4e00-\\u9fa5a-zA-Z0-9]"); 30 | /** 31 | * 从.osu文件中匹配出BG文件名的表达式 32 | */ 33 | public final static Pattern BGNAME_REGEX = Pattern.compile("(?<=[\\d*],[\\d*],\")(?:.*\\\\)*(.*\\.(?i)(jpg|png|jpeg))"); 34 | 35 | 36 | /** 37 | * UNICODE转String的表达式。 38 | */ 39 | public final static Pattern UNICODE_TO_STRING = Pattern.compile("(\\\\u(\\p{XDigit}{4}))"); 40 | 41 | /** 42 | * 这是一个弱智玩意 43 | * 贼他妈弱智 44 | */ 45 | public final static Pattern QIANESE_RECENT = Pattern.compile("((?:4|5|t|f|d|e|r)(?:1|q|w|3|e|r|4|d)(?:x|d|f|v|c)(?:1|q|w|3|e|r|4)(?:b|h|j|m|n)(?:r|5|6|y|g|f|t))"); 46 | /** 47 | * 指定需要以文本方式返回的子命令 48 | */ 49 | public final static Pattern TEXT_VERSION_COMMAND = Pattern.compile("rs|bps|bpus|bpmes|mybps"); 50 | /** 51 | * 赛事分析系统中用来处理bid 52 | */ 53 | public final static Pattern ANALYZE_BID_PARAM = Pattern.compile("[!!]([^ ]*)[ ]?(\\d*)"); 54 | /** 55 | * 匹配osu的UID(1-8个数字) 56 | */ 57 | public final static Pattern OSU_USER_ID = Pattern.compile("^(\\d{1,8})$"); 58 | /** 59 | * 匹配osu的ID(3-15个\w、横杠 左括号右括号空格) 60 | */ 61 | public final static Pattern OSU_USER_NAME = Pattern.compile("([0-9A-Za-z_\\-\\[\\]][0-9A-Za-z_\\-\\[\\] ]{1,13}[0-9A-Za-z_\\-\\[\\]])"); 62 | 63 | /** 64 | * 匹配QQ(5-10个数字) 65 | */ 66 | public final static Pattern QQ = Pattern.compile("^(\\d{3,10})$"); 67 | public final static Pattern BPNUM = Pattern.compile("^(\\d{1,3})$"); 68 | public final static Pattern URL = Pattern.compile("(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]"); 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/constant/pattern/SearchKeywordPattern.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.constant.pattern; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class SearchKeywordPattern { 6 | /** 7 | * osu search命令的表达式 8 | * 2017-12-4 10:45:55 现在支持分隔符后带空格了,横杠前的空格还是手动处理 9 | * 2018-2-24 23:32:22 现在支持全角【】() 10 | */ 11 | public final static Pattern KETWORD = Pattern.compile("^([^-]*)-[ ]?(.*)[ ]?[\\[【](.*)[]】][ ]?[((](.*)[))]\\{(.*)}"); 12 | 13 | /** 14 | * 搞了一半失败的Shell格式 15 | */ 16 | // public final static pattern OSU_SEARCH_KETWORD_SHELL = pattern.compile("((?:-(a|t|d|m|ar|od|cs|hp)) ([^-]*)){1,8}"); 17 | 18 | /** 19 | * MOD 20 | */ 21 | public final static Pattern MOD = Pattern.compile("[!!]([^ ]*)[ ]?(.*)(?:\\+| \\+)(.*)"); 22 | /** 23 | * acc cb miss 24 | */ 25 | public final static Pattern PP_CALC = Pattern.compile("[!!]([^ ]*)[ ]?(.*)[《<](.*)[》>]"); 26 | /** 27 | * mode 28 | */ 29 | public final static Pattern MODE = Pattern.compile("[!!]([^ ]*)[ ]?(.*)(?: :| :|:|:)(.*)"); 30 | /** 31 | * 取出四维 32 | */ 33 | public final static Pattern FOUR_DIMENSIONS_REGEX = Pattern 34 | .compile("(?:AR(\\d{1,2}(?:\\.|。)?(?:\\d{1,2})?))?(?:OD(\\d{1,2}[.。]?(?:\\d{1,2})?))?(?:CS(\\d{1,2}[.。]?(?:\\d{1,2})?))?(?:HP(\\d{1,2}(?:\\.|。)?(?:\\d{1,2})?))?", Pattern.CASE_INSENSITIVE); 35 | 36 | /** 37 | * 用来处理纯数字搜索词(bid)的表达式 38 | */ 39 | public final static Pattern ALL_NUMBER_SEARCH_KEYWORD = Pattern.compile("^(\\d{1,7})$"); 40 | public final static Pattern KEYWORD_ACC = Pattern.compile("^(?:(\\d{1,2}[.。]?(?:\\d{1,2})?))(?:%|acc)$"); 41 | public final static Pattern KEYWORD_COUNT_100 = Pattern.compile("^(\\d{1,4})(?:x100|\\*100)$"); 42 | public final static Pattern KEYWORD_COUNT_50 = Pattern.compile("^(\\d{1,4})(?:x50|\\*50)$"); 43 | public final static Pattern KEYWORD_MISS = Pattern.compile("^(\\d{1,4})(?:x|\\*miss|xm|\\*m|xmiss)$"); 44 | public final static Pattern KEYWORD_COMBO = Pattern.compile("^(\\d{1,5})(?:c|cb)$"); 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/constant/pattern/WebPagePattern.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.constant.pattern; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class WebPagePattern { 6 | /** 7 | * osu官网中添加好友连接的表达式 8 | */ 9 | public final static Pattern ADD_FRIEND_REGEX = Pattern.compile("
\\n(\\d*)(?:.|\\r|\\n)*(\\d*)(?:.|\\r|\\n)*(\\d*)"); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/controller/vo/ChartsVo.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.controller.vo; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class ChartsVo { 9 | private List xAxis; 10 | private List yAxis; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/controller/vo/PPChartVo.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.controller.vo; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | @Data 7 | public class PPChartVo { 8 | private List xAxis; 9 | private List yAxis; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/enums/CompressLevelEnum.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.enums; 2 | 3 | public enum CompressLevelEnum { 4 | JPG,USHORT_555_RGB_PNG,不压缩, 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/enums/ParameterEnum.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.enums; 2 | 3 | public enum ParameterEnum { 4 | USERNAME, 5 | MODE, 6 | DAY, 7 | USER_ID, 8 | QQ, 9 | GROUPID, 10 | ROLE, 11 | FLAG, 12 | HOUR, 13 | NUM, 14 | SEARCH_PARAM, 15 | BOUND, 16 | 17 | USERNAME_LIST, 18 | FILENAME, 19 | URL, 20 | SECOND, 21 | AT, 22 | BEATMAP_ID 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/manager/ApiManager.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.manager; 2 | 3 | import com.google.common.util.concurrent.RateLimiter; 4 | import com.google.gson.*; 5 | import com.google.gson.reflect.TypeToken; 6 | import org.apache.http.NameValuePair; 7 | import org.apache.http.client.utils.URLEncodedUtils; 8 | import org.apache.http.message.BasicNameValuePair; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.Logger; 11 | import org.springframework.stereotype.Component; 12 | import top.mothership.cabbage.constant.Overall; 13 | import top.mothership.cabbage.pojo.osu.*; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.IOException; 17 | import java.io.InputStreamReader; 18 | import java.net.HttpURLConnection; 19 | import java.net.URL; 20 | import java.util.ArrayList; 21 | import java.util.Base64; 22 | import java.util.LinkedList; 23 | import java.util.List; 24 | 25 | @Component 26 | public class ApiManager { 27 | private final String getUserURL = "https://osu.ppy.sh/api/get_user"; 28 | private final String getBPURL = "https://osu.ppy.sh/api/get_user_best"; 29 | private final String getMapURL = "https://osu.ppy.sh/api/get_beatmaps"; 30 | private final String getRecentURL = "https://osu.ppy.sh/api/get_user_recent"; 31 | private final String getScoreURL = "https://osu.ppy.sh/api/get_scores"; 32 | private final String getMatchURL = "https://osu.ppy.sh/api/get_match"; 33 | 34 | private final String getReplayURL = "https://osu.ppy.sh/api/get_replay"; 35 | 36 | private final String key = Overall.CABBAGE_CONFIG.getString("apikey"); 37 | private Logger logger = LogManager.getLogger(this.getClass()); 38 | 39 | 40 | 41 | public Userinfo getUser(Integer mode, String username) { 42 | String result = accessAPI("user", username, "string", null, null, null, null, mode); 43 | Userinfo userFromAPI = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, Userinfo.class); 44 | if (userFromAPI != null) { 45 | //请求API时加入mode的标记,并且修复Rank问题 46 | userFromAPI.setMode(mode); 47 | fixRank(userFromAPI); 48 | } 49 | return userFromAPI; 50 | } 51 | 52 | public Userinfo getUser(Integer mode, Integer userId) { 53 | String result = accessAPI("user", String.valueOf(userId), "id", null, null, null, null, mode); 54 | Userinfo userFromAPI = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, Userinfo.class); 55 | if (userFromAPI != null) { 56 | //请求API时加入mode的标记,并且修复Rank问题 57 | userFromAPI.setMode(mode); 58 | fixRank(userFromAPI); 59 | } 60 | return userFromAPI; 61 | } 62 | 63 | public Beatmap getBeatmap(Integer bid) { 64 | String result = accessAPI("beatmap", null, null, String.valueOf(bid), null, null, null, null); 65 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, Beatmap.class); 66 | } 67 | 68 | public List getBeatmaps(Integer sid) { 69 | String result = accessAPI("beatmaps", null, null, String.valueOf(sid), null, null, null, null); 70 | result = "[" + result + "]"; 71 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, new TypeToken>() {}.getType()); 72 | } 73 | 74 | public Beatmap getBeatmap(String hash) { 75 | String result = accessAPI("beatmapHash", null, null, null, String.valueOf(hash), null, null, null); 76 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, Beatmap.class); 77 | } 78 | 79 | public List getBP(Integer mode, String username) { 80 | String result = accessAPI("bp", username, "string", null, null, null, null, mode); 81 | //由于这里用到List,手动补上双括号 82 | result = "[" + result + "]"; 83 | List list = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create() 84 | .fromJson(result, new TypeToken>() {}.getType()); 85 | for (Score s : list) { 86 | //2018-2-28 15:54:11哪怕是返回单模式的bp也设置模式,避免计算acc时候判断是单模式还是多模式 87 | s.setMode(mode.byteValue()); 88 | } 89 | return list; 90 | } 91 | 92 | public List getBP(Integer mode, Integer userId) { 93 | String result = accessAPI("bp", String.valueOf(userId), "id", null, null, null, null, mode); 94 | //由于这里用到List,手动补上双括号 95 | result = "[" + result + "]"; 96 | List list = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create() 97 | .fromJson(result, new TypeToken>() { 98 | }.getType()); 99 | for (Score s : list) { 100 | s.setMode(mode.byteValue()); 101 | } 102 | return list; 103 | } 104 | 105 | public List getBP(String username) { 106 | List resultList = new ArrayList<>(); 107 | for (int i = 0; i < 4; i++) { 108 | String result = result = accessAPI("bp", username, "string", null, null, null, null, i); 109 | //由于这里用到List,手动补上双括号 110 | result = "[" + result + "]"; 111 | List list = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create() 112 | .fromJson(result, new TypeToken>() { 113 | }.getType()); 114 | for (Score s : list) { 115 | s.setMode((byte) i); 116 | } 117 | resultList.addAll(list); 118 | } 119 | return resultList; 120 | } 121 | 122 | public List> getBP(Integer userId) { 123 | List> resultList = new ArrayList<>(); 124 | //小技巧,这里i设为byte 125 | for (int i = 0; i < 4; i++) { 126 | String result = accessAPI("bp", String.valueOf(userId), "id", null, null, null, null, i); 127 | //由于这里用到List,手动补上双括号 128 | result = "[" + result + "]"; 129 | List list = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create() 130 | .fromJson(result, new TypeToken>() { 131 | }.getType()); 132 | for (Score s : list) { 133 | s.setMode((byte) i); 134 | } 135 | resultList.add(list); 136 | } 137 | return resultList; 138 | } 139 | 140 | public Score getRecent(Integer mode, String username) { 141 | String result = accessAPI("recent", username, "string", null, null, null, null, mode); 142 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 143 | .setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, Score.class); 144 | } 145 | 146 | public Score getRecent(Integer mode, Integer userId) { 147 | String result = accessAPI("recent", String.valueOf(userId), "id", null, null, null, null, mode); 148 | 149 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 150 | .setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, Score.class); 151 | } 152 | 153 | // 用于获取所有的recent 154 | public List getRecents(Integer mode, String username) { 155 | String result = result = accessAPI("recents", username, "string", null, null, null, null, mode); 156 | result = "[" + result + "]"; 157 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 158 | .setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, new TypeToken>() { 159 | }.getType()); 160 | } 161 | 162 | public List getRecents(Integer mode, Integer userId) { 163 | String result = accessAPI("recents", String.valueOf(userId), "id", null, null, null, null, mode); 164 | 165 | result = "[" + result + "]"; 166 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 167 | .setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, new TypeToken>() { 168 | }.getType()); 169 | } 170 | 171 | public List getFirstScore(Integer mode, Integer bid, Integer rank) { 172 | String result = accessAPI("first", null, null, String.valueOf(bid), null, rank, null, mode); 173 | result = "[" + result + "]"; 174 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create() 175 | .fromJson(result, new TypeToken>() { 176 | }.getType()); 177 | 178 | } 179 | 180 | public List getScore(Integer mode, Integer bid, Integer uid) { 181 | String result = accessAPI("score", String.valueOf(uid), "id", String.valueOf(bid), null, null, null, mode); 182 | result = "[" + result + "]"; 183 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create() 184 | .fromJson(result, new TypeToken>() { 185 | }.getType()); 186 | } 187 | 188 | public Lobby getMatch(Integer mid) { 189 | String result = accessAPI("match", null, null, null, null, null, String.valueOf(mid), null); 190 | result = "{" + result + "}"; 191 | logger.info(result); 192 | //他妈的ppysb,上面那些获取单个对象的时候给加个中括号,害的我得在下面删掉再在上面看情况加上 193 | return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 194 | //这个的API,date可能是null,而Gson2.8.1会有问题,因此升级Gson库 195 | .setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(result, Lobby.class); 196 | } 197 | 198 | public byte[] getReplay(Long scoreId) { 199 | RateLimiter rateLimiter = RateLimiter.create(0.17); 200 | if (!rateLimiter.tryAcquire()){ 201 | logger.error("获取replay接口限速"); 202 | return null; 203 | } 204 | String result = accessAPI("replay", null, null, null, null, null, String.valueOf(scoreId), null); 205 | result = "{" + result + "}"; 206 | Replay jsonObject = new Gson().fromJson(result, Replay.class); 207 | String content = jsonObject.getContent(); 208 | if (content == null){ 209 | logger.error("获取replay失败,result:{}", jsonObject); 210 | return null; 211 | } 212 | return Base64.getDecoder().decode(content); 213 | } 214 | 215 | private void fixRank(Userinfo userFromAPI) { 216 | userFromAPI.setCountRankSs(userFromAPI.getCountRankSs() + userFromAPI.getCountRankSsh()); 217 | userFromAPI.setCountRankS(userFromAPI.getCountRankS() + userFromAPI.getCountRankSh()); 218 | } 219 | 220 | private String accessAPI(String apiType, String uid, String uidType, String bid, String hash, Integer rank, String mid, Integer mode) { 221 | String URL; 222 | String failLog; 223 | String output = null; 224 | HttpURLConnection httpConnection; 225 | List params = new LinkedList<>(); 226 | params.add(new BasicNameValuePair("u", uid)); 227 | switch (apiType) { 228 | case "user": 229 | URL = getUserURL + "?k=" + key + "&type=" + uidType + "&m=" + mode + "&" + URLEncodedUtils.format(params, "utf-8"); 230 | failLog = "玩家" + uid + "请求API:get_user失败五次"; 231 | break; 232 | case "bp": 233 | URL = getBPURL + "?k=" + key + "&m=" + mode + "&type=" + uidType + "&limit=100&" + URLEncodedUtils.format(params, "utf-8"); 234 | failLog = "玩家" + uid + "请求API:get_user_best失败五次"; 235 | break; 236 | case "beatmap": 237 | URL = getMapURL + "?k=" + key + "&b=" + bid; 238 | failLog = "谱面" + bid + "请求API:get_beatmaps失败五次"; 239 | break; 240 | case "beatmaps": 241 | URL = getMapURL + "?k=" + key + "&s=" + bid; 242 | failLog = "谱面" + bid + "请求API:get_beatmaps失败五次"; 243 | break; 244 | case "beatmapHash": 245 | URL = getMapURL + "?k=" + key + "&h=" + hash; 246 | failLog = "谱面" + bid + "请求API:get_beatmaps失败五次"; 247 | break; 248 | case "recent": 249 | URL = getRecentURL + "?k=" + key + "&m=" + mode + "&type=" + uidType + "&limit=1&" + URLEncodedUtils.format(params, "utf-8"); 250 | failLog = "玩家" + uid + "请求API:get_recent失败五次"; 251 | break; 252 | case "recents": 253 | URL = getRecentURL + "?k=" + key + "&m=" + mode + "&type=" + uidType + "&limit=100&" + URLEncodedUtils.format(params, "utf-8"); 254 | failLog = "玩家" + uid + "请求API:get_recent失败五次"; 255 | break; 256 | case "first": 257 | URL = getScoreURL + "?k=" + key + "&m=" + mode + "&limit=" + rank + "&b=" + bid; 258 | failLog = "谱面" + bid + "请求API:get_scores失败五次"; 259 | break; 260 | case "score": 261 | URL = getScoreURL + "?k=" + key + "&m=" + mode + "&type=" + uidType + "&u=" + uid + "&b=" + bid; 262 | failLog = "谱面" + bid + "请求API:get_scores失败五次"; 263 | break; 264 | case "match": 265 | URL = getMatchURL + "?k=" + key + "&mp=" + mid; 266 | failLog = "谱面" + bid + "请求API:get_scores失败五次"; 267 | break; 268 | case "replay": 269 | URL = getReplayURL + "?k=" + key + "&s=" + mid; 270 | failLog = "谱面" + bid + "请求API:get_replay失败五次"; 271 | break; 272 | default: 273 | logger.info("apiType错误"); 274 | return null; 275 | 276 | } 277 | 278 | int retry = 0; 279 | while (retry < 5) { 280 | try { 281 | httpConnection = 282 | (HttpURLConnection) new URL(URL).openConnection(); 283 | //设置请求头 284 | httpConnection.setRequestMethod("GET"); 285 | httpConnection.setRequestProperty("Accept", "application/json"); 286 | httpConnection.setConnectTimeout((int) Math.pow(2, retry + 1) * 1000); 287 | httpConnection.setReadTimeout((int) Math.pow(2, retry + 1) * 1000); 288 | if (httpConnection.getResponseCode() != 200) { 289 | logger.info("HTTP GET请求失败: " + httpConnection.getResponseCode() + ",正在重试第" + (retry + 1) + "次"); 290 | retry++; 291 | continue; 292 | } 293 | //读取返回结果 294 | BufferedReader responseBuffer = 295 | new BufferedReader(new InputStreamReader((httpConnection.getInputStream()))); 296 | StringBuilder tmp2 = new StringBuilder(); 297 | String tmp; 298 | while ((tmp = responseBuffer.readLine()) != null) { 299 | tmp2.append(tmp); 300 | } 301 | //去掉两侧的中括号 302 | output = tmp2.toString().substring(1, tmp2.toString().length() - 1); 303 | //手动关闭流 304 | httpConnection.disconnect(); 305 | responseBuffer.close(); 306 | break; 307 | } catch (IOException e) { 308 | logger.error("出现IO异常:" + e.getMessage() + ",正在重试第" + (retry + 1) + "次"); 309 | retry++; 310 | } 311 | } 312 | if (retry == 5) { 313 | logger.error(failLog); 314 | return null; 315 | } 316 | return output; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/manager/DayLilyManager.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.manager; 2 | 3 | import com.google.gson.GsonBuilder; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | import org.springframework.stereotype.Component; 7 | import top.mothership.cabbage.pojo.coolq.CqMsg; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.IOException; 11 | import java.io.InputStreamReader; 12 | import java.io.OutputStream; 13 | import java.net.HttpURLConnection; 14 | import java.net.URL; 15 | 16 | @Component 17 | public class DayLilyManager { 18 | private final String baseURL = "http://123.206.100.246:23333/api/getresponse"; 19 | private Logger logger = LogManager.getLogger(this.getClass()); 20 | 21 | public void sendMsg(CqMsg cqMsg) { 22 | HttpURLConnection httpConnection; 23 | try { 24 | httpConnection = 25 | (HttpURLConnection) new URL(baseURL).openConnection(); 26 | httpConnection.setRequestMethod("POST"); 27 | httpConnection.setRequestProperty("Accept", "application/json"); 28 | httpConnection.setRequestProperty("Content-Type", "application/json"); 29 | httpConnection.setDoOutput(true); 30 | 31 | OutputStream os = httpConnection.getOutputStream(); 32 | os.write(new GsonBuilder().disableHtmlEscaping().create().toJson(cqMsg).getBytes("UTF-8")); 33 | os.flush(); 34 | os.close(); 35 | BufferedReader responseBuffer = 36 | new BufferedReader(new InputStreamReader((httpConnection.getInputStream()))); 37 | StringBuilder tmp2 = new StringBuilder(); 38 | String tmp; 39 | while ((tmp = responseBuffer.readLine()) != null) { 40 | tmp2.append(tmp); 41 | } 42 | //这里不用用到下划线转驼峰 43 | logger.debug(tmp2.toString()); 44 | } catch (IOException e) { 45 | e.printStackTrace(); 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/manager/OneBotManager.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.manager; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.reflect.TypeToken; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | import top.mothership.cabbage.pojo.coolq.CqMsg; 9 | import top.mothership.cabbage.pojo.coolq.CqResponse; 10 | import top.mothership.cabbage.pojo.coolq.OneBotApiRequest; 11 | import top.mothership.cabbage.pojo.coolq.QQInfo; 12 | import top.mothership.cabbage.websocket.OneBotMessageHandler; 13 | 14 | import java.io.BufferedReader; 15 | import java.io.IOException; 16 | import java.io.InputStreamReader; 17 | import java.io.OutputStream; 18 | import java.net.HttpURLConnection; 19 | import java.net.URL; 20 | import java.nio.charset.StandardCharsets; 21 | import java.util.List; 22 | import java.util.Random; 23 | 24 | //将OneBot的WS API封装为接口,并托管到Spring 25 | @Component 26 | public class OneBotManager { 27 | @Autowired 28 | private OneBotMessageHandler handler; 29 | 30 | 31 | public CqResponse warn(String msg) { 32 | CqMsg cqMsg = new CqMsg(); 33 | cqMsg.setMessageType("private"); 34 | cqMsg.setUserId(1335734657L); 35 | cqMsg.setSelfId(1335734629L); 36 | cqMsg.setMessage(msg); 37 | return sendMsg(cqMsg); 38 | } 39 | 40 | public CqResponse warn(String msg, Exception e) { 41 | CqMsg cqMsg = new CqMsg(); 42 | cqMsg.setMessageType("private"); 43 | cqMsg.setUserId(1335734657L); 44 | cqMsg.setSelfId(1335734629L); 45 | cqMsg.setMessage(msg + " " + e.getMessage()); 46 | return sendMsg(cqMsg); 47 | } 48 | 49 | public CqResponse sendMsg(CqMsg cqMsg) { 50 | String baseURL = null; 51 | switch (cqMsg.getSelfId().toString()) { 52 | case "1020640876": 53 | OneBotMessageHandler.sendMessage(cqMsg); 54 | break; 55 | case "1335734629": 56 | OneBotMessageHandler.sendMessage(cqMsg); 57 | return null; 58 | default: 59 | OneBotMessageHandler.sendMessage(cqMsg); 60 | return null; 61 | } 62 | String URL; 63 | switch (cqMsg.getMessageType()) { 64 | case "group": 65 | URL = baseURL + "/send_group_msg"; 66 | break; 67 | case "discuss": 68 | URL = baseURL + "/send_discuss_msg"; 69 | break; 70 | case "private": 71 | URL = baseURL + "/send_private_msg"; 72 | break; 73 | case "smoke": 74 | URL = baseURL + "/set_group_ban"; 75 | break; 76 | case "smokeAll": 77 | URL = baseURL + "/set_group_whole_ban"; 78 | break; 79 | case "handleInvite": 80 | URL = baseURL + "/set_group_add_request"; 81 | break; 82 | case "kick": 83 | URL = baseURL + "/set_group_kick"; 84 | break; 85 | default: 86 | return null; 87 | } 88 | HttpURLConnection httpConnection; 89 | try { 90 | httpConnection = 91 | (HttpURLConnection) new URL(URL).openConnection(); 92 | httpConnection.setRequestMethod("POST"); 93 | httpConnection.setRequestProperty("Accept", "application/json"); 94 | httpConnection.setRequestProperty("Content-Type", "application/json"); 95 | httpConnection.setDoOutput(true); 96 | OutputStream os = httpConnection.getOutputStream(); 97 | //防止转义 98 | //折腾了半天最后是少了UTF-8………………我tm想给自己一巴掌 99 | os.write(new GsonBuilder().disableHtmlEscaping().create().toJson(cqMsg).getBytes(StandardCharsets.UTF_8)); 100 | os.flush(); 101 | os.close(); 102 | BufferedReader responseBuffer = 103 | new BufferedReader(new InputStreamReader((httpConnection.getInputStream()))); 104 | StringBuilder tmp2 = new StringBuilder(); 105 | String tmp; 106 | while ((tmp = responseBuffer.readLine()) != null) { 107 | tmp2.append(tmp); 108 | } 109 | //这里不用用到下划线转驼峰 110 | CqResponse response = new Gson().fromJson(tmp2.toString(), CqResponse.class); 111 | if (response.getRetCode() != 0 && cqMsg.getMessage().contains("base64")) { 112 | warn("图片发送失败,出现的QQ:" + cqMsg.getSelfId()); 113 | } 114 | return response; 115 | } catch (IOException e) { 116 | e.printStackTrace(); 117 | return null; 118 | } 119 | 120 | } 121 | 122 | /** 123 | * 获取单个群成员列表,仅针对原有2个QQ所在的群才有用 124 | * 目前用处是全员循环禁言(?什么傻逼功能),超管group info命令,2个chart群给MP系列主群加人用 125 | * 后续如果自由接入的话获取不到其他接入QQ的群信息(懒得写,得在消息入口把发送人QQ一直保持到调用时候) 126 | * 127 | * @param groupId 目标群 128 | * @return 129 | */ 130 | 131 | public CqResponse> getGroupMembers(Long groupId) { 132 | 133 | CqMsg cqMsg = new CqMsg(); 134 | cqMsg.setGroupId(groupId); 135 | cqMsg.setSelfId(1335734629L); 136 | 137 | OneBotApiRequest request = new OneBotApiRequest(); 138 | request.setAction("get_group_member_list"); 139 | request.setParams(cqMsg); 140 | request.setEcho(getId()); 141 | String response = OneBotMessageHandler.callApi(request); 142 | CqResponse> data = new Gson().fromJson(response, new TypeToken>>() { 143 | }.getType()); 144 | 145 | // 如果报错找不到 146 | if (data == null || data.getRetCode() != 0) { 147 | cqMsg.setSelfId(1020640876L); 148 | response = OneBotMessageHandler.callApi(request); 149 | data = new Gson().fromJson(response, new TypeToken>>() { 150 | }.getType()); 151 | } 152 | return data; 153 | 154 | } 155 | 156 | 157 | /** 158 | * 获取单个群成员信息,仅针对原有2个QQ所在的群才有用 159 | * 后续如果自由接入的话获取不到其他接入QQ的群信息(懒得写,得在消息入口把发送人QQ一直保持到调用时候) 160 | * 目前调用方:超管group info命令,list msg命令,还有每天循环查2个主群名片是否包含osu ID 161 | * 162 | * @param groupId 目标群 163 | * @param userId 目标人 164 | * @return 165 | */ 166 | public CqResponse getGroupMember(Long groupId, Long userId) { 167 | 168 | 169 | CqMsg cqMsg = new CqMsg(); 170 | cqMsg.setGroupId(groupId); 171 | cqMsg.setUserId(userId); 172 | cqMsg.setSelfId(1335734629L); 173 | 174 | OneBotApiRequest request = new OneBotApiRequest(); 175 | request.setAction("get_group_member_list"); 176 | request.setParams(cqMsg); 177 | request.setEcho(getId()); 178 | String response = OneBotMessageHandler.callApi(request); 179 | CqResponse data = new Gson().fromJson(response, new TypeToken>() { 180 | }.getType()); 181 | 182 | if (data.getRetCode() != 0) { 183 | cqMsg.setSelfId(1020640876L); 184 | response = OneBotMessageHandler.callApi(request); 185 | data = new Gson().fromJson(response, new TypeToken>() { 186 | }.getType()); 187 | } 188 | return data; 189 | 190 | } 191 | 192 | private String getId() { 193 | // 创建一个新的Random对象 194 | Random random = new Random(); 195 | 196 | // 生成10位随机数 197 | long randomNumber = random.nextLong() % 10000000000L; 198 | 199 | // 确保随机数是10位的 200 | randomNumber = Math.abs(randomNumber); 201 | if (randomNumber < 1000000000L) { 202 | randomNumber += 1000000000L; 203 | } 204 | return System.currentTimeMillis() + "" + randomNumber; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/manager/OsuApiV2Manager.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.manager; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.core.ParameterizedTypeReference; 7 | import org.springframework.http.*; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.util.LinkedMultiValueMap; 10 | import org.springframework.util.MultiValueMap; 11 | import org.springframework.web.client.RestTemplate; 12 | import org.springframework.web.util.UriComponentsBuilder; 13 | import top.mothership.cabbage.constant.Overall; 14 | import top.mothership.cabbage.pojo.osu.apiv2.OAuthCredentials; 15 | import top.mothership.cabbage.pojo.osu.apiv2.request.UserScoresRequest; 16 | import top.mothership.cabbage.pojo.osu.apiv2.response.ApiV2Score; 17 | import top.mothership.cabbage.pojo.osu.apiv2.response.TokenResponse; 18 | import top.mothership.cabbage.util.qq.ImgUtil; 19 | 20 | import java.time.LocalDateTime; 21 | import java.util.List; 22 | 23 | @Component 24 | 25 | public class OsuApiV2Manager { 26 | private static final String OSU_TOKEN_URL = "https://osu.ppy.sh/oauth/token"; 27 | private static final String OSU_API_BASE_URL = "https://osu.ppy.sh/api/v2"; 28 | private Logger log = LogManager.getLogger(this.getClass()); 29 | 30 | 31 | private final String CLIENT_ID = Overall.CABBAGE_CONFIG.getString("apiV2Id"); 32 | private final String CLIENT_SECRET = Overall.CABBAGE_CONFIG.getString("apiV2Secret"); 33 | private final String REFRESH_TOKEN = Overall.CABBAGE_CONFIG.getString("refreshToken"); 34 | 35 | 36 | private RestTemplate restTemplate = new RestTemplate(); 37 | 38 | 39 | private OAuthCredentials credentials = new OAuthCredentials(); 40 | 41 | private void updateCredentials(TokenResponse tokenResponse) { 42 | credentials.setAccessToken(tokenResponse.getAccessToken()); 43 | credentials.setExpiresIn(tokenResponse.getExpiresIn()); 44 | credentials.setCreatedAt(LocalDateTime.now()); 45 | } 46 | 47 | /** 48 | * 刷新访问令牌 49 | */ 50 | public void refreshAccessToken() { 51 | 52 | 53 | HttpHeaders headers = new HttpHeaders(); 54 | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); 55 | 56 | MultiValueMap requestBody = new LinkedMultiValueMap<>(); 57 | requestBody.add("client_id", CLIENT_ID); 58 | requestBody.add("client_secret", CLIENT_SECRET); 59 | requestBody.add("refresh_token", REFRESH_TOKEN); 60 | requestBody.add("grant_type", "refresh_token"); 61 | 62 | HttpEntity> request = new HttpEntity<>(requestBody, headers); 63 | 64 | 65 | ResponseEntity response = restTemplate.postForEntity( 66 | OSU_TOKEN_URL, request, TokenResponse.class); 67 | 68 | if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { 69 | TokenResponse tokenResponse = response.getBody(); 70 | updateCredentials(tokenResponse); 71 | log.info("更新API V2 Token成功"); 72 | } else { 73 | log.error("更新API V2 Token 失败: {}", response.getStatusCode()); 74 | } 75 | 76 | } 77 | 78 | 79 | /** 80 | * 获取有效的访问令牌 81 | */ 82 | public String getValidAccessToken() { 83 | if (credentials == null) { 84 | throw new IllegalStateException("OAuth credentials not initialized"); 85 | } 86 | 87 | if (credentials.isTokenExpired()) { 88 | refreshAccessToken(); 89 | } 90 | log.info("获取API V2 Access Token成功:{}",credentials.getAccessToken()); 91 | return credentials.getAccessToken(); 92 | } 93 | 94 | 95 | /** 96 | * 获取用户最佳成绩 97 | */ 98 | public List getUserBestScores(UserScoresRequest request) { 99 | return getUserScores(request, "best"); 100 | } 101 | 102 | /** 103 | * 获取用户最近成绩 104 | */ 105 | public List getUserRecentScores(UserScoresRequest request) { 106 | return getUserScores(request, "recent"); 107 | } 108 | 109 | /** 110 | * 获取用户成绩通用方法 111 | */ 112 | private List getUserScores(UserScoresRequest request, String type) { 113 | // 构建URL 114 | UriComponentsBuilder uriBuilder = UriComponentsBuilder 115 | .fromHttpUrl(OSU_API_BASE_URL + "/users/" + request.getUserId() + "/scores/" + type); 116 | 117 | // 添加可选参数 118 | if (request.getLimit() != null) { 119 | uriBuilder.queryParam("limit", request.getLimit()); 120 | } 121 | 122 | if (request.getOffset() != null) { 123 | uriBuilder.queryParam("offset", request.getOffset()); 124 | } 125 | 126 | if ("recent".equals(type) && request.getIncludeFails() != null) { 127 | uriBuilder.queryParam("include_fails", request.getIncludeFails() ? "1" : "0"); 128 | } 129 | 130 | if (request.getMode() != null) { 131 | uriBuilder.queryParam("mode", request.getMode()); 132 | } 133 | 134 | String url = uriBuilder.build().toUriString(); 135 | 136 | // 准备请求头 137 | HttpHeaders headers = new HttpHeaders(); 138 | headers.set("Accept", MediaType.APPLICATION_JSON_VALUE); 139 | headers.set("Authorization", "Bearer " + getValidAccessToken()); 140 | 141 | 142 | HttpEntity entity = new HttpEntity<>(headers); 143 | 144 | 145 | try { 146 | ResponseEntity> response = restTemplate.exchange( 147 | url, 148 | HttpMethod.GET, 149 | entity, 150 | new ParameterizedTypeReference>() { 151 | } 152 | ); 153 | 154 | if (response.getStatusCode() == HttpStatus.OK) { 155 | List scores = response.getBody(); 156 | log.info("Successfully retrieved {} user scores of type '{}'", 157 | scores != null ? scores.size() : 0, type); 158 | return scores; 159 | } else { 160 | log.error("Failed to get user scores: {}", response.getStatusCode()); 161 | throw new RuntimeException("Failed to get user scores from osu! API"); 162 | } 163 | } catch (Exception e) { 164 | System.out.println("发送HTTP请求" + url + entity); 165 | log.error("Error getting user scores: ", e); 166 | throw new RuntimeException("Error getting user scores from osu! API", e); 167 | } 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/mapper/RedisDAO.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.mapper; 2 | 3 | 4 | import com.google.gson.Gson; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.stereotype.Component; 8 | import top.mothership.cabbage.pojo.osu.Userinfo; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * Created by QHS on 2017/5/28. 14 | */ 15 | @Component 16 | public class RedisDAO { 17 | 18 | private final RedisTemplate redisTemplate; 19 | 20 | @Autowired 21 | public RedisDAO(RedisTemplate redisTemplate) { 22 | this.redisTemplate = redisTemplate; 23 | } 24 | 25 | public void add(Integer userId, Userinfo userinfo) { 26 | redisTemplate.opsForHash().put(String.valueOf(userId), String.valueOf(userinfo.getMode()), new Gson().toJson(userinfo)); 27 | } 28 | 29 | public void add(String key, String value) { 30 | redisTemplate.opsForValue().set(key, value); 31 | } 32 | 33 | public void add(String key, String value, Long timeout, TimeUnit timeUnit) { 34 | redisTemplate.opsForValue().set(key, value, timeout, timeUnit); 35 | } 36 | 37 | public Userinfo get(Integer userId, Integer mode) { 38 | return new Gson().fromJson((String) redisTemplate.opsForHash().get(String.valueOf(userId), String.valueOf(mode)), Userinfo.class); 39 | } 40 | 41 | public String get(String key) { 42 | return redisTemplate.opsForValue().get(key); 43 | } 44 | 45 | public void expire(Integer userId, final long timeout, final TimeUnit unit) { 46 | redisTemplate.expire(String.valueOf(userId), timeout, unit); 47 | } 48 | 49 | public void expire(String key, final long timeout, final TimeUnit unit) { 50 | redisTemplate.expire(key, timeout, unit); 51 | } 52 | 53 | public void flushDb() { 54 | redisTemplate.getConnectionFactory().getConnection().flushDb(); 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/mapper/ResDAO.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.mapper; 2 | 3 | import org.apache.ibatis.annotations.Insert; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.apache.ibatis.annotations.Param; 6 | import org.apache.ibatis.annotations.Select; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | @Mapper 13 | @Repository 14 | public interface ResDAO { 15 | 16 | 17 | @Insert("INSERT INTO `osufile` VALUES (null,#{bid},#{data})") 18 | Integer addOsuFile(@Param("bid")Integer bid,@Param("data") String data); 19 | 20 | @Select("SELECT `data` FROM osufile WHERE `bid` = #{bid} ") 21 | String getOsuFileBybid(@Param("bid")Integer bid); 22 | 23 | 24 | 25 | @Insert("INSERT INTO `bgfile` VALUES (null,#{sid},#{name},#{data})") 26 | Integer addBG(@Param("sid")Integer sid, @Param("name") String name,@Param("data") byte[] data); 27 | 28 | @Select("SELECT `data` FROM `bgfile` WHERE `sid` = #{sid} AND `name` = #{name} ") 29 | //这里似乎不能用byte[]? 30 | Object getBGBySidAndName(@Param("sid")Integer sid,@Param("name")String name); 31 | 32 | 33 | 34 | @Select("SELECT `name`,`data` FROM `resource`") 35 | List> getImages(); 36 | 37 | @Select("SELECT `data` FROM `resource` WHERE `name` = #{name}") 38 | Object getImage(@Param("name") String name); 39 | //使用MYSQL特有语法ON DUPLICATE KEY 节省代码量,name必须为unique索引 40 | //干掉报错,换成replace,同样name必须有unique索引 41 | @Insert("REPLACE INTO `resource` VALUES (null,#{name},#{data})") 42 | Integer addImage(@Param("name") String name, @Param("data") byte[] data); 43 | 44 | @Select("SELECT `data` FROM `res_other` WHERE `name` = #{name} ") 45 | Object getResource(@Param("name") String name); 46 | 47 | @Insert("REPLACE INTO `res_other` VALUES (null,#{name},#{data})") 48 | Integer addResource(@Param("name") String name, @Param("data") byte[] data); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/mapper/UserDAO.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.mapper; 2 | 3 | import org.apache.ibatis.annotations.*; 4 | import org.springframework.stereotype.Repository; 5 | import top.mothership.cabbage.pojo.User; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * The interface User dao. 11 | */ 12 | @Mapper 13 | @Repository 14 | public interface UserDAO { 15 | /** 16 | * Gets user. 17 | * 18 | * @param qq the qq 19 | * @param userId the user id 20 | * @return the user 21 | */ 22 | @Select("") 33 | //只能传一个,不能同时处理两个 34 | @Results( 35 | { 36 | //手动绑定这个字段 37 | @Result(column = "is_banned", property = "banned") 38 | }) 39 | User getUser(@Param("qq") Long qq, @Param("userId") Integer userId); 40 | 41 | /** 42 | * List user id by role list. 43 | * 44 | * @param role the role 45 | * @return the list 46 | */ 47 | //加入分隔符处理,在中间的,开头的,结尾的,只有这一个用户组的 48 | @Select("") 60 | List listUserIdByRole(@Param("role") String role, @Param("unbanned") Boolean unbanned); 61 | 62 | /** 63 | * List user id by uname list. 64 | * 改为Gson序列化,只需考虑在中间的问题,同时加入分隔符 65 | * 2018-3-12 16:26:59改为模糊查询 66 | * 2018-3-16 13:29:44没必要用动态sql吧?试试改为多个字段查询 67 | * 2018-3-21 12:13:29直接用OR字段搜user_id=用户名时会返回几百个结果,搜索了一下改为现在的查询 68 | * 69 | * @param keyword 搜索关键字 70 | * @return the list 71 | */ 72 | @Select("SELECT * FROM `userrole` " 73 | + "WHERE concat( `user_id` ,',', `qq` ,',', `legacy_uname`,',', `current_uname`) LIKE CONCAT('%',#{keyword},'%')") 74 | @Results( 75 | { 76 | //手动绑定这个字段 77 | @Result(column = "is_banned", property = "banned") 78 | }) 79 | List searchUser(@Param("keyword") String keyword); 80 | 81 | @Select("SELECT * FROM `userrole` " 82 | + "WHERE `is_banned` =1 ") 83 | @Results( 84 | { 85 | //手动绑定这个字段 86 | @Result(column = "is_banned", property = "banned") 87 | }) 88 | List listBannedUser(); 89 | 90 | /** 91 | * Gets repeat star. 92 | * 去掉100%复读的 93 | * 94 | * @return the repeat star 95 | */ 96 | @Results( 97 | { 98 | //手动绑定这个字段 99 | @Result(column = "is_banned", property = "banned") 100 | }) 101 | @Select("SELECT * FROM `userrole` WHERE `speaking_count` >10 order by `repeat_count`/`speaking_count` desc limit 1") 102 | User getRepeatStar(); 103 | 104 | /** 105 | * Update user integer. 106 | * 由于采用动态SQL,QQ只能是0不能是null 107 | * 108 | * @param user the user 109 | * @return the integer 110 | */ 111 | 112 | @Update("") 127 | Integer updateUser(@Param("user") User user); 128 | 129 | /** 130 | * Add user integer. 131 | * 132 | * @param user the user 133 | * @return the integer 134 | */ 135 | @Insert("INSERT INTO `userrole` VALUES (null," + 136 | "#{user.userId}," + 137 | "#{user.role}," + 138 | "#{user.qq}," + 139 | "#{user.legacyUname}," + 140 | "#{user.currentUname}," + 141 | "#{user.banned}," + 142 | "#{user.repeatCount}," + 143 | "#{user.speakingCount}," + 144 | "#{user.mode}," + 145 | "#{user.mainRole}," + 146 | "#{user.useEloBorder}," + 147 | "#{user.lastActiveDate})") 148 | Integer addUser(@Param("user") User user); 149 | 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/mapper/UserInfoDAO.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.mapper; 2 | 3 | import org.apache.ibatis.annotations.*; 4 | import org.springframework.stereotype.Repository; 5 | import top.mothership.cabbage.pojo.osu.Userinfo; 6 | 7 | import java.time.LocalDate; 8 | import java.util.List; 9 | 10 | @Mapper 11 | @Repository 12 | public interface UserInfoDAO { 13 | 14 | @Insert("INSERT INTO `userinfo` VALUES(null," + 15 | "#{userinfo.mode},#{userinfo.userId}," + 16 | "#{userinfo.count300},#{userinfo.count100}," + 17 | "#{userinfo.count50},#{userinfo.playCount}," + 18 | "#{userinfo.accuracy},#{userinfo.ppRaw}," + 19 | "#{userinfo.rankedScore},#{userinfo.totalScore}," + 20 | "#{userinfo.level},#{userinfo.ppRank}," + 21 | "#{userinfo.countRankSs},#{userinfo.countRankS}," + 22 | "#{userinfo.countRankA},#{userinfo.queryDate}" + 23 | ")") 24 | Integer addUserInfo(@Param("userinfo") Userinfo userinfo); 25 | 26 | @Select("SELECT * FROM `userinfo` WHERE `user_id` = #{userId}") 27 | List listUserInfoByUserId(@Param("userId") Integer userId); 28 | @Select("SELECT * FROM `userinfo` WHERE `user_id` = #{userId} AND `mode` = #{mode}") 29 | List listUserInfoByUserIdAndMode(@Param("userId") Integer userId,@Param("mode") Integer mode); 30 | @Select("SELECT * , abs(UNIX_TIMESTAMP(queryDate) - UNIX_TIMESTAMP(#{queryDate})) AS ds " + 31 | "FROM `userinfo` WHERE `user_id` = #{userId} AND `mode` = #{mode} ORDER BY ds ASC LIMIT 1") 32 | Userinfo getNearestUserInfo(@Param("mode") Integer mode, @Param("userId") Integer userId, @Param("queryDate") LocalDate queryDate); 33 | 34 | @Select("SELECT * FROM `userinfo` WHERE `user_id` = #{userId} AND `queryDate` = #{queryDate} AND `mode` = #{mode}") 35 | Userinfo getUserInfo(@Param("mode") Integer mode, @Param("userId") Integer userId, @Param("queryDate") LocalDate queryDate); 36 | 37 | @Delete("DELETE FROM `userinfo` WHERE `queryDate` = #{queryDate}") 38 | void clearTodayInfo(@Param("queryDate") LocalDate queryDate); 39 | 40 | @Select("") 48 | List batchGetNowUserinfo(@Param("list")List uidList,@Param("start")LocalDate start); 49 | 50 | @Select("SELECT * FROM userinfo WHERE queryDate = DATE_SUB(#{start}, INTERVAL 30 DAY) AND mode=0 " + 51 | "AND user_id IN (" + 52 | "(SELECT user_id FROM (SELECT user_id,pp_raw FROM userinfo WHERE queryDate = DATE_SUB(#{start}, INTERVAL 2 DAY) " + 53 | "AND mode =0 AND pp_raw<#{ppMax} AND pp_raw>#{ppMin} ) AS u)" + 54 | ");") 55 | List getStdUserRegisteredInOneMonth(@Param("ppMin") Integer ppMin, @Param("ppMax") Integer ppMax, @Param("start")LocalDate start); 56 | 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/User.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.LocalDate; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class User { 13 | private Integer userId; 14 | private String role; 15 | private Long qq; 16 | private String legacyUname; 17 | private String currentUname; 18 | private boolean banned; 19 | private Integer mode; 20 | private Long repeatCount; 21 | private Long speakingCount; 22 | private String mainRole; 23 | private Boolean useEloBorder; 24 | private LocalDate lastActiveDate; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/WebResponse.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | public class WebResponse { 9 | private Integer code; 10 | private String status; 11 | private T data; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/coolq/Argument.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.coolq; 2 | 3 | import lombok.Data; 4 | import top.mothership.cabbage.pojo.osu.SearchParam; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * 从命令中解析出的参数 10 | */ 11 | @Data 12 | public class Argument { 13 | 14 | private String subCommandLowCase; 15 | private Integer mode; 16 | private Integer day; 17 | private Integer num; 18 | private boolean text; 19 | private String username; 20 | private Integer userId; 21 | private Long qq; 22 | private Long hour; 23 | private List usernames; 24 | private Long groupId; 25 | private String role; 26 | private String flag; 27 | private SearchParam searchParam; 28 | private String fileName; 29 | private String url; 30 | private Integer second; 31 | private Integer beatmapId; 32 | private String remark; 33 | private String bound; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/coolq/CqMsg.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.coolq; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import top.mothership.cabbage.enums.ParameterEnum; 9 | 10 | //酷Q收到消息之后通过HTTPAPI给白菜的POST请求体 11 | //public class CqMsg implements Comparable { 12 | @Data 13 | @Builder 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class CqMsg { 17 | private Argument argument; 18 | private ParameterEnum[] required; 19 | private ParameterEnum[] optional; 20 | @SerializedName("post_type") 21 | private String postType; 22 | @SerializedName("notice_type") 23 | private String noticeType; 24 | @SerializedName("request_type") 25 | private String requestType; 26 | @SerializedName("message_type") 27 | private String messageType; 28 | @SerializedName("sub_type") 29 | private String subType; 30 | @SerializedName("group_id") 31 | private Long groupId; 32 | @SerializedName("user_id") 33 | private Long userId; 34 | private String message; 35 | @SerializedName("raw_message") 36 | private String rawMessage; 37 | @SerializedName("operator_id") 38 | private Long operatorId; 39 | private Integer duration; 40 | @SerializedName("discuss_id") 41 | private Long discussId; 42 | private String flag; 43 | private String type; 44 | private Boolean approve; 45 | private String reason; 46 | private Boolean enable; 47 | private Long time; 48 | @SerializedName("self_id") 49 | private Long selfId; 50 | private String comment; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/coolq/CqResponse.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.coolq; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Data; 5 | 6 | //调用HTTPAPI发送消息之后的返回体 7 | @Data 8 | public class CqResponse { 9 | 10 | private String status; 11 | @SerializedName("retcode") 12 | private int retCode; 13 | private T data; 14 | private String echo; 15 | 16 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/coolq/OneBotApiRequest.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.coolq; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OneBotApiRequest { 7 | private String echo; 8 | private String action; 9 | private CqMsg params; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/coolq/QQInfo.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.coolq; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class QQInfo { 8 | @SerializedName("group_id") 9 | private Long groupId; 10 | @SerializedName("user_id") 11 | private Long userId; 12 | private String nickname; 13 | private String card; 14 | private String sex; 15 | private Integer age; 16 | private String area; 17 | @SerializedName("join_time") 18 | private Long joinTime; 19 | @SerializedName("last_sent_time") 20 | private Long lastSentTime; 21 | private String level; 22 | private String role; 23 | private Boolean unfriendly; 24 | private String title; 25 | @SerializedName("title_expire_time") 26 | private Long titleExpireTime; 27 | @SerializedName("card_changeable") 28 | private Boolean cardChangeable; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/coolq/RespData.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.coolq; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class RespData { 8 | private Integer id; 9 | private String nickname; 10 | @SerializedName("group_name") 11 | private String groupName; 12 | @SerializedName("group_id") 13 | private Long groupId; 14 | private String cookies; 15 | private Long token; 16 | @SerializedName("coolq_edition") 17 | private String coolqEdition; 18 | @SerializedName("plugin_version") 19 | private String pluginVersion; 20 | 21 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/elo/Elo.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.elo; 2 | 3 | import lombok.Data; 4 | 5 | import java.math.BigDecimal; 6 | 7 | @Data 8 | public class Elo { 9 | private Integer code; 10 | private Integer user_id; 11 | private Integer rank; 12 | private BigDecimal elo; 13 | private BigDecimal init_elo; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/elo/EloChange.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.elo; 2 | 3 | import lombok.Data; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Date; 7 | 8 | @Data 9 | public class EloChange { 10 | private Integer user_id; 11 | private BigDecimal elo_change; 12 | private BigDecimal match_id; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/elo/EloMatch.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.elo; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class EloMatch { 7 | private String tourney_name; 8 | private String match_id; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/Beatmap.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | @Data 9 | public class Beatmap { 10 | //提供兼容osu search的API 11 | @SerializedName("beatmapset_id") 12 | private Integer beatmapSetId; 13 | @SerializedName("beatmap_id") 14 | private Integer beatmapId; 15 | @SerializedName(value = "beatmap_status", alternate = {"approved"}) 16 | private Integer approved; 17 | @SerializedName("total_length") 18 | private Integer totalLength; 19 | @SerializedName(value = "play_length", alternate = {"hit_length"}) 20 | private Integer hitLength; 21 | @SerializedName(value = "difficulty_name", alternate = {"version"}) 22 | private String version; 23 | @SerializedName("file_md5") 24 | private String fileMd5; 25 | @SerializedName(value = "difficulty_cs", alternate = {"diff_size"}) 26 | private Float diffSize; 27 | @SerializedName(value = "difficulty_od", alternate = {"diff_overall"}) 28 | private Float diffOverall; 29 | @SerializedName(value = "difficulty_ar", alternate = {"diff_approach"}) 30 | private Float diffApproach; 31 | @SerializedName(value = "difficulty_hp", alternate = {"diff_drain"}) 32 | private Float diffDrain; 33 | @SerializedName(value = "gamemode", alternate = {"mode"}) 34 | private Integer mode; 35 | @SerializedName(value = "date", alternate = {"approved_date"}) 36 | private Date approvedDate; 37 | @SerializedName("last_update") 38 | private Date lastUpdate; 39 | 40 | private String artist; 41 | 42 | private String title; 43 | @SerializedName(value = "mapper", alternate = {"creator"}) 44 | private String creator; 45 | 46 | private Double bpm; 47 | 48 | private String source; 49 | 50 | private String tags; 51 | 52 | private Integer genreId; 53 | 54 | private Integer languageId; 55 | @SerializedName(value = "favorites", alternate = {"favourite_count"}) 56 | private Integer favouriteCount; 57 | @SerializedName("playcount") 58 | private Long playCount; 59 | @SerializedName("passcount") 60 | private Long passCount; 61 | @SerializedName("max_combo") 62 | private Integer maxCombo; 63 | @SerializedName(value = "difficultyrating", alternate = {"difficulty"}) 64 | private Double difficultyRating; 65 | 66 | @SerializedName("artist_unicode") 67 | private String artistUnicode; 68 | @SerializedName("title_unicode") 69 | private String titleUnicode; 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/CalcResult.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | public class CalcResult { 4 | private ScoreResult scoreResult; 5 | 6 | public ScoreResult getScoreResult() { 7 | return scoreResult; 8 | } 9 | 10 | public void setScoreResult(ScoreResult scoreResult) { 11 | this.scoreResult = scoreResult; 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/CalculateByBidRequest.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | public class CalculateByBidRequest { 4 | private int bid; 5 | private boolean refresh; 6 | private UserScore userScore; 7 | 8 | public int getBid() { 9 | return bid; 10 | } 11 | 12 | public void setBid(int bid) { 13 | this.bid = bid; 14 | } 15 | 16 | public boolean isRefresh() { 17 | return refresh; 18 | } 19 | 20 | public void setRefresh(boolean refresh) { 21 | this.refresh = refresh; 22 | } 23 | 24 | public UserScore getUserScore() { 25 | return userScore; 26 | } 27 | 28 | public void setUserScore(UserScore userScore) { 29 | this.userScore = userScore; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/ClientFile.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ClientFile { 7 | private String fileVersion; 8 | private String fileName; 9 | private String fileHash; 10 | private String filesize; 11 | private String timestamp; 12 | private String patch_id; 13 | private String urlFull; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/Game.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | import java.util.List; 7 | //MP API中的某一场游戏 8 | @Data 9 | public class Game { 10 | private List scores; 11 | private Integer gameId; 12 | private Date startTime; 13 | private Date endTime; 14 | private Integer beatmapId; 15 | private Integer playMode; 16 | private Integer matchType; 17 | private Integer scoringType; 18 | private Integer teamType; 19 | private Integer mods; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/Lobby.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | import java.util.List; 7 | 8 | /** 9 | * 用于存储本地房间的实体类 10 | */ 11 | @Data 12 | public class Lobby { 13 | /** 14 | * 比赛的发起人 15 | */ 16 | private Integer creator; 17 | private LocalDateTime reservedStartTime; 18 | private Match match; 19 | private List games; 20 | private String group; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/Match.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | 7 | @Data 8 | public class Match { 9 | private Integer matchId; 10 | private String name; 11 | private Date startTime; 12 | private Date endTime; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/MpBeatmap.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class MpBeatmap { 7 | private Integer id; 8 | private Integer beatmapId; 9 | private Integer recommender; 10 | private String group; 11 | private Integer mods; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/OppaiResult.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | /** 7 | * Created by QHS on 2017/9/11. 8 | */ 9 | @Data 10 | @AllArgsConstructor 11 | public class OppaiResult { 12 | private String oppaiVersion; 13 | private int code; 14 | private String errstr; 15 | private String artist; 16 | private String artistUnicode; 17 | private String title; 18 | private String titleUnicode; 19 | private String creator; 20 | private String version; 21 | private String modsStr; 22 | private int mods; 23 | private double od; 24 | private double ar; 25 | private double cs; 26 | private double hp; 27 | private int combo; 28 | private int maxCombo; 29 | private int numCircles; 30 | private int numSliders; 31 | private int numSpinners; 32 | private int misses; 33 | private int scoreVersion; 34 | private double stars; 35 | private double starsLegacy; 36 | private double speedStars; 37 | private double aimStars; 38 | private int nsingles; 39 | private int nsinglesThreshold; 40 | private double aimPp; 41 | private double speedPp; 42 | private double accPp; 43 | private double pp; 44 | private double ppLegacy; 45 | private double speedMultiplier; 46 | private double maxPP; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/OsuFile.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | 7 | public class OsuFile { 8 | private Integer version; 9 | @Setter 10 | @Getter 11 | private String bgName; 12 | private String audioFilename; 13 | private Integer audioLeadin; 14 | private Integer previewTime; 15 | private Boolean countdown; 16 | private String sampleSet; 17 | private String stackLeniency; 18 | private Integer mode; 19 | private Boolean letterboxInBreaks; 20 | private Boolean wideScreenStoryBoard; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/OsuSearchResp.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | public class OsuSearchResp { 11 | private int id; 12 | private String title; 13 | private String titleUnicode; 14 | private String artist; 15 | private String artistUnicode; 16 | private String creator; 17 | private String source; 18 | private String tags; 19 | private Covers covers; 20 | private int favouriteCount; 21 | private Object hype; 22 | private boolean nsfw; 23 | private int offset; 24 | private int playCount; 25 | private String previewUrl; 26 | private boolean spotlight; 27 | private String status; 28 | private Object trackId; 29 | private int userId; 30 | private boolean video; 31 | private double bpm; 32 | private boolean canBeHyped; 33 | private Object deletedAt; 34 | private boolean discussionEnabled; 35 | private boolean discussionLocked; 36 | private boolean isScoreable; 37 | private String lastUpdated; 38 | private String legacyThreadUrl; 39 | private NominationsSummary nominationsSummary; 40 | private int ranked; 41 | private String rankedDate; 42 | private boolean storyboard; 43 | private String submittedDate; 44 | private Availability availability; 45 | private boolean hasFavourited; 46 | private List beatmaps; 47 | private List packTags; 48 | private List modes; 49 | private String lastChecked; 50 | 51 | @Data 52 | @NoArgsConstructor 53 | @AllArgsConstructor 54 | public static class Covers { 55 | private String cover; 56 | private String cover2x; 57 | private String card; 58 | private String card2x; 59 | private String list; 60 | private String list2x; 61 | private String slimcover; 62 | private String slimcover2x; 63 | } 64 | 65 | @Data 66 | @NoArgsConstructor 67 | @AllArgsConstructor 68 | public static class NominationsSummary { 69 | private int current; 70 | private List eligibleMainRulesets; 71 | private RequiredMeta requiredMeta; 72 | 73 | @Data 74 | @NoArgsConstructor 75 | @AllArgsConstructor 76 | public static class RequiredMeta { 77 | private int mainRuleset; 78 | private int nonMainRuleset; 79 | } 80 | } 81 | 82 | @Data 83 | @NoArgsConstructor 84 | @AllArgsConstructor 85 | public static class Availability { 86 | private boolean downloadDisabled; 87 | private Object moreInformation; 88 | } 89 | 90 | @Data 91 | @NoArgsConstructor 92 | @AllArgsConstructor 93 | public static class Beatmap { 94 | private int beatmapsetId; 95 | private double difficultyRating; 96 | private int id; 97 | private String mode; 98 | private String status; 99 | private int totalLength; 100 | private int userId; 101 | private String version; 102 | private double accuracy; 103 | private double ar; 104 | private double bpm; 105 | private boolean convert; 106 | private int countCircles; 107 | private int countSliders; 108 | private int countSpinners; 109 | private double cs; 110 | private Object deletedAt; 111 | private double drain; 112 | private int hitLength; 113 | private boolean isScoreable; 114 | private String lastUpdated; 115 | private int modeInt; 116 | private int passcount; 117 | private int playcount; 118 | private int ranked; 119 | private String url; 120 | private String checksum; 121 | private int maxCombo; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/Remark.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Remark { 7 | private Long userId; 8 | private Long beatmapId; 9 | private String text; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/Replay.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Replay { 7 | private String encoding; 8 | private String content; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/Score.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | 8 | /** 9 | * The type Score. 10 | */ 11 | @Data 12 | public class Score { 13 | //仅用于get_user_best,其他API没有 14 | private Integer beatmapId; 15 | //这个不在API返回值,画BP的时候手动拼接的 16 | private String beatmapName; 17 | //这六个仅仅用于db解析 18 | private Byte mode; 19 | private Integer scoreVersion; 20 | private String mapMd5; 21 | private String repMd5; 22 | //永远是-1,在osr文件中代表LZMA流大小? 23 | private Integer size; 24 | //这个可能是get_scores的score_id值 25 | @SerializedName("score_id") 26 | private Long onlineId; 27 | //用于存储BP的位数 28 | private Integer bpId; 29 | private Long score; 30 | @SerializedName("maxcombo") 31 | private Integer maxCombo; 32 | private Integer count50; 33 | private Integer count100; 34 | private Integer count300; 35 | @SerializedName("countmiss") 36 | private Integer countMiss; 37 | @SerializedName("countkatu") 38 | private Integer countKatu; 39 | @SerializedName("countgeki") 40 | private Integer countGeki; 41 | private Integer perfect; 42 | @SerializedName("enabled_mods") 43 | private Integer enabledMods; 44 | //更换为LocalDateTime会出反序列化异常 45 | private Date date; 46 | private String rank; 47 | //recent的API里压根没有这个字段 48 | private Float pp; 49 | //md ppysb 50 | @SerializedName("username") 51 | private String userName; 52 | private Integer userId; 53 | //为兼容get_match 54 | private Integer slot; 55 | private Integer team; 56 | private Integer pass; 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/ScoreResult.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | public class ScoreResult { 4 | private double aim; 5 | private double speed; 6 | private double acc; 7 | private double pp; 8 | 9 | public double getAim() { 10 | return aim; 11 | } 12 | 13 | public void setAim(double aim) { 14 | this.aim = aim; 15 | } 16 | 17 | public double getSpeed() { 18 | return speed; 19 | } 20 | 21 | public void setSpeed(double speed) { 22 | this.speed = speed; 23 | } 24 | 25 | public double getAcc() { 26 | return acc; 27 | } 28 | 29 | public void setAcc(double acc) { 30 | this.acc = acc; 31 | } 32 | 33 | public double getPp() { 34 | return pp; 35 | } 36 | 37 | public void setPp(double pp) { 38 | this.pp = pp; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/SearchParam.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.JsonPrimitive; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class SearchParam { 9 | private String artist; 10 | private String title; 11 | private String diffName; 12 | private String mapper; 13 | private Double ar; 14 | private Double od; 15 | private Double cs; 16 | private Double hp; 17 | private Integer mods; 18 | private String modsString; 19 | private Integer beatmapId; 20 | private Integer countMiss; 21 | private Integer count100; 22 | private Integer count50; 23 | private Integer maxCombo; 24 | private Double acc; 25 | public String toOsuDirectSearchString() { 26 | StringBuilder query = new StringBuilder(); 27 | 28 | query.append("["); 29 | 30 | if (artist != null && !artist.isEmpty()) { 31 | query.append(String.format("artist = \"%s\"", artist)); 32 | } 33 | 34 | if (ar != null) { 35 | appendAnd(query); 36 | query.append(String.format("beatmaps.ar = %s", ar)); 37 | } 38 | 39 | if (mapper != null && !mapper.isEmpty()) { 40 | appendAnd(query); 41 | query.append(String.format("creator = \"%s\"", mapper)); 42 | } 43 | 44 | if (od != null) { 45 | appendAnd(query); 46 | query.append(String.format("beatmaps.accuracy = %s", od)); 47 | } 48 | 49 | if (cs != null) { 50 | appendAnd(query); 51 | query.append(String.format("beatmaps.cs = %s", cs)); 52 | } 53 | 54 | if (hp != null) { 55 | appendAnd(query); 56 | query.append(String.format("beatmaps.drain = %s", hp)); 57 | } 58 | 59 | query.append("]"); 60 | 61 | if (title != null && !title.isEmpty()) { 62 | query.append(title); 63 | } 64 | 65 | return query.toString(); 66 | } 67 | 68 | private void appendAnd(StringBuilder query) { 69 | if (query.length() > 1) { // 确保不是第一个条件 70 | query.append(" AND "); 71 | } 72 | } 73 | @Override 74 | public String toString() { 75 | return "{" + 76 | "艺术家='" + artist + '\'' + 77 | ", 标题='" + title + '\'' + 78 | ", 难度名='" + diffName + '\'' + 79 | ", 作者='" + mapper + '\'' + 80 | ", 谱面id=" + beatmapId + 81 | '}'; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/UserScore.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class UserScore { 6 | private Integer mods; 7 | private Integer combo; 8 | private Integer mode; 9 | private Integer count50; 10 | private Integer count100; 11 | private Integer count300; 12 | private Integer countMiss; 13 | 14 | public Integer getMods() { 15 | return mods; 16 | } 17 | 18 | public void setMods(Integer mods) { 19 | this.mods = mods; 20 | } 21 | 22 | public Integer getCombo() { 23 | return combo; 24 | } 25 | 26 | public void setCombo(Integer combo) { 27 | this.combo = combo; 28 | } 29 | 30 | public Integer getMode() { 31 | return mode; 32 | } 33 | 34 | public void setMode(Integer mode) { 35 | this.mode = mode; 36 | } 37 | 38 | public Integer getCount50() { 39 | return count50; 40 | } 41 | 42 | public void setCount50(Integer count50) { 43 | this.count50 = count50; 44 | } 45 | 46 | public Integer getCount100() { 47 | return count100; 48 | } 49 | 50 | public void setCount100(Integer count100) { 51 | this.count100 = count100; 52 | } 53 | 54 | public Integer getCount300() { 55 | return count300; 56 | } 57 | 58 | public void setCount300(Integer count300) { 59 | this.count300 = count300; 60 | } 61 | 62 | public Integer getCountMiss() { 63 | return countMiss; 64 | } 65 | 66 | public void setCountMiss(Integer countMiss) { 67 | this.countMiss = countMiss; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/Userinfo.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Data; 5 | 6 | import java.time.LocalDate; 7 | @Data 8 | public class Userinfo { 9 | /** 10 | * 这个字段不写入数据库 11 | */ 12 | @SerializedName("username") 13 | private String userName; 14 | private Integer mode; 15 | private int userId; 16 | private int count300; 17 | private int count100; 18 | private int count50; 19 | @SerializedName("playcount") 20 | private int playCount; 21 | private float accuracy; 22 | private float ppRaw; 23 | private long rankedScore; 24 | private long totalScore; 25 | private float level; 26 | private int ppRank; 27 | private int countRankSs; 28 | private int countRankSsh; 29 | private int countRankS; 30 | private int countRankSh; 31 | private int countRankA; 32 | private LocalDate queryDate; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/apiv2/OAuthCredentials.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu.apiv2; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class OAuthCredentials { 13 | 14 | private String accessToken; 15 | private long expiresIn; 16 | private LocalDateTime createdAt; 17 | 18 | public boolean isTokenExpired() { 19 | if (accessToken == null || createdAt == null) { 20 | return true; 21 | } 22 | return LocalDateTime.now().isAfter(createdAt.plusSeconds(expiresIn - 300)); // 5分钟缓冲 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/apiv2/request/UserScoresRequest.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu.apiv2.request; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * 用户成绩请求参数 9 | */ 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class UserScoresRequest { 14 | private String userId; // 用户ID 15 | private String type; // 成绩类型: "best" 或 "recent" 16 | private Integer limit; // 返回结果数量限制 (可选) 17 | private Integer offset; // 结果偏移量 (可选) 18 | private Boolean includeFails; // 是否包含失败成绩 (可选, 仅用于recent类型) 19 | private String mode; // 游戏模式 (可选): "osu", "taiko", "fruits", "mania" 20 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/apiv2/response/ApiV2Score.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu.apiv2.response; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * 用户成绩响应模型 9 | */ 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class ApiV2Score { 14 | private long id; 15 | private long userId; 16 | private float accuracy; 17 | private String rank; // SS, S, A, B, C, D 18 | private long totalScore; 19 | private int maxCombo; 20 | private boolean perfect; 21 | private String createdAt; 22 | private int countMiss; 23 | private int count50; 24 | private int count100; 25 | private int count300; 26 | private int countKatu; 27 | private int countGeki; 28 | 29 | private Beatmap beatmap; 30 | private BeatmapSet beatmapSet; 31 | 32 | @Data 33 | @NoArgsConstructor 34 | @AllArgsConstructor 35 | public static class Beatmap { 36 | private long id; 37 | private String title; 38 | private String difficulty; 39 | private float starRating; 40 | } 41 | 42 | @Data 43 | @NoArgsConstructor 44 | @AllArgsConstructor 45 | public static class BeatmapSet { 46 | private long id; 47 | private String title; 48 | private String artist; 49 | private String creator; 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/pojo/osu/apiv2/response/TokenResponse.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.pojo.osu.apiv2.response; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * Token响应 10 | */ 11 | @Data 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class TokenResponse { 15 | private String accessToken; 16 | private String refreshToken; 17 | private String tokenType; 18 | private long expiresIn; 19 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/service/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import top.mothership.cabbage.mapper.RedisDAO; 6 | import top.mothership.cabbage.util.web.CaptchaUtil; 7 | 8 | @Service 9 | public class UserServiceImpl { 10 | private final CaptchaUtil captchaUtil; 11 | private RedisDAO redisDAO; 12 | 13 | 14 | @Autowired 15 | public UserServiceImpl(CaptchaUtil captchaUtil, RedisDAO redisDAO) { 16 | this.captchaUtil = captchaUtil; 17 | this.redisDAO = redisDAO; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/task/FixErrorBannedTasker.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.task; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.reflect.TypeToken; 6 | import org.apache.logging.log4j.LogManager; 7 | import org.apache.logging.log4j.Logger; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.scheduling.annotation.Scheduled; 10 | import org.springframework.stereotype.Component; 11 | import top.mothership.cabbage.manager.ApiManager; 12 | import top.mothership.cabbage.mapper.UserDAO; 13 | import top.mothership.cabbage.mapper.UserInfoDAO; 14 | import top.mothership.cabbage.pojo.User; 15 | import top.mothership.cabbage.pojo.osu.Userinfo; 16 | 17 | import java.time.LocalDate; 18 | import java.util.List; 19 | @Component 20 | public class FixErrorBannedTasker { 21 | private Logger logger = LogManager.getLogger(this.getClass()); 22 | 23 | private UserDAO userDAO; 24 | 25 | @Autowired 26 | public void setUserDAO(UserDAO userDAO) { 27 | this.userDAO = userDAO; 28 | } 29 | 30 | 31 | private ApiManager apiManager; 32 | 33 | @Autowired 34 | public void setApiManager(ApiManager apiManager) { 35 | this.apiManager = apiManager; 36 | } 37 | 38 | 39 | private UserInfoDAO userInfoDAO; 40 | 41 | @Autowired 42 | public void setUserInfoDAO(UserInfoDAO userInfoDAO) { 43 | this.userInfoDAO = userInfoDAO; 44 | } 45 | 46 | 47 | @Scheduled(cron = "0 0 * * * ?") 48 | public void refreshBannedStatus() { 49 | List list = userDAO.listBannedUser(); 50 | for (User user : list) { 51 | Userinfo userinfo = apiManager.getUser(0, user.getUserId()); 52 | if (userinfo != null) { 53 | //将日期改为一天前写入 54 | userinfo.setQueryDate(LocalDate.now().minusDays(1)); 55 | userInfoDAO.addUserInfo(userinfo); 56 | logger.info("将" + userinfo.getUserName() + "的数据补录成功"); 57 | if (!userinfo.getUserName().equals(user.getCurrentUname())) { 58 | //如果检测到用户改名,取出数据库中的现用名加入到曾用名,并且更新现用名和曾用名 59 | List legacyUname = new GsonBuilder().create().fromJson(user.getLegacyUname(), new TypeToken>() { 60 | }.getType()); 61 | if (user.getCurrentUname() != null) { 62 | legacyUname.add(user.getCurrentUname()); 63 | } 64 | user.setLegacyUname(new Gson().toJson(legacyUname)); 65 | user.setCurrentUname(userinfo.getUserName()); 66 | logger.info("检测到玩家" + userinfo.getUserName() + "改名,已登记"); 67 | } 68 | //如果能获取到userinfo,就把banned设置为0 69 | user.setBanned(false); 70 | userDAO.updateUser(user); 71 | } 72 | 73 | } 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/util/osu/DbFileUtil.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.util.osu; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | import top.mothership.cabbage.manager.ApiManager; 8 | import top.mothership.cabbage.pojo.osu.Beatmap; 9 | import top.mothership.cabbage.pojo.osu.Score; 10 | 11 | import java.io.DataInputStream; 12 | import java.io.DataOutputStream; 13 | import java.io.FileInputStream; 14 | import java.io.IOException; 15 | import java.nio.ByteBuffer; 16 | import java.nio.ByteOrder; 17 | import java.util.ArrayList; 18 | import java.util.Date; 19 | import java.util.LinkedHashMap; 20 | import java.util.List; 21 | 22 | @Component 23 | public class DbFileUtil { 24 | private final ApiManager apiManager; 25 | private final ScoreUtil scoreUtil; 26 | private Logger logger = LogManager.getLogger(this.getClass()); 27 | 28 | @Autowired 29 | public DbFileUtil(ApiManager apiManager, ScoreUtil scoreUtil) { 30 | this.apiManager = apiManager; 31 | this.scoreUtil = scoreUtil; 32 | } 33 | 34 | //到底是把文件名传入构造方法,让一个util对象对应一个collection呢 35 | //还是提供方法,传入文件名,输出Beatmap集合呢 36 | public LinkedHashMap> praseCollectionDB(String filename) throws IOException { 37 | DataInputStream reader = new DataInputStream(new FileInputStream(filename)); 38 | int version = readInt(reader); 39 | int count = readInt(reader); 40 | LinkedHashMap> map = new LinkedHashMap<>(count); 41 | for (int i = 0; i < count; i++) { 42 | String name = readString(reader); 43 | int count2 = readInt(reader); 44 | List md5Hashes = new ArrayList<>(count); 45 | logger.info("开始解析收藏夹" + name); 46 | for (int i2 = 0; i2 < count2; i2++) { 47 | String md5Hash = readString(reader); 48 | Beatmap beatmap = apiManager.getBeatmap(md5Hash); 49 | md5Hashes.add(beatmap); 50 | } 51 | 52 | map.put(name, md5Hashes); 53 | } 54 | return map; 55 | } 56 | 57 | public LinkedHashMap> praseScoresDB(String filename) throws IOException { 58 | DataInputStream reader = new DataInputStream(new FileInputStream("D:\\scores.db")); 59 | int version = readInt(reader); 60 | int count = readInt(reader); 61 | LinkedHashMap> map = new LinkedHashMap<>(count); 62 | for (int i = 0; i < count; i++) { 63 | String md5 = readString(reader); 64 | Beatmap beatmap = apiManager.getBeatmap(md5); 65 | int scoreCount = readInt(reader); 66 | List scores = new ArrayList<>(scoreCount); 67 | for (int i2 = 0; i2 < scoreCount; i2++) { 68 | Score score = new Score(); 69 | byte mode = readByte(reader); 70 | int scoreVersion = readInt(reader); 71 | String mapMd5 = readString(reader); 72 | String username = readString(reader); 73 | String repMd5 = readString(reader); 74 | int count300 = readShort(reader); 75 | int count100 = readShort(reader); 76 | int count50 = readShort(reader); 77 | int countGeki = readShort(reader); 78 | int countKatu = readShort(reader); 79 | int countMiss = readShort(reader); 80 | int scoreValue = readInt(reader); 81 | int maxCombo = readInt(reader); 82 | boolean perfect = readBoolean(reader); 83 | int mods = readInt(reader); 84 | // String empty = readString(reader); 85 | long timestamps = readLong(reader); 86 | int size = readInt(reader); 87 | long onlineId = readLong(reader); 88 | LinkedHashMap modsMap = scoreUtil.convertModToHashMap(mods); 89 | score.setBeatmapId(Integer.valueOf(beatmap.getBeatmapId())); 90 | score.setCount50(count50); 91 | score.setCount100(count100); 92 | score.setCount300(count300); 93 | score.setCountGeki(countGeki); 94 | score.setCountKatu(countKatu); 95 | score.setCountMiss(countMiss); 96 | score.setDate(new Date(timestamps)); 97 | score.setMaxCombo(maxCombo); 98 | score.setEnabledMods(mods); 99 | score.setScore((long) scoreValue); 100 | score.setUserName(username); 101 | if (perfect) { 102 | score.setPerfect(1); 103 | } else { 104 | score.setPerfect(0); 105 | } 106 | int noteCount = count50 + count100 + count300 + countMiss; 107 | float percent300 = (float) count300 / noteCount; 108 | float percent50 = (float) count50 / noteCount; 109 | 110 | if (percent300 < 0.7) { 111 | score.setRank("D"); 112 | } else if (percent300 <= 0.8) { 113 | score.setRank("C"); 114 | } else if (percent300 <= 0.85) { 115 | score.setRank("B"); 116 | } else if (percent300 < 1 && percent50 < 0.1) { 117 | if (modsMap.keySet().contains("HD") || modsMap.keySet().contains("FL")) { 118 | score.setRank("SH"); 119 | } else { 120 | score.setRank("S"); 121 | } 122 | } else { 123 | if (modsMap.keySet().contains("HD") || modsMap.keySet().contains("FL")) { 124 | score.setRank("XH"); 125 | } else { 126 | score.setRank("X"); 127 | } 128 | } 129 | scores.add(score); 130 | } 131 | map.put(beatmap, scores); 132 | } 133 | return map; 134 | } 135 | 136 | private byte readByte(DataInputStream reader) throws IOException { 137 | // 1 byte 138 | return reader.readByte(); 139 | } 140 | 141 | private void writeByte(DataOutputStream writer, byte v) throws IOException { 142 | writer.writeByte(v); 143 | } 144 | 145 | private short readShort(DataInputStream reader) throws IOException { 146 | // 2 bytes, little endian 147 | byte[] bytes = new byte[2]; 148 | reader.readFully(bytes); 149 | //读出两个字节,并且用ByteBuffer转换成short 150 | ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 151 | return bb.getShort(); 152 | } 153 | 154 | private void writeShort(DataOutputStream writer, short n) throws IOException { 155 | //低字节在前的byte数组 156 | byte[] b = new byte[2]; 157 | b[0] = (byte) (n & 0xff); 158 | b[1] = (byte) (n >> 8 & 0xff); 159 | writer.write(b); 160 | } 161 | 162 | 163 | private int readInt(DataInputStream reader) throws IOException { 164 | // 4 bytes, little endian 165 | byte[] bytes = new byte[4]; 166 | reader.readFully(bytes); 167 | ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 168 | return bb.getInt(); 169 | } 170 | 171 | private void writeInt(DataOutputStream writer, int n) throws IOException { 172 | byte[] b = new byte[4]; 173 | b[0] = (byte) (n & 0xff); 174 | b[1] = (byte) (n >> 8 & 0xff); 175 | b[2] = (byte) (n >> 16 & 0xff); 176 | b[3] = (byte) (n >> 24 & 0xff); 177 | writer.write(b); 178 | } 179 | 180 | private long readLong(DataInputStream reader) throws IOException { 181 | // 8 bytes, little endian 182 | byte[] bytes = new byte[8]; 183 | reader.readFully(bytes); 184 | ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 185 | return bb.getLong(); 186 | } 187 | 188 | private void writeLong(DataOutputStream writer, long n) throws IOException { 189 | byte[] b = new byte[8]; 190 | b[0] = (byte) (n & 0xff); 191 | b[1] = (byte) (n >> 8 & 0xff); 192 | b[2] = (byte) (n >> 16 & 0xff); 193 | b[3] = (byte) (n >> 24 & 0xff); 194 | b[4] = (byte) (n >> 32 & 0xff); 195 | b[5] = (byte) (n >> 40 & 0xff); 196 | b[6] = (byte) (n >> 48 & 0xff); 197 | b[7] = (byte) (n >> 56 & 0xff); 198 | writer.write(b); 199 | } 200 | 201 | //该方法把字节数组解码并读取为int,我需要一个传入int并写入自洁 数组的方法 202 | private int readULEB128(DataInputStream reader) throws IOException { 203 | // variable bytes, little endian 204 | // MSB says if there will be more bytes. If cleared, 205 | // that byte is the last. 206 | int value = 0; 207 | for (int shift = 0; shift < 32; shift += 7) { 208 | byte b = reader.readByte(); 209 | value |= ((int) b & 0x7F) << shift; 210 | //value = value|((int) b & 0x7F)<= 0) return value; // MSB is zero. End of value. 212 | } 213 | throw new IOException("ULEB128 too large"); 214 | } 215 | 216 | private void writeULEB128(DataOutputStream writer, int value) throws IOException { 217 | if (value < 0) { 218 | throw new IOException("ULEB128 must >0"); 219 | } 220 | ArrayList bytes = new ArrayList<>(); 221 | do { 222 | byte b = (byte) (value & 0x7f); 223 | value >>= 7; 224 | if (value != 0) { 225 | b |= 0x80; 226 | } 227 | bytes.add(b); 228 | } while (value != 0); 229 | 230 | byte[] ret = new byte[bytes.size()]; 231 | for (int i = 0; i < bytes.size(); i++) { 232 | ret[i] = bytes.get(i); 233 | } 234 | writer.write(ret); 235 | } 236 | 237 | 238 | private float readSingle(DataInputStream reader) throws IOException { 239 | // 4 bytes, little endian 240 | byte[] bytes = new byte[4]; 241 | reader.readFully(bytes); 242 | ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 243 | return bb.getFloat(); 244 | } 245 | 246 | private void writeSingle(DataOutputStream writer, float n) throws IOException { 247 | byte[] b = new byte[4]; 248 | int l = Float.floatToIntBits(n); 249 | for (int i = 0; i < 4; i++) { 250 | b[i] = new Integer(l).byteValue(); 251 | l = l >> 8; 252 | } 253 | writer.write(b); 254 | } 255 | 256 | private double readDouble(DataInputStream reader) throws IOException { 257 | // 8 bytes little endian 258 | byte[] bytes = new byte[8]; 259 | reader.readFully(bytes); 260 | ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 261 | return bb.getDouble(); 262 | } 263 | 264 | private void writeDouble(DataOutputStream writer, double n) throws IOException { 265 | byte[] b = new byte[8]; 266 | long l = Double.doubleToLongBits(n); 267 | for (int i = 0; i < 4; i++) { 268 | b[i] = new Long(l).byteValue(); 269 | l = l >> 8; 270 | } 271 | writer.write(b); 272 | } 273 | 274 | 275 | private boolean readBoolean(DataInputStream reader) throws IOException { 276 | // 1 byte, zero = false, non-zero = true 277 | return reader.readBoolean(); 278 | } 279 | 280 | private void writeBoolean(DataOutputStream writer, boolean n) throws IOException { 281 | byte[] b = new byte[1]; 282 | if (n) { 283 | b[0] = 1; 284 | } else { 285 | b[0] = 0; 286 | } 287 | writer.write(b); 288 | 289 | } 290 | 291 | 292 | private String readString(DataInputStream reader) throws IOException { 293 | // variable length 294 | // 00 = empty string 295 | // 0B * = normal string 296 | // is encoded as an LEB, and is the byte length of the rest. 297 | // * is encoded as UTF8, and is the string content. 298 | byte kind = reader.readByte(); 299 | if (kind == 0) { 300 | return ""; 301 | } 302 | if (kind != 11) { 303 | throw new IOException(String.format("String format error: Expected 0x0B or 0x00, found 0x%02X", (int) kind & 0xFF)); 304 | } 305 | int length = readULEB128(reader); 306 | if (length == 0) { 307 | return ""; 308 | } 309 | byte[] utf8bytes = new byte[length]; 310 | reader.readFully(utf8bytes); 311 | return new String(utf8bytes, "UTF-8"); 312 | } 313 | 314 | private void writeString(DataOutputStream writer, String n) throws IOException { 315 | if ("".equals(n)) { 316 | writer.writeByte(0); 317 | } else { 318 | writer.writeByte(11); 319 | writeULEB128(writer, n.length()); 320 | writer.write(n.getBytes("UTF-8")); 321 | } 322 | } 323 | 324 | 325 | private Date readDate(DataInputStream reader) throws IOException { 326 | long ticks = readLong(reader); 327 | long TICKS_AT_EPOCH = 621355968000000000L; 328 | long TICKS_PER_MILLISECOND = 10000; 329 | 330 | return new Date((ticks - TICKS_AT_EPOCH) / TICKS_PER_MILLISECOND); 331 | } 332 | 333 | private void writeDate(DataOutputStream writer, Date date) throws IOException { 334 | long ticks = date.getTime() * 10000L + 621355968000000000L; 335 | writeLong(writer, ticks); 336 | } 337 | 338 | } 339 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/util/osu/ReplayUtil.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.util.osu; 2 | 3 | import lombok.SneakyThrows; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | import org.springframework.data.util.Pair; 7 | import top.mothership.cabbage.util.qq.ImgUtil; 8 | 9 | import java.io.ByteArrayInputStream; 10 | import java.io.DataInputStream; 11 | import java.io.IOException; 12 | import java.nio.ByteBuffer; 13 | import java.nio.ByteOrder; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.List; 16 | 17 | public class ReplayUtil { 18 | 19 | private static Logger logger = LogManager.getLogger(ReplayUtil.class); 20 | @SneakyThrows 21 | public static String getLifePoint(byte[] data) { 22 | DataInputStream reader = new DataInputStream(new ByteArrayInputStream(data)); 23 | logger.info(readByte(reader)); 24 | logger.info(readInt(reader)); 25 | logger.info(readString(reader)); 26 | logger.info(readString(reader)); 27 | logger.info(readString(reader)); 28 | logger.info(readShort(reader)); 29 | logger.info(readShort(reader)); 30 | logger.info(readShort(reader)); 31 | logger.info(readShort(reader)); 32 | logger.info(readShort(reader)); 33 | logger.info(readShort(reader)); 34 | logger.info(readInt(reader)); 35 | logger.info(readShort(reader)); 36 | logger.info(readByte(reader)); 37 | logger.info(readInt(reader)); 38 | String lifePoint = readString(reader); 39 | logger.info(lifePoint); 40 | return lifePoint; 41 | } 42 | private static byte readByte(DataInputStream reader) throws IOException { 43 | // 1 byte 44 | return reader.readByte(); 45 | } 46 | private static short readShort(DataInputStream reader) throws IOException { 47 | // 2 bytes, little endian 48 | byte[] bytes = new byte[2]; 49 | reader.readFully(bytes); 50 | //读出两个字节,并且用ByteBuffer转换成short 51 | ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 52 | return bb.getShort(); 53 | } 54 | private static int readInt(DataInputStream reader) throws IOException { 55 | // 4 bytes, little endian 56 | byte[] bytes = new byte[4]; 57 | reader.readFully(bytes); 58 | ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 59 | return bb.getInt(); 60 | } 61 | private static String readString(DataInputStream reader) throws IOException { 62 | // variable length 63 | // 00 = empty string 64 | // 0B * = normal string 65 | // is encoded as an LEB, and is the byte length of the rest. 66 | // * is encoded as UTF8, and is the string content. 67 | byte kind = reader.readByte(); 68 | if (kind == 0) { 69 | return ""; 70 | } 71 | if (kind != 11) { 72 | throw new IOException(String.format("String format error: Expected 0x0B or 0x00, found 0x%02X", (int) kind & 0xFF)); 73 | } 74 | int length = readULEB128(reader); 75 | if (length == 0) { 76 | return ""; 77 | } 78 | byte[] utf8bytes = new byte[length]; 79 | reader.readFully(utf8bytes); 80 | return new String(utf8bytes, StandardCharsets.UTF_8); 81 | } 82 | private static long readLong(DataInputStream reader) throws IOException { 83 | // 8 bytes, little endian 84 | byte[] bytes = new byte[8]; 85 | reader.readFully(bytes); 86 | ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 87 | return bb.getLong(); 88 | } 89 | private static int readULEB128(DataInputStream reader) throws IOException { 90 | // variable bytes, little endian 91 | // MSB says if there will be more bytes. If cleared, 92 | // that byte is the last. 93 | int value = 0; 94 | for (int shift = 0; shift < 32; shift += 7) { 95 | byte b = reader.readByte(); 96 | value |= ((int) b & 0x7F) << shift; 97 | //value = value|((int) b & 0x7F)<= 0) return value; // MSB is zero. End of value. 99 | } 100 | throw new IOException("ULEB128 too large"); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/util/osu/StringSimilarityUtil.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.util.osu; 2 | 3 | public class StringSimilarityUtil { 4 | 5 | /** 6 | * 比较两个字符串的相识度 7 | * 核心算法:用一个二维数组记录每个字符串是否相同,如果相同记为0,不相同记为1,每行每列相同个数累加 8 | * 则数组最后一个数为不相同的总数,从而判断这两个字符的相识度 9 | * 10 | * @param str 11 | * @param target 12 | * @return 13 | */ 14 | private static int compare(String str, String target) { 15 | int d[][]; // 矩阵 16 | int n = str.length(); 17 | int m = target.length(); 18 | int i; // 遍历str的 19 | int j; // 遍历target的 20 | char ch1; // str的 21 | char ch2; // target的 22 | int temp; // 记录相同字符,在某个矩阵位置值的增量,不是0就是1 23 | if (n == 0) { 24 | return m; 25 | } 26 | if (m == 0) { 27 | return n; 28 | } 29 | d = new int[n + 1][m + 1]; 30 | // 初始化第一列 31 | for (i = 0; i <= n; i++) { 32 | d[i][0] = i; 33 | } 34 | // 初始化第一行 35 | for (j = 0; j <= m; j++) { 36 | d[0][j] = j; 37 | } 38 | for (i = 1; i <= n; i++) { 39 | // 遍历str 40 | ch1 = str.charAt(i - 1); 41 | // 去匹配target 42 | for (j = 1; j <= m; j++) { 43 | ch2 = target.charAt(j - 1); 44 | if (ch1 == ch2 || ch1 == ch2 + 32 || ch1 + 32 == ch2) { 45 | temp = 0; 46 | } else { 47 | temp = 1; 48 | } 49 | // 左边+1,上边+1, 左上角+temp取最小 50 | d[i][j] = min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + temp); 51 | } 52 | } 53 | return d[n][m]; 54 | } 55 | 56 | 57 | /** 58 | * 获取最小的值 59 | */ 60 | private static int min(int one, int two, int three) { 61 | return (one = one < two ? one : two) < three ? one : three; 62 | } 63 | 64 | /** 65 | * 获取两字符串的相似度 66 | */ 67 | public static float calc(String str, String target) { 68 | int max = Math.max(str.length(), target.length()); 69 | return 1 - (float) compare(str, target) / max; 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/util/osu/UserUtil.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.util.osu; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.reflect.TypeToken; 6 | import org.apache.logging.log4j.LogManager; 7 | import org.apache.logging.log4j.Logger; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.cglib.core.Local; 10 | import org.springframework.stereotype.Component; 11 | import top.mothership.cabbage.constant.Overall; 12 | import top.mothership.cabbage.manager.ApiManager; 13 | import top.mothership.cabbage.mapper.RedisDAO; 14 | import top.mothership.cabbage.mapper.UserDAO; 15 | import top.mothership.cabbage.mapper.UserInfoDAO; 16 | import top.mothership.cabbage.pojo.User; 17 | import top.mothership.cabbage.pojo.osu.Userinfo; 18 | 19 | import java.time.LocalDate; 20 | import java.time.LocalDateTime; 21 | import java.time.LocalTime; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.List; 26 | 27 | @Component 28 | public class UserUtil { 29 | private final UserInfoDAO userInfoDAO; 30 | private final UserDAO userDAO; 31 | private final ApiManager apiManager; 32 | private final RedisDAO redisDAO; 33 | private Logger logger = LogManager.getLogger(this.getClass()); 34 | 35 | @Autowired 36 | public UserUtil(UserInfoDAO userInfoDAO, UserDAO userDAO, ApiManager apiManager, RedisDAO redisDAO) { 37 | this.userInfoDAO = userInfoDAO; 38 | this.userDAO = userDAO; 39 | this.apiManager = apiManager; 40 | this.redisDAO = redisDAO; 41 | } 42 | 43 | 44 | public User registerUser(Integer userId, Integer mode, Long QQ, String role) { 45 | //构造User对象写入数据库,如果指定了mode就使用指定mode 46 | Userinfo userFromAPI = null; 47 | for (int i = 0; i < 4; i++) { 48 | userFromAPI = apiManager.getUser(i, userId); 49 | if (LocalTime.now().isAfter(LocalTime.of(4, 0))) { 50 | userFromAPI.setQueryDate(LocalDate.now()); 51 | } else { 52 | userFromAPI.setQueryDate(LocalDate.now().minusDays(1)); 53 | } 54 | //写入一行userinfo 55 | redisDAO.add(userId, userFromAPI); 56 | userInfoDAO.addUserInfo(userFromAPI); 57 | } 58 | User user = new User(userId, role, QQ, "[]", userFromAPI.getUserName(), false, mode, 0L, 0L, Overall.DEFAULT_ROLE,false, LocalDate.now()); 59 | userDAO.addUser(user); 60 | return user; 61 | } 62 | 63 | 64 | public List sortRoles(String role) { 65 | List roles = Arrays.asList(role.split(",")); 66 | //此处自定义实现排序方法 67 | //dev>分群>主群>比赛 68 | roles.sort((o1, o2) -> { 69 | // mp5s优先级得低于mp5和各个分部,考虑到比赛选手刷超了超过mp4的,也得低于mp4 70 | if (o1.contains("mp5s") && ( 71 | o2.equals("mp5") || o2.equals("mp4") 72 | || o2.equals("mp5mc") || o2.equals("mp5chart"))) { 73 | return -1; 74 | } 75 | if (o2.contains("mp5s") && ( 76 | o1.equals("mp5") || o1.equals("mp4") 77 | || o1.equals("mp5mc") || o1.equals("mp5chart"))) { 78 | return 1; 79 | } 80 | // //比赛期间mp5s优先级比mp5高,只比mc和chart低 81 | // if (o1.contains("mp5s") && (o2.equals("mp5mc") || o2.equals("mp5chart"))) { 82 | // return -1; 83 | // } 84 | //mp4s roles = new ArrayList<>(Arrays.asList(user.getRole().split(","))); 129 | //2017-11-27 21:04:36 增强健壮性,只有在含有这个role的时候才进行移除 130 | if (roles.contains(role)) { 131 | roles.remove(role); 132 | } 133 | if(user.getMainRole().equals(role)){ 134 | user.setMainRole("creep"); 135 | } 136 | if ("All".equals(role) || roles.size() == 0) { 137 | newRole = "creep"; 138 | user.setMainRole("creep"); 139 | } else { 140 | //转换为字符串,此处得去除空格(懒得遍历+拼接了) 141 | newRole = roles.toString().replace(" ", ""). 142 | substring(1, roles.toString().replace(" ", "").indexOf("]")); 143 | } 144 | 145 | user.setRole(newRole); 146 | return user; 147 | } 148 | 149 | public User renameUser(User user, String newName) { 150 | //如果检测到用户改名,取出数据库中的现用名加入到曾用名,并且更新现用名和曾用名 151 | List legacyUname = new GsonBuilder().create().fromJson(user.getLegacyUname(), new TypeToken>() { 152 | }.getType()); 153 | if (user.getCurrentUname() != null) { 154 | legacyUname.add(user.getCurrentUname()); 155 | } 156 | user.setLegacyUname(new Gson().toJson(legacyUname)); 157 | user.setCurrentUname(newName); 158 | logger.info("检测到玩家" + newName + "改名,已登记"); 159 | return user; 160 | } 161 | 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/util/qq/MsgQueue.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.util.qq; 2 | 3 | import top.mothership.cabbage.constant.pattern.CQCodePattern; 4 | import top.mothership.cabbage.constant.pattern.RegularPattern; 5 | import top.mothership.cabbage.pojo.coolq.CqMsg; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.regex.Matcher; 10 | 11 | /** 12 | * 用于存放消息的循环队列 13 | * @author QHS 14 | */ 15 | public class MsgQueue { 16 | 17 | private int start = 0; 18 | private int end = 0; 19 | private int len = 0; 20 | private int N=100; 21 | private CqMsg[] msgs = new CqMsg[N]; 22 | public MsgQueue(){} 23 | public MsgQueue(int N) { 24 | this.N=N; 25 | //2018-2-27 10:50:34构造方法居然没有重做一个数组…… 26 | msgs = new CqMsg[N]; 27 | } 28 | 29 | /** 30 | * 为了避免空指针异常,得new一个 31 | */ 32 | private CqMsg msg = new CqMsg(); 33 | 34 | /** 35 | * 计算最近的消息构成复读的次数 36 | * @return 最近一条消息构成复读的次数 37 | */ 38 | public Integer countRepeat() { 39 | int count = 0; 40 | //根据循环队列情况不同进行遍历 41 | if (start < end) { 42 | for (int i = 0; i < end; i++) { 43 | if (isThisRepeat(i)) { 44 | count++; 45 | } 46 | } 47 | } else { 48 | for (int i = end; i < msgs.length; i++) { 49 | if (isThisRepeat(i)) { 50 | count++; 51 | } 52 | } 53 | for (int i = 0; i < start - 1; i++) { 54 | if (isThisRepeat(i)) { 55 | count++; 56 | } 57 | } 58 | } 59 | return count; 60 | } 61 | /** 62 | * 向队列里增加消息,最近的消息会被写到成员变量里 63 | * @param msg 要增加的消息 64 | */ 65 | public void addMsg(CqMsg msg) { 66 | //循环队列的具体实现 67 | //首先长度增加 68 | len++; 69 | //如果0-100都有了消息,那就把start右移一个 70 | if (len >= N) { 71 | len = N; 72 | start++; 73 | } 74 | //如果end已经达到数组最右端,就移到最左端 75 | if (end == N) { 76 | end = 0; 77 | } 78 | if (start == N) { 79 | start = 0; 80 | } 81 | //把消息存到结束坐标里 82 | msgs[end] = msg; 83 | //将这条消息存储到这个类的成员变量里 84 | this.msg = msg; 85 | //结束坐标+1 86 | end++; 87 | 88 | } 89 | /** 90 | * 把消息列表转为ArrayList……当时为啥要写这个方法来着? 91 | * 改成获取重复消息列表吧。 92 | * @return 消息列表 93 | */ 94 | public ArrayList getRepeatList() { 95 | ArrayList result = new ArrayList<>(); 96 | //根据循环队列情况不同进行遍历 97 | if (start < end) { 98 | for (int i = 0; i < end; i++) { 99 | if (isThisRepeat(i)) { 100 | result.add(msgs[i]); 101 | } 102 | } 103 | } else { 104 | for (int i = end; i < msgs.length; i++) { 105 | if (isThisRepeat(i)) { 106 | result.add(msgs[i]); 107 | } 108 | } 109 | for (int i = 0; i < start - 1; i++) { 110 | if (isThisRepeat(i)) { 111 | result.add(msgs[i]); 112 | } 113 | } 114 | } 115 | 116 | return result; 117 | } 118 | 119 | /** 120 | * 根据QQ从循环队列中提取消息 121 | * 122 | * @param QQ 给的QQ号 123 | * @return 消息列表 124 | */ 125 | public ArrayList getMsgsByQQ(Long QQ) { 126 | ArrayList result = new ArrayList<>(); 127 | if (start < end) { 128 | for (int i = 0; i < end; i++) { 129 | if (QQ.equals(msgs[i].getUserId())) { 130 | result.add(msgs[i]); 131 | } 132 | } 133 | } else { 134 | for (int i = end; i < msgs.length; i++) { 135 | if (QQ.equals(msgs[i].getUserId())) { 136 | result.add(msgs[i]); 137 | } 138 | } 139 | for (int i = 0; i < start - 1; i++) { 140 | if (QQ.equals(msgs[i].getUserId())) { 141 | result.add(msgs[i]); 142 | } 143 | } 144 | //目前会引发一个问题,当容器刚启动,这个群还没有消息的时候,调用这个会出NPE 145 | //但是我并不打算去用if,毕竟几乎不可能出现这个问题…… 146 | 147 | } 148 | return result; 149 | 150 | } 151 | /** 152 | * 判断最近的一条消息是否重复 153 | * 154 | * @param i 循环队列里的坐标 155 | * @return 该消息是否算作复读 156 | */ 157 | private boolean isThisRepeat(int i) { 158 | Matcher cmdMatcher = RegularPattern.REG_CMD_REGEX.matcher(msg.getMessage()); 159 | if (cmdMatcher.find()) { 160 | //如果是命令,直接false 161 | return false; 162 | } 163 | Matcher singleImgMatcher = CQCodePattern.SINGLE_IMG.matcher(msg.getMessage()); 164 | if (singleImgMatcher.find()) { 165 | //如果是纯图片,直接false 166 | return false; 167 | } 168 | String msgFromArray = CQCodePattern.SINGLE_IMG.matcher( 169 | RegularPattern.REPEAT_FILTER_REGEX.matcher(msgs[i].getMessage()).replaceAll("")) 170 | .replaceAll(""); 171 | String msgFromGroup = CQCodePattern.SINGLE_IMG.matcher( 172 | RegularPattern.REPEAT_FILTER_REGEX.matcher(msg.getMessage()).replaceAll("")) 173 | .replaceAll(""); 174 | 175 | if ("".equals(msgFromArray)) { 176 | msgFromArray = msgs[i].getMessage(); 177 | } 178 | if("".equals(msgFromGroup)){ 179 | msgFromGroup = msg.getMessage(); 180 | } 181 | //目前的问题是:图片+符号会被判定复读, 182 | //流程是先去掉干扰,如果去干扰后是空串就恢复,然后判断去干扰后的消息是否相等+原消息是否是纯图片+原消息长度是否大于等于3 183 | //应改为先去掉图片,是空串则 184 | return msgFromArray.equals(msgFromGroup) && msg.getMessage().length() >= 3; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/util/qq/SmokeUtil.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.util.qq; 2 | 3 | import com.google.gson.Gson; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | import top.mothership.cabbage.manager.OneBotManager; 9 | import top.mothership.cabbage.mapper.ResDAO; 10 | import top.mothership.cabbage.mapper.UserDAO; 11 | import top.mothership.cabbage.pojo.User; 12 | import top.mothership.cabbage.pojo.coolq.CqMsg; 13 | 14 | import java.util.Arrays; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | @Component 20 | public class SmokeUtil { 21 | 22 | public final static Map MSG_QUEUE_MAP = new HashMap<>(); 23 | private final static List REPEAT_SMOKE_GROUP = Arrays.asList(( 24 | "201872650," +//MP5 25 | "112177148," +//MP4后花园 26 | "693299572," +//测试群 27 | "136312506," +//MP5赛群136312506 28 | "521774765"//ODNL S3 29 | ).split(",")); 30 | private final static List REPEAT_RECORD_GROUP = Arrays.asList("576214175,532783765".split(",")); 31 | private final OneBotManager oneBotManager; 32 | private final UserDAO userDAO; 33 | private final ResDAO resDAO; 34 | private Logger logger = LogManager.getLogger(this.getClass()); 35 | 36 | 37 | @Autowired 38 | public SmokeUtil(OneBotManager oneBotManager, UserDAO userDAO, ResDAO resDAO) { 39 | this.oneBotManager = oneBotManager; 40 | this.userDAO = userDAO; 41 | this.resDAO = resDAO; 42 | } 43 | 44 | 45 | /** 46 | * Parse smoke. 47 | * 48 | * @param cqMsg the cq msg 49 | */ 50 | public void parseSmoke(CqMsg cqMsg) { 51 | 52 | MsgQueue msgQueue = MSG_QUEUE_MAP.get(cqMsg.getGroupId()); 53 | //如果获取队列失败,而且是开启禁言的群号,直接在这里初始化队列,懒加载避免获取群列表 54 | if (msgQueue == null) { 55 | if (!REPEAT_RECORD_GROUP.contains(String.valueOf(cqMsg.getGroupId())) 56 | && !REPEAT_SMOKE_GROUP.contains(String.valueOf(cqMsg.getGroupId()))) { 57 | // MSG_QUEUE_MAP.put(respData.getGroupId(), new MsgQueue()); 58 | } else { 59 | MSG_QUEUE_MAP.put(cqMsg.getGroupId(), new MsgQueue()); 60 | if ("136312506".equals(String.valueOf(cqMsg.getGroupId()))){ 61 | MSG_QUEUE_MAP.put(cqMsg.getGroupId(), new MsgQueue(300)); 62 | } 63 | } 64 | } 65 | 66 | //进行添加 67 | //判断非空……提高健壮性 68 | if (msgQueue != null) { 69 | msgQueue.addMsg(cqMsg); 70 | //如果是开启禁言的群,并且该条触发了禁言 71 | int countRepeat = msgQueue.countRepeat(); 72 | if ((REPEAT_SMOKE_GROUP.contains(String.valueOf(cqMsg.getGroupId())) && countRepeat >= 6)) { 73 | logger.info("触发复读禁言,正在记录案发现场:" + new Gson().toJson(msgQueue.getRepeatList())); 74 | // 由于onebot实现改造,尽量减少API对接行为,去掉获取群管 75 | // if (GROUP_ADMIN_LIST.get(cqMsg.getGroupId()).contains(cqMsg.getUserId())) { 76 | // logger.info("检测到群管" + cqMsg.getUserId() + "的复读行为"); 77 | // cqMsg.setMessage("[CQ:at,qq=" + cqManager.getOwner(cqMsg.getGroupId()) + "] 检测到群管" + "[CQ:at,qq=" + cqMsg.getUserId() + "] 复读。"); 78 | // } else { 79 | 80 | int time = (countRepeat - 5) * 600; 81 | time = Math.min(time, 8 * 3600); 82 | 83 | if ("136312506".equals(String.valueOf(cqMsg.getGroupId())) 84 | || "693299572".equals(String.valueOf(cqMsg.getGroupId()))) { 85 | // 1 2 4 8 16 24 +24 h 86 | switch ((countRepeat - 5)) { 87 | case 1: 88 | case 2: 89 | case 3: 90 | case 4: 91 | case 5: 92 | time = (int) (Math.pow(2, (countRepeat - 5 - 1)) * 3600); 93 | break; 94 | case 6: 95 | time = 24 * 3600; 96 | break; 97 | default: 98 | time = (countRepeat - 5 - 5) * 24 * 3600; 99 | break; 100 | } 101 | time = Math.min(time, 30 * 24 * 3600); 102 | } 103 | logger.info("检测到最近100条消息中{}发送第{}条复读,正在尝试禁言 {}秒", cqMsg.getUserId(), countRepeat, time); 104 | 105 | cqMsg.setDuration(time); 106 | cqMsg.setMessageType("smoke"); 107 | // } 108 | oneBotManager.sendMsg(cqMsg); 109 | 110 | } 111 | if (REPEAT_RECORD_GROUP.contains(String.valueOf(cqMsg.getGroupId()))) { 112 | User user = userDAO.getUser(cqMsg.getUserId(), null); 113 | if (user != null) { 114 | if (msgQueue.countRepeat() >= 2) { 115 | Long count = user.getRepeatCount(); 116 | user.setRepeatCount(++count); 117 | } 118 | Long count = user.getSpeakingCount(); 119 | user.setSpeakingCount(++count); 120 | userDAO.updateUser(user); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/util/web/CaptchaUtil.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.util.web; 2 | 3 | 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.awt.*; 7 | import java.awt.image.BufferedImage; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Random; 11 | 12 | @Component 13 | public class CaptchaUtil { 14 | // 图片的宽度 15 | private static final int CAPTCHA_WIDTH = 90; 16 | // 图片的高度 17 | private static final int CAPTCHA_HEIGHT = 40; 18 | // 验证码的个数 19 | private static final int CAPTCHA_CODECOUNT = 4; 20 | 21 | private static final int CAPTCHA_CODE_X = 15; 22 | private static final int CAPTCHA_FONT_HEIGHT = 26 ; 23 | private static final int CAPTCHA_CODE_Y = 28; 24 | private static final char[] codeSequence = { 25 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 26 | 'H', 'I', 'J', 'K', 'L', 'M', 'N', 27 | 'O', 'P', 'Q', 'R', 'S', 'T', 28 | 'U', 'V', 'W', 'X', 'Y', 'Z', 29 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; 30 | 31 | public Map genCaptcha() { 32 | 33 | Map captcha = new HashMap(); 34 | // 定义图像 Buffer 35 | BufferedImage buffImg = new BufferedImage(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, BufferedImage.TYPE_INT_RGB); 36 | // 创建一个绘制图像的对象 37 | Graphics2D g = buffImg.createGraphics(); 38 | // 创建一个随机数生成器类 39 | Random random = new Random(); 40 | // 将图像填充为白色 41 | g.setColor(Color.WHITE); 42 | g.fillRect(0, 0, CAPTCHA_WIDTH, CAPTCHA_HEIGHT); 43 | // 设置字体 44 | g.setFont(new Font("Fixedsys", Font.BOLD, CAPTCHA_FONT_HEIGHT)); 45 | // 设置字体边缘光滑 46 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 47 | // 画边框 48 | g.setColor(Color.BLACK); 49 | g.drawRect(0, 0, CAPTCHA_WIDTH - 1, CAPTCHA_HEIGHT - 1); 50 | // 随机产生干扰线,使图象中的认证码不易被其它程序探测到。 51 | g.setColor(Color.BLACK); 52 | for (int i = 0; i < 20; i++) { 53 | int x = random.nextInt(CAPTCHA_WIDTH); 54 | int y = random.nextInt(CAPTCHA_HEIGHT); 55 | int xl = random.nextInt(12); 56 | int yl = random.nextInt(12); 57 | g.drawLine(x, y, x + xl, y + yl); 58 | } 59 | 60 | // 保存随机产生的验证码,以便用户登录后进行验证 61 | StringBuilder randomCode = new StringBuilder(); 62 | int red = 0, green = 0, blue = 0; 63 | // 随机产生验证码 64 | for (int i = 0; i < CAPTCHA_CODECOUNT; i++) { 65 | // 得到随机产生的验证码数字 66 | String code = String.valueOf(codeSequence[random.nextInt(36)]); 67 | // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同 68 | //RGB小于180,避免字母颜色过亮 69 | red = random.nextInt(200); 70 | green = random.nextInt(200); 71 | blue = random.nextInt(200); 72 | // 用随机产生的颜色将验证码绘制到图像中 73 | g.setColor(new Color(red, green, blue)); 74 | g.drawString(code, (i + 1) * CAPTCHA_CODE_X, CAPTCHA_CODE_Y); 75 | // 将产生的随机数组合在一起 76 | randomCode.append(code); 77 | } 78 | g.dispose(); 79 | //解耦,将生成的验证码和图片存入Map供Service使用 80 | //这个轮子把验证码字符串直接和生成图片的逻辑放一起了……懒得取出来复用在mail里了…… 81 | captcha.put("code", randomCode.toString()); 82 | captcha.put("img", buffImg); 83 | return captcha; 84 | 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/websocket/OneBotMessageHandler.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.websocket; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import lombok.SneakyThrows; 6 | import org.apache.logging.log4j.LogManager; 7 | import org.apache.logging.log4j.Logger; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.util.StringUtils; 11 | import org.springframework.web.socket.CloseStatus; 12 | import org.springframework.web.socket.TextMessage; 13 | import org.springframework.web.socket.WebSocketMessage; 14 | import org.springframework.web.socket.WebSocketSession; 15 | import org.springframework.web.socket.handler.TextWebSocketHandler; 16 | import top.mothership.cabbage.controller.CqController; 17 | import top.mothership.cabbage.pojo.coolq.CqMsg; 18 | import top.mothership.cabbage.pojo.coolq.CqResponse; 19 | import top.mothership.cabbage.pojo.coolq.OneBotApiRequest; 20 | 21 | import java.util.Map; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Executors; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | @Component 28 | public class OneBotMessageHandler extends TextWebSocketHandler { 29 | //用来保存连接进来session 30 | private static Map map = new ConcurrentHashMap<>(); 31 | private static Map cqResponseMap = new ConcurrentHashMap<>(); 32 | 33 | @Autowired 34 | private CqController cqController; 35 | private ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100); 36 | private Logger log = LogManager.getLogger(this.getClass()); 37 | 38 | 39 | @SneakyThrows 40 | public static String callApi(OneBotApiRequest request) { 41 | WebSocketSession session = map.get(String.valueOf(request.getParams().getSelfId())); 42 | if (session != null) { 43 | session.sendMessage(new TextMessage(new Gson().toJson(request))); 44 | } 45 | // 自旋等待返回值map里出现要的返回值,直到次数上限 46 | int retry = 0; 47 | while (true) { 48 | String response = cqResponseMap.get(request.getEcho()); 49 | if (response != null) { 50 | return response; 51 | } 52 | TimeUnit.SECONDS.sleep(1); 53 | 54 | retry++; 55 | if (retry > 10) { 56 | return null; 57 | } 58 | } 59 | } 60 | 61 | @SneakyThrows 62 | public static void sendMessage(CqMsg cqMsg) { 63 | String action = null; 64 | switch (cqMsg.getMessageType()) { 65 | case "group": 66 | action = "send_group_msg"; 67 | break; 68 | case "discuss": 69 | action = "send_discuss_msg"; 70 | break; 71 | case "private": 72 | action = "send_private_msg"; 73 | break; 74 | case "smoke": 75 | action = "set_group_ban"; 76 | break; 77 | case "smokeAll": 78 | action = "set_group_whole_ban"; 79 | break; 80 | case "handleInvite": 81 | action = "set_group_add_request"; 82 | break; 83 | case "kick": 84 | action = "set_group_kick"; 85 | break; 86 | default: 87 | return; 88 | } 89 | 90 | WebSocketSession session = map.get(String.valueOf(cqMsg.getSelfId())); 91 | if (session != null) { 92 | OneBotApiRequest request = new OneBotApiRequest(); 93 | request.setParams(cqMsg); 94 | request.setAction(action); 95 | session.sendMessage(new TextMessage(new Gson().toJson(request))); 96 | } 97 | } 98 | 99 | /** 100 | * 关闭连接进入这个方法处理,将session从 list中删除 101 | */ 102 | @Override 103 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { 104 | map.remove(getClientQQ(session)); 105 | log.info("{} 连接已经关闭,现从list中删除 ,状态信息{}", session, status); 106 | } 107 | 108 | /** 109 | * 三次握手成功,进入这个方法处理,将session 加入list 中 110 | */ 111 | @Override 112 | public void afterConnectionEstablished(WebSocketSession session) throws Exception { 113 | map.put(getClientQQ(session), session); 114 | log.info("用户{}连接成功.... ", session); 115 | } 116 | 117 | /** 118 | * 处理客户发送的信息 119 | */ 120 | @SneakyThrows 121 | @Override 122 | public void handleMessage(WebSocketSession session, WebSocketMessage message) { 123 | if (message.getPayload().toString().contains("echo") 124 | && !message.getPayload().toString().contains("post_type")) { 125 | // 这是onebot api调用的回复 126 | 127 | CqResponse response = new Gson().fromJson(message.getPayload().toString(), CqResponse.class); 128 | if (response.getEcho() != null) 129 | cqResponseMap.put(response.getEcho(), message.getPayload().toString()); 130 | 131 | } else { 132 | JsonObject object = new Gson().fromJson(message.getPayload().toString(), JsonObject.class); 133 | //去掉新的onebot实现按数组方式上报的message,保留rawmessage,避免序列化出现异常 134 | object.remove("message"); 135 | CqMsg cqMsg = new Gson().fromJson(object, CqMsg.class); 136 | 137 | if (cqMsg.getRawMessage() != null) { 138 | // 将 Unicode 编码字符串转换为字节数组 139 | byte[] unicodeBytes = cqMsg.getRawMessage().getBytes("Unicode"); 140 | // 将字节数组解码为字符串 141 | String decodedString = new String(unicodeBytes, "Unicode"); 142 | cqMsg.setMessage(decodedString); 143 | } 144 | fixedThreadPool.submit(() -> cqController.doHandle(cqMsg)); 145 | } 146 | 147 | } 148 | 149 | private String getClientQQ(WebSocketSession session) { 150 | // 按one bot文档,取第一个X-Self-ID请求头 151 | return session.getHandshakeHeaders().get("X-Self-ID").get(0); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/top/mothership/cabbage/websocket/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package top.mothership.cabbage.websocket; 2 | 3 | import org.checkerframework.checker.units.qual.A; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.messaging.MessageHandler; 7 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 8 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 9 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 10 | import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; 11 | import top.mothership.cabbage.controller.CqController; 12 | 13 | import java.util.logging.SocketHandler; 14 | 15 | @Configuration 16 | @EnableWebSocket 17 | public class WebSocketConfig implements WebSocketConfigurer { 18 | 19 | @Autowired 20 | OneBotMessageHandler handler; 21 | 22 | @Override 23 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 24 | registry.addHandler(handler, "/onebot") 25 | .addInterceptors(new HttpSessionHandshakeInterceptor()) 26 | .setAllowedOrigins("*"); //允许跨域访问 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/res/font/Aller_Lt_MODFIED.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/Aller_Lt_MODFIED.ttf -------------------------------------------------------------------------------- /src/main/res/font/Aller_Rg_MODFIED.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/Aller_Rg_MODFIED.ttf -------------------------------------------------------------------------------- /src/main/res/font/Futura Std Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/Futura Std Medium.otf -------------------------------------------------------------------------------- /src/main/res/font/Gayatri.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/Gayatri.ttf -------------------------------------------------------------------------------- /src/main/res/font/PingFang Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/PingFang Regular.ttf -------------------------------------------------------------------------------- /src/main/res/font/Ubuntu-M.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/Ubuntu-M.ttf -------------------------------------------------------------------------------- /src/main/res/font/Ubuntu-R.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/Ubuntu-R.ttf -------------------------------------------------------------------------------- /src/main/res/font/msyh.ttc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/msyh.ttc -------------------------------------------------------------------------------- /src/main/res/font/tahoma.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/tahoma.ttf -------------------------------------------------------------------------------- /src/main/res/font/tahomabd.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mother-Ship/cabbageWeb/60c68279b552999acda934f26e5944f3ec97019d/src/main/res/font/tahomabd.ttf -------------------------------------------------------------------------------- /src/main/res/osu.sql: -------------------------------------------------------------------------------- 1 | # Host: localhost (Version 5.7.20-0ubuntu0.16.04.1) 2 | # Date: 2017-12-08 16:52:50 3 | # Generator: MySQL-Front 6.0 (Build 2.20) 4 | 5 | 6 | # 7 | # Structure for table "bgfile" 8 | # 9 | 10 | CREATE TABLE `bgfile` ( 11 | `Id` int(11) NOT NULL AUTO_INCREMENT, 12 | `sid` int(11) unsigned NOT NULL DEFAULT '0', 13 | `name` varchar(255) NOT NULL DEFAULT '', 14 | `data` longblob NOT NULL, 15 | PRIMARY KEY (`Id`) 16 | ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; 17 | 18 | # 19 | # Structure for table "osufile" 20 | # 21 | 22 | CREATE TABLE `osufile` ( 23 | `Id` int(11) NOT NULL AUTO_INCREMENT, 24 | `bid` int(11) unsigned NOT NULL DEFAULT '0', 25 | `data` longtext NOT NULL, 26 | PRIMARY KEY (`Id`) 27 | ) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8; 28 | 29 | # 30 | # Structure for table "resource" 31 | # 32 | 33 | CREATE TABLE `resource` ( 34 | `Id` int(11) NOT NULL AUTO_INCREMENT, 35 | `name` varchar(255) NOT NULL DEFAULT '', 36 | `data` longblob NOT NULL, 37 | PRIMARY KEY (`Id`), 38 | UNIQUE KEY `uk_name` (`name`) 39 | ) ENGINE=InnoDB AUTO_INCREMENT=217 DEFAULT CHARSET=utf8; 40 | 41 | # 42 | # Structure for table "score" 43 | # 44 | 45 | CREATE TABLE `score` ( 46 | `Id` int(11) NOT NULL AUTO_INCREMENT, 47 | `beatmap_id` int(11) DEFAULT NULL, 48 | `mode` bit(1) DEFAULT NULL, 49 | `score_version` int(11) DEFAULT NULL, 50 | `map_md5` varchar(32) DEFAULT NULL, 51 | `rep_md5` varchar(32) DEFAULT NULL, 52 | `size` int(11) DEFAULT '-1', 53 | `score` bigint(20) DEFAULT NULL, 54 | `max_combo` int(11) DEFAULT NULL, 55 | `count50` int(11) DEFAULT NULL, 56 | `count100` int(11) DEFAULT NULL, 57 | `count300` int(11) DEFAULT NULL, 58 | `count_miss` int(11) DEFAULT NULL, 59 | `count_katu` int(11) DEFAULT NULL, 60 | `count_geki` int(11) DEFAULT NULL, 61 | `perfect` int(1) DEFAULT NULL, 62 | `enabled_mods` int(11) DEFAULT NULL, 63 | `date` datetime DEFAULT NULL, 64 | `rank` varchar(2) DEFAULT NULL, 65 | `pp` decimal(10,2) DEFAULT NULL, 66 | `user_id` int(11) DEFAULT NULL, 67 | `username` varchar(255) DEFAULT NULL, 68 | `online_id` bigint(20) DEFAULT NULL, 69 | PRIMARY KEY (`Id`) 70 | ) ENGINE=InnoDB AUTO_INCREMENT=1279 DEFAULT CHARSET=utf8; 71 | 72 | # 73 | # Structure for table "userinfo" 74 | # 75 | 76 | CREATE TABLE `userinfo` ( 77 | `Id` int(11) NOT NULL AUTO_INCREMENT, 78 | `user_id` int(11) DEFAULT NULL, 79 | `count300` int(11) DEFAULT NULL, 80 | `count100` int(11) DEFAULT NULL, 81 | `count50` int(11) DEFAULT NULL, 82 | `playcount` int(11) DEFAULT NULL, 83 | `accuracy` decimal(12,2) DEFAULT NULL, 84 | `pp_raw` decimal(8,3) DEFAULT NULL, 85 | `ranked_score` bigint(10) DEFAULT NULL, 86 | `total_score` bigint(10) DEFAULT NULL, 87 | `level` decimal(10,2) DEFAULT NULL, 88 | `pp_rank` int(11) DEFAULT NULL, 89 | `count_rank_ss` int(11) DEFAULT NULL, 90 | `count_rank_s` int(11) DEFAULT NULL, 91 | `count_rank_a` int(11) DEFAULT NULL, 92 | `queryDate` date DEFAULT NULL, 93 | PRIMARY KEY (`Id`), 94 | KEY `idx_user_id` (`user_id`) 95 | ) ENGINE=InnoDB AUTO_INCREMENT=80966 DEFAULT CHARSET=utf8; 96 | 97 | # 98 | # Structure for table "userrole" 99 | # 100 | 101 | CREATE TABLE `userrole` ( 102 | `Id` int(11) NOT NULL AUTO_INCREMENT, 103 | `user_id` int(11) DEFAULT NULL, 104 | `role` varchar(255) NOT NULL DEFAULT 'creep', 105 | `qq` bigint(13) DEFAULT '0', 106 | `legacy_uname` varchar(255) DEFAULT NULL, 107 | `current_uname` varchar(255) DEFAULT NULL, 108 | `is_banned` tinyint(1) unsigned DEFAULT '0', 109 | `repeat_count` bigint(10) unsigned DEFAULT '0', 110 | `speaking_count` bigint(10) unsigned DEFAULT '0', 111 | PRIMARY KEY (`Id`), 112 | UNIQUE KEY `唯一索引` (`user_id`) 113 | ) ENGINE=InnoDB AUTO_INCREMENT=1744 DEFAULT CHARSET=utf8; 114 | -------------------------------------------------------------------------------- /src/main/resources/freemarker/mail.ftl: -------------------------------------------------------------------------------- 1 |

是这样的。

2 |

数据录入完成。以下是API没有获取到的uid列表,请手动查验/处理。

3 |

${nullList}

4 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /logs/osubot 7 | 8 | {LOG_HOME}/backup 9 | stat 10 | global 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-db.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | springmvc 9 | 10 | org.springframework.web.servlet.DispatcherServlet 11 | 12 | 13 | 14 | contextConfigLocation 15 | classpath:spring/spring-*.xml 16 | 17 | 1 18 | 19 | 20 | springmvc 21 | / 22 | 23 | 24 | 25 | 26 | CharacterEncodingFilter 27 | org.springframework.web.filter.CharacterEncodingFilter 28 | 29 | encoding 30 | utf-8 31 | 32 | 33 | forceEncoding 34 | true 35 | 36 | 37 | 38 | CharacterEncodingFilter 39 | /* 40 | 41 | 42 | 43 | DruidStatView 44 | com.alibaba.druid.support.http.StatViewServlet 45 | 46 | 47 | DruidStatView 48 | /druid/* 49 | 50 | 51 | druidWebStatFilter 52 | com.alibaba.druid.support.http.WebStatFilter 53 | 54 | exclusions 55 | /public/*,*.js,*.css,/druid*,*.jsp,*.swf 56 | 57 | 58 | principalSessionName 59 | sessionInfo 60 | 61 | 62 | profileEnable 63 | true 64 | 65 | 66 | 67 | druidWebStatFilter 68 | /* 69 | 70 | -------------------------------------------------------------------------------- /src/test/SimpleTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.Test; 2 | import top.mothership.cabbage.manager.WebPageManager; 3 | import top.mothership.cabbage.pojo.osu.Beatmap; 4 | 5 | import java.io.IOException; 6 | 7 | public class SimpleTest { 8 | @Test 9 | public void Test() throws IOException { 10 | Beatmap beatmap = new Beatmap(); 11 | 12 | new WebPageManager().getBGBackup(beatmap); 13 | 14 | // String test = "osu file format v12\n" + 15 | // "\n" + 16 | // "[General]\n" + 17 | // "AudioFilename: 03 IMAGE -MATERIAL-2.mp3\n" + 18 | // "AudioLeadIn: 0\n" + 19 | // "PreviewTime: 310262\n" + 20 | // "Countdown: 0\n" + 21 | // "SampleSet: Soft\n" + 22 | // "StackLeniency: 0.7\n" + 23 | // "Mode: 0\n" + 24 | // "LetterboxInBreaks: 0\n" + 25 | // "WidescreenStoryboard: 0\n" + 26 | // "\n" + 27 | // "[Editor]\n" + 28 | // "DistanceSpacing: 1.3\n" + 29 | // "BeatDivisor: 4\n" + 30 | // "GridSize: 32\n" + 31 | // "\n" + 32 | // "[Metadata]\n" + 33 | // "Title:IMAGE -MATERIAL- \n" + 34 | // "TitleUnicode:IMAGE -MATERIAL- \n" + 35 | // "Artist:Tatsh\n" + 36 | // "ArtistUnicode:Tatsh\n" + 37 | // "Creator:Scorpiour\n" + 38 | // "Version:Scorpiour\n" + 39 | // "Source:\n" + 40 | // "Tags:Firce777 Reflec Beat\n" + 41 | // "BeatmapID:252238\n" + 42 | // "BeatmapSetID:93523\n" + 43 | // "\n" + 44 | // "[Difficulty]\n" + 45 | // "HPDrainRate:6\n" + 46 | // "CircleSize:4\n" + 47 | // "OverallDifficulty:8\n" + 48 | // "ApproachRate:10\n" + 49 | // "SliderMultiplier:1.8\n" + 50 | // "SliderTickRate:1\n" + 51 | // "\n" + 52 | // "[Events]\n" + 53 | // "//Background and Video events\n" + 54 | // "0,0,\"bg.jpg\",0,0\n" + 55 | // "//Break Periods\n" + 56 | // "2,49251,55985\n" + 57 | // "//Storyboard Layer 0 (Background)\n" + 58 | // "//Storyboard Layer 1 (Fail)\n" + 59 | // "//Storyboard Layer 2 (Pass)\n" + 60 | // "//Storyboard Layer 3 (Foreground)\n" + 61 | // "Sprite,Foreground,Centre,\"bg.jpg\",320,240\n" + 62 | // " C,0,0,6590,0,0,0\n" + 63 | // " S,0,0,384127,0.8\n" + 64 | // " C,0,6590,21359,0,0,0,128,128,128\n" + 65 | // " C,0,21359,55512,128,128,128,255,255,255\n" + 66 | // " C,0,55512,56666,255,255,255,128,128,128\n" + 67 | // " C,0,56666,114589,128,128,128\n" + 68 | // " C,0,114589,115512,128,128,128,64,64,64\n" + 69 | // " C,0,115512,117589,64,64,64,128,128,128\n" + 70 | // " C,0,117589,227204,128,128,128\n" + 71 | // " C,0,227204,227665,128,128,128,192,192,192\n" + 72 | // " C,0,227665,252702,192,192,192\n" + 73 | // " C,0,252702,258942,192,192,192,128,128,128\n" + 74 | // " C,0,258942,261073,128,128,128,0,0,0\n" + 75 | // " C,0,261073,263204,0,0,0\n" + 76 | // " C,0,263204,384127,128,128,128\n" + 77 | // " C,0,384127,388742,0,0,0\n" + 78 | // " C,0,388742,389665,0,0,0,255,255,255\n" + 79 | // "//Storyboard Sound Samples\n" + 80 | // "//Background Colour Transformations\n" + 81 | // "3,100,0,0,0\n" + 82 | // "\n" + 83 | // "[TimingPoints]\n" + 84 | // "6590,461.538461538462,4,2,1,6,1,0\n" + 85 | // "8436,-100,4,2,1,30,0,0\n" + 86 | // "8551,-100,4,2,1,6,0,0\n" + 87 | // "10282,-100,4,1,1,30,0,0\n" + 88 | // "10397,-100,4,2,1,10,0,0\n" + 89 | // "10744,-100,4,2,1,15,0,0\n" + 90 | // "11206,-100,4,2,1,20,0,0\n" + 91 | // "11667,-100,4,2,1,25,0,0\n" + 92 | // "12129,-100,4,2,1,30,0,0\n" + 93 | // "13513,-100,4,2,1,6,0,0\n" + 94 | // "13974,-100,4,2,1,30,0,0\n" + 95 | // "14090,-100,4,2,1,6,0,0\n" + 96 | // "15820,-100,4,2,1,30,0,0\n" + 97 | // "15936,-100,4,2,1,6,0,0\n" + 98 | // "17666,-100,4,2,1,30,0,0\n" + 99 | // "17782,-100,4,2,1,10,0,0\n" + 100 | // "18128,-100,4,2,1,15,0,0\n" + 101 | // "18590,-100,4,2,1,20,0,0\n" + 102 | // "19051,-100,4,2,1,25,0,0\n" + 103 | // "19513,-100,4,2,1,30,0,0\n" + 104 | // "20782,-100,4,2,1,6,0,0\n" + 105 | // "21243,-200,4,2,1,30,0,0\n" + 106 | // "43513,-200,4,2,1,15,0,0\n" + 107 | // "43974,-200,4,2,1,20,0,0\n" + 108 | // "44436,-200,4,2,1,25,0,0\n" + 109 | // "44897,-200,4,2,1,30,0,0\n" + 110 | // "45359,-200,4,2,1,35,0,0\n" + 111 | // "45820,-200,4,2,1,40,0,0\n" + 112 | // "46282,-200,4,2,1,45,0,0\n" + 113 | // "46743,-200,4,2,1,50,0,0\n" + 114 | // "49051,230.769230769231,4,2,1,5,1,0\n" + 115 | // "56377,-100,4,1,1,60,0,0\n" + 116 | // "70224,-100,4,1,1,55,0,0\n" + 117 | // "70512,-100,4,1,1,50,0,0\n" + 118 | // "70743,-100,4,1,1,45,0,0\n" + 119 | // "70974,-100,4,1,1,40,0,0\n" + 120 | // "71204,-100,4,1,1,60,0,0\n" + 121 | // "77377,-100,4,1,1,50,0,0\n" + 122 | // "78589,-100,4,1,1,60,0,0\n" + 123 | // "86897,-100,4,1,1,55,0,0\n" + 124 | // "87127,-100,4,1,1,50,0,0\n" + 125 | // "87358,-100,4,1,1,45,0,0\n" + 126 | // "87589,-100,4,1,1,40,0,0\n" + 127 | // "87820,-100,4,1,1,45,0,0\n" + 128 | // "88051,-100,4,1,1,50,0,0\n" + 129 | // "88281,-100,4,1,1,55,0,0\n" + 130 | // "88743,-100,4,1,1,60,0,1\n" + 131 | // "88801,-100,4,1,1,60,0,0\n" + 132 | // "96127,-100,4,1,1,55,0,0\n" + 133 | // "98897,-100,4,1,1,50,0,0\n" + 134 | // "99358,-100,4,1,1,45,0,0\n" + 135 | // "100281,-100,4,1,1,50,0,0\n" + 136 | // "100743,-100,4,1,1,55,0,0\n" + 137 | // "101204,-100,4,1,1,60,0,0\n" + 138 | // "103512,-100,4,1,1,55,0,0\n" + 139 | // "118281,-100,4,1,1,40,0,0\n" + 140 | // "118743,-100,4,1,1,45,0,0\n" + 141 | // "119204,-100,4,1,1,50,0,0\n" + 142 | // "119666,-100,4,1,1,55,0,0\n" + 143 | // "120127,-100,4,1,1,60,0,0\n" + 144 | // "130974,-100,4,1,1,40,0,0\n" + 145 | // "131204,-100,4,1,1,60,0,0\n" + 146 | // "133858,-100,4,1,1,50,0,0\n" + 147 | // "138531,-200,4,1,1,60,0,0\n" + 148 | // "150416,-200,4,1,1,55,0,0\n" + 149 | // "151281,-200,4,1,1,50,0,0\n" + 150 | // "152551,-200,4,1,1,49,0,0\n" + 151 | // "152608,-200,4,1,1,48,0,0\n" + 152 | // "152666,-200,4,1,1,47,0,0\n" + 153 | // "152724,-200,4,1,1,46,0,0\n" + 154 | // "152781,-200,4,1,1,45,0,0\n" + 155 | // "152839,-200,4,1,1,44,0,0\n" + 156 | // "152897,-200,4,1,1,43,0,0\n" + 157 | // "152954,-200,4,1,1,42,0,0\n" + 158 | // "153012,-200,4,1,1,41,0,0\n" + 159 | // "153070,-200,4,1,1,40,0,0\n" + 160 | // "153127,-200,4,1,1,39,0,0\n" + 161 | // "153185,-200,4,1,1,38,0,0\n" + 162 | // "153243,-200,4,1,1,37,0,0\n" + 163 | // "153301,-200,4,1,1,36,0,0\n" + 164 | // "153358,-200,4,1,1,35,0,0\n" + 165 | // "160685,-133.333333333333,4,1,1,40,0,0\n" + 166 | // "166280,-100,4,1,1,60,0,1\n" + 167 | // "166397,-100,4,1,1,60,0,0\n" + 168 | // "168993,-100,4,1,1,50,0,0\n" + 169 | // "169916,-100,4,1,1,60,0,0\n" + 170 | // "180070,-100,4,1,1,55,0,0\n" + 171 | // "180589,-100,4,1,1,50,0,0\n" + 172 | // "195012,-100,4,1,1,45,0,0\n" + 173 | // "195243,-100,4,1,1,40,0,0\n" + 174 | // "195474,-100,4,1,1,35,0,0\n" + 175 | // "196281,-100,4,1,1,40,0,0\n" + 176 | // "196743,-100,4,1,1,45,0,0\n" + 177 | // "197204,-100,4,1,1,50,0,0\n" + 178 | // "197666,-100,4,1,1,60,0,1\n" + 179 | // "209666,-100,4,1,1,40,0,1\n" + 180 | // "209897,-100,4,1,1,45,0,1\n" + 181 | // "210127,-100,4,1,1,50,0,1\n" + 182 | // "210358,-100,4,1,1,55,0,1\n" + 183 | // "210589,-100,4,1,1,60,0,1\n" + 184 | // "226743,-100,4,1,1,50,0,1\n" + 185 | // "226974,-100,4,1,1,40,0,1\n" + 186 | // "227204,461.538461538462,4,2,1,25,1,0\n" + 187 | // "251549,697.674418604651,4,2,1,20,1,0\n" + 188 | // "252702,714.285714285714,4,2,1,20,1,0\n" + 189 | // "253509,800,4,2,1,18,1,0\n" + 190 | // "254081,1132.07547169811,4,2,1,15,1,0\n" + 191 | // "255586,1200,4,2,1,10,1,0\n" + 192 | // "256408,1267.10000000001,4,2,1,8,1,0\n" + 193 | // "258906,2131.45714285714,4,2,2,6,1,0\n" + 194 | // "258906,-200,4,2,2,6,0,0\n" + 195 | // "263204,230.769230769231,4,1,1,60,1,1\n" + 196 | // "263261,-100,4,1,1,60,0,0\n" + 197 | // "270530,-100,4,1,1,40,0,0\n" + 198 | // "277973,-100,4,1,1,60,0,0\n" + 199 | // "294530,-100,4,1,1,40,0,0\n" + 200 | // "294819,-100,4,1,1,45,0,0\n" + 201 | // "295050,-100,4,1,1,50,0,0\n" + 202 | // "295280,-100,4,1,1,55,0,0\n" + 203 | // "295511,-100,4,1,1,55,0,1\n" + 204 | // "295627,-100,4,1,1,55,0,0\n" + 205 | // "302896,-100,4,1,1,50,0,0\n" + 206 | // "306127,-100,4,1,1,45,0,0\n" + 207 | // "307050,-100,4,1,1,50,0,0\n" + 208 | // "307511,-100,4,1,1,55,0,0\n" + 209 | // "308838,-100,4,1,1,35,0,0\n" + 210 | // "309127,-100,4,1,1,40,0,0\n" + 211 | // "309357,-100,4,1,1,45,0,0\n" + 212 | // "309588,-100,4,1,1,50,0,0\n" + 213 | // "309819,-100,4,1,1,55,0,0\n" + 214 | // "310280,-100,4,1,1,60,0,1\n" + 215 | // "322280,-100,4,1,1,40,0,1\n" + 216 | // "322511,-100,4,1,1,45,0,1\n" + 217 | // "322684,-100,4,1,1,50,0,1\n" + 218 | // "322973,-100,4,1,1,55,0,1\n" + 219 | // "323204,-100,4,1,1,60,0,1\n" + 220 | // "337050,-100,4,1,1,40,0,1\n" + 221 | // "337280,-100,4,1,1,45,0,1\n" + 222 | // "337511,-100,4,1,1,50,0,1\n" + 223 | // "337742,-100,4,1,1,55,0,1\n" + 224 | // "337973,-100,4,1,1,60,0,1\n" + 225 | // "356434,-100,4,1,1,60,0,0\n" + 226 | // "370338,-100,4,1,1,50,0,0\n" + 227 | // "371204,-100,4,1,1,60,0,0\n" + 228 | // "\n" + 229 | // "\n" + 230 | // "[Colours]\n" + 231 | // "Combo1 : 244,151,11\n" + 232 | // "Combo2 : 11,244,11\n" + 233 | // "Combo3 : 11,244,244\n" + 234 | // "Combo4 : 244,11,11\n" + 235 | // "\n" + 236 | // "[HitObjects]\n" + 237 | // "256,192,6590,12,0,7974,0:0:0:0:\n" + 238 | // "256,192,8436,5,4,0:1:0:0:\n" + 239 | // "256,192,8551,12,0,9820,0:0:0:0:\n" + 240 | // "256,192,10282,5,4,0:1:0:0:\n" + 241 | // "256,192,10397,12,0,13513,0:0:0:0:\n" + 242 | // "256,192,13974,5,4,0:1:0:0:\n" + 243 | // "256,192,14090,12,0,15359,0:0:0:0:\n" + 244 | // "256,192,15820,5,4,0:1:0:0:\n" + 245 | // "256,192,15936,12,0,17205,0:0:0:0:\n" + 246 | // "256,192,17666,5,4,0:1:0:0:\n" + 247 | // "215,217,384127,5,12,0:0:0:0:\n"; 248 | // System.out.println(test.substring(test.indexOf("[HitObjects]")).matches("\\[HitObjects]\\n256,192,\\d*,12,0,\\d*,0:0:0:0:(.|\\r|\\n)*")); 249 | // System.out.println(new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson("{\"beatmapset_id\":\"796766\",\"beatmap_id\":\"1673919\",\"approved\":\"-2\",\"total_length\":\"128\",\"hit_length\":\"124\",\"version\":\"Dored's Hard\",\"file_md5\":\"8af6f7b0c47603fd3121b1e3f3d8a385\",\"diff_size\":\"3.5\",\"diff_overall\":\"6\",\"diff_approach\":\"7.5\",\"diff_drain\":\"5\",\"mode\":\"0\",\"approved_date\":null,\"last_update\":\"2018-09-13 04:48:46\",\"artist\":\"himmel\",\"title\":\"Empyrean\",\"creator\":\"Imouto koko\",\"creator_id\":\"7679162\",\"bpm\":\"175\",\"source\":\"Zyon \\u8f09\\u97f3\",\"tags\":\"heavenly trinity dynamix 84461810 papapa213 dored ametrin fushimi rio firika hanazawa kana vert suzuki_1112\",\"genre_id\":\"1\",\"language_id\":\"1\",\"favourite_count\":\"3\",\"playcount\":\"0\",\"passcount\":\"0\",\"max_combo\":\"683\",\"difficultyrating\":\"3.491027355194092\"}", Beatmap.class)); 250 | // System.out.println(new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson("{\"beatmapset_id\":\"13223\",\"beatmap_id\":\"53554\",\"approved\":\"2\",\"total_length\":\"428\",\"hit_length\":\"340\",\"version\":\"Extra Stage\",\"file_md5\":\"2f6b9a08fb595128073a7a8935572a6c\",\"diff_size\":\"4\",\"diff_overall\":\"9\",\"diff_approach\":\"9\",\"diff_drain\":\"8\",\"mode\":\"0\",\"approved_date\":\"2010-05-13 19:26:19\",\"last_update\":\"2010-05-13 19:14:44\",\"artist\":\"Demetori\",\"title\":\"Emotional Skyscraper ~ World's End\",\"creator\":\"happy30\",\"creator_id\":\"27767\",\"bpm\":\"178\",\"source\":\"Touhou\",\"tags\":\"hijiri byakuren touhou 12 cosmic mind nada upasana pundarika mekadon95 ignorethis 2012\",\"genre_id\":\"2\",\"language_id\":\"5\",\"favourite_count\":\"1355\",\"playcount\":\"3035630\",\"passcount\":\"251323\",\"max_combo\":\"2012\",\"difficultyrating\":\"4.99941873550415\"}", Beatmap.class)); 251 | // 252 | // 253 | 254 | 255 | // UpdateOsuClientTasker tasker = new UpdateOsuClientTasker(); 256 | // tasker.setWebPageManager(new WebPageManager()); 257 | // tasker.updateOsuClient(); 258 | // System.out.println(StringSimilarityUtil.calc("Delis' Insane","insane")); 259 | // System.out.println(StringSimilarityUtil.calc("Insane","insane")); 260 | // DefaultHttpClient client = new DefaultHttpClient(); 261 | // HttpPost post = new HttpPost("https://osu.ppy.sh/forum/ucp.php?mode=login"); 262 | // //添加请求头 263 | // java.util.List urlParameters = new ArrayList<>(); 264 | // urlParameters.add(new BasicNameValuePair("autologin", "on")); 265 | // urlParameters.add(new BasicNameValuePair("login", "login")); 266 | // urlParameters.add(new BasicNameValuePair("username", Overall.CABBAGE_CONFIG.getString("accountForDL"))); 267 | // urlParameters.add(new BasicNameValuePair("password", Overall.CABBAGE_CONFIG.getString("accountForDLPwd"))); 268 | // try { 269 | // post.setEntity(new UrlEncodedFormEntity(urlParameters)); 270 | // client.execute(post); 271 | // } catch (Exception ignored) { } 272 | // List cookies = client.getCookieStore().getCookies(); 273 | // String cookie = ""; 274 | // for (Cookie c : cookies) { 275 | // cookie = cookie.concat(c.getName()).concat("\n"); 276 | // } 277 | // System.out.println(cookie); 278 | // System.out.println("——————————————————"); 279 | // OkHttpClient CLIENT = new OkHttpClient().newBuilder() 280 | // .followRedirects(false) 281 | // .followSslRedirects(false) 282 | // .build(); 283 | // RequestBody formBody = new FormBody.Builder() 284 | // .add("autologin", "on") 285 | // .add("login", "login") 286 | // .add("username", Overall.CABBAGE_CONFIG.getString("accountForDL")) 287 | // .add("password", Overall.CABBAGE_CONFIG.getString("accountForDLPwd")) 288 | // .build(); 289 | // Request request = new Request.Builder() 290 | // .url("https://osu.ppy.sh/forum/ucp.php?mode=login") 291 | // .post(formBody) 292 | // .build(); 293 | // StringBuilder cookie2 = new StringBuilder(); 294 | // try (Response response = CLIENT.newCall(request).execute()) { 295 | // List cookies2 = okhttp3.Cookie.parseAll(request.url(), response.headers()); 296 | // for (okhttp3.Cookie c : cookies2) { 297 | // cookie2.append(c.name()+"\n"); 298 | // } 299 | // } catch (Exception ignored) { } 300 | // System.out.println(cookie2.toString()); 301 | 302 | 303 | // String a = "123asd"; 304 | // Instant s = Instant.now(); 305 | // pattern ALL_NUMBER_SEARCH_KEYWORD = pattern.compile("^(\\d{1,7})$"); 306 | // for (int i = 0; i < 100000; i++) { 307 | // try { 308 | // Integer in = Integer.valueOf(a); 309 | // } catch (Exception ignore) { 310 | ////239 311 | // } 312 | //// ALL_NUMBER_SEARCH_KEYWORD.matcher(a).find(); 313 | ////48 314 | // } 315 | // System.out.println(Duration.between(s, Instant.now()).toMillis()); 316 | // User a = new User(); 317 | // a.setBanned(false); 318 | // swap(a); 319 | // System.out.println(a.isBanned()); 320 | 321 | // org.jsoup.nodes.Document doc = Jsoup.connect("https://syrin.me/pp+/api/user/2545898").timeout(10000).get(); 322 | // System.out.println(doc); 323 | } 324 | 325 | // void swap(User a) { 326 | // a.setBanned(true); 327 | // } 328 | } 329 | --------------------------------------------------------------------------------