MiraiForum

    • Register
    • Login
    • Search
    • Popular
    • Recent
    • Unsolved
    • Tags
    • Groups
    • 友情链接

    Java + Springboot 2.X+ mirai最新RELEASE の采坑记录

    精华主题
    spring boot mirai core java
    7
    26
    6203
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • wssy001
      wssy001 ⭐2021⭐ last edited by wssy001

      环境:
      SpringBoot 2.2.11.RELEASE
      Mirai-core-jvm 2.6.0(时刻试水最新release版)
      Open JDK 11

      Spring 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>
      
      1 Reply Last reply Reply Quote 2
      • wssy001
        wssy001 ⭐2021⭐ last edited by

        边写代码 边采坑 获得了解决方案就更新

        1 Reply Last reply Reply Quote 1
        • wssy001
          wssy001 ⭐2021⭐ last edited by wssy001

          别忘了在主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,封装常用方法,凭君喜爱……
          ———————————————————————————————————————————————————

          1 Reply Last reply Reply Quote 0
          • Him188
            Him188 last edited by

            好耶, 不过 2.7 就会尝试解决依赖冲突问题 (以更大的体积来解决冲突)

            1 Reply Last reply Reply Quote 0
            • wssy001
              wssy001 ⭐2021⭐ last edited by wssy001

              开发第三天 (其实是对第二天的代码补充):

              先说说将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();
              }
              
              1 Reply Last reply Reply Quote 2
              • wssy001
                wssy001 ⭐2021⭐ last edited by wssy001

                写在开头: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)
                

                第三天内容补完了,后续就要进入实际业务开发了,我会优先展示群文件的相关操作,主要手头上刚好有项工作需要和群文件打交道

                1 Reply Last reply Reply Quote 2
                • wssy001
                  wssy001 ⭐2021⭐ last edited by wssy001

                  ——————————这是分隔符——————————
                  第四天开发报告 (摸鱼记录):
                  年初我就一直期待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()
                  第四天补充完成,我当前的业务是以群文件为主。后续的话,新的消息我可能用不了多少,但是会逐步完善功能。
                  ——————————这是分隔符——————————

                  1 Reply Last reply Reply Quote 0
                  • wssy001
                    wssy001 ⭐2021⭐ last edited by

                    不好意思,好久没更新了,最近接了一个项目,是给一个玩家论坛开发一个机器人。这个机器人的业务逻辑很简单,群友查重&清理长期潜水的人。
                    这里的话分享几个小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的坑比较少,暂时分享这么多,后续我采取编辑回复的方式更新。也欢迎大佬不吝赐教!

                    1 Reply Last reply Reply Quote 0
                    • wssy001
                      wssy001 ⭐2021⭐ last edited by

                      更新至 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>
                      

                      不多说,继续试用新功能

                      Him188 1 Reply Last reply Reply Quote 1
                      • Him188
                        Him188 @wssy001 last edited by

                        @wssy001 2.7进行评估后没有找到合适解决方案,还是放弃了

                        1 Reply Last reply Reply Quote 0
                        • wssy001
                          wssy001 ⭐2021⭐ last edited by wssy001

                          更新至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旧版本导致账号被风控,故此升级至最新版本,同时替换旧版本功能,本文将持续更新

                          1 Reply Last reply Reply Quote 0
                          • X
                            Xiaojiuc last edited by

                            亲,方便的话可以加个QQ好友吗,关于springboot整合机器人可能有些问题想要向您请教一下,QQ:11218664

                            1 Reply Last reply Reply Quote 0
                            • wssy001
                              wssy001 ⭐2021⭐ last edited by

                              有问题发到论坛里即可

                              1 Reply Last reply Reply Quote 0
                              • Kloping
                                Kloping ⭐2021⭐ last edited by

                                This post is deleted!
                                1 Reply Last reply Reply Quote 0
                                • wssy001
                                  wssy001 ⭐2021⭐ last edited by

                                  更新至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 二位大佬的指点。

                                  1 Reply Last reply Reply Quote 0
                                  • wssy001
                                    wssy001 ⭐2021⭐ last edited by

                                    要想写一个自定义事件,怎么少得了官方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()这个方法。。。

                                    1 Reply Last reply Reply Quote 0
                                    • H
                                      hangq last edited by

                                      为什么发送图片我这里没没反应呢 sendMessage(new PlainText("你要的图片是:").plus(image));
                                      Send: MessageSvc.PbSendMsg
                                      Recv: MessageSvcPbSendMsg.Response.SUCCESS
                                      <- 你要的图片是:[mirai:image:{7698BBAA-096E-8452-BBCF-26CF942CCBD4}.png]

                                      wssy001 1 Reply Last reply Reply Quote 0
                                      • wssy001
                                        wssy001 ⭐2021⭐ @hangq last edited by

                                        @hangq 原因很多,建议论坛咨询

                                        H 1 Reply Last reply Reply Quote 0
                                        • H
                                          hangq @wssy001 last edited by

                                          @wssy001 好的可以了更换了最新的jvm,有些蛋疼

                                          1 Reply Last reply Reply Quote 0
                                          • X
                                            Xiaojiuc last edited by

                                            您好,这边也是在整合SpringBoot,当前本地开发环境登录无异常,就是部署到Linux环境之后,无法登录,为验证码的问题,不知道可否看一下,问题已经提交了:https://mirai.mamoe.net/topic/854/整合springboot无法登录的问题

                                            1 Reply Last reply Reply Quote 0
                                            • 1
                                            • 2
                                            • 1 / 2
                                            • First post
                                              Last post
                                            Powered by Mamoe Technologies & NodeBB | 友情链接 | 服务监控 | Contact