diff --git a/CHANGELOG.md b/CHANGELOG.md index 8679417be..83ea0e429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,53 @@ 开发版本. 频繁更新, 不保证高稳定性 +## `0.17.0` 2020/2/20 + +### mirai-core +- 支持原生表情 `Face` +- 修正 `groupCardOrNick` 为 `nameCardOrNick` +- 增加 `MessageChain.foreachContent(lambda)` 和 `Message.hasContent(): Boolean` + +### mirai-core-qqandroid +- 提高重连速度 +- 修复重连后某些情况不会心跳 +- 修复收包时可能产生异常 + +## `0.16.0` 2020/2/19 + +### mirai-core +- 添加 `Bot.subscribe` 等筛选 Bot 实例的监听方法 +- 其他一些小问题修复 + +### mirai-core-qqandroid +- 优化重连处理逻辑 +- 确保好友消息和历史事件在初始化结束前同步完成 +- 同步好友消息记录时不广播 + +## `0.15.5` 2020/2/19 + +### mirai-core +- 为 `MiraiLogger` 添加 common property `val isEnabled: Boolean` +- 修复 #62: 掉线重连后无 heartbeat +- 修复 #65: `Bot` close 后仍会重连 +- 修复 #70: ECDH is not available on Android platform + +### mirai-core-qqandroid +- 从服务器收到的事件将会额外使用 `bot.logger` 记录 (verbose). +- 降低包记录的等级: `info` -> `verbose` +- 改善 `Bot` 的 log 记录 +- 加载好友列表失败时会重试 +- 改善 `Bot` 或 `NetworkHandler` 关闭时取消 job 的逻辑 +- 修复初始化(init)时同步历史好友消息时出错的问题 + +## `0.15.4` 2020/2/18 + +- 放弃使用 `atomicfu` 以解决其编译错误的问题. (#60) + +## `0.15.3` 2020/2/18 + +- 修复无法引入依赖的问题. + ## `0.15.2` 2020/2/18 ### mirai-core diff --git a/README.md b/README.md index e8ed69843..cbc1fe1a0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@
- + + + logo
+ title ---- + [![Gitter](https://badges.gitter.im/mamoe/mirai.svg)](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Actions Status](https://github.com/mamoe/mirai/workflows/CI/badge.svg)](https://github.com/mamoe/mirai/actions) [![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/) @@ -16,6 +20,7 @@ Mirai 是一个在全平台下运行,提供 QQ Android 和 TIM PC 协议支持
## Mirai + **[English](README-eng.md)** 多平台 **QQ Android 和 TimPC** 协议支持库与高效率的机器人框架. @@ -27,94 +32,59 @@ Mirai既可以作为你项目中的QQ协议支持Lib, 也可以作为单独的Ap 加入 Gitter, 或加入 QQ 群: 655057127 + + +## 开始使用Mirai + +Mirai支持以多种方式进行部署,但是目前,我们在集中对mirai-core,mirai-japt以及mirai-api-http等核心模块进行特性的开发,对于非开发者的使用暂时不做过多支持,仅展示开发计划。 + +### 开发者 + +- 假如你熟悉Kotlin及包管理工具,请参阅[Mirai Guide - Quick Start](/docs/guide_quick_start.md) +- 假如你不熟悉Kotlin,希望一份较详细的起步教程,请参阅[Mirai Guide - Getting Started](/docs/guide_getting_started.md) +- 假如你使用Java作为开发语言,请参阅[mirai-japt](/mirai-japt/README.md) +- 假如你是其他平台开发者,可以通过了解 [mirai-api-http](https://github.com/mamoe/mirai/tree/master/mirai-api-http) 进行接入,欢迎开发不同平台的mirai-sdk +- 此外,你还可以在 [Wiki](https://github.com/mamoe/mirai/wiki/Home) 中查看各类帮助,**如 API 示例**。 + +### 使用者 + +- [mirai-console](https://github.com/mamoe/mirai/tree/master/mirai-console) 支持插件, 在终端中启动 Mirai 并获得机器人服务,**本模块还未完善**,请耐心等待开发完成。 +- mirai-webpanel Mirai的Web控制台,支持在网页中管理机器人与插件。本模块目前在计划中。在其他模块稳定后,将开始进行开发。 + + + ## CHANGELOG + 在 [Project](https://github.com/mamoe/mirai/projects/3) 查看已支持功能和计划 在 [CHANGELOG](https://github.com/mamoe/mirai/blob/master/CHANGELOG.md) 查看版本更新记录 (仅发布的版本) + + ## Modules + ### mirai-core + 通用 API 模块,一套 API 适配两套协议。 **请参考此模块的 API** - + ### mirai-core-qqandroid + QQ for Android (8.2.0 版本,2019 年 12 月)协议的实现,目前完成大部分。 + - 高兼容性:协议仅含极少部分为硬编码,其余全部随官方方式动态生成 - 高安全性:密匙随机,ECDH 动态计算 - 已支持大部分使用场景, 详情请在[Project](https://github.com/mamoe/mirai/projects/3)查看 ### mirai-core-timpc -TIM PC (2.3.2 版本,2019 年 8 月)协议的实现,相较于 core,仅新增少量 API. 详见 [README.md](mirai-core-timpc/) + +TIM PC (2.3.2 版本,2019 年 8 月)协议的实现 支持的功能: + - 消息收发:图片文字复合消息,图片消息 - 群管功能:群员列表,禁言 -(目前不再更新此协议,请关注上文的安卓协议) + (目前不再更新此协议,请关注上文的安卓协议) -## Use directly -**直接使用 Mirai(终端环境/网页面板(将来)).** -[Mirai-Console](https://github.com/mamoe/mirai/tree/master/mirai-console) 插件支持, 在终端中启动 Mirai 并获得机器人服务 -本模块还未完善。 -## Use as a library -**mirai-core 为独立设计, 可以作为库内置于任意 Java(JVM)/Android 项目中使用.** - -请将 `VERSION` 替换为最新的版本(如 `0.15.0`): -[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/) -**Mirai 目前还处于实验性阶段, 我们无法保证任何稳定性, API 也可能会随时修改.** - -### Maven -Kotlin 在 Maven 上只支持 JVM 平台. -```xml - - - jcenter - https://jcenter.bintray.com/ - - -``` -```xml - - - net.mamoe - mirai-core-qqandroid - 0.15.1 - - -``` - -### Gradle -Mirai 只发布在 `jcenter`, 因此请确保添加 `jcenter()` 仓库: -```kotlin -repositories{ - jcenter() -} -``` -若您需要使用在跨平台项目, 则要对各个目标平台添加不同的依赖,这与 kotlin 相关多平台库的依赖是类似的。 -**若您只需要使用在单一平台, 则只需要添加一项该平台的依赖.** - -**注意:** -Mirai 核心由 API 模块(`mirai-core`)和协议模块组成。 -只添加 API 模块将无法正常工作。 -现在只推荐使用 QQAndroid 协议,请参照下文选择对应目标平台的依赖添加。 - -**jvm** (JVM 平台) -```kotlin -implementation("net.mamoe:mirai-core-qqandroid:VERSION") -``` -**common** (通用平台) -```kotlin -implementation("net.mamoe:mirai-core-qqandroid-common:VERSION") -``` -**android** (Android 平台) -```kotlin -implementation("net.mamoe:mirai-core-qqandroid-android:VERSION") -``` - -## Java Compatibility -**若你希望使用 Java 开发**, 请查看: [mirai-japt](mirai-japt/README.md) - -### Performance -Android 上, Mirai 运行需使用 80M 内存. -JVM 上启动需 80M 内存, 每多一个机器人实例需要 30M 内存. ## Contribution @@ -125,41 +95,12 @@ JVM 上启动需 80M 内存, 每多一个机器人实例需要 30M 内存. 您的 star 是对我们最大的鼓励(点击项目右上角) -## Wiki -在 [Wiki](https://github.com/mamoe/mirai/wiki/Home) 中查看各类帮助,**如 API 示例**(可能过时,待 QQ Android 协议完成后会重写)。 - -## Try - -### On JVM or Android -现在体验低付出高效率的 Mirai - -```kotlin -val bot = Bot(qqId, password).alsoLogin() -bot.subscribeMessages { - "你好" reply "你好!" - "profile" reply { sender.queryProfile() } - contains("图片"){ File(imagePath).send() } -} -bot.subscribeAlways { - if (it.kind == BECOME_OPERATOR) - reply("${it.member.id} 成为了管理员") -} -``` - -1. Clone -2. Import as Gradle project -3. 运行 Demo 程序: [mirai-demo](#mirai-demo) 示例和演示程序 -## Build Requirements +## Libraries used -- Kotlin 1.3.61 -- JDK 8 (required) -- JDK 11(for protocol tools, optional) -- Android SDK 29 (for Android target, optional) - -#### Libraries used 感谢: + - [kotlin-stdlib](https://github.com/JetBrains/kotlin) - [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines) - [kotlinx-io](https://github.com/Kotlin/kotlinx-io) @@ -176,15 +117,21 @@ bot.subscribeAlways { - [toml4j](https://github.com/mwanji/toml4j) - [snakeyaml](https://mvnrepository.com/artifact/org.yaml/snakeyaml) + + ## License + 协议原版权归属腾讯科技股份有限公司所有,本项目其他代码遵守: **GNU AFFERO GENERAL PUBLIC LICENSE version 3** 其中部分要求: + - (见 LICENSE 第 13 节) 尽管本许可协议有其他规定,但如果您修改本程序,则修改后的版本必须显着地为所有通过计算机网络与它进行远程交互的用户(如果您的版本支持这种交互)提供从网络服务器通过一些标准或惯用的软件复制方法**免费**访问相应的**源代码**的机会 - (见 LICENSE 第 4 节) 您可以免费或收费地传递这个项目的源代码或目标代码(即编译结果), **但前提是提供明显的版权声明** (您需要标注本 `GitHub` 项目地址) + + ## Acknowledgement + 特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 为开源项目提供免费的 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 的授权 -[](https://www.jetbrains.com/?from=mirai) - \ No newline at end of file +[](https://www.jetbrains.com/?from=mirai) \ No newline at end of file diff --git a/docs/guide_getting_started.md b/docs/guide_getting_started.md new file mode 100644 index 000000000..3cd88b1db --- /dev/null +++ b/docs/guide_getting_started.md @@ -0,0 +1,129 @@ +# Mirai Guide - Getting Started + +由于Mirai项目在快速推进中,因此内容时有变动,本文档的最后更新日期为```2020-02-20```,对应版本```0.17.0``` + +假如仅仅使用Mirai,不需要对整个项目进行Clone,只需在项目内添加Gradle Dependency或使用即可。 + +下面介绍详细的入门步骤。 + +本页采用Kotlin作为开发语言,**若你希望使用 Java 开发**, 请参阅: [mirai-japt](mirai-japt/README.md) + +## Use Console + +使用mirai-console,以插件形式对服务器功能进行管理,启动无需任何IDE。 + +**由于mirai-console还没有开发完成,暂时不提供入门** + +## Use Loader + +通过编写Kotlin程序启动mirai-core,并定义你的Mirai Bot行为。 + +假如已经对Gradle有一定了解,可跳过1,2 + +### 1 安装IDEA与JDK + +JDK要求6以上 + +### 2 新建Gradle项目 + +- 在```File->new project```中选择```Gradle``` +- 在面板中的```Additional Libraries and Frameworks```中勾选```Java```以及```Kotlin/JVM``` +- 点击```next```,填入```GroupId```与```ArtifactId```(对于测试项目来说,可随意填写) +- 点击```next```,点击```Use default gradle wrapper(recommended)``` +- 创建项目完成 + +### 3 添加依赖 + +- 打开项目的```Project```面板,点击编辑```build.gradle``` + +- 首先添加repositories + + ```groovy + //添加jcenter仓库 + /* + repositories { + mavenCentral() + } + 原文内容,更新为下文 + */ + + repositories { + mavenCentral() + jcenter() + } + ``` + +- 添加依赖,将dependencies部分覆盖。 `mirai-core` 的最新版本为: [![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/) + + ```groovy + dependencies { + implementation 'net.mamoe:mirai-core-qqandroid-jvm:0.17.0'//此处版本应替换为当前最新 + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + testCompile group: 'junit', name: 'junit', version: '4.12' + } + ``` + +- 打开右侧Gradle面板,点击刷新按钮 +- 至此,依赖添加完成 + +### 4 Try Bot + +- 在src/main文件夹下新建文件夹,命名为```kotlin``` +- 在```kotlin```下新建包(在```kotlin```文件夹上右键-```New```-```Packages```) 包名为```net.mamoe.mirai.simpleloader``` + +- 在包下新建kotlin文件```MyLoader.kt``` + +```kotlin +package net.mamoe.mirai.simpleloader + +import kotlinx.coroutines.* +import net.mamoe.mirai.Bot +import net.mamoe.mirai.alsoLogin +import net.mamoe.mirai.event.subscribeMessages + +suspend fun main() { + val qqId = 10000L//Bot的QQ号,需为Long类型,在结尾处添加大写L + val password = "your_password"//Bot的密码 + val miraiBot = Bot(qqId, password).alsoLogin()//新建Bot并登录 + miraiBot.subscribeMessages { + "你好" reply "你好!" + case("at me") { + reply(sender.at() + " 给爷爬 ") + } + + (contains("舔") or contains("刘老板")) { + "刘老板太强了".reply() + } + } + miraiBot.join() // 等待 Bot 离线, 避免主线程退出 +} +``` + +- 单击编辑器内第8行(```suspend fun main```)左侧的run按钮(绿色三角),等待,MiraiBot成功登录。 +- 本例的功能中,在任意群内任意成员发送包含“舔”字或“刘老板”字样的消息,MiraiBot会回复“刘老板太强了” + + + +至此,简单的入门已经结束,下面可根据不同的需求参阅wiki进行功能的添加。 + + +### 此外,还可以使用Maven作为包管理工具 +本项目推荐使用gradle,因此不提供详细入门指导 + +```xml + + + jcenter + https://jcenter.bintray.com/ + + +``` +```xml + + + net.mamoe + mirai-core-qqandroid-jvm + 0.17.0 + + +``` diff --git a/docs/guide_quick_start.md b/docs/guide_quick_start.md new file mode 100644 index 000000000..4602ba6e3 --- /dev/null +++ b/docs/guide_quick_start.md @@ -0,0 +1,114 @@ +# Mirai Guide - Quick Start + +由于Mirai项目在快速推进中,因此内容时有变动,本文档的最后更新日期为```2020-02-20```,对应版本```0.17.0``` + +本文适用于对kotlin较熟悉的开发者 + +**若你希望一份更为基础且详细的guide**, 请参阅: [mirai-guide-getting-started](guide_getting_started.md) + +**若你希望使用 Java 开发**, 请参阅: [mirai-japt](/mirai-japt/README.md) + +## Build Requirements + +- Kotlin 1.3.61 +- JDK 6 (required) +- JDK 11(for protocol tools, optional) +- Android SDK 29 (for Android target, optional) + +## Use directly + +**直接使用 Mirai(终端环境/网页面板(将来)).** +[Mirai-Console](https://github.com/mamoe/mirai/tree/master/mirai-console) 插件支持, 在终端中启动 Mirai 并获得机器人服务 +本模块还未完善。 + +## Use as a library + +**mirai-core 为独立设计, 可以作为库内置于任意 Java(JVM)/Android 项目中使用.** + +请将 `VERSION` 替换为最新的版本(如 `0.15.0`): +[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/) +**Mirai 目前还处于实验性阶段, 我们无法保证任何稳定性, API 也可能会随时修改.** + +### Maven + +Kotlin 在 Maven 上只支持 JVM 平台. + +```xml + + + jcenter + https://jcenter.bintray.com/ + + +``` + +```xml + + + net.mamoe + mirai-core-qqandroid-jvm + 0.15.1 + + +``` + +### Gradle + +Mirai 只发布在 `jcenter`, 因此请确保添加 `jcenter()` 仓库: + +```kotlin +repositories{ + jcenter() +} +``` + +若您需要使用在跨平台项目, 则要对各个目标平台添加不同的依赖,这与 kotlin 相关多平台库的依赖是类似的。 +**若您只需要使用在单一平台, 则只需要添加一项该平台的依赖.** + +**注意:** +Mirai 核心由 API 模块(`mirai-core`)和协议模块组成。 +只添加 API 模块将无法正常工作。 +现在只推荐使用 QQAndroid 协议,请参照下文选择对应目标平台的依赖添加。 + +**jvm** (JVM 平台) + +```kotlin +implementation("net.mamoe:mirai-core-qqandroid-jvm:VERSION") +``` + +**common** (通用平台) + +```kotlin +implementation("net.mamoe:mirai-core-qqandroid-common:VERSION") +``` + +**android** (Android 平台) + +```kotlin +implementation("net.mamoe:mirai-core-qqandroid-android:VERSION") +``` + +## Try + +### On JVM or Android + +现在体验低付出高效率的 Mirai + +```kotlin +val bot = Bot(qqId, password).alsoLogin() +bot.subscribeMessages { + "你好" reply "你好!" + "profile" reply { sender.queryProfile() } + contains("图片"){ File(imagePath).send() } +} +bot.subscribeAlways { + if (it.kind == BECOME_OPERATOR) + reply("${it.member.id} 成为了管理员") +} +``` + +### Performance + +Android 上, Mirai 运行需使用 80M 内存. +JVM 上启动需 80M 内存, 每多一个机器人实例需要 30M 内存. + diff --git a/gradle.properties b/gradle.properties index 525f0504f..4a4049d31 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # style guide kotlin.code.style=official # config -mirai_version=0.15.2 +mirai_version=0.17.0 mirai_japt_version=1.0.1 kotlin.incremental.multiplatform=true kotlin.parallel.tasks.in.project=true diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 6d58f8d23..842ca6910 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -94,7 +94,7 @@ publishing { it.artifactId = "$project.name-common" break case 'jvm': - it.artifactId = "${project.name.replace("-jvm", "")}" + it.artifactId = "$project.name-jvm" break case 'js': case 'native': diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3dbe6db2a..6768581dc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Thu Feb 06 14:10:33 CST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/mirai-api-http/README_CH.md b/mirai-api-http/README_CH.md index d056170e4..b5454b89e 100644 --- a/mirai-api-http/README_CH.md +++ b/mirai-api-http/README_CH.md @@ -12,10 +12,12 @@ fun main() { MiraiHttpAPIServer.start() - bot.network.awaitDisconnection() + bot.join() } ``` + + ## 认证相关 ### 开始会话-认证(Authorize) @@ -141,6 +143,8 @@ fun main() { > SessionKey与Bot 对应错误时将会返回状态码5:指定对象不存在 + + ## 消息相关 @@ -261,10 +265,10 @@ fun main() { ### 发送图片消息(通过URL) ``` -[POST] /sendGroupMessage +[POST] /sendImageMessage ``` -使用此方法向指定群发送消息 +使用此方法向指定对象(群或好友)发送图片消息 #### 请求 @@ -303,7 +307,7 @@ fun main() { ### 图片文件上传 ``` -[POST] /sendGroupMessage +[POST] /uploadImage ``` 使用此方法上传图片文件至服务器并返回ImageId @@ -414,10 +418,10 @@ Content-Type:multipart/form-data } ``` -| 名字 | 类型 | 说明 | -| ------- | ------ | ------------------------- | -| target | Long | 群员QQ号 | -| display | String | @时显示的文本如:"@Mirai" | +| 名字 | 类型 | 说明 | +| ------- | ------ | ---------------------------------------------- | +| target | Long | 群员QQ号 | +| dispaly | String | At时显示的文字,发送消息时无效,自动使用群名片 | #### AtAll @@ -463,7 +467,7 @@ Content-Type:multipart/form-data { "type": "Image", "imageId": "{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png" //群图片格式 - "imageId": "/f8f1ab55-bf8e-4236-b55e-955848d7069f" //好友图片格式 + //"imageId": "/f8f1ab55-bf8e-4236-b55e-955848d7069f" //好友图片格式 } ``` @@ -517,6 +521,7 @@ Content-Type:multipart/form-data ``` + ### 获取群列表 使用此方法获取bot的群列表 @@ -786,6 +791,8 @@ Content-Type:multipart/form-data } ``` + + ### 获取群设置 使用此方法获取群设置 @@ -816,6 +823,7 @@ Content-Type:multipart/form-data ``` + ### 修改群员资料 使用此方法修改群员资料(需要有相关限权) @@ -856,6 +864,8 @@ Content-Type:multipart/form-data } ``` + + ### 获取群员资料 使用此方法获取群员资料 diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt index fda1a0395..32f7aeeb4 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/Session.kt @@ -13,6 +13,8 @@ import kotlinx.coroutines.* import net.mamoe.mirai.Bot import net.mamoe.mirai.api.http.queue.MessageQueue import net.mamoe.mirai.event.Listener +import net.mamoe.mirai.event.events.BotEvent +import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.event.subscribeMessages import net.mamoe.mirai.message.MessagePacket import kotlin.coroutines.CoroutineContext @@ -102,12 +104,10 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext) : Session(coroutineContext) { val messageQueue = MessageQueue() - private val _listener: Listener> + private val _listener: Listener init { - bot.subscribeMessages { - _listener = always { this.run(messageQueue::add) } // this aka messagePacket - } + _listener = bot.subscribeAlways{ this.run(messageQueue::add) } } override fun close() { diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/BotEventDTO.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/BotEventDTO.kt new file mode 100644 index 000000000..ded3ea9a4 --- /dev/null +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/BotEventDTO.kt @@ -0,0 +1,118 @@ +package net.mamoe.mirai.api.http.data.common + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.event.events.BotEvent +import net.mamoe.mirai.event.events.* +import net.mamoe.mirai.message.MessagePacket +import net.mamoe.mirai.utils.MiraiExperimentalAPI + +@Serializable +sealed class BotEventDTO : EventDTO() + +@UseExperimental(MiraiExperimentalAPI::class) +fun BotEvent.toDTO() = when(this) { + is MessagePacket<*, *> -> toDTO() + else -> when(this) { + is BotOnlineEvent -> BotOnlineEventDTO(bot.uin) + is BotOfflineEvent.Active -> BotOfflineEventActiveDTO(bot.uin) + is BotOfflineEvent.Force -> BotOfflineEventForceDTO(bot.uin, title, message) + is BotOfflineEvent.Dropped -> BotOfflineEventDroppedDTO(bot.uin) + is BotReloginEvent -> BotReloginEventDTO(bot.uin) +// is MessageSendEvent.GroupMessageSendEvent -> {} +// is MessageSendEvent.FriendMessageSendEvent -> {} +// is BeforeImageUploadEvent -> {} +// is ImageUploadEvent.Succeed -> {} + is BotGroupPermissionChangeEvent -> BotGroupPermissionChangeEventDTO(origin, new, GroupDTO(group)) + is BotMuteEvent -> BotMuteEventDTO(durationSeconds, MemberDTO(operator)) + is BotUnmuteEvent -> BotUnmuteEventDTO(MemberDTO(operator)) + is BotJoinGroupEvent -> BotJoinGroupEventDTO(GroupDTO(group)) +// is GroupSettingChangeEvent<*> -> {} // 不知道会改什么 + is GroupNameChangeEvent -> GroupNameChangeEventDTO(origin, new, GroupDTO(group), isByBot) + is GroupEntranceAnnouncementChangeEvent -> GroupEntranceAnnouncementChangeEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO)) + is GroupMuteAllEvent -> GroupMuteAllEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO)) + is GroupAllowAnonymousChatEvent -> GroupAllowAnonymousChatEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO)) + is GroupAllowConfessTalkEvent -> GroupAllowConfessTalkEventDTO(origin, new, GroupDTO(group), isByBot) + is GroupAllowMemberInviteEvent -> GroupAllowMemberInviteEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO)) + is MemberJoinEvent -> MemberJoinEventDTO(MemberDTO(member)) + is MemberLeaveEvent.Kick -> MemberLeaveEventKickDTO(MemberDTO(member), operator?.let(::MemberDTO)) + is MemberLeaveEvent.Quit -> MemberLeaveEventQuitDTO(MemberDTO(member)) + is MemberCardChangeEvent -> MemberCardChangeEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO)) + is MemberSpecialTitleChangeEvent -> MemberSpecialTitleChangeEventDTO(origin, new, MemberDTO(member)) + is MemberPermissionChangeEvent -> MemberPermissionChangeEventDTO(origin, new, MemberDTO(member)) + is MemberMuteEvent -> MemberMuteEventDTO(durationSeconds, MemberDTO(member), operator?.let(::MemberDTO)) + is MemberUnmuteEvent -> MemberUnmuteEventDTO(MemberDTO(member), operator?.let(::MemberDTO)) + else -> IgnoreEventDTO + } +} + +@Serializable +@SerialName("BotOnlineEvent") +data class BotOnlineEventDTO(val qq: Long) : BotEventDTO() +@Serializable +@SerialName("BotOfflineEventActive") +data class BotOfflineEventActiveDTO(val qq: Long) : BotEventDTO() +@Serializable +@SerialName("BotOfflineEventForce") +data class BotOfflineEventForceDTO(val qq: Long, val title: String, val message: String) : BotEventDTO() +@Serializable +@SerialName("BotOfflineEventDropped") +data class BotOfflineEventDroppedDTO(val qq: Long) : BotEventDTO() +@Serializable +@SerialName("BotReloginEvent") +data class BotReloginEventDTO(val qq: Long) : BotEventDTO() +@Serializable +@SerialName("BotGroupPermissionChangeEvent") +data class BotGroupPermissionChangeEventDTO(val origin: MemberPermission, val new: MemberPermission, val group: GroupDTO) : BotEventDTO() +@Serializable +@SerialName("BotMuteEvent") +data class BotMuteEventDTO(val durationSeconds: Int, val operator: MemberDTO) : BotEventDTO() +@Serializable +@SerialName("BotUnmuteEvent") +data class BotUnmuteEventDTO(val operator: MemberDTO) : BotEventDTO() +@Serializable +@SerialName("BotJoinGroupEvent") +data class BotJoinGroupEventDTO(val group: GroupDTO) : BotEventDTO() +@Serializable +@SerialName("GroupNameChangeEvent") +data class GroupNameChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val isByBot: Boolean) : BotEventDTO() +@Serializable +@SerialName("GroupEntranceAnnouncementChangeEvent") +data class GroupEntranceAnnouncementChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO() +@Serializable +@SerialName("GroupMuteAllEvent") +data class GroupMuteAllEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO() +@Serializable +@SerialName("GroupAllowAnonymousChatEvent") +data class GroupAllowAnonymousChatEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO() +@Serializable +@SerialName("GroupAllowConfessTalkEvent") +data class GroupAllowConfessTalkEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val isByBot: Boolean) : BotEventDTO() +@Serializable +@SerialName("GroupAllowMemberInviteEvent") +data class GroupAllowMemberInviteEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO() +@Serializable +@SerialName("MemberJoinEvent") +data class MemberJoinEventDTO(val member: MemberDTO) : BotEventDTO() +@Serializable +@SerialName("MemberLeaveEventKick") +data class MemberLeaveEventKickDTO(val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO() +@Serializable +@SerialName("MemberLeaveEventQuit") +data class MemberLeaveEventQuitDTO(val member: MemberDTO) : BotEventDTO() +@Serializable +@SerialName("MemberCardChangeEvent") +data class MemberCardChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO() +@Serializable +@SerialName("MemberSpecialTitleChangeEvent") +data class MemberSpecialTitleChangeEventDTO(val origin: String, val new: String, val member: MemberDTO) : BotEventDTO() +@Serializable +@SerialName("MemberPermissionChangeEvent") +data class MemberPermissionChangeEventDTO(val origin: MemberPermission, val new: MemberPermission, val member: MemberDTO) : BotEventDTO() +@Serializable +@SerialName("MemberMuteEvent") +data class MemberMuteEventDTO(val durationSeconds: Int, val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO() +@Serializable +@SerialName("MemberUnmuteEvent") +data class MemberUnmuteEventDTO(val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO() diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/ContactDTO.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/ContactDTO.kt index 3381390fb..8b538c35f 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/ContactDTO.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/ContactDTO.kt @@ -37,7 +37,7 @@ data class MemberDTO( val group: GroupDTO ) : ContactDTO() { constructor(member: Member) : this( - member.id, member.groupCardOrNick, member.permission, + member.id, member.nameCardOrNick, member.permission, GroupDTO(member.group) ) } diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/DTO.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/DTO.kt index 157f739bc..c6339a200 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/DTO.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/DTO.kt @@ -1,8 +1,7 @@ package net.mamoe.mirai.api.http.data.common -import kotlinx.serialization.* -import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import net.mamoe.mirai.api.http.AuthedSession interface DTO @@ -16,3 +15,8 @@ abstract class VerifyDTO : DTO { @Transient lateinit var session: AuthedSession // 反序列化验证成功后传入 } + +@Serializable +abstract class EventDTO : DTO + +object IgnoreEventDTO : EventDTO() \ No newline at end of file diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/MessageDTO.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/MessageDTO.kt index f50f00e11..61ebc46f8 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/MessageDTO.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/data/common/MessageDTO.kt @@ -11,6 +11,8 @@ package net.mamoe.mirai.api.http.data.common import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import net.mamoe.mirai.contact.Contact +import net.mamoe.mirai.contact.Group import net.mamoe.mirai.message.FriendMessage import net.mamoe.mirai.message.GroupMessage import net.mamoe.mirai.message.MessagePacket @@ -30,32 +32,36 @@ data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO() @SerialName("GroupMessage") data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO() -@Serializable -@SerialName("UnKnownMessage") -data class UnKnownMessagePacketDTO(val msg: String) : MessagePacketDTO() // Message @Serializable @SerialName("Source") data class MessageSourceDTO(val uid: Long) : MessageDTO() + @Serializable @SerialName("At") -data class AtDTO(val target: Long, val display: String) : MessageDTO() +data class AtDTO(val target: Long, val display: String = "") : MessageDTO() + @Serializable @SerialName("AtAll") data class AtAllDTO(val target: Long = 0) : MessageDTO() // target为保留字段 + @Serializable @SerialName("Face") data class FaceDTO(val faceId: Int) : MessageDTO() + @Serializable @SerialName("Plain") data class PlainDTO(val text: String) : MessageDTO() + @Serializable @SerialName("Image") data class ImageDTO(val imageId: String) : MessageDTO() + @Serializable @SerialName("Xml") data class XmlDTO(val xml: String) : MessageDTO() + @Serializable @SerialName("Unknown") data class UnknownMessageDTO(val text: String) : MessageDTO() @@ -64,11 +70,11 @@ data class UnknownMessageDTO(val text: String) : MessageDTO() * Abstract Class * */ @Serializable -sealed class MessagePacketDTO : DTO { - lateinit var messageChain : MessageChainDTO +sealed class MessagePacketDTO : EventDTO() { + lateinit var messageChain: MessageChainDTO } -typealias MessageChainDTO = Array +typealias MessageChainDTO = List @Serializable sealed class MessageDTO : DTO @@ -77,21 +83,25 @@ sealed class MessageDTO : DTO /* Extend function */ -suspend fun MessagePacket<*, *>.toDTO(): MessagePacketDTO = when (this) { +fun MessagePacket<*, *>.toDTO() = when (this) { is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender)) is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender)) - else -> UnKnownMessagePacketDTO("UnKnown Message Packet") -}.apply { messageChain = Array(message.size){ message[it].toDTO() }} + else -> IgnoreEventDTO +}.apply { + if (this is MessagePacketDTO) { + messageChain = mutableListOf().also { ls -> message.foreachContent { ls.add(it.toDTO()) } } + } +} -fun MessageChainDTO.toMessageChain() = - MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage()) } } +fun MessageChainDTO.toMessageChain(contact: Contact) = + MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage(contact)) } } @UseExperimental(ExperimentalUnsignedTypes::class) fun Message.toDTO() = when (this) { is MessageSource -> MessageSourceDTO(messageUid) is At -> AtDTO(target, display) is AtAll -> AtAllDTO(0L) - is Face -> FaceDTO(id.value.toInt()) + is Face -> FaceDTO(id) is PlainText -> PlainDTO(stringValue) is Image -> ImageDTO(imageId) is XMLMessage -> XmlDTO(stringValue) @@ -99,15 +109,13 @@ fun Message.toDTO() = when (this) { } @UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class) -fun MessageDTO.toMessage() = when (this) { - is AtDTO -> At(target, display) +fun MessageDTO.toMessage(contact: Contact) = when (this) { + is AtDTO -> At((contact as Group)[target]) is AtAllDTO -> AtAll - is FaceDTO -> Face(FaceId(faceId.toUByte())) + is FaceDTO -> Face(faceId) is PlainDTO -> PlainText(text) is ImageDTO -> Image(imageId) is XmlDTO -> XMLMessage(xml) is MessageSourceDTO, is UnknownMessageDTO -> PlainText("assert cannot reach") } - - diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/queue/MessageQueue.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/queue/MessageQueue.kt index e3a1637ef..eee8f85de 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/queue/MessageQueue.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/queue/MessageQueue.kt @@ -9,26 +9,35 @@ package net.mamoe.mirai.api.http.queue +import net.mamoe.mirai.api.http.data.common.EventDTO +import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO +import net.mamoe.mirai.api.http.data.common.toDTO +import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.message.GroupMessage -import net.mamoe.mirai.message.MessagePacket import net.mamoe.mirai.message.data.MessageSource import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedDeque -class MessageQueue : ConcurrentLinkedDeque>() { +class MessageQueue : ConcurrentLinkedDeque() { val quoteCache = ConcurrentHashMap() - fun fetch(size: Int): List> { + fun fetch(size: Int): List { var count = size quoteCache.clear() - val ret = ArrayList>(count) - while (!this.isEmpty() && count-- > 0) { - val packet = pop() - ret.add(packet) + val ret = ArrayList(count) + while (!this.isEmpty() && count > 0) { + val event = pop() - if (packet is GroupMessage) { - addCache(packet) + event.toDTO().also { + if (it != IgnoreEventDTO) { + ret.add(it) + count-- + } + } + + if (event is GroupMessage) { + addCache(event) } } return ret diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt index 40caf465d..45adcf2b3 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/AuthRouteModule.kt @@ -18,6 +18,7 @@ import net.mamoe.mirai.api.http.AuthedSession import net.mamoe.mirai.api.http.SessionManager import net.mamoe.mirai.api.http.data.NoSuchBotException import net.mamoe.mirai.api.http.data.StateCode +import net.mamoe.mirai.api.http.data.common.DTO import net.mamoe.mirai.api.http.data.common.VerifyDTO import kotlin.coroutines.EmptyCoroutineContext @@ -28,7 +29,7 @@ fun Application.authModule() { if (it.authKey != SessionManager.authKey) { call.respondStateCode(StateCode(1, "Auth Key错误")) } else { - call.respondStateCode(StateCode(0, SessionManager.createTempSession().key)) + call.respondDTO(AuthRetDTO(0, SessionManager.createTempSession().key)) } } @@ -55,6 +56,9 @@ fun Application.authModule() { } } +@Serializable +private data class AuthRetDTO(val code: Int, val session: String) : DTO + @Serializable private data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO() diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/SendMessageRouteModule.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/SendMessageRouteModule.kt index aa2beb5b8..be9be7b03 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/SendMessageRouteModule.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/route/SendMessageRouteModule.kt @@ -37,24 +37,28 @@ fun Application.messageModule() { miraiGet("/fetchMessage") { val count: Int = paramOrNull("count") val fetch = it.messageQueue.fetch(count) - val ls = Array(fetch.size) { index -> fetch[index].toDTO() } - call.respondJson(ls.toList().toJson()) + call.respondJson(fetch.toJson()) } miraiVerify("/sendFriendMessage") { - it.session.bot.getFriend(it.target).sendMessage(it.messageChain.toMessageChain()) + it.session.bot.getFriend(it.target).apply { + sendMessage(it.messageChain.toMessageChain(this)) // this aka QQ + } call.respondStateCode(StateCode.Success) } miraiVerify("/sendGroupMessage") { - it.session.bot.getGroup(it.target).sendMessage(it.messageChain.toMessageChain()) + it.session.bot.getGroup(it.target).apply { + sendMessage(it.messageChain.toMessageChain(this)) // this aka Group + } call.respondStateCode(StateCode.Success) } miraiVerify("/quoteMessage") { - it.session.messageQueue.quoteCache[it.target]?.quoteReply(it.messageChain.toMessageChain()) - ?: throw NoSuchElementException() + it.session.messageQueue.quoteCache[it.target]?.apply { + quoteReply(it.messageChain.toMessageChain(group)) + } ?: throw NoSuchElementException() call.respondStateCode(StateCode.Success) } diff --git a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/Json.kt b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/Json.kt index e46fbb1f5..7769a411e 100644 --- a/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/Json.kt +++ b/mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/util/Json.kt @@ -13,7 +13,6 @@ import kotlinx.serialization.* import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import net.mamoe.mirai.api.http.data.common.* -import net.mamoe.mirai.message.data.MessageSource // 解析失败时直接返回null,由路由判断响应400状态 @UseExperimental(ImplicitReflectionSerializer::class) @@ -45,20 +44,46 @@ else MiraiJson.json.stringify(serializer, this) */ object MiraiJson { val json = Json(context = SerializersModule { - polymorphic(MessagePacketDTO.serializer()) { + + polymorphic(EventDTO.serializer()) { GroupMessagePacketDTO::class with GroupMessagePacketDTO.serializer() FriendMessagePacketDTO::class with FriendMessagePacketDTO.serializer() - UnKnownMessagePacketDTO::class with UnKnownMessagePacketDTO.serializer() - } - polymorphic(MessageDTO.serializer()) { - MessageSourceDTO::class with MessageSourceDTO.serializer() - AtDTO::class with AtDTO.serializer() - AtAllDTO::class with AtAllDTO.serializer() - FaceDTO::class with FaceDTO.serializer() - PlainDTO::class with PlainDTO.serializer() - ImageDTO::class with ImageDTO.serializer() - XmlDTO::class with XmlDTO.serializer() - UnknownMessageDTO::class with UnknownMessageDTO.serializer() + + BotOnlineEventDTO::class with BotOnlineEventDTO.serializer() + BotOfflineEventActiveDTO::class with BotOfflineEventActiveDTO.serializer() + BotOfflineEventForceDTO::class with BotOfflineEventForceDTO.serializer() + BotOfflineEventDroppedDTO::class with BotOfflineEventDroppedDTO.serializer() + BotReloginEventDTO::class with BotReloginEventDTO.serializer() + BotGroupPermissionChangeEventDTO::class with BotGroupPermissionChangeEventDTO.serializer() + BotMuteEventDTO::class with BotMuteEventDTO.serializer() + BotUnmuteEventDTO::class with BotUnmuteEventDTO.serializer() + BotJoinGroupEventDTO::class with BotJoinGroupEventDTO.serializer() + GroupNameChangeEventDTO::class with GroupNameChangeEventDTO.serializer() + GroupEntranceAnnouncementChangeEventDTO::class with GroupEntranceAnnouncementChangeEventDTO.serializer() + GroupMuteAllEventDTO::class with GroupMuteAllEventDTO.serializer() + GroupAllowAnonymousChatEventDTO::class with GroupAllowAnonymousChatEventDTO.serializer() + GroupAllowConfessTalkEventDTO::class with GroupAllowConfessTalkEventDTO.serializer() + GroupAllowMemberInviteEventDTO::class with GroupAllowMemberInviteEventDTO.serializer() + MemberJoinEventDTO::class with MemberJoinEventDTO.serializer() + MemberLeaveEventKickDTO::class with MemberLeaveEventKickDTO.serializer() + MemberLeaveEventQuitDTO::class with MemberLeaveEventQuitDTO.serializer() + MemberCardChangeEventDTO::class with MemberCardChangeEventDTO.serializer() + MemberSpecialTitleChangeEventDTO::class with MemberSpecialTitleChangeEventDTO.serializer() + MemberPermissionChangeEventDTO::class with MemberPermissionChangeEventDTO.serializer() + MemberMuteEventDTO::class with MemberMuteEventDTO.serializer() + MemberUnmuteEventDTO::class with MemberUnmuteEventDTO.serializer() } + + // Message Polymorphic +// polymorphic(MessageDTO.serializer()) { +// MessageSourceDTO::class with MessageSourceDTO.serializer() +// AtDTO::class with AtDTO.serializer() +// AtAllDTO::class with AtAllDTO.serializer() +// FaceDTO::class with FaceDTO.serializer() +// PlainDTO::class with PlainDTO.serializer() +// ImageDTO::class with ImageDTO.serializer() +// XmlDTO::class with XmlDTO.serializer() +// UnknownMessageDTO::class with UnknownMessageDTO.serializer() +// } }) } \ No newline at end of file diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt index 7e5633dee..1a45580fa 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/MiraiGraphical.kt @@ -2,7 +2,8 @@ package net.mamoe.mirai.console.graphical import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController -import net.mamoe.mirai.console.graphical.view.PrimaryView +import net.mamoe.mirai.console.graphical.styleSheet.PrimaryStyleSheet +import net.mamoe.mirai.console.graphical.view.Decorator import tornadofx.App import tornadofx.find import tornadofx.launch @@ -11,7 +12,7 @@ fun main(args: Array) { launch(args) } -class MiraiGraphicalUI: App(PrimaryView::class) { +class MiraiGraphicalUI : App(Decorator::class, PrimaryStyleSheet::class) { override fun init() { super.init() @@ -23,4 +24,4 @@ class MiraiGraphicalUI: App(PrimaryView::class) { super.stop() MiraiConsole.stop() } -} \ No newline at end of file +} diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt index 8d15d6998..542f8b146 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/model/BotModel.kt @@ -11,3 +11,9 @@ class BotModel(val uin: Long) { val logHistory = observableListOf() val admins = observableListOf() } + +class BotViewModel(botModel: BotModel? = null) : ItemViewModel(botModel) { + val bot = bind(BotModel::botProperty) + val logHistory = bind(BotModel::logHistory) + val admins = bind(BotModel::admins) +} \ No newline at end of file diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/LoginViewStyleSheet.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/LoginViewStyleSheet.kt new file mode 100644 index 000000000..6e0b1f69c --- /dev/null +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/LoginViewStyleSheet.kt @@ -0,0 +1,47 @@ +package net.mamoe.mirai.console.graphical.styleSheet + +import javafx.scene.Cursor +import javafx.scene.effect.BlurType +import javafx.scene.effect.DropShadow +import javafx.scene.paint.Color +import javafx.scene.text.FontWeight +import tornadofx.* + +class LoginViewStyleSheet : Stylesheet() { + + companion object { + val vBox by csselement("VBox") + } + + init { + + vBox { + maxWidth = 500.px + maxHeight = 500.px + + backgroundColor += c("39c5BB", 0.3) + backgroundRadius += box(15.px) + + padding = box(50.px, 100.px) + spacing = 25.px + + borderRadius += box(15.px) + effect = DropShadow(BlurType.THREE_PASS_BOX, Color.GRAY, 10.0, 0.0, 15.0, 15.0) + } + + textField { + prefHeight = 30.px + textFill = Color.BLACK + fontWeight = FontWeight.BOLD + } + + button { + backgroundColor += c("00BCD4", 0.8) + padding = box(10.px, 0.px) + prefWidth = 500.px + textFill = Color.WHITE + fontWeight = FontWeight.BOLD + cursor = Cursor.HAND + } + } +} \ No newline at end of file diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/PrimaryStyleSheet.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/PrimaryStyleSheet.kt new file mode 100644 index 000000000..8f47b7a39 --- /dev/null +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/styleSheet/PrimaryStyleSheet.kt @@ -0,0 +1,21 @@ +package net.mamoe.mirai.console.graphical.styleSheet + +import tornadofx.* + +class PrimaryStyleSheet : Stylesheet() { + companion object { + val jfxTitle by cssclass("jfx-decorator-buttons-container") + val container by cssclass("jfx-decorator-content-container") + } + + init { + jfxTitle { + backgroundColor += c("00BCD4") + } + + container { + borderColor += box(c("00BCD4")) + borderWidth += box(0.px, 4.px, 4.px, 4.px) + } + } +} \ No newline at end of file diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt index 94663e510..5590d9c44 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/util/JFoenixAdaptor.kt @@ -17,20 +17,20 @@ internal fun EventTarget.jfxButton(text: String = "", graphic: Node? = null, op: if (graphic != null) it.graphic = graphic } -fun EventTarget.jfxTextfield(value: String? = null, op: TextField.() -> Unit = {}) = JFXTextField().attachTo(this, op) { +fun EventTarget.jfxTextfield(value: String? = null, op: JFXTextField.() -> Unit = {}) = JFXTextField().attachTo(this, op) { if (value != null) it.text = value } -fun EventTarget.jfxTextfield(property: ObservableValue, op: TextField.() -> Unit = {}) = jfxTextfield().apply { +fun EventTarget.jfxTextfield(property: ObservableValue, op: JFXTextField.() -> Unit = {}) = jfxTextfield().apply { bind(property) op(this) } -fun EventTarget.jfxPasswordfield(value: String? = null, op: TextField.() -> Unit = {}) = JFXPasswordField().attachTo(this, op) { +fun EventTarget.jfxPasswordfield(value: String? = null, op: JFXPasswordField.() -> Unit = {}) = JFXPasswordField().attachTo(this, op) { if (value != null) it.text = value } -fun EventTarget.jfxPasswordfield(property: ObservableValue, op: TextField.() -> Unit = {}) = jfxPasswordfield().apply { +fun EventTarget.jfxPasswordfield(property: ObservableValue, op: JFXPasswordField.() -> Unit = {}) = jfxPasswordfield().apply { bind(property) op(this) } diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt new file mode 100644 index 000000000..9ecff5fda --- /dev/null +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/Decorator.kt @@ -0,0 +1,9 @@ +package net.mamoe.mirai.console.graphical.view + +import com.jfoenix.controls.JFXDecorator +import tornadofx.View + +class Decorator: View() { + + override val root = JFXDecorator(primaryStage, find().root) +} \ No newline at end of file diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginFragment.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginFragment.kt deleted file mode 100644 index 437031ca4..000000000 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginFragment.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.mamoe.mirai.console.graphical.view - -import javafx.beans.property.SimpleStringProperty -import kotlinx.coroutines.runBlocking -import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController -import net.mamoe.mirai.console.graphical.util.jfxButton -import net.mamoe.mirai.console.graphical.util.jfxPasswordfield -import net.mamoe.mirai.console.graphical.util.jfxTextfield -import tornadofx.* - -class LoginFragment : Fragment() { - - private val controller = find(FX.defaultScope) - private val qq = SimpleStringProperty("0") - private val psd = SimpleStringProperty("") - - override val root = form { - fieldset("登录") { - field("QQ") { - jfxTextfield(qq) - } - field("密码") { - jfxPasswordfield(psd) - } - } - jfxButton("登录").action { - runBlocking { - controller.login(qq.value, psd.value) - } - close() - } - } -} \ No newline at end of file diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt new file mode 100644 index 000000000..46b9d59dc --- /dev/null +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/LoginView.kt @@ -0,0 +1,50 @@ +package net.mamoe.mirai.console.graphical.view + +import javafx.beans.property.SimpleStringProperty +import javafx.geometry.Pos +import javafx.scene.image.Image +import kotlinx.coroutines.runBlocking +import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController +import net.mamoe.mirai.console.graphical.styleSheet.LoginViewStyleSheet +import net.mamoe.mirai.console.graphical.util.jfxButton +import net.mamoe.mirai.console.graphical.util.jfxPasswordfield +import net.mamoe.mirai.console.graphical.util.jfxTextfield +import tornadofx.* + +class LoginView : View("CNM") { + + private val controller = find() + private val qq = SimpleStringProperty("") + private val psd = SimpleStringProperty("") + + override val root = borderpane { + + addStylesheet(LoginViewStyleSheet::class) + + center = vbox { + + imageview(Image(LoginView::class.java.classLoader.getResourceAsStream("character.png"))) { + alignment = Pos.CENTER + } + + jfxTextfield(qq) { + promptText = "QQ" + isLabelFloat = true + } + + jfxPasswordfield(psd) { + promptText = "Password" + isLabelFloat = true + } + + jfxButton("Login").action { + runAsync { + runBlocking { controller.login(qq.value, psd.value) } + }.ui { + qq.value = "" + psd.value = "" + } + } + } + } +} \ No newline at end of file diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt index 58036ac6e..1a81c3721 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt @@ -1,20 +1,15 @@ package net.mamoe.mirai.console.graphical.view -import com.jfoenix.controls.JFXListCell -import javafx.geometry.Insets -import javafx.geometry.Pos +import com.jfoenix.controls.* +import javafx.collections.ObservableList import javafx.scene.control.Tab import javafx.scene.control.TabPane import javafx.scene.image.Image -import javafx.scene.paint.Color -import javafx.scene.text.FontWeight import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController import net.mamoe.mirai.console.graphical.model.BotModel -import net.mamoe.mirai.console.graphical.util.jfxButton import net.mamoe.mirai.console.graphical.util.jfxListView import net.mamoe.mirai.console.graphical.util.jfxTabPane import tornadofx.* -import java.io.FileInputStream class PrimaryView : View() { @@ -35,20 +30,12 @@ class PrimaryView : View() { setCellFactory { object : JFXListCell() { - var tab: Tab? = null - init { onDoubleClick { - if (tab == null) { - (center as TabPane).tab(item.uin.toString()) { - listview(item.logHistory) - onDoubleClick { close() } - tab = this - } - } else { - (center as TabPane).tabs.add(tab) - } - tab?.select() + (center as TabPane).logTab( + text = item.uin.toString(), + logs = item.logHistory + ).select() } } @@ -65,44 +52,37 @@ class PrimaryView : View() { } } } - - hbox { - padding = Insets(10.0) - spacing = 10.0 - alignment = Pos.CENTER - - jfxButton("L").action { - find().openModal() - } - jfxButton("P") - jfxButton("S") - - - style { backgroundColor += c("00BCD4") } - children.style(true) { - backgroundColor += c("00BCD4") - fontSize = 15.px - fontWeight = FontWeight.BOLD - textFill = Color.WHITE - borderRadius += box(25.px) - backgroundRadius += box(25.px) - } - } } center = jfxTabPane { - tab("Main") { - listview(controller.mainLog) { - fitToParentSize() - cellFormat { - graphic = label(it) { - maxWidthProperty().bind(this@listview.widthProperty()) - isWrapText = true - } - } - } + tab("Login") { + this += find().root } + + tab("Plugin") + + tab("Settings") + + logTab("Main", controller.mainLog) } } } + +private fun TabPane.logTab( + text: String? = null, + logs: ObservableList, + op: Tab.() -> Unit = {} +)= tab(text) { + listview(logs) { + + fitToParentSize() + cellFormat { + graphic = label(it) { + maxWidthProperty().bind(this@listview.widthProperty()) + isWrapText = true + } + } + } + also(op) +} \ No newline at end of file diff --git a/mirai-console-graphical/src/main/resources/character.png b/mirai-console-graphical/src/main/resources/character.png new file mode 100644 index 000000000..b55153f1e Binary files /dev/null and b/mirai-console-graphical/src/main/resources/character.png differ diff --git a/mirai-console-graphical/src/main/resources/logo.png b/mirai-console-graphical/src/main/resources/logo.png index 7be6c6871..bded5f137 100644 Binary files a/mirai-console-graphical/src/main/resources/logo.png and b/mirai-console-graphical/src/main/resources/logo.png differ diff --git a/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt b/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt index e20fc6372..8998c0b83 100644 --- a/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt +++ b/mirai-console-terminal/src/main/kotlin/net/mamoe/mirai/console/MiraiConsoleTerminalUI.kt @@ -187,8 +187,8 @@ object MiraiConsoleTerminalUI : MiraiConsoleUI { override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? { pushLog(0, "[Login Solver]需要进行账户安全认证") - pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登陆地点]/[不常用设备登陆]的问题") - pushLog(0, "[Login Solver]完成以下账号认证即可成功登陆|理论本认证在mirai每个账户中最多出现1次") + pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题") + pushLog(0, "[Login Solver]完成以下账号认证即可成功登录|理论本认证在mirai每个账户中最多出现1次") pushLog(0, "[Login Solver]请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符") pushLog(0, "[Login Solver]这步操作将在后续的版本中优化") pushLog(0, url) diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt index e58d65a38..f1049b290 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/MiraiConsole.kt @@ -77,7 +77,7 @@ object MiraiConsole { logger("Mirai-console 启动完成") logger("\"/login qqnumber qqpassword \" to login a bot") - logger("\"/login qq号 qq密码 \" 来登陆一个BOT") + logger("\"/login qq号 qq密码 \" 来登录一个BOT") } fun stop() { @@ -208,7 +208,7 @@ object MiraiConsole { } val bot: Bot? = if (it.size == 2) { if (bots.size == 0) { - logger("还没有BOT登陆") + logger("还没有BOT登录") return@onCommand false } bots[0].get() diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt index 5cad965a2..29a17b2c9 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt @@ -162,16 +162,11 @@ internal class QQImpl( TODO("not implemented") } - override fun equals(other: Any?): Boolean { - if (this === other) return true - return other is QQ && other.id == this.id - } - - override fun hashCode(): Int = super.hashCode() + override fun toString(): String = "QQ($id)" } -@Suppress("MemberVisibilityCanBePrivate") +@Suppress("MemberVisibilityCanBePrivate", "DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE") internal class MemberImpl( qq: QQImpl, group: GroupImpl, @@ -182,9 +177,21 @@ internal class MemberImpl( val qq: QQImpl by qq.unsafeWeakRef() override var permission: MemberPermission = memberInfo.permission + @Suppress("PropertyName") internal var _nameCard: String = memberInfo.nameCard + @Suppress("PropertyName") internal var _specialTitle: String = memberInfo.specialTitle + @Suppress("PropertyName") + var _muteTimestamp: Int = memberInfo.muteTimestamp + + override val muteTimeRemaining: Int = + if (_muteTimestamp == 0 || _muteTimestamp == 0xFFFFFFFF.toInt()) { + 0 + } else { + _muteTimestamp - currentTimeSeconds.toInt() - bot.client.timeDifference.toInt() + } + override var nameCard: String get() = _nameCard set(newValue) { @@ -220,7 +227,7 @@ internal class MemberImpl( newValue ).sendWithoutExpect() } - MemberSpecialTitleChangeEvent(oldValue, newValue, this@MemberImpl).broadcast() + MemberSpecialTitleChangeEvent(oldValue, newValue, this@MemberImpl, null).broadcast() } } } @@ -279,12 +286,9 @@ internal class MemberImpl( } } - override fun equals(other: Any?): Boolean { - if (this === other) return true - return other is Member && other.id == this.id + override fun toString(): String { + return "Member($id)" } - - override fun hashCode(): Int = super.hashCode() } internal class MemberInfoImpl( @@ -301,6 +305,7 @@ internal class MemberInfoImpl( else -> MemberPermission.MEMBER } override val specialTitle: String get() = jceInfo.sSpecialTitle ?: "" + override val muteTimestamp: Int get() = jceInfo.dwShutupTimestap?.toInt() ?: 0 } /** @@ -323,13 +328,13 @@ internal class GroupImpl( @UseExperimental(MiraiExperimentalAPI::class) override lateinit var botPermission: MemberPermission - var _botMuteRemaining: Int = groupInfo.botMuteRemaining + var _botMuteTimestamp: Int = groupInfo.botMuteRemaining override val botMuteRemaining: Int = - if (_botMuteRemaining == 0 || _botMuteRemaining == 0xFFFFFFFF.toInt()) { + if (_botMuteTimestamp == 0 || _botMuteTimestamp == 0xFFFFFFFF.toInt()) { 0 } else { - _botMuteRemaining - currentTimeSeconds.toInt() - bot.client.timeDifference.toInt() + _botMuteTimestamp - currentTimeSeconds.toInt() - bot.client.timeDifference.toInt() } override val members: ContactList = ContactList(members.mapNotNull { @@ -600,10 +605,7 @@ internal class GroupImpl( image.input.close() } - override fun equals(other: Any?): Boolean { - if (this === other) return true - return other is Group && other.id == this.id + override fun toString(): String { + return "Group($id)" } - - override fun hashCode(): Int = super.hashCode() } \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt index 6670eed35..5c6c89699 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt @@ -20,7 +20,6 @@ import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.data.FriendInfo import net.mamoe.mirai.data.GroupInfo import net.mamoe.mirai.data.MemberInfo -import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler import net.mamoe.mirai.qqandroid.network.QQAndroidClient @@ -116,10 +115,6 @@ internal abstract class QQAndroidBotBase constructor( return sequence } - override fun onEvent(event: BotEvent): Boolean { - return firstLoginSucceed - } - override suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult { TODO("not implemented") } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/SerializationUtils.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt similarity index 97% rename from mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/SerializationUtils.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt index 5e5e732d4..fe8e46dd8 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/SerializationUtils.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/utils.kt @@ -7,6 +7,9 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:JvmName("SerializationUtils") +@file:JvmMultifileClass + package net.mamoe.mirai.qqandroid.io.serialization import kotlinx.io.core.* @@ -20,7 +23,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestDataVersion3 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket import net.mamoe.mirai.utils.firstValue import net.mamoe.mirai.utils.io.read -import net.mamoe.mirai.utils.io.toUHexString +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName fun ByteArray.loadAs(deserializer: DeserializationStrategy, c: JceCharset = JceCharset.UTF8): T { diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceFromMsg.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceFromMsg.kt index c8e77f7d3..fe4aab43f 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceFromMsg.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceFromMsg.kt @@ -21,6 +21,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg internal inline class MessageSourceFromServer( val delegate: ImMsgBody.SourceMsg ) : MessageSource { + override val time: Long get() = delegate.time.toLong() and 0xFFFFFFFF override val messageUid: Long get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!! override val sourceMessage: MessageChain get() = delegate.toMessageChain() override val senderId: Long get() = delegate.senderUin @@ -32,6 +33,7 @@ internal inline class MessageSourceFromServer( internal inline class MessageSourceFromMsg( val delegate: MsgComm.Msg ) : MessageSource { + override val time: Long get() = delegate.msgHead.msgTime.toLong() and 0xFFFFFFFF override val messageUid: Long get() = delegate.msgBody.richText.attr!!.random.toLong() override val sourceMessage: MessageChain get() = delegate.toMessageChain() override val senderId: Long get() = delegate.msgHead.fromUin diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageQQA.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt similarity index 92% rename from mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageQQA.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt index ac3c7bade..5d0068691 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageQQA.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt @@ -9,6 +9,8 @@ package net.mamoe.mirai.qqandroid.message +import kotlinx.io.core.buildPacket +import kotlinx.io.core.readBytes import kotlinx.io.core.readUInt import net.mamoe.mirai.message.data.* import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody @@ -20,13 +22,18 @@ import net.mamoe.mirai.utils.io.hexToBytes import net.mamoe.mirai.utils.io.read import net.mamoe.mirai.utils.io.toByteArray -private val AT_BUF_1 = byteArrayOf(0x00, 0x01, 0x00, 0x00, 0x00, 0x0A, 0x00) -private val AT_BUF_2 = ByteArray(2) - internal fun At.toJceData(): ImMsgBody.Text { + val text = this.toString() return ImMsgBody.Text( - str = this.toString(), - attr6Buf = AT_BUF_1 + this.target.toInt().toByteArray() + AT_BUF_2 + str = text, + attr6Buf = buildPacket { + writeShort(1) + writeShort(0) + writeShort(text.length.toShort()) + writeByte(1) + writeInt(target.toInt()) + writeShort(0) + }.readBytes() ) } @@ -86,6 +93,16 @@ _400Height=0x000000EB(235) pbReserve= } */ +val FACE_BUF = "00 01 00 04 52 CC F5 D0".hexToBytes() + +internal fun Face.toJceData(): ImMsgBody.Face { + return ImMsgBody.Face( + index = this.id, + old = (0x1445 - 4 + this.id).toShort().toByteArray(), + buf = FACE_BUF + ) +} + internal fun CustomFaceFromFile.toJceData(): ImMsgBody.CustomFace { return ImMsgBody.CustomFace( filePath = this.filepath, @@ -213,6 +230,7 @@ internal fun MessageChain.toRichTextElems(): MutableList { is NotOnlineImageFromServer -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate)) is NotOnlineImageFromFile -> elements.add(ImMsgBody.Elem(notOnlineImage = it.toJceData())) is AtAll -> elements.add(atAllData) + is Face -> elements.add(ImMsgBody.Elem(face = it.toJceData())) is QuoteReply, is MessageSource -> { @@ -312,18 +330,20 @@ internal fun List.joinToMessageChain(message: MessageChain) { it.srcMsg != null -> message.add(QuoteReply(MessageSourceFromServer(it.srcMsg))) it.notOnlineImage != null -> message.add(NotOnlineImageFromServer(it.notOnlineImage)) it.customFace != null -> message.add(CustomFaceFromServer(it.customFace)) + it.face != null -> message.add(Face(it.face.index)) it.text != null -> { if (it.text.attr6Buf.isEmpty()) { message.add(it.text.str.toMessage()) } else { - //00 01 00 00 00 05 01 00 00 00 00 00 00 all - //00 01 00 00 00 0A 00 3E 03 3F A2 00 00 one + // 00 01 00 00 00 05 01 00 00 00 00 00 00 all + // 00 01 00 00 00 0A 00 3E 03 3F A2 00 00 one/nick + // 00 01 00 00 00 07 00 44 71 47 90 00 00 one/groupCard val id: Long it.text.attr6Buf.read { discardExact(7) id = readUInt().toLong() } - if (id == 0L){ + if (id == 0L) { message.add(AtAll) } else { message.add(At(id, it.text.str)) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt index 232e06e7a..4746a5fff 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt @@ -20,10 +20,7 @@ import kotlinx.io.core.buildPacket import kotlinx.io.core.use import net.mamoe.mirai.data.MultiPacket import net.mamoe.mirai.data.Packet -import net.mamoe.mirai.event.BroadcastControllable -import net.mamoe.mirai.event.CancellableEvent -import net.mamoe.mirai.event.Event -import net.mamoe.mirai.event.broadcast +import net.mamoe.mirai.event.* import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotOnlineEvent import net.mamoe.mirai.network.BotNetworkHandler @@ -78,7 +75,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler } catch (e: CancellationException) { return@launch } catch (e: Throwable) { - BotOfflineEvent.Dropped(bot).broadcast() + if (this@QQAndroidBotNetworkHandler.isActive) { + BotOfflineEvent.Dropped(bot, e).broadcast() + } return@launch } packetReceiveLock.withLock { @@ -88,25 +87,40 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler }.also { _packetReceiverJob = it } } - override suspend fun relogin() { - heartbeatJob?.cancel() + private fun startHeartbeatJobOrKill(cancelCause: CancellationException? = null): Job { + heartbeatJob?.cancel(cancelCause) + + return this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) { + while (this.isActive) { + delay(bot.configuration.heartbeatPeriodMillis) + val failException = doHeartBeat() + if (failException != null) { + delay(bot.configuration.firstReconnectDelayMillis) + close(failException) + BotOfflineEvent.Dropped(bot, failException).broadcast() + } + } + }.also { heartbeatJob = it } + } + + override suspend fun relogin(cause: Throwable?) { + heartbeatJob?.cancel(CancellationException("relogin", cause)) if (::channel.isInitialized) { if (channel.isOpen) { kotlin.runCatching { - registerClientOnline() + registerClientOnline(500) }.exceptionOrNull() ?: return logger.info("Cannot do fast relogin. Trying slow relogin") } channel.close() } channel = PlatformSocket() - // TODO: 2020/2/14 连接多个服务器 + // TODO: 2020/2/14 连接多个服务器, #52 withTimeoutOrNull(3000) { channel.connect("113.96.13.208", 8080) } ?: error("timeout connecting server") - startPacketReceiverJobOrKill(CancellationException("reconnect")) + startPacketReceiverJobOrKill(CancellationException("relogin", cause)) - // logger.info("Trying login") var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect() mainloop@ while (true) { when (response) { @@ -157,10 +171,11 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler // println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}") registerClientOnline() + startHeartbeatJobOrKill() } - private suspend fun registerClientOnline() { - StatSvc.Register(bot.client).sendAndExpect() + private suspend fun registerClientOnline(timeoutMillis: Long = 3000) { + StatSvc.Register(bot.client).sendAndExpect(timeoutMillis) } // caches @@ -170,25 +185,34 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler @UseExperimental(MiraiExperimentalAPI::class, ExperimentalTime::class) override suspend fun init(): Unit = coroutineScope { - MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendWithoutExpect() + check(bot.isActive) { "bot is dead therefore network can't init" } + check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't init" } bot.qqs.delegate.clear() bot.groups.delegate.clear() val friendListJob = launch { - try { + lateinit var loadFriends: suspend () -> Unit + // 不要用 fun, 不要 join declaration, 不要用 val, 编译失败警告 + loadFriends = suspend loadFriends@{ logger.info("开始加载好友信息") var currentFriendCount = 0 var totalFriendCount: Short while (true) { - val data = FriendList.GetFriendGroupList( - bot.client, - currentFriendCount, - 150, - 0, - 0 - ).sendAndExpect(timeoutMillis = 5000, retry = 2) - + val data = runCatching { + FriendList.GetFriendGroupList( + bot.client, + currentFriendCount, + 150, + 0, + 0 + ).sendAndExpect(timeoutMillis = 5000, retry = 2) + }.getOrElse { + logger.error("无法加载好友列表", it) + this@QQAndroidBotNetworkHandler.launch { delay(10.secondsToMillis); loadFriends() } + logger.error("稍后重试加载好友列表") + return@loadFriends + } totalFriendCount = data.totalFriendCount data.friendList.forEach { // atomic add @@ -196,16 +220,16 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler currentFriendCount++ } } - logger.verbose("正在加载好友列表 ${currentFriendCount}/${totalFriendCount}") + logger.verbose { "正在加载好友列表 ${currentFriendCount}/${totalFriendCount}" } if (currentFriendCount >= totalFriendCount) { break } // delay(200) } - logger.info("好友列表加载完成, 共 ${currentFriendCount}个") - } catch (e: Exception) { - logger.error("加载好友列表失败|一般这是由于加载过于频繁导致/将以热加载方式加载好友列表") + logger.info { "好友列表加载完成, 共 ${currentFriendCount}个" } } + + loadFriends() } val groupJob = launch { @@ -247,7 +271,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler )) ) }?.let { - logger.error("群${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试") + logger.error { "群${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试" } logger.error(it) this@QQAndroidBotNetworkHandler.launch { delay(10_000) @@ -260,26 +284,25 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler loadGroup() } } - logger.info("群组列表与群成员加载完成, 共 ${troopListData.groups.size}个") + logger.info { "群组列表与群成员加载完成, 共 ${troopListData.groups.size}个" } } catch (e: Exception) { - logger.error("加载组信息失败|一般这是由于加载过于频繁导致/将以热加载方式加载群列表") + logger.error { "加载组信息失败|一般这是由于加载过于频繁导致/将以热加载方式加载群列表" } logger.error(e) } } joinAll(friendListJob, groupJob) - heartbeatJob = this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) { - while (this.isActive) { - delay(bot.configuration.heartbeatPeriodMillis) - val failException = doHeartBeat() - if (failException != null) { - delay(bot.configuration.firstReconnectDelayMillis) - close() - BotOfflineEvent.Dropped(bot).broadcast() + withTimeoutOrNull(5000) { + lateinit var listener: Listener + listener = this.subscribeAlways { + if (it.packet is MessageSvc.PbGetMsg.GetMsgSuccess) { + listener.complete() } } - } + + MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendWithoutExpect() + } ?: error("timeout syncing friend message history") bot.firstLoginSucceed = true @@ -288,10 +311,12 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler @Suppress("UNCHECKED_CAST") KnownPacketFactories.handleIncomingPacket(it as KnownPacketFactories.IncomingPacket, bot, it.flag2, it.consumer) } - pendingIncomingPackets = null // release + val list = pendingIncomingPackets + pendingIncomingPackets = null // release, help gc + list?.clear() // help gc BotOnlineEvent(bot).broadcast() - Unit + Unit // dont remove. can help type inference } suspend fun doHeartBeat(): Exception? { @@ -347,7 +372,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler } // with generic type, less mistakes - private suspend inline fun

generifiedParsePacket(input: Input) { + private suspend fun

generifiedParsePacket(input: Input) { KnownPacketFactories.parseIncomingPacket(bot, input) { packetFactory: PacketFactory

, packet: P, commandName: String, sequenceId: Int -> handlePacket(packetFactory, packet, commandName, sequenceId) if (packet is MultiPacket<*>) { @@ -361,7 +386,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler /** * 处理解析完成的包. */ - suspend fun

handlePacket(packetFactory: PacketFactory

?, packet: P, commandName: String, sequenceId: Int) { + suspend fun

handlePacket(packetFactory: PacketFactory

?, packet: P, commandName: String, sequenceId: Int) { // highest priority: pass to listeners (attached by sendAndExpect). packetListeners.forEach { listener -> if (listener.filter(commandName, sequenceId) && packetListeners.remove(listener)) { @@ -370,7 +395,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler } // check top-level cancelling - if (PacketReceivedEvent(packet).broadcast().isCancelled) { + if (packet != null && PacketReceivedEvent(packet).broadcast().isCancelled) { return } @@ -386,7 +411,13 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler if (packet is CancellableEvent && packet.isCancelled) return } - logger.info("Received: ${packet.toString().replace("\n", """\n""").replace("\r", "")}") + if (packet != null && (bot.logger.isEnabled || logger.isEnabled)) { + val logMessage = "Received: ${packet.toString().replace("\n", """\n""").replace("\r", "")}" + + if (packet is Event) { + bot.logger.verbose(logMessage) + } else logger.verbose(logMessage) + } packetFactory?.run { when (this) { @@ -480,7 +511,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler * 发送一个包, 但不期待任何返回. */ suspend fun OutgoingPacket.sendWithoutExpect() { - logger.info("Send: ${this.commandName}") + check(bot.isActive) { "bot is dead therefore can't send any packet" } + check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" } + logger.verbose("Send: ${this.commandName}") withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) { channel.send(delegate) } @@ -495,6 +528,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler require(timeoutMillis > 0) { "timeoutMillis must > 0" } require(retry >= 0) { "retry must >= 0" } + check(bot.isActive) { "bot is dead therefore can't send any packet" } + check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" } + var lastException: Exception? = null if (retry == 0) { val handler = PacketListener(commandName = commandName, sequenceId = sequenceId) @@ -503,7 +539,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) { channel.send(delegate) } - logger.info("Send: ${this.commandName}") + logger.verbose("Send: ${this.commandName}") return withTimeoutOrNull(timeoutMillis) { @Suppress("UNCHECKED_CAST") handler.await() as E @@ -522,7 +558,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) { channel.send(data, 0, length) } - logger.info("Send: ${this.commandName}") + logger.verbose("Send: ${this.commandName}") return withTimeoutOrNull(timeoutMillis) { @Suppress("UNCHECKED_CAST") handler.await() as E @@ -547,7 +583,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler internal inner class PacketListener( // callback val commandName: String, val sequenceId: Int - ) : CompletableDeferred by CompletableDeferred(supervisor) { + ) : CompletableDeferred by CompletableDeferred(supervisor) { fun filter(commandName: String, sequenceId: Int) = this.commandName == commandName && this.sequenceId == sequenceId } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt index 344c9dba2..80d0d6141 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/EncryptMethod.kt @@ -15,12 +15,10 @@ import kotlinx.io.core.buildPacket import kotlinx.io.core.writeFully import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.utils.cryptor.ECDH +import net.mamoe.mirai.utils.cryptor.ECDHKeyPair import net.mamoe.mirai.utils.io.encryptAndWrite import net.mamoe.mirai.utils.io.writeShortLVByteArray -/** - * Encryption method to be used for packet body. - */ @UseExperimental(ExperimentalUnsignedTypes::class) internal interface EncryptMethod { val id: Int @@ -33,16 +31,6 @@ internal interface EncryptMethodSessionKey : EncryptMethod { val currentLoginState: Int val sessionKey: ByteArray - /** - * buildPacket{ - * byte 1 - * byte if (currentLoginState == 2) 3 else 2 - * fully key - * short 258 - * short 0 - * fully encrypted - * } - */ override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket = buildPacket { require(currentLoginState == 2 || currentLoginState == 3) { "currentLoginState must be either 2 or 3" } @@ -65,29 +53,27 @@ inline class EncryptMethodSessionKeyLoginState3(override val sessionKey: ByteArr override val currentLoginState: Int get() = 3 } -inline class EncryptMethodECDH135(override val ecdh: ECDH) : +internal inline class EncryptMethodECDH135(override val ecdh: ECDH) : EncryptMethodECDH { override val id: Int get() = 135 } -inline class EncryptMethodECDH7(override val ecdh: ECDH) : +internal inline class EncryptMethodECDH7(override val ecdh: ECDH) : EncryptMethodECDH { override val id: Int get() = 7 } internal interface EncryptMethodECDH : EncryptMethod { + companion object { + operator fun invoke(ecdh: ECDH): EncryptMethodECDH { + return if (ecdh.keyPair === ECDHKeyPair.DefaultStub) { + EncryptMethodECDH135(ecdh) + } else EncryptMethodECDH7(ecdh) + } + } + val ecdh: ECDH - /** - * **Packet Structure** - * byte 1 - * byte 1 - * byte[] [ECDH.privateKey] - * short 258 - * short [ECDH.publicKey].size - * byte[] [ECDH.publicKey] - * byte[] encrypted `body()` by [ECDH.shareKey] - */ override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket = buildPacket { writeByte(1) // const @@ -95,15 +81,15 @@ internal interface EncryptMethodECDH : EncryptMethod { writeFully(client.randomKey) writeShort(258) // const - // writeShortLVByteArray("04 CB 36 66 98 56 1E 93 6E 80 C1 57 E0 74 CA B1 3B 0B B6 8D DE B2 82 45 48 A1 B1 8D D4 FB 61 22 AF E1 2F E4 8C 52 66 D8 D7 26 9D 76 51 A8 EB 6F E7".hexToBytes()) + if (ecdh.keyPair === ECDHKeyPair.DefaultStub) { + writeShortLVByteArray(ECDHKeyPair.DefaultStub.defaultPublicKey) + encryptAndWrite(ECDHKeyPair.DefaultStub.defaultShareKey, body) + } else { + writeShortLVByteArray(ecdh.keyPair.publicKey.getEncoded().drop(23).take(49).toByteArray().also { + check(it[0].toInt() == 0x04) { "Bad publicKey generated. Expected first element=0x04, got${it[0]}" } + }) - writeShortLVByteArray(ecdh.keyPair.publicKey.getEncoded().drop(23).take(49).toByteArray().also { - // it.toUHexString().debugPrint("PUBLIC KEY") - check(it[0].toInt() == 0x04) { "Bad publicKey generated. Expected first element=0x04, got${it[0]}" } - //check(ecdh.calculateShareKeyByPeerPublicKey(it.adjustToPublicKey()).contentEquals(ecdh.keyPair.shareKey)) { "PublicKey Validation failed" } - }) - - // encryptAndWrite("26 33 BA EC 86 EB 79 E6 BC E0 20 06 5E A9 56 6C".hexToBytes(), body) - encryptAndWrite(ecdh.keyPair.initialShareKey, body) + encryptAndWrite(ecdh.keyPair.initialShareKey, body) + } } } \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt index e3978b082..55b6dae7b 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt @@ -33,7 +33,7 @@ import kotlin.contracts.contract import kotlin.jvm.JvmName -internal sealed class PacketFactory { +internal sealed class PacketFactory { /** * 筛选从服务器接收到的包时的 commandName */ @@ -49,7 +49,7 @@ internal sealed class PacketFactory { * @param TPacket 服务器回复包解析结果 */ @UseExperimental(ExperimentalUnsignedTypes::class) -internal abstract class OutgoingPacketFactory( +internal abstract class OutgoingPacketFactory( /** * 命令名. 如 `wtlogin.login`, `ConfigPushSvc.PushDomain` */ @@ -73,7 +73,7 @@ internal abstract class OutgoingPacketFactory( * 这个工厂可以在 [handle] 时回复一个 commandId 为 [responseCommandName] 的包, 也可以不回复. * 必须先到 [KnownPacketFactories] 中注册工厂, 否则不能处理. */ -internal abstract class IncomingPacketFactory( +internal abstract class IncomingPacketFactory( /** * 接收自服务器的包的 commandName */ @@ -97,10 +97,10 @@ internal abstract class IncomingPacketFactory( } @JvmName("decode0") -private suspend inline fun

OutgoingPacketFactory

.decode(bot: QQAndroidBot, packet: ByteReadPacket): P = packet.decode(bot) +private suspend inline fun

OutgoingPacketFactory

.decode(bot: QQAndroidBot, packet: ByteReadPacket): P = packet.decode(bot) @JvmName("decode1") -private suspend inline fun

IncomingPacketFactory

.decode(bot: QQAndroidBot, packet: ByteReadPacket, sequenceId: Int): P = +private suspend inline fun

IncomingPacketFactory

.decode(bot: QQAndroidBot, packet: ByteReadPacket, sequenceId: Int): P = packet.decode(bot, sequenceId) internal val DECRYPTER_16_ZERO = ByteArray(16) @@ -169,7 +169,7 @@ internal object KnownPacketFactories { // do not inline. Exceptions thrown will not be reported correctly @UseExperimental(MiraiInternalAPI::class) @Suppress("UNCHECKED_CAST") - suspend fun parseIncomingPacket(bot: QQAndroidBot, rawInput: Input, consumer: PacketConsumer) = with(rawInput) { + suspend fun parseIncomingPacket(bot: QQAndroidBot, rawInput: Input, consumer: PacketConsumer) = with(rawInput) { // login val flag1 = readInt() @@ -229,7 +229,7 @@ internal object KnownPacketFactories { } @UseExperimental(MiraiInternalAPI::class) - internal suspend fun handleIncomingPacket(it: IncomingPacket, bot: QQAndroidBot, flag2: Int, consumer: PacketConsumer) { + internal suspend fun handleIncomingPacket(it: IncomingPacket, bot: QQAndroidBot, flag2: Int, consumer: PacketConsumer) { if (it.packetFactory == null) { bot.network.logger.debug("Received commandName: ${it.commandName}") PacketLogger.warning { "找不到 PacketFactory" } @@ -263,7 +263,7 @@ internal object KnownPacketFactories { private inline fun inline(block: () -> R): R = block() - class IncomingPacket( + class IncomingPacket( val packetFactory: PacketFactory?, val sequenceId: Int, val data: ByteReadPacket, @@ -337,7 +337,7 @@ internal object KnownPacketFactories { return IncomingPacket(packetFactory, ssoSequenceId, packet, commandName) } - private suspend fun ByteReadPacket.parseOicqResponse( + private suspend fun ByteReadPacket.parseOicqResponse( bot: QQAndroidBot, packetFactory: OutgoingPacketFactory, ssoSequenceId: Int, diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt index 7aad3fefa..76d8e6e33 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt @@ -16,7 +16,6 @@ import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MultiPacket import net.mamoe.mirai.data.Packet -import net.mamoe.mirai.event.BroadcastControllable import net.mamoe.mirai.event.events.BotJoinGroupEvent import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.MemberJoinEvent @@ -103,23 +102,23 @@ internal class MessageSvc { } @UseExperimental(MiraiInternalAPI::class) - internal class GetMsgSuccess(delegate: List) : Response(MsgSvc.SyncFlag.STOP, delegate) + open class GetMsgSuccess(delegate: List) : Response(MsgSvc.SyncFlag.STOP, delegate) { + override fun toString(): String { + return "MessageSvc.PbGetMsg.GetMsgSuccess(messages=List(size=${this.size}))" + } + } /** - * 不要直接 expect 这个 class. 它可能 + * 不要直接 expect 这个 class. 它可能还没同步完成 */ @MiraiInternalAPI - open class Response(internal val syncFlagFromServer: MsgSvc.SyncFlag, delegate: List) : MultiPacket(delegate), - BroadcastControllable { - override val shouldBroadcast: Boolean - get() = syncFlagFromServer == MsgSvc.SyncFlag.STOP - + open class Response(internal val syncFlagFromServer: MsgSvc.SyncFlag, delegate: List) : MultiPacket(delegate) { override fun toString(): String { return "MessageSvc.PbGetMsg.Response($syncFlagFromServer=$syncFlagFromServer, messages=List(size=${this.size}))" } } - object EmptyResponse : Response(MsgSvc.SyncFlag.STOP, emptyList()) + object EmptyResponse : GetMsgSuccess(emptyList()) @UseExperimental(MiraiInternalAPI::class, MiraiExperimentalAPI::class) override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { @@ -127,8 +126,8 @@ internal class MessageSvc { val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer()) if (resp.result != 0) { - // println("!!! Result=${resp.result} !!!: " + resp.contentToString()) - return GetMsgSuccess(mutableListOf()) + bot.network.logger.warning("MessageSvc.PushNotify: result != 0, result = ${resp.result}, errorMsg=${resp.errmsg}") + return EmptyResponse } bot.client.c2cMessageSync.syncCookie = resp.syncCookie @@ -149,7 +148,7 @@ internal class MessageSvc { val group = bot.getGroupByUinOrNull(msg.msgHead.fromUin) if (msg.msgHead.authUin == bot.uin) { if (group != null) { - error("group is not null while bot is invited to the group") + return@mapNotNull null } // 新群 @@ -159,6 +158,7 @@ internal class MessageSvc { }.groups.first { it.groupUin == msg.msgHead.fromUin } + @Suppress("DuplicatedCode") val newGroup = GroupImpl( bot = bot, coroutineContext = bot.coroutineContext, @@ -194,6 +194,7 @@ internal class MessageSvc { override val nameCard: String get() = "" override val permission: MemberPermission get() = MemberPermission.MEMBER override val specialTitle: String get() = "" + override val muteTimestamp: Int get() = 0 override val uin: Long get() = msg.msgHead.authUin override val nick: String get() = msg.msgHead.authNick.takeIf { it.isNotEmpty() } ?: msg.msgHead.fromNick }).also { group.members.delegate.addLast(it) }) @@ -264,6 +265,7 @@ internal class MessageSvc { /** * 发送好友消息 */ + @Suppress("FunctionName") fun ToFriend( client: QQAndroidClient, toUin: Long, @@ -293,6 +295,7 @@ internal class MessageSvc { /** * 发送群消息 */ + @Suppress("FunctionName") fun ToGroup( client: QQAndroidClient, groupCode: Long, diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt index 24710fb7c..117b1601e 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt @@ -15,9 +15,9 @@ import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.readBytes import kotlinx.io.core.readUInt import net.mamoe.mirai.contact.MemberPermission +import net.mamoe.mirai.data.MultiPacket import net.mamoe.mirai.data.NoPacket import net.mamoe.mirai.data.Packet -import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.* import net.mamoe.mirai.message.GroupMessage import net.mamoe.mirai.qqandroid.GroupImpl @@ -41,58 +41,44 @@ import net.mamoe.mirai.utils.io.read import net.mamoe.mirai.utils.io.readString import net.mamoe.mirai.utils.io.toUHexString -internal inline class GroupMessageOrNull(val delegate: GroupMessage?) : Packet { - override fun toString(): String { - return delegate?.toString() ?: "" - } -} - internal class OnlinePush { /** * 接受群消息 */ - internal object PbPushGroupMsg : IncomingPacketFactory("OnlinePush.PbPushGroupMsg") { + internal object PbPushGroupMsg : IncomingPacketFactory("OnlinePush.PbPushGroupMsg") { @UseExperimental(ExperimentalStdlibApi::class) - override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): GroupMessageOrNull { + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): GroupMessage? { // 00 00 02 E4 0A D5 05 0A 4F 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 52 20 00 28 BC 3D 30 8C 82 AB F1 05 38 D2 80 E0 8C 80 80 80 80 02 4A 21 08 E7 C1 AD B8 02 10 01 18 BA 05 22 09 48 69 6D 31 38 38 6D 6F 65 30 06 38 02 42 05 4D 69 72 61 69 50 01 58 01 60 00 88 01 08 12 06 08 01 10 00 18 00 1A F9 04 0A F6 04 0A 26 08 00 10 87 82 AB F1 05 18 B7 B4 BF 30 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 E6 03 42 E3 03 12 2A 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 22 00 2A 04 03 00 00 00 32 60 15 36 20 39 36 6B 45 31 41 38 35 32 32 39 64 63 36 39 38 34 37 39 37 37 62 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 31 32 31 32 41 38 C6 BB 8A A9 08 40 FB AE 9E C2 09 48 50 50 41 5A 00 60 01 6A 10 4E 18 58 22 0E 7B F8 0F C5 B1 34 48 83 74 D3 9C 72 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 31 39 38 3F 74 65 72 6D 3D 32 82 01 57 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 30 3F 74 65 72 6D 3D 32 B0 01 4D B8 01 2E C8 01 FF 05 D8 01 4D E0 01 2E FA 01 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 34 30 30 3F 74 65 72 6D 3D 32 80 02 4D 88 02 2E 12 45 AA 02 42 50 03 60 00 68 00 9A 01 39 08 09 20 BF 50 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 08 01 90 04 80 80 80 10 B8 04 00 C0 04 00 12 06 4A 04 08 00 40 01 12 14 82 01 11 0A 09 48 69 6D 31 38 38 6D 6F 65 18 06 20 08 28 03 10 8A CA 9D A1 07 1A 00 - if (!bot.firstLoginSucceed) return GroupMessageOrNull(null) + if (!bot.firstLoginSucceed) return null val pbPushMsg = readProtoBuf(MsgOnlinePush.PbPushMsg.serializer()) val extraInfo: ImMsgBody.ExtraInfo? = pbPushMsg.msg.msgBody.richText.elems.firstOrNull { it.extraInfo != null }?.extraInfo if (pbPushMsg.msg.msgHead.fromUin == bot.uin) { - return GroupMessageOrNull(null) + return null } val group = bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode) // println(pbPushMsg.msg.msgBody.richText.contentToString()) val flags = extraInfo?.flags ?: 0 - return GroupMessageOrNull( - GroupMessage( - bot = bot, - group = group, - senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard, - sender = group[pbPushMsg.msg.msgHead.fromUin], - message = pbPushMsg.msg.toMessageChain(), - permission = when { - flags and 16 != 0 -> MemberPermission.ADMINISTRATOR - flags and 8 != 0 -> MemberPermission.OWNER - flags == 0 -> MemberPermission.MEMBER - else -> { - bot.logger.warning("判断群员权限失败") - MemberPermission.MEMBER - } + return GroupMessage( + bot = bot, + group = group, + senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard, + sender = group[pbPushMsg.msg.msgHead.fromUin], + message = pbPushMsg.msg.toMessageChain(), + permission = when { + flags and 16 != 0 -> MemberPermission.ADMINISTRATOR + flags and 8 != 0 -> MemberPermission.OWNER + flags == 0 -> MemberPermission.MEMBER + else -> { + bot.logger.warning("判断群员权限失败") + MemberPermission.MEMBER } - ) + } ) } - - override suspend fun QQAndroidBot.handle(packet: GroupMessageOrNull, sequenceId: Int): OutgoingPacket? { - packet.delegate?.broadcast() - return null - } - } internal object PbPushTransMsg : IncomingPacketFactory("OnlinePush.PbPushTransMsg", "OnlinePush.RespPush") { @@ -156,10 +142,10 @@ internal class OnlinePush { @UseExperimental(ExperimentalStdlibApi::class) override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet { val reqPushMsg = decodeUniPacket(OnlinePushPack.SvcReqPushMsg.serializer(), "req") - reqPushMsg.vMsgInfos.forEach { msgInfo: MsgInfo -> - msgInfo.vMsg!!.read { - // TODO: 2020/2/13 可能会同时收到多个事件. 使用 map 而不要直接 return + @Suppress("USELESS_CAST") // 不要信任 kotlin 类型推断 + val packets: List = reqPushMsg.vMsgInfos.mapNotNull { msgInfo: MsgInfo -> + msgInfo.vMsg!!.read { when { msgInfo.shMsgType.toInt() == 732 -> { val group = bot.getGroup(this.readUInt().toLong()) @@ -169,7 +155,7 @@ internal class OnlinePush { 3073 -> { // mute val operatorUin = this.readUInt().toLong() if (operatorUin == bot.uin) { - return NoPacket + return@mapNotNull null } val operator = group[operatorUin] this.readUInt().toLong() // time @@ -177,42 +163,57 @@ internal class OnlinePush { val target = this.readUInt().toLong() val time = this.readInt() - return if (target == 0L) { + if (target == 0L) { if (time == 0) { - GroupMuteAllEvent( + return@mapNotNull GroupMuteAllEvent( origin = group.isMuteAll.also { group._muteAll = false }, new = false, operator = operator, group = group - ) + ) as Packet } else { - GroupMuteAllEvent( + return@mapNotNull GroupMuteAllEvent( origin = group.isMuteAll.also { group._muteAll = true }, new = true, operator = operator, group = group - ) + ) as Packet } } else { - return if (target == bot.uin) { - if (time == 0) { - BotUnmuteEvent(operator) - } else - BotMuteEvent(durationSeconds = time, operator = operator) + if (target == bot.uin) { + if (group._botMuteTimestamp != time) { + if (time == 0) { + group._botMuteTimestamp = 0 + return@mapNotNull BotUnmuteEvent(operator) as Packet + } else { + group._botMuteTimestamp = time + return@mapNotNull BotMuteEvent(durationSeconds = time, operator = operator) as Packet + } + } else { + return@mapNotNull null + } } else { val member = group[target] - if (time == 0) { - MemberUnmuteEvent(operator = operator, member = member) + member as MemberImpl + if (member._muteTimestamp != time) { + if (time == 0) { + member._muteTimestamp = 0 + return@mapNotNull MemberUnmuteEvent(member, operator) as Packet + } else { + member._muteTimestamp = time + return@mapNotNull MemberMuteEvent(member, time, operator) as Packet + } } else { - MemberMuteEvent(operator = operator, member = member, durationSeconds = time) + return@mapNotNull null } } } } - 3585 -> { // 匿名 + 3585 -> { + // 匿名 val operator = group[this.readUInt().toLong()] val switch = this.readInt() == 0 - return GroupAllowAnonymousChatEvent( + return@mapNotNull GroupAllowAnonymousChatEvent( origin = group.isAnonymousChatEnabled.also { group._anonymousChat = switch }, new = switch, operator = operator, @@ -225,7 +226,7 @@ internal class OnlinePush { // println(dataBytes.toUHexString()) if (dataBytes[0].toInt() != 59) { - return GroupNameChangeEvent( + return@mapNotNull GroupNameChangeEvent( origin = group.name.also { group._name = message }, new = message, group = group, @@ -235,7 +236,7 @@ internal class OnlinePush { //println(message + ":" + dataBytes.toUHexString()) when (message) { "管理员已关闭群聊坦白说" -> { - return GroupAllowConfessTalkEvent( + return@mapNotNull GroupAllowConfessTalkEvent( origin = group.isConfessTalkEnabled.also { group._confessTalk = false }, new = false, group = group, @@ -243,7 +244,7 @@ internal class OnlinePush { ) } "管理员已开启群聊坦白说" -> { - return GroupAllowConfessTalkEvent( + return@mapNotNull GroupAllowConfessTalkEvent( origin = group.isConfessTalkEnabled.also { group._confessTalk = true }, new = true, group = group, @@ -252,7 +253,7 @@ internal class OnlinePush { } else -> { bot.network.logger.debug { "Unknown server messages $message" } - return NoPacket + return@mapNotNull null } } } @@ -263,6 +264,7 @@ internal class OnlinePush { // } else -> { bot.network.logger.debug { "unknown group internal type $internalType , data: " + this.readBytes().toUHexString() + " " } + return@mapNotNull null } } } @@ -270,18 +272,18 @@ internal class OnlinePush { bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" } // val content = msgInfo.vMsg.loadAs(OnlinePushPack.MsgType0x210.serializer()) // println(content.contentToString()) + return@mapNotNull null } else -> { bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" } + return@mapNotNull null } } } } - - return NoPacket + return MultiPacket(packets) } - override suspend fun QQAndroidBot.handle(packet: Packet, sequenceId: Int): OutgoingPacket? { return buildResponseUniPacket(client, sequenceId = sequenceId) { diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt index 6c7c5b26b..29ea3b873 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/WtLogin.kt @@ -47,7 +47,7 @@ internal class WtLogin { ticket: String ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(2) // subCommand writeShort(4) // count of TLVs t193(ticket) @@ -64,7 +64,7 @@ internal class WtLogin { captchaAnswer: String ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(2) // subCommand writeShort(4) // count of TLVs t2(captchaAnswer, captchaSign, 0) @@ -83,7 +83,7 @@ internal class WtLogin { t402: ByteArray ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(20) // subCommand writeShort(4) // count of TLVs, probably ignored by server? t8(2052) @@ -103,7 +103,7 @@ internal class WtLogin { client: QQAndroidClient ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId, unknownHex = "01 00 00 00 00 00 00 00 00 00 01 00") { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(8) // subCommand writeShort(6) // count of TLVs, probably ignored by server?TODO t8(2052) @@ -131,7 +131,7 @@ internal class WtLogin { client: QQAndroidClient ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { - writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) { writeShort(9) // subCommand writeShort(17) // count of TLVs, probably ignored by server? //writeShort(LoginType.PASSWORD.value.toShort()) @@ -325,7 +325,7 @@ internal class WtLogin { 2 -> onSolveLoginCaptcha(tlvMap, bot) 160 /*-96*/ -> onUnsafeDeviceLogin(tlvMap) 204 /*-52*/ -> onSMSVerifyNeeded(tlvMap, bot) - else -> tlvMap[0x149]?.let { analysisTlv149(it) } ?: error("unknown login result type: $type") + else -> tlvMap[0x149]?.let { analysisTlv149(it) } ?: error("unknown login result type: $type, TLVMap = ${tlvMap.contentToString()}") } } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/Guid.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/GuidSource.kt similarity index 95% rename from mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/Guid.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/GuidSource.kt index ee2085cec..f2405cde6 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/Guid.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/GuidSource.kt @@ -7,9 +7,13 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:JvmName("Utils") +@file:JvmMultifileClass + package net.mamoe.mirai.qqandroid.utils -import net.mamoe.mirai.utils.md5 +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName import kotlin.jvm.JvmStatic /** diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/Flags.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/flags.kt similarity index 88% rename from mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/Flags.kt rename to mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/flags.kt index a79cba42a..141d44bea 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/Flags.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/flags.kt @@ -7,8 +7,14 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:JvmName("Utils") +@file:JvmMultifileClass + package net.mamoe.mirai.qqandroid.utils +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + inline class MacOrAndroidIdChangeFlag(val value: Long = 0) { fun macChanged(): MacOrAndroidIdChangeFlag = MacOrAndroidIdChangeFlag(this.value or 0x1) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/inline.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/inline.kt index 26d1d44a0..ba27ce7d5 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/inline.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/inline.kt @@ -7,11 +7,16 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:JvmName("Utils") +@file:JvmMultifileClass + package net.mamoe.mirai.qqandroid.utils import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName /** * Inline the block diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/type.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/type.kt index b295ccfa7..08dfe3c07 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/type.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/type.kt @@ -7,10 +7,16 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:JvmName("Utils") +@file:JvmMultifileClass + package net.mamoe.mirai.qqandroid.utils +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName -fun Int.toIpV4AddressString(): String { + +internal fun Int.toIpV4AddressString(): String { @Suppress("NAME_SHADOWING") var var0 = this.toLong() and 0xFFFFFFFF return buildString { diff --git a/mirai-core/build.gradle.kts b/mirai-core/build.gradle.kts index ebecaf2f9..c543159c5 100644 --- a/mirai-core/build.gradle.kts +++ b/mirai-core/build.gradle.kts @@ -8,8 +8,6 @@ plugins { id("com.jfrog.bintray") version "1.8.4-jetbrains-3" } -apply(from = rootProject.file("gradle/publish.gradle")) - val kotlinVersion: String by rootProject.ext val atomicFuVersion: String by rootProject.ext val coroutinesVersion: String by rootProject.ext diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/MiraiEnvironment.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/MiraiEnvironment.kt deleted file mode 100644 index d7fce339d..000000000 --- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/MiraiEnvironment.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai - -actual object MiraiEnvironment { - actual val platform: Platform get() = Platform.ANDROID -} \ No newline at end of file diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/event/internal/MiraiAtomicBoolean.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/event/internal/MiraiAtomicBoolean.kt new file mode 100644 index 000000000..1b3fb785f --- /dev/null +++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/event/internal/MiraiAtomicBoolean.kt @@ -0,0 +1,17 @@ +package net.mamoe.mirai.event.internal + +import java.util.concurrent.atomic.AtomicBoolean + +internal actual class MiraiAtomicBoolean actual constructor(initial: Boolean) { + private val delegate: AtomicBoolean = AtomicBoolean(initial) + + actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean { + return delegate.compareAndSet(expect, update) + } + + actual var value: Boolean + get() = delegate.get() + set(value) { + delegate.set(value) + } +} \ No newline at end of file diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/BotConfigurationAndroid.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/BotConfigurationAndroid.kt index e781c9697..c245d0cde 100644 --- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/BotConfigurationAndroid.kt +++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/BotConfigurationAndroid.kt @@ -68,11 +68,11 @@ actual open class BotConfiguration actual constructor() { /** * 重连失败后, 继续尝试的每次等待时间 */ - actual var reconnectPeriodMillis: Long = 60.secondsToMillis + actual var reconnectPeriodMillis: Long = 5.secondsToMillis /** * 最多尝试多少次重连 */ - actual var reconnectionRetryTimes: Int = 3 + actual var reconnectionRetryTimes: Int = Int.MAX_VALUE /** * 验证码处理器 */ diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt index 98db0234f..5fe233778 100644 --- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt +++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHAndroid.kt @@ -9,8 +9,10 @@ package net.mamoe.mirai.utils.cryptor +import android.annotation.SuppressLint import net.mamoe.mirai.utils.md5 import java.security.* +import java.security.spec.ECGenParameterSpec import java.security.spec.X509EncodedKeySpec import javax.crypto.KeyAgreement @@ -18,13 +20,13 @@ import javax.crypto.KeyAgreement actual typealias ECDHPrivateKey = PrivateKey actual typealias ECDHPublicKey = PublicKey -actual class ECDHKeyPair( +internal actual class ECDHKeyPairImpl( private val delegate: KeyPair -) { - actual val privateKey: ECDHPrivateKey get() = delegate.private - actual val publicKey: ECDHPublicKey get() = delegate.public +) : ECDHKeyPair { + override val privateKey: ECDHPrivateKey get() = delegate.private + override val publicKey: ECDHPublicKey get() = delegate.public - actual val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey) + override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey) } @Suppress("FunctionName") @@ -32,8 +34,41 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair()) actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual companion object { + @Suppress("ObjectPropertyName") + private var _isECDHAvailable: Boolean = false // because `runCatching` has no contract. + actual val isECDHAvailable: Boolean get() = _isECDHAvailable + + init { + kotlin.runCatching { + @SuppressLint("PrivateApi") + val clazz = Class.forName( + "com.android.org.bouncycastle.jce.provider.BouncyCastleProvider", + true, + ClassLoader.getSystemClassLoader() + ) + + val providerName = clazz.getDeclaredField("PROVIDER_NAME").get(null) as String + + if (Security.getProvider(providerName) != null) { + Security.removeProvider(providerName) + } + Security.addProvider(clazz.newInstance() as Provider) + generateKeyPair() + _isECDHAvailable = true + }.exceptionOrNull()?.let { + throw IllegalStateException("cannot init BouncyCastle", it) + } + _isECDHAvailable = false + } + + actual fun generateKeyPair(): ECDHKeyPair { - return ECDHKeyPair(KeyPairGenerator.getInstance("ECDH").genKeyPair()) + if (!isECDHAvailable) { + return ECDHKeyPair.DefaultStub + } + return ECDHKeyPairImpl(KeyPairGenerator.getInstance("ECDH") + .also { it.initialize(ECGenParameterSpec("secp192k1")) } + .genKeyPair()) } actual fun calculateShareKey( diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/io/PlatformSocket.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/io/PlatformSocket.kt index f20543980..f6c1d2513 100644 --- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/io/PlatformSocket.kt +++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/io/PlatformSocket.kt @@ -30,9 +30,16 @@ actual class PlatformSocket : Closeable { private lateinit var socket: Socket actual val isOpen: Boolean - get() = socket.isConnected + get() = + if (::socket.isInitialized) + socket.isConnected + else false - actual override fun close() = socket.close() + actual override fun close() { + if (::socket.isInitialized) { + socket.close() + } + } @PublishedApi internal lateinit var writeChannel: BufferedOutputStream diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsAndroid.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt similarity index 100% rename from mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/PlatformUtilsAndroid.kt rename to mirai-core/src/androidMain/kotlin/net/mamoe/mirai/utils/platformAndroid.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt index a8dea8551..96fd0db38 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE") +@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE", "UnusedImport") package net.mamoe.mirai @@ -35,7 +35,8 @@ import kotlin.jvm.JvmStatic * * 注: Bot 为全协程实现, 没有其他任务时若不使用 [join], 主线程将会退出. * - * @see Contact + * @see Contact 联系人 + * @see kotlinx.coroutines.isActive 判断 [Bot] 是否正常运行中. (在线, 且没有被 [close]) */ @UseExperimental(MiraiInternalAPI::class) abstract class Bot : CoroutineScope { @@ -195,7 +196,9 @@ abstract class Bot : CoroutineScope { /** * 登录, 或重新登录. - * 重新登录时不会再次拉取联系人列表. + * 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表. + * + * 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况. * * 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin] * @@ -231,24 +234,19 @@ abstract class Bot : CoroutineScope { // endregion /** - * 关闭这个 [Bot], 停止一切相关活动. 所有引用都会被释放. + * 关闭这个 [Bot], 立即取消 [Bot] 的 [kotlinx.coroutines.SupervisorJob]. + * 之后 [kotlinx.coroutines.isActive] 将会返回 `false`. * - * 注: 不可重新登录. 必须重新实例化一个 [Bot]. + * **注意:** 不可重新登录. 必须重新实例化一个 [Bot]. * * @param cause 原因. 为 null 时视为正常关闭, 非 null 时视为异常关闭 * - * @see closeAndJoin + * @see closeAndJoin 取消并 [Bot.join], 以确保 [Bot] 相关的活动被完全关闭 */ abstract fun close(cause: Throwable? = null) // region extensions - @Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("getFriend(this.toLong())")) - fun Int.qq(): QQ = getFriend(this.toLong()) - - @Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("getFriend(this)")) - fun Long.qq(): QQ = getFriend(this) - final override fun toString(): String { return "Bot(${uin})" } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt index a8686f90f..7ee7f84b3 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/BotImpl.kt @@ -14,7 +14,6 @@ package net.mamoe.mirai import kotlinx.coroutines.* import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.broadcast -import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotReloginEvent import net.mamoe.mirai.event.subscribeAlways @@ -73,11 +72,6 @@ abstract class BotImpl constructor( } } - /** - * 可阻止事件广播 - */ - abstract fun onEvent(event: BotEvent): Boolean - // region network final override val network: N get() = _network @@ -89,21 +83,22 @@ abstract class BotImpl constructor( private val offlineListener: Listener = this.subscribeAlways { event -> when (event) { is BotOfflineEvent.Dropped -> { - bot.logger.info("Connection dropped or lost by server, retrying login") + if (!_network.isActive) { + return@subscribeAlways + } + bot.logger.info("Connection dropped by server or lost, retrying login") - var lastFailedException: Throwable? = null - repeat(configuration.reconnectionRetryTimes) { - try { - network.relogin() - logger.info("Reconnected successfully") - return@subscribeAlways - } catch (e: Throwable) { - lastFailedException = e + tryNTimesOrException(configuration.reconnectionRetryTimes) { tryCount -> + if (tryCount != 0) { delay(configuration.reconnectPeriodMillis) } - } - if (lastFailedException != null) { - throw lastFailedException!! + network.relogin(event.cause) + logger.info("Reconnected successfully") + BotReloginEvent(bot, event.cause).broadcast() + return@subscribeAlways + }?.let { + logger.info("Cannot reconnect") + throw it } } is BotOfflineEvent.Active -> { @@ -112,17 +107,21 @@ abstract class BotImpl constructor( } else { " with exception: " + event.cause.message } - bot.logger.info("Bot is closed manually$msg") - close(CancellationException(event.toString())) + bot.logger.info { "Bot is closed manually$msg" } + closeAndJoin(CancellationException(event.toString())) } is BotOfflineEvent.Force -> { - bot.logger.info("Connection occupied by another android device: ${event.message}") - close(ForceOfflineException(event.toString())) + bot.logger.info { "Connection occupied by another android device: ${event.message}" } + closeAndJoin(ForceOfflineException(event.toString())) } } } - final override suspend fun login() = reinitializeNetworkHandler(null) + final override suspend fun login() { + logger.info("Logging in...") + reinitializeNetworkHandler(null) + logger.info("Login successful") + } private suspend fun reinitializeNetworkHandler( cause: Throwable? @@ -176,15 +175,19 @@ abstract class BotImpl constructor( @UseExperimental(MiraiInternalAPI::class) override fun close(cause: Throwable?) { + if (!this.botJob.isActive) { + // already cancelled + return + } kotlin.runCatching { if (cause == null) { + this.botJob.cancel() network.close() - this.botJob.complete() - offlineListener.complete() + offlineListener.cancel() } else { + this.botJob.cancel(CancellationException("bot cancelled", cause)) network.close(cause) - this.botJob.completeExceptionally(cause) - offlineListener.completeExceptionally(cause) + offlineListener.cancel(CancellationException("bot cancelled", cause)) } } groups.delegate.clear() diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/MiraiEnvironment.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/MiraiEnvironment.kt deleted file mode 100644 index ea33d13e1..000000000 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/MiraiEnvironment.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai - -/** - * 平台相关环境属性 - */ -expect object MiraiEnvironment { - val platform: Platform -} - -/** - * 可用平台列表 - */ -enum class Platform { - ANDROID, - JVM -} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt index 80c2900bc..dcb80fe9f 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt @@ -74,6 +74,16 @@ interface Contact : CoroutineScope { * 而 [QQ] 含义为一个独立的人, 可以是好友, 也可以是陌生人. */ override fun equals(other: Any?): Boolean + + /** + * @return `bot.hashCode() * 31 + id.hashCode()` + */ + override fun hashCode(): Int + + /** + * @return "QQ($id)" or "Group($id)" or "Member($id)" + */ + override fun toString(): String } suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain()) diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt index a3c2a8ff2..3d618c9aa 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt @@ -12,6 +12,7 @@ package net.mamoe.mirai.contact import kotlinx.coroutines.CoroutineScope +import net.mamoe.mirai.Bot import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.event.events.* import net.mamoe.mirai.utils.MiraiExperimentalAPI @@ -89,19 +90,19 @@ interface Group : Contact, CoroutineScope { /** * 机器人被禁言还剩余多少秒 * - * @see BotMuteEvent - * @see isBotMuted + * @see BotMuteEvent 机器人被禁言事件 + * @see isBotMuted 判断机器人是否正在被禁言 */ val botMuteRemaining: Int /** * 机器人在这个群里的权限 * - * **MiraiExperimentalAPI**: 在未来可能会被修改 + * @see Group.checkBotPermission 检查 [Bot] 在这个群里的权限 + * @see Group.checkBotPermissionOperator 要求 [Bot] 在这个群里的权限为 [管理员或群主][MemberPermission.isOperator] * - * @see BotGroupPermissionChangeEvent + * @see BotGroupPermissionChangeEvent 机器人群员修改 */ - @MiraiExperimentalAPI val botPermission: MemberPermission @@ -129,6 +130,7 @@ interface Group : Contact, CoroutineScope { /** * 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败 */ + @MiraiExperimentalAPI("还未支持") suspend fun quit(): Boolean /** diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt index 66e8f2ca3..74f0a1431 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt @@ -1,7 +1,7 @@ /* * Copyright 2020 Mamoe Technologies and contributors. * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可在以下链接找到该许可证. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. * * https://github.com/mamoe/mirai/blob/master/LICENSE @@ -29,33 +29,50 @@ interface Member : QQ, Contact { /** * 成员的权限, 动态更新. + * + * @see MemberPermissionChangeEvent 权限变更事件. 由群主或机器人的操作触发. */ val permission: MemberPermission /** - * 群名片. 可能为空. 修改时将会触发事件 + * 群名片. 可能为空. + * + * 管理员和群主都可修改任何人(包括群主)的群名片. * * 在修改时将会异步上传至服务器. * - * @see [groupCardOrNick] 获取非空群名片或昵称 + * @see [nameCardOrNick] 获取非空群名片或昵称 * - * @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件 + * @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件. * @throws PermissionDeniedException 无权限修改时 */ var nameCard: String /** - * 群头衔 + * 群头衔. + * + * 仅群主可以修改群头衔. * * 在修改时将会异步上传至服务器. * - * @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件 + * @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件. * @throws PermissionDeniedException 无权限修改时 */ var specialTitle: String /** - * 禁言 + * 被禁言剩余时长. 单位为秒. + * + * @see isMuted 判断改成员是否处于禁言状态 + * @see mute 设置禁言 + * @see unmute 取消禁言 + */ + val muteTimeRemaining: Int + + /** + * 禁言. + * + * 管理员可禁言成员, 群主可禁言管理员和群员. * * @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常. * @return 机器人无权限时返回 `false` @@ -72,6 +89,8 @@ interface Member : QQ, Contact { /** * 解除禁言. * + * 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言. + * * @see MemberUnmuteEvent 成员被取消禁言事件. * @throws PermissionDeniedException 无权限修改时 */ @@ -80,6 +99,8 @@ interface Member : QQ, Contact { /** * 踢出该成员. * + * 管理员可踢出成员, 群主可踢出管理员和群员. + * * @see MemberLeaveEvent.Kick 成员被踢出事件. * @throws PermissionDeniedException 无权限修改时 */ @@ -96,7 +117,14 @@ interface Member : QQ, Contact { * * 若 [群名片][Member.nameCard] 不为空则返回群名片, 为空则返回 [QQ.nick] */ -val Member.groupCardOrNick: String get() = this.nameCard.takeIf { it.isNotEmpty() } ?: this.nick +val Member.nameCardOrNick: String get() = this.nameCard.takeIf { it.isNotEmpty() } ?: this.nick + +/** + * 判断改成员是否处于禁言状态. + */ +fun Member.isMuted(): Boolean { + return muteTimeRemaining != 0 && muteTimeRemaining != 0xFFFFFFFF.toInt() +} @ExperimentalTime suspend inline fun Member.mute(duration: Duration) { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Permission.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Permission.kt index ac1137744..baeb5951f 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Permission.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Permission.kt @@ -9,6 +9,7 @@ package net.mamoe.mirai.contact +import net.mamoe.mirai.Bot import net.mamoe.mirai.utils.MiraiExperimentalAPI @@ -68,7 +69,6 @@ inline fun Member.isAdministrator(): Boolean = this.permission.isAdministrator() inline fun Member.isOperator(): Boolean = this.permission.isOperator() - /** * 权限不足 */ @@ -77,6 +77,11 @@ expect class PermissionDeniedException : IllegalStateException { constructor(message: String?) } +/** + * 要求 [Bot] 在这个群里的权限为 [required], 否则抛出异常 [PermissionDeniedException] + * + * @throws PermissionDeniedException + */ @UseExperimental(MiraiExperimentalAPI::class) inline fun Group.checkBotPermission( required: MemberPermission, @@ -89,6 +94,11 @@ inline fun Group.checkBotPermission( } } +/** + * 要求 [Bot] 在这个群里的权限为 [管理员或群主][MemberPermission.isOperator], 否则抛出异常 [PermissionDeniedException] + * + * @throws PermissionDeniedException + */ @UseExperimental(MiraiExperimentalAPI::class) inline fun Group.checkBotPermissionOperator( lazyMessage: () -> String = { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/EventPacket.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/EventPacket.kt deleted file mode 100644 index 630db03e1..000000000 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/EventPacket.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai.data - -import net.mamoe.mirai.event.Event - -/** - * 事件包. 可被监听. - * - * @see Event - */ -interface EventPacket : Event, Packet \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/MemberInfo.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/MemberInfo.kt index 3ed39ef10..37e6ba4b0 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/MemberInfo.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/MemberInfo.kt @@ -17,4 +17,6 @@ interface MemberInfo : FriendInfo { val permission: MemberPermission val specialTitle: String + + val muteTimestamp: Int } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/MessageSubscribers.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/MessageSubscribers.kt index 5cb5489c0..8c19b90fc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/MessageSubscribers.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/MessageSubscribers.kt @@ -25,6 +25,8 @@ import net.mamoe.mirai.message.data.Message import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * 订阅来自所有 [Bot] 的所有联系人的消息事件. 联系人可以是任意群或任意好友或临时会话. @@ -33,7 +35,10 @@ import kotlin.contracts.contract */ @UseExperimental(ExperimentalContracts::class) @MessageDsl -inline fun CoroutineScope.subscribeMessages(crossinline listeners: MessageSubscribersBuilder>.() -> R): R { +inline fun CoroutineScope.subscribeMessages( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + crossinline listeners: MessageSubscribersBuilder>.() -> R +): R { // contract 可帮助 IDE 进行类型推断. 无实际代码作用. contract { callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) @@ -42,7 +47,7 @@ inline fun CoroutineScope.subscribeMessages(crossinline listeners: MessageSu return MessageSubscribersBuilder { messageListener: MessageListener> -> // subscribeAlways 即注册一个监听器. 这个监听器收到消息后就传递给 [listener] // listener 即为 DSL 里 `contains(...) { }`, `startsWith(...) { }` 的代码块. - subscribeAlways { + subscribeAlways(coroutineContext) { messageListener.invoke(this, this.message.toString()) // this.message.toString() 即为 messageListener 中 it 接收到的值 } @@ -56,12 +61,15 @@ inline fun CoroutineScope.subscribeMessages(crossinline listeners: MessageSu */ @UseExperimental(ExperimentalContracts::class) @MessageDsl -inline fun CoroutineScope.subscribeGroupMessages(crossinline listeners: MessageSubscribersBuilder.() -> R): R { +inline fun CoroutineScope.subscribeGroupMessages( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + crossinline listeners: MessageSubscribersBuilder.() -> R +): R { contract { callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) } return MessageSubscribersBuilder { listener -> - subscribeAlways { + subscribeAlways(coroutineContext) { listener(this, this.message.toString()) } }.run(listeners) @@ -74,12 +82,15 @@ inline fun CoroutineScope.subscribeGroupMessages(crossinline listeners: Mess */ @UseExperimental(ExperimentalContracts::class) @MessageDsl -inline fun CoroutineScope.subscribeFriendMessages(crossinline listeners: MessageSubscribersBuilder.() -> R): R { +inline fun CoroutineScope.subscribeFriendMessages( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + crossinline listeners: MessageSubscribersBuilder.() -> R +): R { contract { callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) } return MessageSubscribersBuilder { listener -> - subscribeAlways { + subscribeAlways(coroutineContext) { listener(this, this.message.toString()) } }.run(listeners) @@ -92,12 +103,15 @@ inline fun CoroutineScope.subscribeFriendMessages(crossinline listeners: Mes */ @UseExperimental(ExperimentalContracts::class) @MessageDsl -inline fun Bot.subscribeMessages(crossinline listeners: MessageSubscribersBuilder>.() -> R): R { +inline fun Bot.subscribeMessages( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + crossinline listeners: MessageSubscribersBuilder>.() -> R +): R { contract { callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) } return MessageSubscribersBuilder> { listener -> - this.subscribeAlways { + this.subscribeAlways(coroutineContext) { listener(this, this.message.toString()) } }.run(listeners) @@ -106,16 +120,21 @@ inline fun Bot.subscribeMessages(crossinline listeners: MessageSubscribersBu /** * 订阅来自这个 [Bot] 的所有群消息事件 * + * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext] + * * @see CoroutineScope.incoming */ @UseExperimental(ExperimentalContracts::class) @MessageDsl -inline fun Bot.subscribeGroupMessages(crossinline listeners: MessageSubscribersBuilder.() -> R): R { +inline fun Bot.subscribeGroupMessages( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + crossinline listeners: MessageSubscribersBuilder.() -> R +): R { contract { callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) } return MessageSubscribersBuilder { listener -> - this.subscribeAlways { + this.subscribeAlways(coroutineContext) { listener(this, this.message.toString()) } }.run(listeners) @@ -128,12 +147,15 @@ inline fun Bot.subscribeGroupMessages(crossinline listeners: MessageSubscrib */ @UseExperimental(ExperimentalContracts::class) @MessageDsl -inline fun Bot.subscribeFriendMessages(crossinline listeners: MessageSubscribersBuilder.() -> R): R { +inline fun Bot.subscribeFriendMessages( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + crossinline listeners: MessageSubscribersBuilder.() -> R +): R { contract { callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) } return MessageSubscribersBuilder { listener -> - this.subscribeAlways { + this.subscribeAlways(coroutineContext) { listener(this, this.message.toString()) } }.run(listeners) @@ -148,9 +170,12 @@ inline fun Bot.subscribeFriendMessages(crossinline listeners: MessageSubscri * @see subscribeMessages * @see subscribeGroupMessages */ -inline fun CoroutineScope.incoming(capacity: Int = Channel.RENDEZVOUS): ReceiveChannel { +inline fun CoroutineScope.incoming( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + capacity: Int = Channel.RENDEZVOUS +): ReceiveChannel { return Channel(capacity).apply { - subscribeAlways { + subscribeAlways(coroutineContext) { send(this) } } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribable.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribable.kt index 3aff7e809..241656fdc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribable.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribable.kt @@ -11,8 +11,6 @@ package net.mamoe.mirai.event -import net.mamoe.mirai.BotImpl -import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.event.internal.broadcastInternal import net.mamoe.mirai.utils.MiraiInternalAPI @@ -22,7 +20,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI * 若监听这个类, 监听器将会接收所有事件的广播. * * @see subscribeAlways - * @see subscribeWhile + * @see subscribeOnce * * @see subscribeMessages * @@ -73,9 +71,6 @@ suspend fun E.broadcast(): E = apply { if (this is BroadcastControllable && !this.shouldBroadcast) { return@apply } - if (this is BotEvent && !(this.bot as BotImpl<*>).onEvent(this)) { - return@apply - } this@broadcast.broadcastInternal() // inline, no extra cost } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt index fd6887e85..eeca3a407 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt @@ -14,10 +14,16 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.GlobalScope import net.mamoe.mirai.Bot +import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.event.internal.Handler import net.mamoe.mirai.event.internal.subscribeInternal import net.mamoe.mirai.utils.MiraiInternalAPI +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.jvm.JvmName /* * 该文件为所有的订阅事件的方法. @@ -68,15 +74,16 @@ interface Listener : CompletableJob { * `runBlocking` 不会结束, 也就是下一行 `foo()` 不会被执行. 直到监听时创建的 `Listener` 被停止. * * - * 要创建一个全局都存在的监听, 即守护协程, 请在 [GlobalScope] 下调用本函数: + * 要创建一个仅在某个机器人在线时的监听, 请在 [Bot] 下调用本函数 (因为 [Bot] 也实现 [CoroutineScope]). + * 这种方式创建的监听会自动筛选 [Bot]. * ```kotlin - * GlobalScope.subscribe { /* 一些处理 */ } + * bot1.subscribe { /* 只会处理来自 bot1 的事件 */ } * ``` * * - * 要创建一个仅在某个机器人在线时的监听, 请在 [Bot] 下调用本函数 (因为 [Bot] 也实现 [CoroutineScope]): + * 要创建一个全局都存在的监听, 即守护协程, 请在 [GlobalScope] 下调用本函数: * ```kotlin - * bot.subscribe { /* 一些处理 */ } + * GlobalScope.subscribe { /* 会收到来自全部 Bot 的事件和与 Bot 不相关的事件 */ } * ``` * * @@ -86,122 +93,137 @@ interface Listener : CompletableJob { * 若 [this] 没有 [CoroutineExceptionHandler], 则在事件广播方的 [CoroutineExceptionHandler] 处理 * 若均找不到, 则会触发 logger warning. * - 事件处理时抛出异常不会停止监听器. - * - 建议在事件处理中, 即 [handler] 里处理异常, 或在 [this] 指定 [CoroutineExceptionHandler]. + * - 建议在事件处理中 (即 [handler] 里) 处理异常, + * 或在 [this] 的 [CoroutineScope.coroutineContext] 中添加 [CoroutineExceptionHandler]. * * - * **注意:** 事件处理是 `suspend` 的, 请严格控制 JVM 阻塞方法的使用. 若致事件处理阻塞, 则会导致一些逻辑无法进行. + * **注意:** 事件处理是 `suspend` 的, 请规范处理 JVM 阻塞方法. * - * // TODO: 2020/2/13 在 bot 下监听时同时筛选对应 bot 实例 + * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext] * - * @see subscribeMessages 监听消息 DSL - * @see subscribeGroupMessages 监听群消息 DSL + * @see subscribeAlways 一直监听 + * @see subscribeOnce 只监听一次 + * + * @see subscribeMessages 监听消息 DSL + * @see subscribeGroupMessages 监听群消息 DSL * @see subscribeFriendMessages 监听好友消息 DSL */ @UseExperimental(MiraiInternalAPI::class) -inline fun CoroutineScope.subscribe(crossinline handler: suspend E.(E) -> ListeningStatus): Listener = - E::class.subscribeInternal(Handler { it.handler(it); }) +inline fun CoroutineScope.subscribe( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + noinline handler: suspend E.(E) -> ListeningStatus +): Listener = + E::class.subscribeInternal(Handler(coroutineContext) { it.handler(it); }) /** * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行. * - * 仅当 [Listener.complete] 或 [Listener.cancel] 时结束. + * 可在任意时候通过 [Listener.complete] 来主动停止监听. + * [Bot] 被关闭后事件监听会被 [取消][Listener.cancel]. + * + * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext] * * @see subscribe 获取更多说明 */ -@UseExperimental(MiraiInternalAPI::class) -inline fun CoroutineScope.subscribeAlways(crossinline listener: suspend E.(E) -> Unit): Listener = - E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.LISTENING }) +@UseExperimental(MiraiInternalAPI::class, ExperimentalContracts::class) +inline fun CoroutineScope.subscribeAlways( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + noinline listener: suspend E.(E) -> Unit +): Listener { + contract { + callsInPlace(listener, InvocationKind.UNKNOWN) + } + return E::class.subscribeInternal(Handler(coroutineContext) { it.listener(it); ListeningStatus.LISTENING }) +} /** * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 仅在第一次 [事件广播][Event.broadcast] 时, [listener] 会被执行. * - * 在这之前, 可通过 [Listener.complete] 来停止监听. + * 可在任意时候通过 [Listener.complete] 来主动停止监听. + * [Bot] 被关闭后事件监听会被 [取消][Listener.cancel]. + * + * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext] * * @see subscribe 获取更多说明 */ @UseExperimental(MiraiInternalAPI::class) -inline fun CoroutineScope.subscribeOnce(crossinline listener: suspend E.(E) -> Unit): Listener = - E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.STOPPED }) +inline fun CoroutineScope.subscribeOnce( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + noinline listener: suspend E.(E) -> Unit +): Listener = + E::class.subscribeInternal(Handler(coroutineContext) { it.listener(it); ListeningStatus.STOPPED }) + + +// +// 以下为带筛选 Bot 的监听 +// + /** - * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. - * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行, 直到 [listener] 的返回值 [equals] 于 [valueIfStop] + * 在 [Bot] 的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. + * 每当 [事件广播][Event.broadcast] 时, [handler] 都会被执行, + * 当 [handler] 返回 [ListeningStatus.STOPPED] 时停止监听 * - * 可在任意时刻通过 [Listener.complete] 来停止监听. + * 可在任意时候通过 [Listener.complete] 来主动停止监听. + * [Bot] 被关闭后事件监听会被 [取消][Listener.cancel]. + * + * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext] * * @see subscribe 获取更多说明 */ +@JvmName("subscribeAlwaysForBot") @UseExperimental(MiraiInternalAPI::class) -inline fun CoroutineScope.subscribeUntil(valueIfStop: T, crossinline listener: suspend E.(E) -> T): Listener = - E::class.subscribeInternal(Handler { if (it.listener(it) == valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) +inline fun Bot.subscribe( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + noinline handler: suspend E.(E) -> ListeningStatus +): Listener = + E::class.subscribeInternal(Handler(coroutineContext) { if (it.bot === this) it.handler(it) else ListeningStatus.LISTENING }) + /** - * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. - * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行, - * 如果 [listener] 的返回值 [equals] 于 [valueIfContinue], 则继续监听, 否则停止 + * 在 [Bot] 的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. + * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行. * - * 可在任意时刻通过 [Listener.complete] 来停止监听. + * 可在任意时候通过 [Listener.complete] 来主动停止监听. + * [Bot] 被关闭后事件监听会被 [取消][Listener.cancel]. + * + * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext] * * @see subscribe 获取更多说明 */ +@JvmName("subscribeAlwaysForBot1") @UseExperimental(MiraiInternalAPI::class) -inline fun CoroutineScope.subscribeWhile(valueIfContinue: T, crossinline listener: suspend E.(E) -> T): Listener = - E::class.subscribeInternal(Handler { if (it.listener(it) != valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) - -// endregion - -// region ListenerBuilder DSL - -/* -/** - * 监听构建器. 可同时进行多种方式的监听 - * - * ```kotlin - * FriendMessageEvent.subscribe { - * always{ - * it.reply("永远发生") - * } - * - * untilFalse { - * it.reply("你发送了 ${it.event}") - * it.event eq "停止" - * } - * } - * ``` - */ -@ListenersBuilderDsl -@Suppress("MemberVisibilityCanBePrivate", "unused") -inline class ListenerBuilder( - @PublishedApi internal inline val handlerConsumer: CoroutineCoroutineScope.(Listener) -> Unit -) { - fun CoroutineCoroutineScope.handler(listener: suspend E.(E) -> ListeningStatus) { - handlerConsumer(Handler { it.listener(it) }) - } - - fun CoroutineCoroutineScope.always(listener: suspend E.(E) -> Unit) = handler { listener(it); ListeningStatus.LISTENING } - - fun CoroutineCoroutineScope.until(until: T, listener: suspend E.(E) -> T) = - handler { if (listener(it) == until) ListeningStatus.STOPPED else ListeningStatus.LISTENING } - - fun CoroutineCoroutineScope.untilFalse(listener: suspend E.(E) -> Boolean) = until(false, listener) - fun CoroutineCoroutineScope.untilTrue(listener: suspend E.(E) -> Boolean) = until(true, listener) - fun CoroutineCoroutineScope.untilNull(listener: suspend E.(E) -> Any?) = until(null, listener) - - - fun CoroutineCoroutineScope.`while`(until: T, listener: suspend E.(E) -> T) = - handler { if (listener(it) !== until) ListeningStatus.STOPPED else ListeningStatus.LISTENING } - - fun CoroutineCoroutineScope.whileFalse(listener: suspend E.(E) -> Boolean) = `while`(false, listener) - fun CoroutineCoroutineScope.whileTrue(listener: suspend E.(E) -> Boolean) = `while`(true, listener) - fun CoroutineCoroutineScope.whileNull(listener: suspend E.(E) -> Any?) = `while`(null, listener) - - - fun CoroutineCoroutineScope.once(listener: suspend E.(E) -> Unit) = handler { listener(it); ListeningStatus.STOPPED } +inline fun Bot.subscribeAlways( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + noinline listener: suspend E.(E) -> Unit +): Listener { + return E::class.subscribeInternal(Handler(coroutineContext) { if (it.bot === this) it.listener(it); ListeningStatus.LISTENING }) } -@DslMarker -annotation class ListenersBuilderDsl -*/ +/** + * 在 [Bot] 的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. + * 仅在第一次 [事件广播][Event.broadcast] 时, [listener] 会被执行. + * + * 可在任意时候通过 [Listener.complete] 来主动停止监听. + * [Bot] 被关闭后事件监听会被 [取消][Listener.cancel]. + * + * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext] + * + * @see subscribe 获取更多说明 + */ +@JvmName("subscribeOnceForBot2") +@UseExperimental(MiraiInternalAPI::class) +inline fun Bot.subscribeOnce( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + noinline listener: suspend E.(E) -> Unit +): Listener = + E::class.subscribeInternal(Handler(coroutineContext) { + if (it.bot === this) { + it.listener(it) + ListeningStatus.STOPPED + } else ListeningStatus.LISTENING + }) + // endregion \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt index a855e40b2..749697d01 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt @@ -7,6 +7,8 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ +@file:Suppress("unused") + package net.mamoe.mirai.event.events import net.mamoe.mirai.Bot @@ -57,7 +59,7 @@ sealed class BotOfflineEvent : BotEvent { /** * 被服务器断开或因网络问题而掉线 */ - data class Dropped(override val bot: Bot) : BotOfflineEvent(), Packet, BotPassiveEvent + data class Dropped(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), Packet, BotPassiveEvent } /** @@ -194,7 +196,7 @@ data class GroupNameChangeEvent( override val origin: String, override val new: String, override val group: Group, - val isByBot: Boolean + val isByBot: Boolean // 无法获取 operator ) : GroupSettingChangeEvent, Packet /** @@ -210,6 +212,8 @@ data class GroupEntranceAnnouncementChangeEvent( val operator: Member? ) : GroupSettingChangeEvent, Packet +val GroupEntranceAnnouncementChangeEvent.isByBot: Boolean get() = operator != null + /** * 群 "全员禁言" 功能状态改变. 此事件广播前修改就已经完成. @@ -224,6 +228,8 @@ data class GroupMuteAllEvent( val operator: Member? ) : GroupSettingChangeEvent, Packet +val GroupMuteAllEvent.isByBot: Boolean get() = operator != null + /** * 群 "匿名聊天" 功能状态改变. 此事件广播前修改就已经完成. */ @@ -237,6 +243,8 @@ data class GroupAllowAnonymousChatEvent( val operator: Member? ) : GroupSettingChangeEvent, Packet +val GroupAllowAnonymousChatEvent.isByBot: Boolean get() = operator != null + /** * 群 "坦白说" 功能状态改变. 此事件广播前修改就已经完成. */ @@ -260,6 +268,8 @@ data class GroupAllowMemberInviteEvent( val operator: Member? ) : GroupSettingChangeEvent, Packet +val GroupAllowMemberInviteEvent.isByBot: Boolean get() = operator != null + // endregion @@ -293,6 +303,8 @@ sealed class MemberLeaveEvent : GroupMemberEvent { data class Quit(override val member: Member) : MemberLeaveEvent() } +val MemberLeaveEvent.Kick.isByBot: Boolean get() = operator != null + // endregion // region 名片和头衔 @@ -319,6 +331,8 @@ data class MemberCardChangeEvent( val operator: Member? ) : GroupMemberEvent +val MemberCardChangeEvent.isByBot: Boolean get() = operator != null + /** * 群头衔改动. 一定为群主操作 */ @@ -333,9 +347,18 @@ data class MemberSpecialTitleChangeEvent( */ val new: String, - override val member: Member + override val member: Member, + + /** + * 操作人. + * 不为 null 时一定为群主. 可能与 [member] 引用相同, 此时为群员自己修改. + * 为 null 时则是机器人操作. + */ + val operator: Member? ) : GroupMemberEvent +val MemberSpecialTitleChangeEvent.isByBot: Boolean get() = operator != null + // endregion @@ -367,6 +390,8 @@ data class MemberMuteEvent( val operator: Member? ) : GroupMemberEvent, Packet +val MemberMuteEvent.isByBot: Boolean get() = operator != null + /** * 群成员被取消禁言事件. 被禁言的成员都不可能是机器人本人 */ @@ -378,6 +403,8 @@ data class MemberUnmuteEvent( val operator: Member? ) : GroupMemberEvent, Packet +val MemberUnmuteEvent.isByBot: Boolean get() = operator != null + // endregion // endregion diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt index e439a0974..c09b5f303 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/internal/InternalEventListeners.kt @@ -9,7 +9,6 @@ package net.mamoe.mirai.event.internal -import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.EventDisabled @@ -32,8 +31,12 @@ fun , E : Event> KClass.subscribeInternal(listener: L): L @PublishedApi @Suppress("FunctionName") -internal fun CoroutineScope.Handler(handler: suspend (E) -> ListeningStatus): Handler { - return Handler(coroutineContext[Job], coroutineContext, handler) +internal fun CoroutineScope.Handler( + coroutineContext: CoroutineContext, + handler: suspend (E) -> ListeningStatus +): Handler { + val context = this.newCoroutineContext(coroutineContext) + return Handler(context[Job], context, handler) } private inline fun inline(block: () -> Unit) = block() @@ -77,7 +80,32 @@ internal class Handler */ internal fun KClass.listeners(): EventListeners = EventListenerManager.get(this) -internal class EventListeners : LockFreeLinkedList>() +internal class EventListeners(clazz: KClass) : LockFreeLinkedList>() { + @Suppress("UNCHECKED_CAST") + val supertypes: Set> by lazy { + val supertypes = mutableSetOf>() + + fun addSupertypes(clazz: KClass) { + clazz.supertypes.forEach { + val classifier = it.classifier as? KClass + if (classifier != null) { + supertypes.add(classifier) + addSupertypes(classifier) + } + } + } + addSupertypes(clazz) + + supertypes + } +} + +internal expect class MiraiAtomicBoolean(initial: Boolean) { + + fun compareAndSet(expect: Boolean, update: Boolean): Boolean + + var value: Boolean +} /** * 管理每个事件 class 的 [EventListeners]. @@ -88,16 +116,8 @@ internal object EventListenerManager { private val registries = LockFreeLinkedList>() - private val lock = atomic(false) - - private fun setLockValue(value: Boolean) { - lock.value = value - } - - @Suppress("BooleanLiteralArgument") - private fun trySetLockTrue(): Boolean { - return lock.compareAndSet(false, true) - } + // 不要用 atomicfu. 在 publish 后会出现 VerifyError + private val lock: MiraiAtomicBoolean = MiraiAtomicBoolean(false) @Suppress("UNCHECKED_CAST", "BooleanLiteralArgument") internal tailrec fun get(clazz: KClass): EventListeners { @@ -106,11 +126,11 @@ internal object EventListenerManager { return it.listeners as EventListeners } } - if (trySetLockTrue()) { - val registry = Registry(clazz, EventListeners()) + if (lock.compareAndSet(false, true)) { + val registry = Registry(clazz as KClass, EventListeners(clazz)) registries.addLast(registry) - setLockValue(false) - return registry.listeners as EventListeners + lock.value = false + return registry.listeners } return get(clazz) } @@ -123,19 +143,10 @@ internal suspend inline fun Event.broadcastInternal() { EventLogger.info { "Event broadcast: $this" } - callAndRemoveIfRequired(this::class.listeners()) - - var supertypes = this::class.supertypes - while (true) { - val superSubscribableType = supertypes.firstOrNull { - it.classifier as? KClass != null - } - - superSubscribableType?.let { - callAndRemoveIfRequired((it.classifier as KClass).listeners()) - } - - supertypes = (superSubscribableType?.classifier as? KClass<*>)?.supertypes ?: return + val listeners = this::class.listeners() + callAndRemoveIfRequired(listeners) + listeners.supertypes.forEach { + callAndRemoveIfRequired(it.listeners()) } } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/FriendMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/FriendMessage.kt index 14c42998e..8ead2fe6a 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/FriendMessage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/FriendMessage.kt @@ -13,7 +13,6 @@ import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.event.BroadcastControllable import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.utils.MiraiInternalAPI class FriendMessage( bot: Bot, diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/GroupMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/GroupMessage.kt index 0232fcecf..efd272542 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/GroupMessage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/GroupMessage.kt @@ -14,7 +14,6 @@ import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.event.Event -import net.mamoe.mirai.message.data.At import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.utils.getValue @@ -38,7 +37,6 @@ class GroupMessage( override val subject: Group get() = group - inline fun At.member(): Member = group[this.target] inline fun Long.member(): Member = group[this] diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt index 4aeb9c04c..a42a778fc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt @@ -19,7 +19,7 @@ import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.QQ -import net.mamoe.mirai.data.EventPacket +import net.mamoe.mirai.data.Packet import net.mamoe.mirai.event.events.BotEvent import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.* @@ -37,7 +37,7 @@ expect abstract class MessagePacket(bot: Bot) */ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构 @Suppress("NOTHING_TO_INLINE") @MiraiInternalAPI -abstract class MessagePacketBase(_bot: Bot) : EventPacket, BotEvent { +abstract class MessagePacketBase(_bot: Bot) : Packet, BotEvent { /** * 接受到这条消息的 */ @@ -115,6 +115,8 @@ abstract class MessagePacketBase(_bot: Bot) : */ inline fun QQ.at(): At = At(this as? Member ?: error("`QQ.at` can only be used in GroupMessage")) + inline fun At.member(): Member = (this@MessagePacketBase as? GroupMessage)?.group?.get(this.target) ?: error("`At.member` can only be used in GroupMessage") + // endregion // region 下载图片 diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt index a0e103fd0..949b7e6ae 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt @@ -15,7 +15,7 @@ package net.mamoe.mirai.message.data import net.mamoe.mirai.contact.Member -import net.mamoe.mirai.contact.groupCardOrNick +import net.mamoe.mirai.contact.nameCardOrNick import net.mamoe.mirai.utils.MiraiInternalAPI import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -28,7 +28,7 @@ import kotlin.jvm.JvmName */ class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message { @UseExperimental(MiraiInternalAPI::class) - constructor(member: Member) : this(member.id, "@${member.groupCardOrNick}") + constructor(member: Member) : this(member.id, "@${member.nameCardOrNick}") override fun toString(): String = display diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Face.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Face.kt index 53a35641e..69ab4596c 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Face.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Face.kt @@ -14,309 +14,161 @@ package net.mamoe.mirai.message.data import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName -import kotlin.jvm.JvmStatic /** * QQ 自带表情 */ -inline class Face(val id: FaceId) : Message { - override fun toString(): String = "[face${id.value}]" +class Face(val id: Int) : Message { + override fun toString(): String = "[mirai:face$id]" - companion object Key : Message.Key + /** + * @author LamGC + */ + @Suppress("SpellCheckingInspection", "unused") + companion object IdList : Message.Key { + const val unknown: Int = 0xff + const val jingya: Int = 0 + const val piezui: Int = 1 + const val se: Int = 2 + const val fadai: Int = 3 + const val deyi: Int = 4 + const val liulei: Int = 5 + const val haixiu: Int = 6 + const val bizui: Int = 7 + const val shui: Int = 8 + const val daku: Int = 9 + const val ganga: Int = 10 + const val fanu: Int = 11 + const val tiaopi: Int = 12 + const val ciya: Int = 13 + const val weixiao: Int = 14 + const val nanguo: Int = 15 + const val ku: Int = 16 + const val zhuakuang: Int = 18 + const val tu: Int = 19 + const val touxiao: Int = 20 + const val keai: Int = 21 + const val baiyan: Int = 22 + const val aoman: Int = 23 + const val ji_e: Int = 24 + const val kun: Int = 25 + const val jingkong: Int = 26 + const val liuhan: Int = 27 + const val hanxiao: Int = 28 + const val dabing: Int = 29 + const val fendou: Int = 30 + const val zhouma: Int = 31 + const val yiwen: Int = 32 + const val yun: Int = 34 + const val zhemo: Int = 35 + const val shuai: Int = 36 + const val kulou: Int = 37 + const val qiaoda: Int = 38 + const val zaijian: Int = 39 + const val fadou: Int = 41 + const val aiqing: Int = 42 + const val tiaotiao: Int = 43 + const val zhutou: Int = 46 + const val yongbao: Int = 49 + const val dan_gao: Int = 53 + const val shandian: Int = 54 + const val zhadan: Int = 55 + const val dao: Int = 56 + const val zuqiu: Int = 57 + const val bianbian: Int = 59 + const val kafei: Int = 60 + const val fan: Int = 61 + const val meigui: Int = 63 + const val diaoxie: Int = 64 + const val aixin: Int = 66 + const val xinsui: Int = 67 + const val liwu: Int = 69 + const val taiyang: Int = 74 + const val yueliang: Int = 75 + const val qiang: Int = 76 + const val ruo: Int = 77 + const val woshou: Int = 78 + const val shengli: Int = 79 + const val feiwen: Int = 85 + const val naohuo: Int = 86 + const val xigua: Int = 89 + const val lenghan: Int = 96 + const val cahan: Int = 97 + const val koubi: Int = 98 + const val guzhang: Int = 99 + const val qiudale: Int = 100 + const val huaixiao: Int = 101 + const val zuohengheng: Int = 102 + const val youhengheng: Int = 103 + const val haqian: Int = 104 + const val bishi: Int = 105 + const val weiqu: Int = 106 + const val kuaikule: Int = 107 + const val yinxian: Int = 108 + const val qinqin: Int = 109 + const val xia: Int = 110 + const val kelian: Int = 111 + const val caidao: Int = 112 + const val pijiu: Int = 113 + const val lanqiu: Int = 114 + const val pingpang: Int = 115 + const val shiai: Int = 116 + const val piaochong: Int = 117 + const val baoquan: Int = 118 + const val gouyin: Int = 119 + const val quantou: Int = 120 + const val chajin: Int = 121 + const val aini: Int = 122 + const val bu: Int = 123 + const val hao: Int = 124 + const val zhuanquan: Int = 125 + const val ketou: Int = 126 + const val huitou: Int = 127 + const val tiaosheng: Int = 128 + const val huishou: Int = 129 + const val jidong: Int = 130 + const val jiewu: Int = 131 + const val xianwen: Int = 132 + const val zuotaiji: Int = 133 + const val youtaiji: Int = 134 + const val shuangxi: Int = 136 + const val bianpao: Int = 137 + const val denglong: Int = 138 + const val facai: Int = 139 + const val K_ge: Int = 140 + const val gouwu: Int = 141 + const val youjian: Int = 142 + const val shuai_qi: Int = 143 + const val hecai: Int = 144 + const val qidao: Int = 145 + const val baojin: Int = 146 + const val bangbangtang: Int = 147 + const val he_nai: Int = 148 + const val xiamian: Int = 149 + const val xiangjiao: Int = 150 + const val feiji: Int = 151 + const val kaiche: Int = 152 + const val gaotiezuochetou: Int = 153 + const val chexiang: Int = 154 + const val gaotieyouchetou: Int = 155 + const val duoyun: Int = 156 + const val xiayu: Int = 157 + const val chaopiao: Int = 158 + const val xiongmao: Int = 159 + const val dengpao: Int = 160 + const val fengche: Int = 161 + const val naozhong: Int = 162 + const val dasan: Int = 163 + const val caiqiu: Int = 164 + const val zuanjie: Int = 165 + const val shafa: Int = 166 + const val zhijin: Int = 167 + const val yao: Int = 168 + const val shouqiang: Int = 169 + const val qingwa: Int = 170 + } override fun eq(other: Message): Boolean { return other is Face && other.id == this.id } -} - -/** - * @author LamGC - */ -@Suppress("SpellCheckingInspection", "unused") -@UseExperimental(ExperimentalUnsignedTypes::class) -inline class FaceId constructor(inline val value: UByte) { - companion object { - @JvmStatic - val unknown: FaceId = FaceId(0xffu) - @JvmStatic - val jingya: FaceId = FaceId(0u) - @JvmStatic - val piezui: FaceId = FaceId(1u) - @JvmStatic - val se: FaceId = FaceId(2u) - @JvmStatic - val fadai: FaceId = FaceId(3u) - @JvmStatic - val deyi: FaceId = FaceId(4u) - @JvmStatic - val liulei: FaceId = FaceId(5u) - @JvmStatic - val haixiu: FaceId = FaceId(6u) - @JvmStatic - val bizui: FaceId = FaceId(7u) - @JvmStatic - val shui: FaceId = FaceId(8u) - @JvmStatic - val daku: FaceId = FaceId(9u) - @JvmStatic - val ganga: FaceId = FaceId(10u) - @JvmStatic - val fanu: FaceId = FaceId(11u) - @JvmStatic - val tiaopi: FaceId = FaceId(12u) - @JvmStatic - val ciya: FaceId = FaceId(13u) - @JvmStatic - val weixiao: FaceId = FaceId(14u) - @JvmStatic - val nanguo: FaceId = FaceId(15u) - @JvmStatic - val ku: FaceId = FaceId(16u) - @JvmStatic - val zhuakuang: FaceId = FaceId(18u) - @JvmStatic - val tu: FaceId = FaceId(19u) - @JvmStatic - val touxiao: FaceId = FaceId(20u) - @JvmStatic - val keai: FaceId = FaceId(21u) - @JvmStatic - val baiyan: FaceId = FaceId(22u) - @JvmStatic - val aoman: FaceId = FaceId(23u) - @JvmStatic - val ji_e: FaceId = FaceId(24u) - @JvmStatic - val kun: FaceId = FaceId(25u) - @JvmStatic - val jingkong: FaceId = FaceId(26u) - @JvmStatic - val liuhan: FaceId = FaceId(27u) - @JvmStatic - val hanxiao: FaceId = FaceId(28u) - @JvmStatic - val dabing: FaceId = FaceId(29u) - @JvmStatic - val fendou: FaceId = FaceId(30u) - @JvmStatic - val zhouma: FaceId = FaceId(31u) - @JvmStatic - val yiwen: FaceId = FaceId(32u) - @JvmStatic - val yun: FaceId = FaceId(34u) - @JvmStatic - val zhemo: FaceId = FaceId(35u) - @JvmStatic - val shuai: FaceId = FaceId(36u) - @JvmStatic - val kulou: FaceId = FaceId(37u) - @JvmStatic - val qiaoda: FaceId = FaceId(38u) - @JvmStatic - val zaijian: FaceId = FaceId(39u) - @JvmStatic - val fadou: FaceId = FaceId(41u) - @JvmStatic - val aiqing: FaceId = FaceId(42u) - @JvmStatic - val tiaotiao: FaceId = FaceId(43u) - @JvmStatic - val zhutou: FaceId = FaceId(46u) - @JvmStatic - val yongbao: FaceId = FaceId(49u) - @JvmStatic - val dan_gao: FaceId = FaceId(53u) - @JvmStatic - val shandian: FaceId = FaceId(54u) - @JvmStatic - val zhadan: FaceId = FaceId(55u) - @JvmStatic - val dao: FaceId = FaceId(56u) - @JvmStatic - val zuqiu: FaceId = FaceId(57u) - @JvmStatic - val bianbian: FaceId = FaceId(59u) - @JvmStatic - val kafei: FaceId = FaceId(60u) - @JvmStatic - val fan: FaceId = FaceId(61u) - @JvmStatic - val meigui: FaceId = FaceId(63u) - @JvmStatic - val diaoxie: FaceId = FaceId(64u) - @JvmStatic - val aixin: FaceId = FaceId(66u) - @JvmStatic - val xinsui: FaceId = FaceId(67u) - @JvmStatic - val liwu: FaceId = FaceId(69u) - @JvmStatic - val taiyang: FaceId = FaceId(74u) - @JvmStatic - val yueliang: FaceId = FaceId(75u) - @JvmStatic - val qiang: FaceId = FaceId(76u) - @JvmStatic - val ruo: FaceId = FaceId(77u) - @JvmStatic - val woshou: FaceId = FaceId(78u) - @JvmStatic - val shengli: FaceId = FaceId(79u) - @JvmStatic - val feiwen: FaceId = FaceId(85u) - @JvmStatic - val naohuo: FaceId = FaceId(86u) - @JvmStatic - val xigua: FaceId = FaceId(89u) - @JvmStatic - val lenghan: FaceId = FaceId(96u) - @JvmStatic - val cahan: FaceId = FaceId(97u) - @JvmStatic - val koubi: FaceId = FaceId(98u) - @JvmStatic - val guzhang: FaceId = FaceId(99u) - @JvmStatic - val qiudale: FaceId = FaceId(100u) - @JvmStatic - val huaixiao: FaceId = FaceId(101u) - @JvmStatic - val zuohengheng: FaceId = FaceId(102u) - @JvmStatic - val youhengheng: FaceId = FaceId(103u) - @JvmStatic - val haqian: FaceId = FaceId(104u) - @JvmStatic - val bishi: FaceId = FaceId(105u) - @JvmStatic - val weiqu: FaceId = FaceId(106u) - @JvmStatic - val kuaikule: FaceId = FaceId(107u) - @JvmStatic - val yinxian: FaceId = FaceId(108u) - @JvmStatic - val qinqin: FaceId = FaceId(109u) - @JvmStatic - val xia: FaceId = FaceId(110u) - @JvmStatic - val kelian: FaceId = FaceId(111u) - @JvmStatic - val caidao: FaceId = FaceId(112u) - @JvmStatic - val pijiu: FaceId = FaceId(113u) - @JvmStatic - val lanqiu: FaceId = FaceId(114u) - @JvmStatic - val pingpang: FaceId = FaceId(115u) - @JvmStatic - val shiai: FaceId = FaceId(116u) - @JvmStatic - val piaochong: FaceId = FaceId(117u) - @JvmStatic - val baoquan: FaceId = FaceId(118u) - @JvmStatic - val gouyin: FaceId = FaceId(119u) - @JvmStatic - val quantou: FaceId = FaceId(120u) - @JvmStatic - val chajin: FaceId = FaceId(121u) - @JvmStatic - val aini: FaceId = FaceId(122u) - @JvmStatic - val bu: FaceId = FaceId(123u) - @JvmStatic - val hao: FaceId = FaceId(124u) - @JvmStatic - val zhuanquan: FaceId = FaceId(125u) - @JvmStatic - val ketou: FaceId = FaceId(126u) - @JvmStatic - val huitou: FaceId = FaceId(127u) - @JvmStatic - val tiaosheng: FaceId = FaceId(128u) - @JvmStatic - val huishou: FaceId = FaceId(129u) - @JvmStatic - val jidong: FaceId = FaceId(130u) - @JvmStatic - val jiewu: FaceId = FaceId(131u) - @JvmStatic - val xianwen: FaceId = FaceId(132u) - @JvmStatic - val zuotaiji: FaceId = FaceId(133u) - @JvmStatic - val youtaiji: FaceId = FaceId(134u) - @JvmStatic - val shuangxi: FaceId = FaceId(136u) - @JvmStatic - val bianpao: FaceId = FaceId(137u) - @JvmStatic - val denglong: FaceId = FaceId(138u) - @JvmStatic - val facai: FaceId = FaceId(139u) - @JvmStatic - val K_ge: FaceId = FaceId(140u) - @JvmStatic - val gouwu: FaceId = FaceId(141u) - @JvmStatic - val youjian: FaceId = FaceId(142u) - @JvmStatic - val shuai_qi: FaceId = FaceId(143u) - @JvmStatic - val hecai: FaceId = FaceId(144u) - @JvmStatic - val qidao: FaceId = FaceId(145u) - @JvmStatic - val baojin: FaceId = FaceId(146u) - @JvmStatic - val bangbangtang: FaceId = FaceId(147u) - @JvmStatic - val he_nai: FaceId = FaceId(148u) - @JvmStatic - val xiamian: FaceId = FaceId(149u) - @JvmStatic - val xiangjiao: FaceId = FaceId(150u) - @JvmStatic - val feiji: FaceId = FaceId(151u) - @JvmStatic - val kaiche: FaceId = FaceId(152u) - @JvmStatic - val gaotiezuochetou: FaceId = FaceId(153u) - @JvmStatic - val chexiang: FaceId = FaceId(154u) - @JvmStatic - val gaotieyouchetou: FaceId = FaceId(155u) - @JvmStatic - val duoyun: FaceId = FaceId(156u) - @JvmStatic - val xiayu: FaceId = FaceId(157u) - @JvmStatic - val chaopiao: FaceId = FaceId(158u) - @JvmStatic - val xiongmao: FaceId = FaceId(159u) - @JvmStatic - val dengpao: FaceId = FaceId(160u) - @JvmStatic - val fengche: FaceId = FaceId(161u) - @JvmStatic - val naozhong: FaceId = FaceId(162u) - @JvmStatic - val dasan: FaceId = FaceId(163u) - @JvmStatic - val caiqiu: FaceId = FaceId(164u) - @JvmStatic - val zuanjie: FaceId = FaceId(165u) - @JvmStatic - val shafa: FaceId = FaceId(166u) - @JvmStatic - val zhijin: FaceId = FaceId(167u) - @JvmStatic - val yao: FaceId = FaceId(168u) - @JvmStatic - val shouqiang: FaceId = FaceId(169u) - @JvmStatic - val qingwa: FaceId = FaceId(170u) - } - - override fun toString(): String = "$FaceId($value)" -} +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt index 509dffe4a..f4e6dd98f 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt @@ -47,7 +47,7 @@ sealed class Image : Message { abstract val imageId: String final override fun toString(): String { - return "[image::$imageId]" + return "[mirai:$imageId]" } final override fun eq(other: Message): Boolean { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt index 4234275ce..7614f5d4c 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt @@ -57,7 +57,7 @@ interface Message { */ interface Key - infix fun eq(other: Message): Boolean = this == other + infix fun eq(other: Message): Boolean = this.toString() == other.toString() /** * 将 [toString] 与 [other] 比较 diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt index e9412ab1f..af0781be1 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt @@ -38,6 +38,7 @@ import kotlin.reflect.KProperty interface MessageChain : Message, MutableList { // region Message override override operator fun contains(sub: String): Boolean + override fun followedBy(tail: Message): MessageChain // endregion @@ -67,6 +68,36 @@ interface MessageChain : Message, MutableList { } } +/** + * 遍历每一个有内容的消息, 即 [At], [AtAll], [PlainText], [Image], [Face], [XMLMessage] + */ +inline fun MessageChain.foreachContent(block: (Message) -> Unit) { + this.forEachIndexed { index: Int, message: Message -> + if (message is At) { + if (index == 0 || this[index - 1] !is QuoteReply) { + block(message) + } + } else if (message.hasContent()) { + block(message) + } + } +} + +/** + * 判断这个 [Message] 是否含有内容, 即是否为 [At], [AtAll], [PlainText], [Image], [Face], [XMLMessage] + */ +fun Message.hasContent(): Boolean { + return when (this) { + is At, + is AtAll, + is PlainText, + is Image, + is Face, + is XMLMessage -> true + else -> false + } +} + /** * 提供一个类型的值. 若不存在则会抛出异常 [NoSuchElementException] */ diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt index 3653cfabb..e5b831b17 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt @@ -31,6 +31,11 @@ interface MessageSource : Message { */ val messageUid: Long + /** + * 发送时间, 单位为秒 + */ + val time: Long + /** * 发送人号码 */ diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt index 66e4a544e..b2f36d33b 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotNetworkHandler.kt @@ -68,7 +68,7 @@ abstract class BotNetworkHandler : CoroutineScope { */ @Suppress("SpellCheckingInspection") @MiraiInternalAPI - abstract suspend fun relogin() + abstract suspend fun relogin(cause: Throwable? = null) /** * 初始化获取好友列表等值. diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/MiraiLogger.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/MiraiLogger.kt index 068e67d4a..f912a8e0f 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/MiraiLogger.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/MiraiLogger.kt @@ -70,6 +70,13 @@ interface MiraiLogger { */ val identity: String? + /** + * 获取 [MiraiLogger] 是否已开启 + * + * 除 [MiraiLoggerWithSwitch] 可控制开关外, 其他的所有 [MiraiLogger] 均一直开启. + */ + val isEnabled: Boolean + /** * 随从. 在 this 中调用所有方法后都应继续往 [follower] 传递调用. * [follower] 的存在可以让一次日志被多个日志记录器记录. @@ -151,43 +158,43 @@ interface MiraiLogger { inline fun MiraiLogger.verbose(lazyMessage: () -> String) { - if (this is MiraiLoggerWithSwitch && switch) verbose(lazyMessage()) + if (isEnabled) verbose(lazyMessage()) } inline fun MiraiLogger.verbose(lazyMessage: () -> String, e: Throwable?) { - if (this is MiraiLoggerWithSwitch && switch) verbose(lazyMessage(), e) + if (isEnabled) verbose(lazyMessage(), e) } inline fun MiraiLogger.debug(lazyMessage: () -> String?) { - if (this is MiraiLoggerWithSwitch && switch) debug(lazyMessage()) + if (isEnabled) debug(lazyMessage()) } inline fun MiraiLogger.debug(lazyMessage: () -> String?, e: Throwable?) { - if (this is MiraiLoggerWithSwitch && switch) debug(lazyMessage(), e) + if (isEnabled) debug(lazyMessage(), e) } inline fun MiraiLogger.info(lazyMessage: () -> String?) { - if (this is MiraiLoggerWithSwitch && switch) info(lazyMessage()) + if (isEnabled) info(lazyMessage()) } inline fun MiraiLogger.info(lazyMessage: () -> String?, e: Throwable?) { - if (this is MiraiLoggerWithSwitch && switch) info(lazyMessage(), e) + if (isEnabled) info(lazyMessage(), e) } inline fun MiraiLogger.warning(lazyMessage: () -> String?) { - if (this is MiraiLoggerWithSwitch && switch) warning(lazyMessage()) + if (isEnabled) warning(lazyMessage()) } inline fun MiraiLogger.warning(lazyMessage: () -> String?, e: Throwable?) { - if (this is MiraiLoggerWithSwitch && switch) warning(lazyMessage(), e) + if (isEnabled) warning(lazyMessage(), e) } inline fun MiraiLogger.error(lazyMessage: () -> String?) { - if (this is MiraiLoggerWithSwitch && switch) error(lazyMessage()) + if (isEnabled) error(lazyMessage()) } inline fun MiraiLogger.error(lazyMessage: () -> String?, e: Throwable?) { - if (this is MiraiLoggerWithSwitch && switch) error(lazyMessage(), e) + if (isEnabled) error(lazyMessage(), e) } /** @@ -268,7 +275,7 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg @PublishedApi internal var switch: Boolean = default - val isEnabled: Boolean get() = switch + override val isEnabled: Boolean get() = switch fun enable() { switch = true @@ -278,16 +285,16 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg switch = false } - override fun verbose0(message: String?) = if (switch) delegate.verbose(message) else Unit - override fun verbose0(message: String?, e: Throwable?) = if (switch) delegate.verbose(message, e) else Unit - override fun debug0(message: String?) = if (switch) delegate.debug(message) else Unit - override fun debug0(message: String?, e: Throwable?) = if (switch) delegate.debug(message, e) else Unit - override fun info0(message: String?) = if (switch) delegate.info(message) else Unit - override fun info0(message: String?, e: Throwable?) = if (switch) delegate.info(message, e) else Unit - override fun warning0(message: String?) = if (switch) delegate.warning(message) else Unit - override fun warning0(message: String?, e: Throwable?) = if (switch) delegate.warning(message, e) else Unit - override fun error0(message: String?) = if (switch) delegate.error(message) else Unit - override fun error0(message: String?, e: Throwable?) = if (switch) delegate.error(message, e) else Unit + override fun verbose0(message: String?) = delegate.verbose(message) + override fun verbose0(message: String?, e: Throwable?) = delegate.verbose(message, e) + override fun debug0(message: String?) = delegate.debug(message) + override fun debug0(message: String?, e: Throwable?) = delegate.debug(message, e) + override fun info0(message: String?) = delegate.info(message) + override fun info0(message: String?, e: Throwable?) = delegate.info(message, e) + override fun warning0(message: String?) = delegate.warning(message) + override fun warning0(message: String?, e: Throwable?) = delegate.warning(message, e) + override fun error0(message: String?) = delegate.error(message) + override fun error0(message: String?, e: Throwable?) = delegate.error(message, e) } /** @@ -298,54 +305,65 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg * 在定义 logger 变量时, 请一直使用 [MiraiLogger] 或者 [MiraiLoggerWithSwitch]. */ abstract class MiraiLoggerPlatformBase : MiraiLogger { + override val isEnabled: Boolean get() = true final override var follower: MiraiLogger? = null final override fun verbose(message: String?) { + if (!isEnabled) return follower?.verbose(message) verbose0(message) } final override fun verbose(message: String?, e: Throwable?) { + if (!isEnabled) return follower?.verbose(message, e) verbose0(message, e) } final override fun debug(message: String?) { + if (!isEnabled) return follower?.debug(message) debug0(message) } final override fun debug(message: String?, e: Throwable?) { + if (!isEnabled) return follower?.debug(message, e) debug0(message, e) } final override fun info(message: String?) { + if (!isEnabled) return follower?.info(message) info0(message) } final override fun info(message: String?, e: Throwable?) { + if (!isEnabled) return follower?.info(message, e) info0(message, e) } final override fun warning(message: String?) { + if (!isEnabled) return follower?.warning(message) warning0(message) } final override fun warning(message: String?, e: Throwable?) { + if (!isEnabled) return follower?.warning(message, e) warning0(message, e) } final override fun error(message: String?) { + if (!isEnabled) return follower?.error(message) error0(message) } final override fun error(message: String?, e: Throwable?) { + if (!isEnabled) return follower?.error(message, e) error0(message, e) } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Annotations.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/annotataions.kt similarity index 100% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Annotations.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/annotataions.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt index 36ac8186e..0b93f83b5 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/ECDH.kt @@ -19,7 +19,9 @@ expect interface ECDHPublicKey { fun getEncoded(): ByteArray } -expect class ECDHKeyPair { +internal expect class ECDHKeyPairImpl : ECDHKeyPair + +interface ECDHKeyPair { val privateKey: ECDHPrivateKey val publicKey: ECDHPublicKey @@ -27,6 +29,15 @@ expect class ECDHKeyPair { * 私匙和固定公匙([initialPublicKey]) 计算得到的 shareKey */ val initialShareKey: ByteArray + + object DefaultStub : ECDHKeyPair { + val defaultPublicKey = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128".chunkedHexToBytes() + val defaultShareKey = "4da0f614fc9f29c2054c77048a6566d7".chunkedHexToBytes() + + override val privateKey: Nothing get() = error("stub!") + override val publicKey: Nothing get() = error("stub!") + override val initialShareKey: ByteArray get() = defaultShareKey + } } /** @@ -41,6 +52,8 @@ expect class ECDH(keyPair: ECDHKeyPair) { fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray companion object { + val isECDHAvailable: Boolean + /** * 由完整的 publicKey ByteArray 得到 [ECDHPublicKey] */ @@ -60,14 +73,11 @@ expect class ECDH(keyPair: ECDHKeyPair) { override fun toString(): String } -/** - * - */ @Suppress("FunctionName") expect fun ECDH(): ECDH -val initialPublicKey = - ECDH.constructPublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8".chunkedHexToBytes()) +val initialPublicKey + get() = ECDH.constructPublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8".chunkedHexToBytes()) private val commonHeadFor02 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes() private val commonHeadForNot02 = "3046301006072A8648CE3D020106052B8104001F033200".chunkedHexToBytes() private const val constantHead = "3046301006072A8648CE3D020106052B8104001F03320004" diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/Proto.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/protoBuf.kt similarity index 99% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/Proto.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/protoBuf.kt index 1096a3d6d..61dc928be 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/Proto.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/cryptor/protoBuf.kt @@ -17,7 +17,6 @@ import kotlinx.io.core.readUInt import kotlinx.io.core.readULong import net.mamoe.mirai.utils.MiraiDebugAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI -import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.io.* import kotlin.jvm.JvmStatic diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/ByteArrayUtil.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/byteArrays.kt similarity index 100% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/ByteArrayUtil.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/byteArrays.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConversion.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt similarity index 97% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConversion.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt index 75a93a88e..5e5f6973e 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConversion.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/conversion.kt @@ -21,6 +21,16 @@ import kotlin.random.nextInt * 这些函数为内部函数, 可能会改变 */ +/** + * 255 -> 00 FF + */ +fun Short.toByteArray(): ByteArray = with(toInt()) { + byteArrayOf( + (shr(8) and 0xFF).toByte(), + (shr(0) and 0xFF).toByte() + ) +} + /** * 255 -> 00 00 00 FF */ diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/DebugUtil.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/debugging.kt similarity index 100% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/DebugUtil.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/debugging.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/InputUtils.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/input.kt similarity index 100% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/InputUtils.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/input.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/OutputUtils.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/output.kt similarity index 100% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/OutputUtils.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/output.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/map.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/maps.kt similarity index 100% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/map.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/maps.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/NumberUtils.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt similarity index 100% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/NumberUtils.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/numbers.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/PlatformUtils.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt similarity index 100% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/PlatformUtils.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/platform.kt diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Time.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/time.kt similarity index 98% rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Time.kt rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/time.kt index b8cfd9ff8..81eba7d3b 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Time.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/time.kt @@ -14,7 +14,6 @@ package net.mamoe.mirai.utils import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName -import kotlin.time.seconds // 临时使用, 待 Kotlin Duration 稳定后使用 Duration. // 内联属性, 则将来删除这些 API 将不会导致二进制不兼容. diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/tryNTimes.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/tryNTimes.kt index 417d8536a..c6b6884d0 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/tryNTimes.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/tryNTimes.kt @@ -15,17 +15,16 @@ expect fun Throwable.addSuppressed(e: Throwable) @MiraiInternalAPI @Suppress("DuplicatedCode") -inline fun tryNTimes(repeat: Int, block: () -> R): R { +inline fun tryNTimes(repeat: Int, block: (Int) -> R): R { var lastException: Throwable? = null repeat(repeat) { try { - return block() + return block(it) } catch (e: Throwable) { if (lastException == null) { lastException = e - } - lastException!!.addSuppressed(e) + } else lastException!!.addSuppressed(e) } } @@ -34,17 +33,16 @@ inline fun tryNTimes(repeat: Int, block: () -> R): R { @MiraiInternalAPI @Suppress("DuplicatedCode") -inline fun tryNTimesOrNull(repeat: Int, block: () -> R): R? { +inline fun tryNTimesOrNull(repeat: Int, block: (Int) -> R): R? { var lastException: Throwable? = null repeat(repeat) { try { - return block() + return block(it) } catch (e: Throwable) { if (lastException == null) { lastException = e - } - lastException!!.addSuppressed(e) + } else lastException!!.addSuppressed(e) } } @@ -53,18 +51,17 @@ inline fun tryNTimesOrNull(repeat: Int, block: () -> R): R? { @MiraiInternalAPI @Suppress("DuplicatedCode") -inline fun tryNTimesOrException(repeat: Int, block: () -> R): Throwable? { +inline fun tryNTimesOrException(repeat: Int, block: (Int) -> R): Throwable? { var lastException: Throwable? = null repeat(repeat) { try { - block() + block(it) return null } catch (e: Throwable) { if (lastException == null) { lastException = e - } - lastException!!.addSuppressed(e) + } else lastException!!.addSuppressed(e) } } diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotFactoryJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotFactoryJvm.kt index e1fff684c..ea27fb1eb 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotFactoryJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotFactoryJvm.kt @@ -29,13 +29,13 @@ internal val factory: BotFactory = run { """ No BotFactory found. Please ensure that you've added dependency of protocol modules. Available modules: - - net.mamoe:mirai-core-timpc + - net.mamoe:mirai-core-timpc (stays at 0.12.0) - net.mamoe:mirai-core-qqandroid (recommended) You should have at lease one protocol module installed. ------------------------------------------------------- 找不到 BotFactory. 请确保你依赖了至少一个协议模块. 可用的协议模块: - - net.mamoe:mirai-core-timpc + - net.mamoe:mirai-core-timpc (0.12.0 后停止更新) - net.mamoe:mirai-core-qqandroid (推荐) 请添加上述任一模块的依赖(与 mirai-core 版本相同) """.trimIndent() diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/MiraiEnvironmentJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/MiraiEnvironmentJvm.kt deleted file mode 100644 index ceebf145b..000000000 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/MiraiEnvironmentJvm.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -@file:Suppress("MayBeConstant", "unused") - -package net.mamoe.mirai - -actual object MiraiEnvironment { - @JvmStatic - actual val platform: Platform - get() = Platform.JVM -} \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/EventInternalJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/EventInternalJvm.kt index ccc2a718f..ddae0cdd2 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/EventInternalJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/EventInternalJvm.kt @@ -16,15 +16,16 @@ import net.mamoe.mirai.event.ListeningStatus import net.mamoe.mirai.utils.MiraiInternalAPI import java.util.function.Consumer import java.util.function.Function +import kotlin.coroutines.EmptyCoroutineContext @MiraiInternalAPI @Suppress("FunctionName") fun Class._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Function): Listener { - return this.kotlin.subscribeInternal(scope.Handler { onEvent.apply(it) }) + return this.kotlin.subscribeInternal(scope.Handler(EmptyCoroutineContext) { onEvent.apply(it) }) } @MiraiInternalAPI @Suppress("FunctionName") fun Class._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Consumer): Listener { - return this.kotlin.subscribeInternal(scope.Handler { onEvent.accept(it); ListeningStatus.LISTENING; }) + return this.kotlin.subscribeInternal(scope.Handler(EmptyCoroutineContext) { onEvent.accept(it); ListeningStatus.LISTENING; }) } \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/MiraiAtomicBoolean.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/MiraiAtomicBoolean.kt new file mode 100644 index 000000000..0b468cf04 --- /dev/null +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/event/internal/MiraiAtomicBoolean.kt @@ -0,0 +1,18 @@ +package net.mamoe.mirai.event.internal + +import java.util.concurrent.atomic.AtomicBoolean + + +internal actual class MiraiAtomicBoolean actual constructor(initial: Boolean) { + private val delegate: AtomicBoolean = AtomicBoolean(initial) + + actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean { + return delegate.compareAndSet(expect, update) + } + + actual var value: Boolean + get() = delegate.get() + set(value) { + delegate.set(value) + } +} \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/BotConfigurationJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/BotConfigurationJvm.kt index fe4402025..5f9115b07 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/BotConfigurationJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/BotConfigurationJvm.kt @@ -105,8 +105,8 @@ class DefaultLoginSolver( override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? = loginSolverLock.withLock { val logger = getLogger(bot) logger.info("需要进行账户安全认证") - logger.info("该账户有[设备锁]/[不常用登陆地点]/[不常用设备登陆]的问题") - logger.info("完成以下账号认证即可成功登陆|理论本认证在mirai每个账户中最多出现1次") + logger.info("该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题") + logger.info("完成以下账号认证即可成功登录|理论本认证在mirai每个账户中最多出现1次") logger.info("请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符") logger.info("这步操作将在后续的版本中优化") logger.info(url) @@ -221,11 +221,11 @@ actual open class BotConfiguration actual constructor() { /** * 重连失败后, 继续尝试的每次等待时间 */ - actual var reconnectPeriodMillis: Long = 60.secondsToMillis + actual var reconnectPeriodMillis: Long = 5.secondsToMillis /** * 最多尝试多少次重连 */ - actual var reconnectionRetryTimes: Int = 3 + actual var reconnectionRetryTimes: Int = Int.MAX_VALUE /** * 验证码处理器 */ diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt index 0d6e93829..a0e236b71 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/cryptor/ECDHJvm.kt @@ -9,7 +9,6 @@ package net.mamoe.mirai.utils.cryptor -import net.mamoe.mirai.utils.io.chunkedHexToBytes import net.mamoe.mirai.utils.md5 import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.* @@ -21,20 +20,13 @@ import javax.crypto.KeyAgreement actual typealias ECDHPrivateKey = PrivateKey actual typealias ECDHPublicKey = PublicKey -actual class ECDHKeyPair( - private val delegate: KeyPair? -) { - actual val privateKey: ECDHPrivateKey get() = delegate?.private ?: error("ECDH is not available") - actual val publicKey: ECDHPublicKey get() = delegate?.public ?: defaultPublicKey +internal actual class ECDHKeyPairImpl( + private val delegate: KeyPair +) : ECDHKeyPair { + override val privateKey: ECDHPrivateKey get() = delegate.private + override val publicKey: ECDHPublicKey get() = delegate.public - actual val initialShareKey: ByteArray = if (delegate == null) { - defaultShareKey - } else ECDH.calculateShareKey(privateKey, initialPublicKey) - - companion object { - internal val defaultPublicKey = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128".chunkedHexToBytes().adjustToPublicKey() - internal val defaultShareKey = "4da0f614fc9f29c2054c77048a6566d7".chunkedHexToBytes() - } + override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey) } @Suppress("FunctionName") @@ -42,33 +34,34 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair()) actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual companion object { - private var isECDHAvailable = true + @Suppress("ObjectPropertyName") + private val _isECDHAvailable: Boolean = kotlin.runCatching { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + } + Security.addProvider(BouncyCastleProvider()) + generateKeyPair() // try if it is working + }.isSuccess - init { - isECDHAvailable = kotlin.runCatching { - Security.addProvider(BouncyCastleProvider()) - generateKeyPair() // try if it is working - }.isSuccess - } + actual val isECDHAvailable: Boolean get() = _isECDHAvailable actual fun generateKeyPair(): ECDHKeyPair { - return if (!isECDHAvailable) { - ECDHKeyPair(null) - } else ECDHKeyPair(KeyPairGenerator.getInstance("EC", "BC").apply { initialize(ECGenParameterSpec("secp192k1")) }.genKeyPair()) + if (!isECDHAvailable) { + return ECDHKeyPair.DefaultStub + } + return ECDHKeyPairImpl(KeyPairGenerator.getInstance("ECDH") + .also { it.initialize(ECGenParameterSpec("secp192k1")) } + .genKeyPair()) } actual fun calculateShareKey( privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey ): ByteArray { - return if (!isECDHAvailable) { - ECDHKeyPair.defaultShareKey - } else { - val instance = KeyAgreement.getInstance("ECDH", "BC") - instance.init(privateKey) - instance.doPhase(publicKey, true) - md5(instance.generateSecret()) - } + val instance = KeyAgreement.getInstance("ECDH", "BC") + instance.init(privateKey) + instance.doPhase(publicKey, true) + return md5(instance.generateSecret()) } actual fun constructPublicKey(key: ByteArray): ECDHPublicKey { diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/io/PlatformSocket.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/io/PlatformSocket.kt index 07f22e1e3..f6c1d2513 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/io/PlatformSocket.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/io/PlatformSocket.kt @@ -30,7 +30,10 @@ actual class PlatformSocket : Closeable { private lateinit var socket: Socket actual val isOpen: Boolean - get() = socket.isConnected + get() = + if (::socket.isInitialized) + socket.isConnected + else false actual override fun close() { if (::socket.isInitialized) { diff --git a/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/utils/LockFreeLinkedListTest.kt b/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/utils/LockFreeLinkedListTest.kt index 413a510ce..1ffc86c6a 100644 --- a/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/utils/LockFreeLinkedListTest.kt +++ b/mirai-core/src/jvmTest/kotlin/net/mamoe/mirai/utils/LockFreeLinkedListTest.kt @@ -63,9 +63,6 @@ internal class LockFreeLinkedListTest { val addJob = async { list.concurrentDo(2, 30000) { addLast(1) } } //delay(1) // let addJob fly - if (addJob.isCompleted) { - println("Number of elements are not enough") - } val foreachJob = async { list.concurrentDo(1, 10000) { forEach { it + it } diff --git a/mirai-japt/README.md b/mirai-japt/README.md index 234ef52b6..73586ab92 100644 --- a/mirai-japt/README.md +++ b/mirai-japt/README.md @@ -32,7 +32,7 @@ Mirai Java Apt net.mamoe - mirai-core-qqandroid + mirai-core-qqandroid-jvm CORE_VERSION @@ -51,7 +51,7 @@ repositories { } dependencies { - implementation("net.mamoe:mirai-core-qqandroid:CORE_VERSION") + implementation("net.mamoe:mirai-core-qqandroid-jvm:CORE_VERSION") implementation("net.mamoe:mirai-japt:JAPT_VERSION") } ``` diff --git a/mirai-japt/build.gradle.kts b/mirai-japt/build.gradle.kts index 3bd7d20a5..423789c2e 100644 --- a/mirai-japt/build.gradle.kts +++ b/mirai-japt/build.gradle.kts @@ -59,7 +59,7 @@ fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$v fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version" dependencies { - api(project(":mirai-core")) + implementation(project(":mirai-core")) runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main")) // classpath is not added correctly by IDE api(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-javafx", version = "1.3.2") diff --git a/mirai-japt/src/main/java/net/mamoe/mirai/japt/BlockingBot.java b/mirai-japt/src/main/java/net/mamoe/mirai/japt/BlockingBot.java index fb55493a6..6b2e17197 100644 --- a/mirai-japt/src/main/java/net/mamoe/mirai/japt/BlockingBot.java +++ b/mirai-japt/src/main/java/net/mamoe/mirai/japt/BlockingBot.java @@ -184,5 +184,13 @@ public interface BlockingBot { /** * 关闭这个 [Bot], 停止一切相关活动. 不可重新登录. */ - void dispose(@Nullable Throwable throwable); + void close(@Nullable Throwable throwable); + + /** + * @deprecated 使用 {@link #close(Throwable)} + */ + @Deprecated + default void dispose(@Nullable Throwable throwable) { + close(throwable); + } } diff --git a/mirai-japt/src/main/kotlin/net/mamoe/mirai/japt/internal/BlockingBotImpl.kt b/mirai-japt/src/main/kotlin/net/mamoe/mirai/japt/internal/BlockingBotImpl.kt index 0fbc212b4..d589fed7c 100644 --- a/mirai-japt/src/main/kotlin/net/mamoe/mirai/japt/internal/BlockingBotImpl.kt +++ b/mirai-japt/src/main/kotlin/net/mamoe/mirai/japt/internal/BlockingBotImpl.kt @@ -65,5 +65,5 @@ internal class BlockingBotImpl(private val bot: Bot) : BlockingBot { override fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult = runBlocking { bot.addFriend(id, message, remark) } override fun approveFriendAddRequest(id: Long, remark: String?) = runBlocking { bot.approveFriendAddRequest(id, remark) } - override fun dispose(throwable: Throwable?) = bot.close(throwable) + override fun close(throwable: Throwable?) = bot.close(throwable) } \ No newline at end of file