skyone 发表于 2022-11-1 21:30:54

【科技圆桌】原生Mirai实现QQ机器人

前言

Mirai 是继CQ之用的最多的机器人框架,目前主要支持两种方式运行:
[*]Mirai开发者提供 mirai console 作为宿主程序,开发者的代码已插件的形式运行
[*]把 mirai 作为一个普通的 java 库使用,开发者完全掌控整个程序
我选择了后者,原因如下:
[*]懒,不想因为插件更新了每次都要手动下载、配置
[*]懒,自己打包一个容器显然更方便
[*]懒,GitHub整个CI,代码写完就自动部署
当然,使用原生 Mirai 的缺点也很明显:不能使用其他人开发的插件本贴完全针对原生 Mirai ,如果你没有动手实现自己想要的功能的愿望,那么本文不适合你目标
这个帖子的目标,就是帮助有编程基础的同学从第一行代码开始实现一个 涩图机器人

https://i.imgtg.com/2022/11/01/RrtPi.png

skyone 发表于 2022-11-1 21:34:32

准备库和我一样懒的话就打开GitHub,克隆个 project-mirai/mirai-hello-worldmirai是跑在 JVM 上的,这里我使用 Gradle 作为构建工具因此你需要准保好:
[*]正确安装 JDK(其实就是去官网下个压缩包,加个环境变量)
[*]安装 Gradle(同上)
[*]一个好的网络环境(这个我也没办法帮你,自己解决)
首先找到一个干净的文件夹,打开命令行,初始化项目,如果不懂选项的意思,照着我的选就行了
$ gradle init
Select type of project to generate:
1: basic
2: application
3: library
4: Gradle plugin
Enter selection (default: basic) 2
Select implementation language:
1: C++
2: Groovy
3: Java
4: Kotlin
5: Scala
6: Swift
Enter selection (default: Java) 4
Select build script DSL:
1: Groovy
2: Kotlin
Enter selection (default: Groovy) 2
Select test framework:
1: JUnit 4
2: TestNG
3: Spock
4: JUnit Jupiter
Enter selection (default: JUnit 4)
Project name (default: demo): 随你
Source package (default: demo): me.bot
BUILD SUCCESSFUL
2 actionable tasks: 2 executed


打开自己喜欢的编辑器,当然,开发 Java , IDEA 天下第一!编辑 build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile
plugins {
    application
    kotlin("jvm") version "1.7.20"    id("io.ktor.plugin") version "2.1.3"
    kotlin("plugin.serialization") version "1.7.20"
    id("com.github.johnrengelman.shadow") version "7.1.2"
}
group = "robot"
version = "0.1.0"
repositories {
    mavenCentral()
}
tasks.withType(KotlinJvmCompile::class.java) {
    kotlinOptions.jvmTarget = "1.8"
}
dependencies {
    // Mirai 库
    api("net.mamoe:mirai-core-api:2.12.3")
    runtimeOnly("net.mamoe:mirai-core:2.12.3")
    // 日志库
    implementation("org.slf4j:slf4j-log4j12:2.0.3")
    // Json 序列化
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
    // Ktor Http 请求    implementation("io.ktor:ktor-client-core:2.1.3")
    implementation("io.ktor:ktor-client-cio:2.1.3")

}
tasks.jar {
    manifest {
      attributes(
            "Main-Class" to "me.bot.Application",
            "Implementation-Title" to project.name,
            "Implementation-Version" to project.version
      )
    }
}


截至我写完这篇文章,Mirai的最新版是 2.12.3,你可以在这里看到最新版:Releases · mamoe/mirai同样的,kotlin 也推荐最新版,此外,buildSrc 之类的不在本文范围内编辑 src/main/resources/log4j.properties


log4j.rootLogger=INFO,stdout
log4j.logger.mirai=INFO
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%p\t%d{ISO8601}\t%r\t%c\t%m%n



然后我们先写个 Hello world 吧删除 src/main/kotlin/Application.kt 下的文件,然后编辑 src/main/kotlin/Application.kt
package me.bot

object Application {
    private val logger = LoggerFactory.getLogger(this.javaClass)

    @JvmStatic
    fun main(args: Array<String>): Unit = runBlocking {
      logger.info("Hello world!")
    }
}


运行看看是不是打印了 "Hello world"如果是的,那么环境已经准备成功了

skyone 发表于 2022-11-1 21:38:39

先发个图吧这一楼我们实现机器人的登录和发消息要启动QQ机器人,当然需要一个QQ号,然后登录就一句话object Application {
    private val logger = LoggerFactory.getLogger(this.javaClass)
    @JvmStatic
    fun main(args: Array<String>): Unit = runBlocking {
      // 配置日志,为了不报错,可以不搞
      logger.asMiraiLogger()

      // 账号、密码
      val bot = BotFactory.newBot(100001L, "MaHuaTengSiMa") {
            // 设备消息,以文本形式自动生成 `device.json`
            fileBasedDeviceInfo()
            // 协议,我这里使用的是 安卓平板 协议
            protocol = BotConfiguration.MiraiProtocol.ANDROID_PAD
      }.alsoLogin()
    }
}

