Java + Springboot 2.X+ mirai最新RELEASE の采坑记录
-
环境:
SpringBoot 2.2.11.RELEASE
Mirai-core-jvm 2.6.0(时刻试水最新release版)
Open JDK 11Spring Cloud版
开发第一天:
新建了Spring Boot项目,鉴于前几次mirai的开发,我轻车熟路地加上了这个<dependency> <groupId>net.mamoe</groupId> <artifactId>mirai-core-all</artifactId> <version>2.6.0</version> </dependency>
当然成功翻车。
所以说 拥有一个良好的看wiki-->使用 Maven习惯是很重要的!下载了最新的依赖后,鉴于是springboot项目,还需要做的一件事是查看mirai依赖的kotlin版本,可以直接去wiki找,也可以自己点开依赖挖
别忘了在主POM加上Mirai所依赖的kotlin版本
<properties> <kotlin.version>1.4.32</kotlin.version> </properties>
-
边写代码 边采坑 获得了解决方案就更新
-
别忘了在主POM加上滑动验证模块,
<dependency> <groupId>net.mamoe</groupId> <artifactId>mirai-login-solver-selenium</artifactId> <version>1.0-dev-16</version> </dependency>
貌似MAC环境下貌似无法自动化操作,但是WIN下OK,到时候把deviceInfo.json ctrl cv过去即可
———————————————————————————————————————————————————
开发第二天:
解决完项目创建,就要监听游戏中的事件了,逛了wiki-->Event发现如今的事件注册变成了bot.getEventChannel().registerListenerHost(groupMessageHandler);
但如果想使用自定义鉴权注解,事件注册不能采取前一者(暂时还未能解决@EventHandler与自定义注解共同作用于同一方法,@EventHandler失效的问题),而是:
bot.getEventChannel().subscribeAlways(GroupMessageEvent.class, groupMessageHandler::onMessage);
而且要注意,注册完事件再执行
bot.join();
别问我是怎么知道了,
我是不会告诉你我因为无法注册事件查了几小时的wiki,CSDN,etc....Spring最大的核心是AOP & IOC。实际应用中,难免少不了鉴权,AOP就能很好滴防止我们在handler中每个方法都写上一个if(!permission(...));对于IOC,可以将bot对象放入,然后controller中注入,直接调用bot对象,也可以写一个BotService,封装常用方法,凭君喜爱……
——————————————————————————————————————————————————— -
好耶, 不过 2.7 就会尝试解决依赖冲突问题 (以更大的体积来解决冲突)
-
开发第三天
(其实是对第二天的代码补充):先说说将bot加载至IOC,这个其实很简单,考虑到个别springboot新手,故贴一下代码
(这么好的水贴例子怎么能放过呢?)@Bean public Bot bot() { return BotFactory.INSTANCE.newBot(yourQQNumber, "password", new BotConfiguration() { { fileBasedDeviceInfo("deviceInfo.json"); setProtocol(MiraiProtocol.ANDROID_PHONE); } }); }
如果需要设置事件监听,可以这样:
@Bean public Bot bot() { Bot bot = BotFactory.INSTANCE.newBot(yourQQNumber, "password", new BotConfiguration() { { fileBasedDeviceInfo("deviceInfo.json"); setProtocol(MiraiProtocol.ANDROID_PHONE); } }); bot.getEventChannel().registerListenerHost(groupMessageHandler); return bot; }
这里我调用了一个handler来处理群消息,详见:wiki-->使用 @EventHandler 注解标注的方法监听事件。
(P.S. Spring已经不推荐使用@Autowired注入,IDEA也明确给了一个warning,推荐使用构造器的方式注入,配合Lombok神器,构造器注入比前者更简单)
然后再写了个QQRobotService,完成机器人登录。
@PostConstruct public void init() { bot.login(); }
-
写在开头:mirai 2.6.2出了,下文mirai版本为:2.6.2,同样的,滑动登录模块也更新了,这里一同贴出
maven:<dependency> <groupId>net.mamoe</groupId> <artifactId>mirai-core-jvm</artifactId> <version>2.6.2</version> </dependency> <dependency> <groupId>net.mamoe</groupId> <artifactId>mirai-login-solver-selenium</artifactId> <version>1.0-dev-17</version> </dependency>
——————————这是分隔符——————————
完成了IOC,就要用AOP来完成鉴权了,大概需要几步
- 1:写一个注解
- 2:写一个bean
- 3:写一个切面
记得先添加Spring AOP的依赖,最好是用AspectJ进行切面,毕竟基于动态代理的Spring AOP局限性太大!!!!
GAV坐标:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
先写一个注解,我写的注解是可以加在类上和方法上的,用于鉴权。我在其他权限管理项目中可遇到过要求某用户具有A权限,但没有B权限的业务,故有了这俩。未来打算适配Spring Security
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface CheckPermission { String[] roles() default {}; String[] nonRoles() default {}; boolean isGroupOwnerOnly() default false; boolean isAdminOnly() default false; String description() default "QQRobot自定义权限校验注解"; }
然后就是切面
@Aspect @Component @RequiredArgsConstructor @Slf4j public class CheckPermissionAspect { @Pointcut("@annotation(cyou.wssy001.qqrobot.annotation.CheckPermission)") public void annotatedMethods() { } @Pointcut("@within(cyou.wssy001.qqrobot.annotation.CheckPermission)") public void annotatedClasses() { } @Before("annotatedClasses() || annotatedMethods()") public Object checkPermission(JoinPoint point) { Object[] args = point.getArgs(); if (args == null) throw new RuntimeException("无发送者!!!"); //这个event中可以获取发送者的信息,以便下文鉴权 GroupMessageEvent event = (GroupMessageEvent) args[1]; MethodSignature signature = (MethodSignature) point.getSignature(); CheckPermission methodAnnotation = signature.getMethod().getAnnotation(CheckPermission.class); Class<?> aClass = point.getSignature().getDeclaringType(); CheckPermission classAnnotation = aClass.getAnnotation(CheckPermission.class); Permission permission = getCheckPermission(methodAnnotation, classAnnotation); //这里的checkA()方法和checkB()方法是具体的判断逻辑 if (checkA() && checkB()) { return point.getArgs(); } else { throw new RuntimeException("无权访问!"); } } private Permission getCheckPermission(CheckPermission methodAnnotation, CheckPermission classAnnotation) { Permission permission = new Permission(); CheckPermission check = methodAnnotation != null ? methodAnnotation : classAnnotation; if (check != null) { permission.getRoles().addAll(Arrays.asList(check.roles())); permission.getNonRoles().addAll(Arrays.asList(check.nonRoles())); permission.setAdminOnly(check.isAdminOnly()); permission.setGroupOwnerOnly(check.isGroupOwnerOnly()); } return permission; } }
我写了个Bean用于存储注解包含的信息
@Data @AllArgsConstructor @NoArgsConstructor public class Permission { //存放所需要的角色 Set<String> roles; //存放所不需要的角色 Set<String> nonRoles; //是否仅限于群主 boolean isGroupOwnerOnly; //是否仅限于管理(这里考虑的是 管理+群主,毕竟群主具有最高权限) boolean isAdminOnly; }
使用的方法也很简单,举个栗子:
@Component @Slf4j @RequiredArgsConstructor public class GroupMessageHandler { private final AdminMapService adminMapService; @CheckPermission(isAdminOnly = true) public void onMessage(@NotNull GroupMessageEvent event) throws Exception { Member sender = event.getSender(); Group group = event.getSource().getGroup(); StringBuilder reply = new StringBuilder(); ………………这是省略号………………
这样一来 所有调用onMessage(@NotNull GroupMessageEvent event)的方法都会先判断调用者是不是管理员 or 群主。
最后别忘启用切面
@EnableAspectJAutoProxy(exposeProxy = true)
第三天内容补完了,后续就要进入实际业务开发了,我会优先展示群文件的相关操作,主要手头上刚好有项工作需要和群文件打交道
-
——————————这是分隔符——————————
第四天开发报告(摸鱼记录):
年初我就一直期待mirai对群文件的支持,正好我手上就有个业务需要远程传送文件。文件这块内容在官方文档中还未来得及更新,故我仅依靠着对代码中相关注释的理解进行开发,若有纰漏之处望不吝赐教。准备开始之前,必须打开 RemoteFile.kt 或在IDEA中 搜索 net.mamoe.mirai.utils.RemoteFile,请仔细阅读作者给出的注释,它能给你开发思路!!!
关于群文件的操作,我只举例上传与下载,其余的可以翻阅源码,食用方法注释写得很详细。
上文说过,我写了一个handler用于处理群消息,下文是一个栗子
@CheckPermission(isAdminOnly = true) public void onMessage(@NotNull GroupMessageEvent event) { //发送人信息 Member sender = event.getSender(); //目标消息所在群 Group group = event.getSource().getGroup(); //消息文本 String msg = event.getMessage().contentToString(); }
group.getFilesRoot()方法可以让我们定位到群文件的根目录“/”,亦是我们在客户端中点开群文件第一个显示的目录界面。
resolve()方法可以将操作定位到我们需要前去的目录或所需要操作的文件,详见注释。
我们可以这样遍历群文件,不过在键入group.getFilesRoot().listFilesIterator(false).var后回车,IDEA自动创建的变量的类型直接为Iterator,兴许是还没这么智能,建议手动补全为Iterator<RemoteFile>。善于挖掘源码的你也一定注意到了,listFilesIterator()方法支持懒加载。
if (msg.contains("遍历群文件")) { if (!permission) return; Iterator<RemoteFile> iterator = group.getFilesRoot().listFilesIterator(false); List<RemoteFile> list = group.getFilesRoot().listFilesCollection(); }
RemoteFile还提供了俩方法 isFile()和isDirectory()用于判断目标类型。反正要么是文件,要么是文档,任君选择。
getDownloadInfo()方法会返回一个叫DownloadInfo的bean,其中包含了该文件在群文件的路径,ID,下载链接等一系列信息,值得一提的是,它提供了md5和sha1,方便我们对下载的文件进行完整性校验。下文便是一个文件下载栗子,文件下载轮子我用的是hutool,GAV附上
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-http</artifactId> <version>5.5.1</version> </dependency>
List<RemoteFile> list = group.getFilesRoot().listFilesCollection(); //一系列的判断,校验 String url = files.get(0).getDownloadInfo().getUrl(); file = HttpUtil.downloadFileFromUrl(url, "temp");
mirai更新2.6.3了,只需要更新一下版本号即可,GAV:
<dependency> <groupId>net.mamoe</groupId> <artifactId>mirai-core-jvm</artifactId> <version>2.6.3</version> </dependency>
然后就是文件上传,文件上传这块感觉和OSS(对象存储)很相似,不过暂时mirai还不能做到查看所给的“文件名”,自动创建不存在的文件夹。废话不多说,上代码
if (msg.startsWith("文件上传")) { File file = new File("C:\\deviceInfo.json"); RemoteFile remoteFile = group.getFilesRoot().resolve("/test").resolve(file.getName()); remoteFile.uploadAndSend(file); }
其实在文件上传这块 resolve()方法其实相当于是设置你上传的文件的“文件名”,如果给定的“文件名”包含一个路径(例 “\test\Test.java”),则自动将上传的Test.java放入根目录文件中的test文件夹。如果这个“test”文件夹不存在,mirai不会自动创建之,取而代之的是一个报错。
我给出一个方案if (msg.startsWith("文件上传")) { File file = new File("C:\\deviceInfo.json"); // 这是一个不存在的文件夹 String path = "/fileTestDocument"; RemoteFile resolve = group.getFilesRoot().resolve(path); if (!resolve.exists()) { resolve.mkdir(); } resolve.resolve(file.getName()).uploadAndSend(file); }
上述方案中,我选择exists()方法作为目标文件夹是否存在的判断方法,如果你的bot在这个业务中具有管理员权限,那可以直接调用mkdir()
第四天补充完成,我当前的业务是以群文件为主。后续的话,新的消息我可能用不了多少,但是会逐步完善功能。
——————————这是分隔符—————————— -
不好意思,好久没更新了,最近接了一个项目,是给一个玩家论坛开发一个机器人。这个机器人的业务逻辑很简单,群友查重&清理长期潜水的人。
这里的话分享几个小tip
1:mirai机器人有个isOnline方法(net.mamoe.mirai.Bot#isOnline),查询机器人是否在线。实际使用中需要bot.login()方法执行完毕后才可使用,不然会报一个network还未初始化的exception。2:使用mirai机器人踢人时要注意频率,一次性踢太多会报一个kick failed 255错误。实际使用中使用kick()方法(net.mamoe.mirai.contact.NormalMember#kick(java.lang.String, kotlin.coroutines.Continuation<? super kotlin.Unit>))时,应该设置一个延时。
try { normalMember.kick(reason); Thread.sleep(1000); } catch (Exception e) { log.warn("******踢出成员失败" + userQQ); }
如果配合MQ或者队列食用那就更棒了!
3:消息转发,大都数的机器人都具备消息转发、通知的能力,mirai也不例外,实际编写代码时,对方要求不高,转发些文字消息,必要时at一下全体成员。我的思路大概是这样的:
MessageChain message = event.getMessage(); String msg = message.contentToString(); if (msg.startsWith("/转发")) { if (sender.getPermission().equals(MemberPermission.MEMBER)) return; MessageChainBuilder messages = new MessageChainBuilder(); for (SingleMessage singleMessage : message) { String s = singleMessage.contentToString().trim(); if (s.startsWith("/转发")) { s = s.replace("/转发", ""); if (s.length() > 0) messages.add(s); } else if (s.startsWith("@")) { if (s.equals("@全体成员")) { messages.add(AtAll.INSTANCE); } else { s = s.replace("@", ""); messages.add(new At(Long.parseLong(s))); } } else { messages.add(s); } } Set<Long> groups = appConfig.getGroups(); groups.remove(group.getId()); for (Long groupId : groups) { group.getBot().getGroup(groupId).sendMessage(messages.build()); } }
获取到的message信息包含了消息来源,但实际遍历时转码为空,可以通过
Group group = event.getSource().getGroup();
获取来源的QQ群群号,然后遍历消息,进行封装与转发。
由于项目难度偏低,涉及到mirai的坑比较少,暂时分享这么多,后续我采取编辑回复的方式更新。也欢迎大佬不吝赐教!
-
更新至 2.7-M2 版本
GAV:<dependency> <groupId>net.mamoe</groupId> <artifactId>mirai-core-jvm</artifactId> <version>2.7-M2</version> </dependency>
之前有dalao说 2.7会解决 mirai-core-all 不过我目前还没看到解决方案,于是继续使用mirai-core-jvm
这次更新,kotlin的版本需要进行大浮动升级,这里直接给出解决方案:<properties> <kotlin.version>1.5.10</kotlin.version> <kotlin-coroutines.version>1.5.0</kotlin-coroutines.version> </properties>
不多说,继续试用新功能
-
@wssy001 2.7进行评估后没有找到合适解决方案,还是放弃了
-
更新至2.7.1版本 GAV:
<dependency> <groupId>net.mamoe</groupId> <artifactId>mirai-core-jvm</artifactId> <version>2.7.1</version> </dependency>
需要更新的kotlin依赖版本:
<properties> <kotlin.version>1.5.10</kotlin.version> <kotlin-coroutines.version>1.5.0</kotlin-coroutines.version> </properties>
先前看到gitter有人提出使用mirai旧版本导致账号被风控,故此升级至最新版本,同时替换旧版本功能,本文将持续更新
-
亲,方便的话可以加个QQ好友吗,关于springboot整合机器人可能有些问题想要向您请教一下,QQ:11218664
-
有问题发到论坛里即可
-
此回复已被删除! -
更新至2.8.1
GAV:<dependency> <groupId>net.mamoe</groupId> <artifactId>mirai-core-jvm</artifactId> <version>2.8.1</version> </dependency>
此次版本升级不需要重新变更kotlin及其相关依赖版本(从2.7.1升级的话)
最近一直在处理自定义事件,需求其实很简单,获取XX通知群中获取管理员发的文件,然后快速解析其中有价值的信息并通知相关人员。
无奈mirai的wiki在这方面比较精简,幸而得到了@cssxsh @MrXiaoM 二位大佬的指点。 -
要想写一个自定义事件,怎么少得了官方wiki -->实现事件呢?
本次demo是实现一个GroupFileUploadEvent,了解一下具体怎么玩转这个“实现事件”
首先写个事件类,成员变量大致写这么多
public class GroupFileUploadEvent extends AbstractEvent { // 发送者的权限 private MemberPermission permission; // 发送者 private Member sender; // 发送时间 private Integer time; // 事件发生所在群 private Group group; // 机器人 private Bot bot; // 群文件名称 private String fileName; // 群文件大小(byte) private Long size; }
然后直接调用EventKt.broadcast()将自定义事件进行广播
GroupFileUploadEvent groupFileUploadEvent = new GroupFileUploadEvent(); EventKt.broadcast(groupFileUploadEvent);
最后调用GlobalEventChannel.INSTANCE,过滤出我们自定义的事件,完成事件监听
GlobalEventChannel.INSTANCE .filterIsInstance(groupFileUploadEvent.class) .subscribeAlways(GroupFileUploadEvent.class, groupFileMessageHandler::onMessage);
这里要注意,必须得是GlobalEventChannel.INSTANCE而不是bot.getEventChannel()
不过这样的做法在单体APP中很鸡肋,所以大佬建议我们使用wiki -->链式调用
所以其实没必要自定义一个事件,只需要官方写好一个message即可,最后奉上我事件监听实际应用的代码
GlobalEventChannel.INSTANCE .filterIsInstance(GroupMessageEvent.class) .filter(v -> v.getMessage().contains(FileMessage.Key)) .subscribeAlways(GroupMessageEvent.class, groupFileMessageHandler::onMessage);
P.S. 为啥没有EventChannel.map()这个方法。。。
-
为什么发送图片我这里没没反应呢 sendMessage(new PlainText("你要的图片是:").plus(image));
Send: MessageSvc.PbSendMsg
Recv: MessageSvcPbSendMsg.Response.SUCCESS
<- 你要的图片是:[mirai:image:{7698BBAA-096E-8452-BBCF-26CF942CCBD4}.png] -
@hangq 原因很多,建议论坛咨询
-
@wssy001 好的可以了更换了最新的jvm,有些蛋疼
-
您好,这边也是在整合SpringBoot,当前本地开发环境登录无异常,就是部署到Linux环境之后,无法登录,为验证码的问题,不知道可否看一下,问题已经提交了:https://mirai.mamoe.net/topic/854/整合springboot无法登录的问题