然后让机器人发点东西吧,准备一张图放到项目根目录,或者只发条消息也行

bot.getFriend(114514L) // 接收消息的 QQ 号
    ?.also { master -> master.sendMessage("QQ机器人已就位" + Face(Face.KE_AI)) }
    ?.also { master -> master.sendImage(File("文件名.png")) }      
    ?: bot.logger.warning("未找到 master")




Alanplayer 发表于 2022-11-2 07:33:14

不懂,但是感觉很nb

skyone 发表于 2022-11-2 22:32:12

获取一张 Setu要实现随机 Setu 必然是 LSP,咳,就必然需要大量图源,幸运的是已经有LSP大佬做好了这里我们使用 lolicon.app 的接口(一看域名就是个 hentai),链接是 随机色图 (lolicon.app)当然你也可以去 lolicon.app 看看https://i.imgtg.com/2022/11/02/RyyDD.md.png这里只讲基础的,看上图那么多就行了如果你直接打开会返回一个Json,类似这样{
    "error": "",
    "data": [
      {
            "pid": 91736941,
            "p": 0,
            "uid": 5101014,
            "title": "commission",
            "author": "ColdNoodle",
            "r18": false,
            "width": 900,
            "height": 1500,
            "tags": [
                "女の子",
                "女孩子"
            ],
            "ext": "jpg",
            "uploadDate": 1628117887000,
            "urls": {
                "original": "https://i.pixiv.re/img-original/img/2021/08/05/07/58/07/91736941_p0.jpg"
            }
      }
    ]
}也可以使用 POST 请求自定义一些选项。这里我准备实现一下配置:
[*]r18 = 0
[*]tag = 纳西妲 或者 白丝萝莉

[*]proxy = false 服务器在国外,不需要代理

skyone 发表于 2022-11-2 22:42:38

定义数据
首先定义请求体@Serializable
data class RequestData(
    val r18: Int,
    val tag: List<List<String>>,
    val proxy: Boolean
)
没啥可说的,看单词就能知道是啥意思然后是响应体,这个依照上楼的例子对照着写就行了@Serializable
data class ResponseData(
    val error: String,
    val data: List<SetuInfo>
)

@Serializable
data class SetuInfo(
    val pid: Long,
    val p: Int,
    val uid: Long,
    val title: String,
    val author: String,
    val r18: Int,
    val width: Int,
    val height: Int,
    val tags: List<String>,
    val ext: String,
    val uploadDate: Long,
    val urls: Map<String, String>
)
有点多,但是也很简单的。

skyone 发表于 2022-11-2 23:08:16

获取 Setu简简单单,看注释就能懂// 获取一张涩图的信息
suspend fun getSetuInfo(): List<SetuInfo>? {
    // 发送 POST 请求
    val request = HttpClient(CIO).post("https://api.lolicon.app/setu/v2") {
      // 请求体是一个 JSON
      contentType(ContentType.Application.Json)
      setBody(RequestData(
            r18 = 0, // 我是正人君子
            tag = listOf(
                listOf("纳西妲"), // 嘿嘿嘿,嘿嘿嘿.......
                listOf("白丝", "萝莉")
            ),
            proxy = false
      ))
    }
    // 接收数据
    val response = request.body<ResponseData>()
   
    // 如果 data 不为空就返回 setu
    return response.data.ifEmpty { null }?.first()
}

suspend fun getPixivImage(url: String): InputStream {
    val request = HttpClient(CIO).get(url) {
      // 伪造请求头
      header("Referer", "https://www.pixiv.net/")
      header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26")
    }
    return request.body()
}

skyone 发表于 2022-11-2 23:17:48

最后,我们把这张图发出去!


object Application {
    private val logger = LoggerFactory.getLogger(this.javaClass)

    @JvmStatic
    fun main(args: Array<String>): Unit = runBlocking {
      // 配置日志,为了不报错,可以不搞
      logger.asMiraiLogger()
      
      // 账号、密码
      val bot = BotFactory.newBot(100001L, "MaHuaTengSiMa") {
            // 设备消息,以文本形式自动生成 `device.json`
            fileBasedDeviceInfo()
            // 协议,我这里使用的是 安卓平板 协议
            protocol = BotConfiguration.MiraiProtocol.ANDROID_PAD
      }.alsoLogin()
      // 监听好友的消息
      bot.eventChannel.subscribeAlways<FriendMessageEvent> {
            // 如果是指定的好友发出的
            if (sender.id == 114514L) {
                getSetuInfo()?.run {
                  val setu = getPixivImage(urls["original"]!!)
                  // 获取图片,发出去
                  subject.sendImage(setu)
                }
            }
      }
    }
}【完】
页: [1]
查看完整版本: 【科技圆桌】原生Mirai实现QQ机器人