Commit 687a773e authored by jiahua.liu's avatar jiahua.liu

Merge remote-tracking branch 'origin/master'

parents cfeb794e 1f4a4123
...@@ -2,6 +2,53 @@ ...@@ -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 ## `0.15.2` 2020/2/18
### mirai-core ### mirai-core
......
<div align="center"> <div align="center">
<img width="160" src="http://img.mamoe.net/2020/02/16/a759783b42f72.png" alt="logo"></br> <img width="160" src="http://img.mamoe.net/2020/02/16/a759783b42f72.png" alt="logo"></br>
<img width="95" src="http://img.mamoe.net/2020/02/16/c4aece361224d.png" alt="title"> <img width="95" src="http://img.mamoe.net/2020/02/16/c4aece361224d.png" alt="title">
---- ----
[![Gitter](https://badges.gitter.im/mamoe/mirai.svg)](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![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) [![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/) [![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 协议支持 ...@@ -16,6 +20,7 @@ Mirai 是一个在全平台下运行,提供 QQ Android 和 TIM PC 协议支持
</div> </div>
## Mirai ## Mirai
**[English](README-eng.md)** **[English](README-eng.md)**
多平台 **QQ Android 和 TimPC** 协议支持库与高效率的机器人框架. 多平台 **QQ Android 和 TimPC** 协议支持库与高效率的机器人框架.
...@@ -27,94 +32,59 @@ Mirai既可以作为你项目中的QQ协议支持Lib, 也可以作为单独的Ap ...@@ -27,94 +32,59 @@ Mirai既可以作为你项目中的QQ协议支持Lib, 也可以作为单独的Ap
加入 Gitter, 或加入 QQ 群: 655057127 加入 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 ## CHANGELOG
[Project](https://github.com/mamoe/mirai/projects/3) 查看已支持功能和计划 [Project](https://github.com/mamoe/mirai/projects/3) 查看已支持功能和计划
[CHANGELOG](https://github.com/mamoe/mirai/blob/master/CHANGELOG.md) 查看版本更新记录 (仅发布的版本) [CHANGELOG](https://github.com/mamoe/mirai/blob/master/CHANGELOG.md) 查看版本更新记录 (仅发布的版本)
## Modules ## Modules
### mirai-core ### mirai-core
通用 API 模块,一套 API 适配两套协议。 通用 API 模块,一套 API 适配两套协议。
**请参考此模块的 API** **请参考此模块的 API**
### mirai-core-qqandroid ### mirai-core-qqandroid
QQ for Android (8.2.0 版本,2019 年 12 月)协议的实现,目前完成大部分。 QQ for Android (8.2.0 版本,2019 年 12 月)协议的实现,目前完成大部分。
- 高兼容性:协议仅含极少部分为硬编码,其余全部随官方方式动态生成 - 高兼容性:协议仅含极少部分为硬编码,其余全部随官方方式动态生成
- 高安全性:密匙随机,ECDH 动态计算 - 高安全性:密匙随机,ECDH 动态计算
- 已支持大部分使用场景, 详情请在[Project](https://github.com/mamoe/mirai/projects/3)查看 - 已支持大部分使用场景, 详情请在[Project](https://github.com/mamoe/mirai/projects/3)查看
### mirai-core-timpc ### 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
<repositories>
<repository>
<id>jcenter</id>
<url>https://jcenter.bintray.com/</url>
</repository>
</repositories>
```
```xml
<dependencies>
<dependency>
<groupId>net.mamoe</groupId>
<artifactId>mirai-core-qqandroid</artifactId>
<version>0.15.1</version> <!-- 替换版本为最新版本 -->
</dependency>
</dependencies>
```
### 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 ## Contribution
...@@ -125,41 +95,12 @@ JVM 上启动需 80M 内存, 每多一个机器人实例需要 30M 内存. ...@@ -125,41 +95,12 @@ JVM 上启动需 80M 内存, 每多一个机器人实例需要 30M 内存.
您的 star 是对我们最大的鼓励(点击项目右上角) 您的 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<MemberPermissionChangedEvent> {
if (it.kind == BECOME_OPERATOR)
reply("${it.member.id} 成为了管理员")
}
```
1. Clone
2. Import as Gradle project
3. 运行 Demo 程序: [mirai-demo](#mirai-demo) 示例和演示程序
## Libraries used
## Build Requirements
- 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) - [kotlin-stdlib](https://github.com/JetBrains/kotlin)
- [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines) - [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines)
- [kotlinx-io](https://github.com/Kotlin/kotlinx-io) - [kotlinx-io](https://github.com/Kotlin/kotlinx-io)
...@@ -176,15 +117,21 @@ bot.subscribeAlways<MemberPermissionChangedEvent> { ...@@ -176,15 +117,21 @@ bot.subscribeAlways<MemberPermissionChangedEvent> {
- [toml4j](https://github.com/mwanji/toml4j) - [toml4j](https://github.com/mwanji/toml4j)
- [snakeyaml](https://mvnrepository.com/artifact/org.yaml/snakeyaml) - [snakeyaml](https://mvnrepository.com/artifact/org.yaml/snakeyaml)
## License ## License
协议原版权归属腾讯科技股份有限公司所有,本项目其他代码遵守: 协议原版权归属腾讯科技股份有限公司所有,本项目其他代码遵守:
**GNU AFFERO GENERAL PUBLIC LICENSE version 3** **GNU AFFERO GENERAL PUBLIC LICENSE version 3**
其中部分要求: 其中部分要求:
- (见 LICENSE 第 13 节) 尽管本许可协议有其他规定,但如果您修改本程序,则修改后的版本必须显着地为所有通过计算机网络与它进行远程交互的用户(如果您的版本支持这种交互)提供从网络服务器通过一些标准或惯用的软件复制方法**免费**访问相应的**源代码**的机会 - (见 LICENSE 第 13 节) 尽管本许可协议有其他规定,但如果您修改本程序,则修改后的版本必须显着地为所有通过计算机网络与它进行远程交互的用户(如果您的版本支持这种交互)提供从网络服务器通过一些标准或惯用的软件复制方法**免费**访问相应的**源代码**的机会
- (见 LICENSE 第 4 节) 您可以免费或收费地传递这个项目的源代码或目标代码(即编译结果), **但前提是提供明显的版权声明** (您需要标注本 `GitHub` 项目地址) - (见 LICENSE 第 4 节) 您可以免费或收费地传递这个项目的源代码或目标代码(即编译结果), **但前提是提供明显的版权声明** (您需要标注本 `GitHub` 项目地址)
## Acknowledgement ## Acknowledgement
特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 为开源项目提供免费的 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 的授权 特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 为开源项目提供免费的 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 的授权
[<img src=".github/jetbrains-variant-3.png" width="200"/>](https://www.jetbrains.com/?from=mirai) [<img src=".github/jetbrains-variant-3.png" width="200"/>](https://www.jetbrains.com/?from=mirai)
\ No newline at end of file
\ No newline at end of file
# 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
<repositories>
<repository>
<id>jcenter</id>
<url>https://jcenter.bintray.com/</url>
</repository>
</repositories>
```
```xml
<dependencies>
<dependency>
<groupId>net.mamoe</groupId>
<artifactId>mirai-core-qqandroid-jvm</artifactId>
<version>0.17.0</version> <!-- 替换版本为最新版本 -->
</dependency>
</dependencies>
```
# 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
<repositories>
<repository>
<id>jcenter</id>
<url>https://jcenter.bintray.com/</url>
</repository>
</repositories>
```
```xml
<dependencies>
<dependency>
<groupId>net.mamoe</groupId>
<artifactId>mirai-core-qqandroid-jvm</artifactId>
<version>0.15.1</version> <!-- 替换版本为最新版本 -->
</dependency>
</dependencies>
```
### 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<MemberPermissionChangedEvent> {
if (it.kind == BECOME_OPERATOR)
reply("${it.member.id} 成为了管理员")
}
```
### Performance
Android 上, Mirai 运行需使用 80M 内存.
JVM 上启动需 80M 内存, 每多一个机器人实例需要 30M 内存.
# style guide # style guide
kotlin.code.style=official kotlin.code.style=official
# config # config
mirai_version=0.15.2 mirai_version=0.17.0
mirai_japt_version=1.0.1 mirai_japt_version=1.0.1
kotlin.incremental.multiplatform=true kotlin.incremental.multiplatform=true
kotlin.parallel.tasks.in.project=true kotlin.parallel.tasks.in.project=true
......
...@@ -94,7 +94,7 @@ publishing { ...@@ -94,7 +94,7 @@ publishing {
it.artifactId = "$project.name-common" it.artifactId = "$project.name-common"
break break
case 'jvm': case 'jvm':
it.artifactId = "${project.name.replace("-jvm", "")}" it.artifactId = "$project.name-jvm"
break break
case 'js': case 'js':
case 'native': case 'native':
......
#Thu Feb 06 14:10:33 CST 2020 #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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
......
...@@ -12,10 +12,12 @@ fun main() { ...@@ -12,10 +12,12 @@ fun main() {
MiraiHttpAPIServer.start() MiraiHttpAPIServer.start()
bot.network.awaitDisconnection() bot.join()
} }
``` ```
## 认证相关 ## 认证相关
### 开始会话-认证(Authorize) ### 开始会话-认证(Authorize)
...@@ -141,6 +143,8 @@ fun main() { ...@@ -141,6 +143,8 @@ fun main() {
> SessionKey与Bot 对应错误时将会返回状态码5:指定对象不存在 > SessionKey与Bot 对应错误时将会返回状态码5:指定对象不存在
## 消息相关 ## 消息相关
...@@ -261,10 +265,10 @@ fun main() { ...@@ -261,10 +265,10 @@ fun main() {
### 发送图片消息(通过URL) ### 发送图片消息(通过URL)
``` ```
[POST] /sendGroupMessage [POST] /sendImageMessage
``` ```
使用此方法向指定群发送消息 使用此方法向指定对象(群或好友)发送图片消息
#### 请求 #### 请求
...@@ -303,7 +307,7 @@ fun main() { ...@@ -303,7 +307,7 @@ fun main() {
### 图片文件上传 ### 图片文件上传
``` ```
[POST] /sendGroupMessage [POST] /uploadImage
``` ```
使用此方法上传图片文件至服务器并返回ImageId 使用此方法上传图片文件至服务器并返回ImageId
...@@ -414,10 +418,10 @@ Content-Type:multipart/form-data ...@@ -414,10 +418,10 @@ Content-Type:multipart/form-data
} }
``` ```
| 名字 | 类型 | 说明 | | 名字 | 类型 | 说明 |
| ------- | ------ | ------------------------- | | ------- | ------ | ---------------------------------------------- |
| target | Long | 群员QQ号 | | target | Long | 群员QQ号 |
| display | String | @时显示的文本如:"@Mirai" | | dispaly | String | At时显示的文字,发送消息时无效,自动使用群名片 |
#### AtAll #### AtAll
...@@ -463,7 +467,7 @@ Content-Type:multipart/form-data ...@@ -463,7 +467,7 @@ Content-Type:multipart/form-data
{ {
"type": "Image", "type": "Image",
"imageId": "{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png" //群图片格式 "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 ...@@ -517,6 +521,7 @@ Content-Type:multipart/form-data
``` ```
### 获取群列表 ### 获取群列表
使用此方法获取bot的群列表 使用此方法获取bot的群列表
...@@ -786,6 +791,8 @@ Content-Type:multipart/form-data ...@@ -786,6 +791,8 @@ Content-Type:multipart/form-data
} }
``` ```
### 获取群设置 ### 获取群设置
使用此方法获取群设置 使用此方法获取群设置
...@@ -816,6 +823,7 @@ Content-Type:multipart/form-data ...@@ -816,6 +823,7 @@ Content-Type:multipart/form-data
``` ```
### 修改群员资料 ### 修改群员资料
使用此方法修改群员资料(需要有相关限权) 使用此方法修改群员资料(需要有相关限权)
...@@ -856,6 +864,8 @@ Content-Type:multipart/form-data ...@@ -856,6 +864,8 @@ Content-Type:multipart/form-data
} }
``` ```
### 获取群员资料 ### 获取群员资料
使用此方法获取群员资料 使用此方法获取群员资料
......
...@@ -13,6 +13,8 @@ import kotlinx.coroutines.* ...@@ -13,6 +13,8 @@ import kotlinx.coroutines.*
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.api.http.queue.MessageQueue import net.mamoe.mirai.api.http.queue.MessageQueue
import net.mamoe.mirai.event.Listener 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.event.subscribeMessages
import net.mamoe.mirai.message.MessagePacket import net.mamoe.mirai.message.MessagePacket
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
...@@ -102,12 +104,10 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses ...@@ -102,12 +104,10 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses
class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext) : Session(coroutineContext) { class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext) : Session(coroutineContext) {
val messageQueue = MessageQueue() val messageQueue = MessageQueue()
private val _listener: Listener<MessagePacket<*, *>> private val _listener: Listener<BotEvent>
init { init {
bot.subscribeMessages { _listener = bot.subscribeAlways{ this.run(messageQueue::add) }
_listener = always { this.run(messageQueue::add) } // this aka messagePacket
}
} }
override fun close() { override fun close() {
......
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()
...@@ -37,7 +37,7 @@ data class MemberDTO( ...@@ -37,7 +37,7 @@ data class MemberDTO(
val group: GroupDTO val group: GroupDTO
) : ContactDTO() { ) : ContactDTO() {
constructor(member: Member) : this( constructor(member: Member) : this(
member.id, member.groupCardOrNick, member.permission, member.id, member.nameCardOrNick, member.permission,
GroupDTO(member.group) GroupDTO(member.group)
) )
} }
......
package net.mamoe.mirai.api.http.data.common package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.* import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.Transient
import kotlinx.serialization.modules.SerializersModule
import net.mamoe.mirai.api.http.AuthedSession import net.mamoe.mirai.api.http.AuthedSession
interface DTO interface DTO
...@@ -16,3 +15,8 @@ abstract class VerifyDTO : DTO { ...@@ -16,3 +15,8 @@ abstract class VerifyDTO : DTO {
@Transient @Transient
lateinit var session: AuthedSession // 反序列化验证成功后传入 lateinit var session: AuthedSession // 反序列化验证成功后传入
} }
@Serializable
abstract class EventDTO : DTO
object IgnoreEventDTO : EventDTO()
\ No newline at end of file
...@@ -11,6 +11,8 @@ package net.mamoe.mirai.api.http.data.common ...@@ -11,6 +11,8 @@ package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable 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.FriendMessage
import net.mamoe.mirai.message.GroupMessage import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessagePacket import net.mamoe.mirai.message.MessagePacket
...@@ -30,32 +32,36 @@ data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO() ...@@ -30,32 +32,36 @@ data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO()
@SerialName("GroupMessage") @SerialName("GroupMessage")
data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO() data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO()
@Serializable
@SerialName("UnKnownMessage")
data class UnKnownMessagePacketDTO(val msg: String) : MessagePacketDTO()
// Message // Message
@Serializable @Serializable
@SerialName("Source") @SerialName("Source")
data class MessageSourceDTO(val uid: Long) : MessageDTO() data class MessageSourceDTO(val uid: Long) : MessageDTO()
@Serializable @Serializable
@SerialName("At") @SerialName("At")
data class AtDTO(val target: Long, val display: String) : MessageDTO() data class AtDTO(val target: Long, val display: String = "") : MessageDTO()
@Serializable @Serializable
@SerialName("AtAll") @SerialName("AtAll")
data class AtAllDTO(val target: Long = 0) : MessageDTO() // target为保留字段 data class AtAllDTO(val target: Long = 0) : MessageDTO() // target为保留字段
@Serializable @Serializable
@SerialName("Face") @SerialName("Face")
data class FaceDTO(val faceId: Int) : MessageDTO() data class FaceDTO(val faceId: Int) : MessageDTO()
@Serializable @Serializable
@SerialName("Plain") @SerialName("Plain")
data class PlainDTO(val text: String) : MessageDTO() data class PlainDTO(val text: String) : MessageDTO()
@Serializable @Serializable
@SerialName("Image") @SerialName("Image")
data class ImageDTO(val imageId: String) : MessageDTO() data class ImageDTO(val imageId: String) : MessageDTO()
@Serializable @Serializable
@SerialName("Xml") @SerialName("Xml")
data class XmlDTO(val xml: String) : MessageDTO() data class XmlDTO(val xml: String) : MessageDTO()
@Serializable @Serializable
@SerialName("Unknown") @SerialName("Unknown")
data class UnknownMessageDTO(val text: String) : MessageDTO() data class UnknownMessageDTO(val text: String) : MessageDTO()
...@@ -64,11 +70,11 @@ data class UnknownMessageDTO(val text: String) : MessageDTO() ...@@ -64,11 +70,11 @@ data class UnknownMessageDTO(val text: String) : MessageDTO()
* Abstract Class * Abstract Class
* */ * */
@Serializable @Serializable
sealed class MessagePacketDTO : DTO { sealed class MessagePacketDTO : EventDTO() {
lateinit var messageChain : MessageChainDTO lateinit var messageChain: MessageChainDTO
} }
typealias MessageChainDTO = Array<MessageDTO> typealias MessageChainDTO = List<MessageDTO>
@Serializable @Serializable
sealed class MessageDTO : DTO sealed class MessageDTO : DTO
...@@ -77,21 +83,25 @@ sealed class MessageDTO : DTO ...@@ -77,21 +83,25 @@ sealed class MessageDTO : DTO
/* /*
Extend function Extend function
*/ */
suspend fun MessagePacket<*, *>.toDTO(): MessagePacketDTO = when (this) { fun MessagePacket<*, *>.toDTO() = when (this) {
is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender)) is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender))
is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender)) is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender))
else -> UnKnownMessagePacketDTO("UnKnown Message Packet") else -> IgnoreEventDTO
}.apply { messageChain = Array(message.size){ message[it].toDTO() }} }.apply {
if (this is MessagePacketDTO) {
messageChain = mutableListOf<MessageDTO>().also { ls -> message.foreachContent { ls.add(it.toDTO()) } }
}
}
fun MessageChainDTO.toMessageChain() = fun MessageChainDTO.toMessageChain(contact: Contact) =
MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage()) } } MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage(contact)) } }
@UseExperimental(ExperimentalUnsignedTypes::class) @UseExperimental(ExperimentalUnsignedTypes::class)
fun Message.toDTO() = when (this) { fun Message.toDTO() = when (this) {
is MessageSource -> MessageSourceDTO(messageUid) is MessageSource -> MessageSourceDTO(messageUid)
is At -> AtDTO(target, display) is At -> AtDTO(target, display)
is AtAll -> AtAllDTO(0L) is AtAll -> AtAllDTO(0L)
is Face -> FaceDTO(id.value.toInt()) is Face -> FaceDTO(id)
is PlainText -> PlainDTO(stringValue) is PlainText -> PlainDTO(stringValue)
is Image -> ImageDTO(imageId) is Image -> ImageDTO(imageId)
is XMLMessage -> XmlDTO(stringValue) is XMLMessage -> XmlDTO(stringValue)
...@@ -99,15 +109,13 @@ fun Message.toDTO() = when (this) { ...@@ -99,15 +109,13 @@ fun Message.toDTO() = when (this) {
} }
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class) @UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
fun MessageDTO.toMessage() = when (this) { fun MessageDTO.toMessage(contact: Contact) = when (this) {
is AtDTO -> At(target, display) is AtDTO -> At((contact as Group)[target])
is AtAllDTO -> AtAll is AtAllDTO -> AtAll
is FaceDTO -> Face(FaceId(faceId.toUByte())) is FaceDTO -> Face(faceId)
is PlainDTO -> PlainText(text) is PlainDTO -> PlainText(text)
is ImageDTO -> Image(imageId) is ImageDTO -> Image(imageId)
is XmlDTO -> XMLMessage(xml) is XmlDTO -> XMLMessage(xml)
is MessageSourceDTO, is UnknownMessageDTO -> PlainText("assert cannot reach") is MessageSourceDTO, is UnknownMessageDTO -> PlainText("assert cannot reach")
} }
...@@ -9,26 +9,35 @@ ...@@ -9,26 +9,35 @@
package net.mamoe.mirai.api.http.queue 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.GroupMessage
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSource
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.ConcurrentLinkedDeque
class MessageQueue : ConcurrentLinkedDeque<MessagePacket<*, *>>() { class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
val quoteCache = ConcurrentHashMap<Long, GroupMessage>() val quoteCache = ConcurrentHashMap<Long, GroupMessage>()
fun fetch(size: Int): List<MessagePacket<*, *>> { fun fetch(size: Int): List<EventDTO> {
var count = size var count = size
quoteCache.clear() quoteCache.clear()
val ret = ArrayList<MessagePacket<*, *>>(count) val ret = ArrayList<EventDTO>(count)
while (!this.isEmpty() && count-- > 0) { while (!this.isEmpty() && count > 0) {
val packet = pop() val event = pop()
ret.add(packet)
if (packet is GroupMessage) { event.toDTO().also {
addCache(packet) if (it != IgnoreEventDTO) {
ret.add(it)
count--
}
}
if (event is GroupMessage) {
addCache(event)
} }
} }
return ret return ret
......
...@@ -18,6 +18,7 @@ import net.mamoe.mirai.api.http.AuthedSession ...@@ -18,6 +18,7 @@ import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.api.http.SessionManager import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.api.http.data.NoSuchBotException import net.mamoe.mirai.api.http.data.NoSuchBotException
import net.mamoe.mirai.api.http.data.StateCode 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 net.mamoe.mirai.api.http.data.common.VerifyDTO
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
...@@ -28,7 +29,7 @@ fun Application.authModule() { ...@@ -28,7 +29,7 @@ fun Application.authModule() {
if (it.authKey != SessionManager.authKey) { if (it.authKey != SessionManager.authKey) {
call.respondStateCode(StateCode(1, "Auth Key错误")) call.respondStateCode(StateCode(1, "Auth Key错误"))
} else { } else {
call.respondStateCode(StateCode(0, SessionManager.createTempSession().key)) call.respondDTO(AuthRetDTO(0, SessionManager.createTempSession().key))
} }
} }
...@@ -55,6 +56,9 @@ fun Application.authModule() { ...@@ -55,6 +56,9 @@ fun Application.authModule() {
} }
} }
@Serializable
private data class AuthRetDTO(val code: Int, val session: String) : DTO
@Serializable @Serializable
private data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO() private data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO()
......
...@@ -37,24 +37,28 @@ fun Application.messageModule() { ...@@ -37,24 +37,28 @@ fun Application.messageModule() {
miraiGet("/fetchMessage") { miraiGet("/fetchMessage") {
val count: Int = paramOrNull("count") val count: Int = paramOrNull("count")
val fetch = it.messageQueue.fetch(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<SendDTO>("/sendFriendMessage") { miraiVerify<SendDTO>("/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) call.respondStateCode(StateCode.Success)
} }
miraiVerify<SendDTO>("/sendGroupMessage") { miraiVerify<SendDTO>("/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) call.respondStateCode(StateCode.Success)
} }
miraiVerify<SendDTO>("/quoteMessage") { miraiVerify<SendDTO>("/quoteMessage") {
it.session.messageQueue.quoteCache[it.target]?.quoteReply(it.messageChain.toMessageChain()) it.session.messageQueue.quoteCache[it.target]?.apply {
?: throw NoSuchElementException() quoteReply(it.messageChain.toMessageChain(group))
} ?: throw NoSuchElementException()
call.respondStateCode(StateCode.Success) call.respondStateCode(StateCode.Success)
} }
......
...@@ -13,7 +13,6 @@ import kotlinx.serialization.* ...@@ -13,7 +13,6 @@ import kotlinx.serialization.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import net.mamoe.mirai.api.http.data.common.* import net.mamoe.mirai.api.http.data.common.*
import net.mamoe.mirai.message.data.MessageSource
// 解析失败时直接返回null,由路由判断响应400状态 // 解析失败时直接返回null,由路由判断响应400状态
@UseExperimental(ImplicitReflectionSerializer::class) @UseExperimental(ImplicitReflectionSerializer::class)
...@@ -45,20 +44,46 @@ else MiraiJson.json.stringify(serializer, this) ...@@ -45,20 +44,46 @@ else MiraiJson.json.stringify(serializer, this)
*/ */
object MiraiJson { object MiraiJson {
val json = Json(context = SerializersModule { val json = Json(context = SerializersModule {
polymorphic(MessagePacketDTO.serializer()) {
polymorphic(EventDTO.serializer()) {
GroupMessagePacketDTO::class with GroupMessagePacketDTO.serializer() GroupMessagePacketDTO::class with GroupMessagePacketDTO.serializer()
FriendMessagePacketDTO::class with FriendMessagePacketDTO.serializer() FriendMessagePacketDTO::class with FriendMessagePacketDTO.serializer()
UnKnownMessagePacketDTO::class with UnKnownMessagePacketDTO.serializer()
} BotOnlineEventDTO::class with BotOnlineEventDTO.serializer()
polymorphic(MessageDTO.serializer()) { BotOfflineEventActiveDTO::class with BotOfflineEventActiveDTO.serializer()
MessageSourceDTO::class with MessageSourceDTO.serializer() BotOfflineEventForceDTO::class with BotOfflineEventForceDTO.serializer()
AtDTO::class with AtDTO.serializer() BotOfflineEventDroppedDTO::class with BotOfflineEventDroppedDTO.serializer()
AtAllDTO::class with AtAllDTO.serializer() BotReloginEventDTO::class with BotReloginEventDTO.serializer()
FaceDTO::class with FaceDTO.serializer() BotGroupPermissionChangeEventDTO::class with BotGroupPermissionChangeEventDTO.serializer()
PlainDTO::class with PlainDTO.serializer() BotMuteEventDTO::class with BotMuteEventDTO.serializer()
ImageDTO::class with ImageDTO.serializer() BotUnmuteEventDTO::class with BotUnmuteEventDTO.serializer()
XmlDTO::class with XmlDTO.serializer() BotJoinGroupEventDTO::class with BotJoinGroupEventDTO.serializer()
UnknownMessageDTO::class with UnknownMessageDTO.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
...@@ -2,7 +2,8 @@ package net.mamoe.mirai.console.graphical ...@@ -2,7 +2,8 @@ package net.mamoe.mirai.console.graphical
import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController 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.App
import tornadofx.find import tornadofx.find
import tornadofx.launch import tornadofx.launch
...@@ -11,7 +12,7 @@ fun main(args: Array<String>) { ...@@ -11,7 +12,7 @@ fun main(args: Array<String>) {
launch<MiraiGraphicalUI>(args) launch<MiraiGraphicalUI>(args)
} }
class MiraiGraphicalUI: App(PrimaryView::class) { class MiraiGraphicalUI : App(Decorator::class, PrimaryStyleSheet::class) {
override fun init() { override fun init() {
super.init() super.init()
...@@ -23,4 +24,4 @@ class MiraiGraphicalUI: App(PrimaryView::class) { ...@@ -23,4 +24,4 @@ class MiraiGraphicalUI: App(PrimaryView::class) {
super.stop() super.stop()
MiraiConsole.stop() MiraiConsole.stop()
} }
} }
\ No newline at end of file
...@@ -11,3 +11,9 @@ class BotModel(val uin: Long) { ...@@ -11,3 +11,9 @@ class BotModel(val uin: Long) {
val logHistory = observableListOf<String>() val logHistory = observableListOf<String>()
val admins = observableListOf<Long>() val admins = observableListOf<Long>()
} }
class BotViewModel(botModel: BotModel? = null) : ItemViewModel<BotModel>(botModel) {
val bot = bind(BotModel::botProperty)
val logHistory = bind(BotModel::logHistory)
val admins = bind(BotModel::admins)
}
\ No newline at end of file
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
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
...@@ -17,20 +17,20 @@ internal fun EventTarget.jfxButton(text: String = "", graphic: Node? = null, op: ...@@ -17,20 +17,20 @@ internal fun EventTarget.jfxButton(text: String = "", graphic: Node? = null, op:
if (graphic != null) it.graphic = graphic 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 if (value != null) it.text = value
} }
fun EventTarget.jfxTextfield(property: ObservableValue<String>, op: TextField.() -> Unit = {}) = jfxTextfield().apply { fun EventTarget.jfxTextfield(property: ObservableValue<String>, op: JFXTextField.() -> Unit = {}) = jfxTextfield().apply {
bind(property) bind(property)
op(this) 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 if (value != null) it.text = value
} }
fun EventTarget.jfxPasswordfield(property: ObservableValue<String>, op: TextField.() -> Unit = {}) = jfxPasswordfield().apply { fun EventTarget.jfxPasswordfield(property: ObservableValue<String>, op: JFXPasswordField.() -> Unit = {}) = jfxPasswordfield().apply {
bind(property) bind(property)
op(this) op(this)
} }
......
package net.mamoe.mirai.console.graphical.view
import com.jfoenix.controls.JFXDecorator
import tornadofx.View
class Decorator: View() {
override val root = JFXDecorator(primaryStage, find<PrimaryView>().root)
}
\ No newline at end of file
package net.mamoe.mirai.console.graphical.view package net.mamoe.mirai.console.graphical.view
import javafx.beans.property.SimpleStringProperty import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Pos
import javafx.scene.image.Image
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController 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.jfxButton
import net.mamoe.mirai.console.graphical.util.jfxPasswordfield import net.mamoe.mirai.console.graphical.util.jfxPasswordfield
import net.mamoe.mirai.console.graphical.util.jfxTextfield import net.mamoe.mirai.console.graphical.util.jfxTextfield
import tornadofx.* import tornadofx.*
class LoginFragment : Fragment() { class LoginView : View("CNM") {
private val controller = find<MiraiGraphicalUIController>(FX.defaultScope) private val controller = find<MiraiGraphicalUIController>()
private val qq = SimpleStringProperty("0") private val qq = SimpleStringProperty("")
private val psd = SimpleStringProperty("") private val psd = SimpleStringProperty("")
override val root = form { override val root = borderpane {
fieldset("登录") {
field("QQ") { addStylesheet(LoginViewStyleSheet::class)
jfxTextfield(qq)
center = vbox {
imageview(Image(LoginView::class.java.classLoader.getResourceAsStream("character.png"))) {
alignment = Pos.CENTER
}
jfxTextfield(qq) {
promptText = "QQ"
isLabelFloat = true
} }
field("密码") {
jfxPasswordfield(psd) jfxPasswordfield(psd) {
promptText = "Password"
isLabelFloat = true
} }
}
jfxButton("登录").action { jfxButton("Login").action {
runBlocking { runAsync {
controller.login(qq.value, psd.value) runBlocking { controller.login(qq.value, psd.value) }
}.ui {
qq.value = ""
psd.value = ""
}
} }
close()
} }
} }
} }
\ No newline at end of file
package net.mamoe.mirai.console.graphical.view package net.mamoe.mirai.console.graphical.view
import com.jfoenix.controls.JFXListCell import com.jfoenix.controls.*
import javafx.geometry.Insets import javafx.collections.ObservableList
import javafx.geometry.Pos
import javafx.scene.control.Tab import javafx.scene.control.Tab
import javafx.scene.control.TabPane import javafx.scene.control.TabPane
import javafx.scene.image.Image 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.controller.MiraiGraphicalUIController
import net.mamoe.mirai.console.graphical.model.BotModel 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.jfxListView
import net.mamoe.mirai.console.graphical.util.jfxTabPane import net.mamoe.mirai.console.graphical.util.jfxTabPane
import tornadofx.* import tornadofx.*
import java.io.FileInputStream
class PrimaryView : View() { class PrimaryView : View() {
...@@ -35,20 +30,12 @@ class PrimaryView : View() { ...@@ -35,20 +30,12 @@ class PrimaryView : View() {
setCellFactory { setCellFactory {
object : JFXListCell<BotModel>() { object : JFXListCell<BotModel>() {
var tab: Tab? = null
init { init {
onDoubleClick { onDoubleClick {
if (tab == null) { (center as TabPane).logTab(
(center as TabPane).tab(item.uin.toString()) { text = item.uin.toString(),
listview(item.logHistory) logs = item.logHistory
onDoubleClick { close() } ).select()
tab = this
}
} else {
(center as TabPane).tabs.add(tab)
}
tab?.select()
} }
} }
...@@ -65,44 +52,37 @@ class PrimaryView : View() { ...@@ -65,44 +52,37 @@ class PrimaryView : View() {
} }
} }
} }
}
hbox { center = jfxTabPane {
padding = Insets(10.0)
spacing = 10.0
alignment = Pos.CENTER
jfxButton("L").action { tab("Login") {
find<LoginFragment>().openModal() this += find<LoginView>().root
}
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)
}
} }
tab("Plugin")
tab("Settings")
logTab("Main", controller.mainLog)
} }
}
}
center = jfxTabPane { private fun TabPane.logTab(
tab("Main") { text: String? = null,
listview(controller.mainLog) { logs: ObservableList<String>,
op: Tab.() -> Unit = {}
fitToParentSize() )= tab(text) {
cellFormat { listview(logs) {
graphic = label(it) {
maxWidthProperty().bind(this@listview.widthProperty()) fitToParentSize()
isWrapText = true cellFormat {
} graphic = label(it) {
} maxWidthProperty().bind(this@listview.widthProperty())
} isWrapText = true
} }
} }
} }
} also(op)
}
\ No newline at end of file
...@@ -187,8 +187,8 @@ object MiraiConsoleTerminalUI : MiraiConsoleUI { ...@@ -187,8 +187,8 @@ object MiraiConsoleTerminalUI : MiraiConsoleUI {
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? { override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
pushLog(0, "[Login Solver]需要进行账户安全认证") pushLog(0, "[Login Solver]需要进行账户安全认证")
pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登陆地点]/[不常用设备登陆]的问题") pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题")
pushLog(0, "[Login Solver]完成以下账号认证即可成功登|理论本认证在mirai每个账户中最多出现1次") pushLog(0, "[Login Solver]完成以下账号认证即可成功登|理论本认证在mirai每个账户中最多出现1次")
pushLog(0, "[Login Solver]请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符") pushLog(0, "[Login Solver]请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符")
pushLog(0, "[Login Solver]这步操作将在后续的版本中优化") pushLog(0, "[Login Solver]这步操作将在后续的版本中优化")
pushLog(0, url) pushLog(0, url)
......
...@@ -77,7 +77,7 @@ object MiraiConsole { ...@@ -77,7 +77,7 @@ object MiraiConsole {
logger("Mirai-console 启动完成") logger("Mirai-console 启动完成")
logger("\"/login qqnumber qqpassword \" to login a bot") logger("\"/login qqnumber qqpassword \" to login a bot")
logger("\"/login qq号 qq密码 \" 来登一个BOT") logger("\"/login qq号 qq密码 \" 来登一个BOT")
} }
fun stop() { fun stop() {
...@@ -208,7 +208,7 @@ object MiraiConsole { ...@@ -208,7 +208,7 @@ object MiraiConsole {
} }
val bot: Bot? = if (it.size == 2) { val bot: Bot? = if (it.size == 2) {
if (bots.size == 0) { if (bots.size == 0) {
logger("还没有BOT登") logger("还没有BOT登")
return@onCommand false return@onCommand false
} }
bots[0].get() bots[0].get()
......
...@@ -162,16 +162,11 @@ internal class QQImpl( ...@@ -162,16 +162,11 @@ internal class QQImpl(
TODO("not implemented") TODO("not implemented")
} }
override fun equals(other: Any?): Boolean { override fun toString(): String = "QQ($id)"
if (this === other) return true
return other is QQ && other.id == this.id
}
override fun hashCode(): Int = super.hashCode()
} }
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate", "DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
internal class MemberImpl( internal class MemberImpl(
qq: QQImpl, qq: QQImpl,
group: GroupImpl, group: GroupImpl,
...@@ -182,9 +177,21 @@ internal class MemberImpl( ...@@ -182,9 +177,21 @@ internal class MemberImpl(
val qq: QQImpl by qq.unsafeWeakRef() val qq: QQImpl by qq.unsafeWeakRef()
override var permission: MemberPermission = memberInfo.permission override var permission: MemberPermission = memberInfo.permission
@Suppress("PropertyName")
internal var _nameCard: String = memberInfo.nameCard internal var _nameCard: String = memberInfo.nameCard
@Suppress("PropertyName")
internal var _specialTitle: String = memberInfo.specialTitle 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 override var nameCard: String
get() = _nameCard get() = _nameCard
set(newValue) { set(newValue) {
...@@ -220,7 +227,7 @@ internal class MemberImpl( ...@@ -220,7 +227,7 @@ internal class MemberImpl(
newValue newValue
).sendWithoutExpect() ).sendWithoutExpect()
} }
MemberSpecialTitleChangeEvent(oldValue, newValue, this@MemberImpl).broadcast() MemberSpecialTitleChangeEvent(oldValue, newValue, this@MemberImpl, null).broadcast()
} }
} }
} }
...@@ -279,12 +286,9 @@ internal class MemberImpl( ...@@ -279,12 +286,9 @@ internal class MemberImpl(
} }
} }
override fun equals(other: Any?): Boolean { override fun toString(): String {
if (this === other) return true return "Member($id)"
return other is Member && other.id == this.id
} }
override fun hashCode(): Int = super.hashCode()
} }
internal class MemberInfoImpl( internal class MemberInfoImpl(
...@@ -301,6 +305,7 @@ internal class MemberInfoImpl( ...@@ -301,6 +305,7 @@ internal class MemberInfoImpl(
else -> MemberPermission.MEMBER else -> MemberPermission.MEMBER
} }
override val specialTitle: String get() = jceInfo.sSpecialTitle ?: "" override val specialTitle: String get() = jceInfo.sSpecialTitle ?: ""
override val muteTimestamp: Int get() = jceInfo.dwShutupTimestap?.toInt() ?: 0
} }
/** /**
...@@ -323,13 +328,13 @@ internal class GroupImpl( ...@@ -323,13 +328,13 @@ internal class GroupImpl(
@UseExperimental(MiraiExperimentalAPI::class) @UseExperimental(MiraiExperimentalAPI::class)
override lateinit var botPermission: MemberPermission override lateinit var botPermission: MemberPermission
var _botMuteRemaining: Int = groupInfo.botMuteRemaining var _botMuteTimestamp: Int = groupInfo.botMuteRemaining
override val botMuteRemaining: Int = override val botMuteRemaining: Int =
if (_botMuteRemaining == 0 || _botMuteRemaining == 0xFFFFFFFF.toInt()) { if (_botMuteTimestamp == 0 || _botMuteTimestamp == 0xFFFFFFFF.toInt()) {
0 0
} else { } else {
_botMuteRemaining - currentTimeSeconds.toInt() - bot.client.timeDifference.toInt() _botMuteTimestamp - currentTimeSeconds.toInt() - bot.client.timeDifference.toInt()
} }
override val members: ContactList<Member> = ContactList(members.mapNotNull { override val members: ContactList<Member> = ContactList(members.mapNotNull {
...@@ -600,10 +605,7 @@ internal class GroupImpl( ...@@ -600,10 +605,7 @@ internal class GroupImpl(
image.input.close() image.input.close()
} }
override fun equals(other: Any?): Boolean { override fun toString(): String {
if (this === other) return true return "Group($id)"
return other is Group && other.id == this.id
} }
override fun hashCode(): Int = super.hashCode()
} }
\ No newline at end of file
...@@ -20,7 +20,6 @@ import net.mamoe.mirai.data.AddFriendResult ...@@ -20,7 +20,6 @@ import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.data.FriendInfo import net.mamoe.mirai.data.FriendInfo
import net.mamoe.mirai.data.GroupInfo import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.QQAndroidClient
...@@ -116,10 +115,6 @@ internal abstract class QQAndroidBotBase constructor( ...@@ -116,10 +115,6 @@ internal abstract class QQAndroidBotBase constructor(
return sequence return sequence
} }
override fun onEvent(event: BotEvent): Boolean {
return firstLoginSucceed
}
override suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult { override suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult {
TODO("not implemented") TODO("not implemented")
} }
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:JvmName("SerializationUtils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.io.serialization package net.mamoe.mirai.qqandroid.io.serialization
import kotlinx.io.core.* import kotlinx.io.core.*
...@@ -20,7 +23,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestDataVersion3 ...@@ -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.qqandroid.network.protocol.data.jce.RequestPacket
import net.mamoe.mirai.utils.firstValue import net.mamoe.mirai.utils.firstValue
import net.mamoe.mirai.utils.io.read import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.toUHexString import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
fun <T : JceStruct> ByteArray.loadAs(deserializer: DeserializationStrategy<T>, c: JceCharset = JceCharset.UTF8): T { fun <T : JceStruct> ByteArray.loadAs(deserializer: DeserializationStrategy<T>, c: JceCharset = JceCharset.UTF8): T {
......
...@@ -21,6 +21,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg ...@@ -21,6 +21,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg
internal inline class MessageSourceFromServer( internal inline class MessageSourceFromServer(
val delegate: ImMsgBody.SourceMsg val delegate: ImMsgBody.SourceMsg
) : MessageSource { ) : 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 messageUid: Long get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!
override val sourceMessage: MessageChain get() = delegate.toMessageChain() override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.senderUin override val senderId: Long get() = delegate.senderUin
...@@ -32,6 +33,7 @@ internal inline class MessageSourceFromServer( ...@@ -32,6 +33,7 @@ internal inline class MessageSourceFromServer(
internal inline class MessageSourceFromMsg( internal inline class MessageSourceFromMsg(
val delegate: MsgComm.Msg val delegate: MsgComm.Msg
) : MessageSource { ) : 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 messageUid: Long get() = delegate.msgBody.richText.attr!!.random.toLong()
override val sourceMessage: MessageChain get() = delegate.toMessageChain() override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.msgHead.fromUin override val senderId: Long get() = delegate.msgHead.fromUin
......
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
package net.mamoe.mirai.qqandroid.message package net.mamoe.mirai.qqandroid.message
import kotlinx.io.core.buildPacket
import kotlinx.io.core.readBytes
import kotlinx.io.core.readUInt import kotlinx.io.core.readUInt
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
...@@ -20,13 +22,18 @@ import net.mamoe.mirai.utils.io.hexToBytes ...@@ -20,13 +22,18 @@ import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.io.read import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.toByteArray 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 { internal fun At.toJceData(): ImMsgBody.Text {
val text = this.toString()
return ImMsgBody.Text( return ImMsgBody.Text(
str = this.toString(), str = text,
attr6Buf = AT_BUF_1 + this.target.toInt().toByteArray() + AT_BUF_2 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) ...@@ -86,6 +93,16 @@ _400Height=0x000000EB(235)
pbReserve=<Empty ByteArray> pbReserve=<Empty ByteArray>
} }
*/ */
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 { internal fun CustomFaceFromFile.toJceData(): ImMsgBody.CustomFace {
return ImMsgBody.CustomFace( return ImMsgBody.CustomFace(
filePath = this.filepath, filePath = this.filepath,
...@@ -213,6 +230,7 @@ internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> { ...@@ -213,6 +230,7 @@ internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
is NotOnlineImageFromServer -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate)) is NotOnlineImageFromServer -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
is NotOnlineImageFromFile -> elements.add(ImMsgBody.Elem(notOnlineImage = it.toJceData())) is NotOnlineImageFromFile -> elements.add(ImMsgBody.Elem(notOnlineImage = it.toJceData()))
is AtAll -> elements.add(atAllData) is AtAll -> elements.add(atAllData)
is Face -> elements.add(ImMsgBody.Elem(face = it.toJceData()))
is QuoteReply, is QuoteReply,
is MessageSource -> { is MessageSource -> {
...@@ -312,18 +330,20 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChain) { ...@@ -312,18 +330,20 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChain) {
it.srcMsg != null -> message.add(QuoteReply(MessageSourceFromServer(it.srcMsg))) it.srcMsg != null -> message.add(QuoteReply(MessageSourceFromServer(it.srcMsg)))
it.notOnlineImage != null -> message.add(NotOnlineImageFromServer(it.notOnlineImage)) it.notOnlineImage != null -> message.add(NotOnlineImageFromServer(it.notOnlineImage))
it.customFace != null -> message.add(CustomFaceFromServer(it.customFace)) it.customFace != null -> message.add(CustomFaceFromServer(it.customFace))
it.face != null -> message.add(Face(it.face.index))
it.text != null -> { it.text != null -> {
if (it.text.attr6Buf.isEmpty()) { if (it.text.attr6Buf.isEmpty()) {
message.add(it.text.str.toMessage()) message.add(it.text.str.toMessage())
} else { } else {
//00 01 00 00 00 05 01 00 00 00 00 00 00 all // 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 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 val id: Long
it.text.attr6Buf.read { it.text.attr6Buf.read {
discardExact(7) discardExact(7)
id = readUInt().toLong() id = readUInt().toLong()
} }
if (id == 0L){ if (id == 0L) {
message.add(AtAll) message.add(AtAll)
} else { } else {
message.add(At(id, it.text.str)) message.add(At(id, it.text.str))
......
...@@ -20,10 +20,7 @@ import kotlinx.io.core.buildPacket ...@@ -20,10 +20,7 @@ import kotlinx.io.core.buildPacket
import kotlinx.io.core.use import kotlinx.io.core.use
import net.mamoe.mirai.data.MultiPacket import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.BroadcastControllable import net.mamoe.mirai.event.*
import net.mamoe.mirai.event.CancellableEvent
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.BotOnlineEvent import net.mamoe.mirai.event.events.BotOnlineEvent
import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
...@@ -78,7 +75,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -78,7 +75,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
} catch (e: CancellationException) { } catch (e: CancellationException) {
return@launch return@launch
} catch (e: Throwable) { } catch (e: Throwable) {
BotOfflineEvent.Dropped(bot).broadcast() if (this@QQAndroidBotNetworkHandler.isActive) {
BotOfflineEvent.Dropped(bot, e).broadcast()
}
return@launch return@launch
} }
packetReceiveLock.withLock { packetReceiveLock.withLock {
...@@ -88,25 +87,40 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -88,25 +87,40 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}.also { _packetReceiverJob = it } }.also { _packetReceiverJob = it }
} }
override suspend fun relogin() { private fun startHeartbeatJobOrKill(cancelCause: CancellationException? = null): Job {
heartbeatJob?.cancel() 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.isInitialized) {
if (channel.isOpen) { if (channel.isOpen) {
kotlin.runCatching { kotlin.runCatching {
registerClientOnline() registerClientOnline(500)
}.exceptionOrNull() ?: return }.exceptionOrNull() ?: return
logger.info("Cannot do fast relogin. Trying slow relogin") logger.info("Cannot do fast relogin. Trying slow relogin")
} }
channel.close() channel.close()
} }
channel = PlatformSocket() channel = PlatformSocket()
// TODO: 2020/2/14 连接多个服务器 // TODO: 2020/2/14 连接多个服务器, #52
withTimeoutOrNull(3000) { withTimeoutOrNull(3000) {
channel.connect("113.96.13.208", 8080) channel.connect("113.96.13.208", 8080)
} ?: error("timeout connecting server") } ?: 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() var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
mainloop@ while (true) { mainloop@ while (true) {
when (response) { when (response) {
...@@ -157,10 +171,11 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -157,10 +171,11 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
// println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}") // println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
registerClientOnline() registerClientOnline()
startHeartbeatJobOrKill()
} }
private suspend fun registerClientOnline() { private suspend fun registerClientOnline(timeoutMillis: Long = 3000) {
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>() StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(timeoutMillis)
} }
// caches // caches
...@@ -170,25 +185,34 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -170,25 +185,34 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
@UseExperimental(MiraiExperimentalAPI::class, ExperimentalTime::class) @UseExperimental(MiraiExperimentalAPI::class, ExperimentalTime::class)
override suspend fun init(): Unit = coroutineScope { 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.qqs.delegate.clear()
bot.groups.delegate.clear() bot.groups.delegate.clear()
val friendListJob = launch { val friendListJob = launch {
try { lateinit var loadFriends: suspend () -> Unit
// 不要用 fun, 不要 join declaration, 不要用 val, 编译失败警告
loadFriends = suspend loadFriends@{
logger.info("开始加载好友信息") logger.info("开始加载好友信息")
var currentFriendCount = 0 var currentFriendCount = 0
var totalFriendCount: Short var totalFriendCount: Short
while (true) { while (true) {
val data = FriendList.GetFriendGroupList( val data = runCatching {
bot.client, FriendList.GetFriendGroupList(
currentFriendCount, bot.client,
150, currentFriendCount,
0, 150,
0 0,
).sendAndExpect<FriendList.GetFriendGroupList.Response>(timeoutMillis = 5000, retry = 2) 0
).sendAndExpect<FriendList.GetFriendGroupList.Response>(timeoutMillis = 5000, retry = 2)
}.getOrElse {
logger.error("无法加载好友列表", it)
this@QQAndroidBotNetworkHandler.launch { delay(10.secondsToMillis); loadFriends() }
logger.error("稍后重试加载好友列表")
return@loadFriends
}
totalFriendCount = data.totalFriendCount totalFriendCount = data.totalFriendCount
data.friendList.forEach { data.friendList.forEach {
// atomic add // atomic add
...@@ -196,16 +220,16 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -196,16 +220,16 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
currentFriendCount++ currentFriendCount++
} }
} }
logger.verbose("正在加载好友列表 ${currentFriendCount}/${totalFriendCount}") logger.verbose { "正在加载好友列表 ${currentFriendCount}/${totalFriendCount}" }
if (currentFriendCount >= totalFriendCount) { if (currentFriendCount >= totalFriendCount) {
break break
} }
// delay(200) // delay(200)
} }
logger.info("好友列表加载完成, 共 ${currentFriendCount}个") logger.info { "好友列表加载完成, 共 ${currentFriendCount}个" }
} catch (e: Exception) {
logger.error("加载好友列表失败|一般这是由于加载过于频繁导致/将以热加载方式加载好友列表")
} }
loadFriends()
} }
val groupJob = launch { val groupJob = launch {
...@@ -247,7 +271,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -247,7 +271,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
)) ))
) )
}?.let { }?.let {
logger.error("群${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试") logger.error { "群${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试" }
logger.error(it) logger.error(it)
this@QQAndroidBotNetworkHandler.launch { this@QQAndroidBotNetworkHandler.launch {
delay(10_000) delay(10_000)
...@@ -260,26 +284,25 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -260,26 +284,25 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
loadGroup() loadGroup()
} }
} }
logger.info("群组列表与群成员加载完成, 共 ${troopListData.groups.size}个") logger.info { "群组列表与群成员加载完成, 共 ${troopListData.groups.size}个" }
} catch (e: Exception) { } catch (e: Exception) {
logger.error("加载组信息失败|一般这是由于加载过于频繁导致/将以热加载方式加载群列表") logger.error { "加载组信息失败|一般这是由于加载过于频繁导致/将以热加载方式加载群列表" }
logger.error(e) logger.error(e)
} }
} }
joinAll(friendListJob, groupJob) joinAll(friendListJob, groupJob)
heartbeatJob = this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) { withTimeoutOrNull(5000) {
while (this.isActive) { lateinit var listener: Listener<PacketReceivedEvent>
delay(bot.configuration.heartbeatPeriodMillis) listener = this.subscribeAlways {
val failException = doHeartBeat() if (it.packet is MessageSvc.PbGetMsg.GetMsgSuccess) {
if (failException != null) { listener.complete()
delay(bot.configuration.firstReconnectDelayMillis)
close()
BotOfflineEvent.Dropped(bot).broadcast()
} }
} }
}
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendWithoutExpect()
} ?: error("timeout syncing friend message history")
bot.firstLoginSucceed = true bot.firstLoginSucceed = true
...@@ -288,10 +311,12 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -288,10 +311,12 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
KnownPacketFactories.handleIncomingPacket(it as KnownPacketFactories.IncomingPacket<Packet>, bot, it.flag2, it.consumer) KnownPacketFactories.handleIncomingPacket(it as KnownPacketFactories.IncomingPacket<Packet>, bot, it.flag2, it.consumer)
} }
pendingIncomingPackets = null // release val list = pendingIncomingPackets
pendingIncomingPackets = null // release, help gc
list?.clear() // help gc
BotOnlineEvent(bot).broadcast() BotOnlineEvent(bot).broadcast()
Unit Unit // dont remove. can help type inference
} }
suspend fun doHeartBeat(): Exception? { suspend fun doHeartBeat(): Exception? {
...@@ -347,7 +372,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -347,7 +372,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
} }
// with generic type, less mistakes // with generic type, less mistakes
private suspend inline fun <P : Packet> generifiedParsePacket(input: Input) { private suspend fun <P : Packet?> generifiedParsePacket(input: Input) {
KnownPacketFactories.parseIncomingPacket(bot, input) { packetFactory: PacketFactory<P>, packet: P, commandName: String, sequenceId: Int -> KnownPacketFactories.parseIncomingPacket(bot, input) { packetFactory: PacketFactory<P>, packet: P, commandName: String, sequenceId: Int ->
handlePacket(packetFactory, packet, commandName, sequenceId) handlePacket(packetFactory, packet, commandName, sequenceId)
if (packet is MultiPacket<*>) { if (packet is MultiPacket<*>) {
...@@ -361,7 +386,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -361,7 +386,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
/** /**
* 处理解析完成的包. * 处理解析完成的包.
*/ */
suspend fun <P : Packet> handlePacket(packetFactory: PacketFactory<P>?, packet: P, commandName: String, sequenceId: Int) { suspend fun <P : Packet?> handlePacket(packetFactory: PacketFactory<P>?, packet: P, commandName: String, sequenceId: Int) {
// highest priority: pass to listeners (attached by sendAndExpect). // highest priority: pass to listeners (attached by sendAndExpect).
packetListeners.forEach { listener -> packetListeners.forEach { listener ->
if (listener.filter(commandName, sequenceId) && packetListeners.remove(listener)) { if (listener.filter(commandName, sequenceId) && packetListeners.remove(listener)) {
...@@ -370,7 +395,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -370,7 +395,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
} }
// check top-level cancelling // check top-level cancelling
if (PacketReceivedEvent(packet).broadcast().isCancelled) { if (packet != null && PacketReceivedEvent(packet).broadcast().isCancelled) {
return return
} }
...@@ -386,7 +411,13 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -386,7 +411,13 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
if (packet is CancellableEvent && packet.isCancelled) return 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 { packetFactory?.run {
when (this) { when (this) {
...@@ -480,7 +511,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -480,7 +511,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
* 发送一个包, 但不期待任何返回. * 发送一个包, 但不期待任何返回.
*/ */
suspend fun OutgoingPacket.sendWithoutExpect() { 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")) { withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
channel.send(delegate) channel.send(delegate)
} }
...@@ -495,6 +528,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -495,6 +528,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
require(timeoutMillis > 0) { "timeoutMillis must > 0" } require(timeoutMillis > 0) { "timeoutMillis must > 0" }
require(retry >= 0) { "retry 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 var lastException: Exception? = null
if (retry == 0) { if (retry == 0) {
val handler = PacketListener(commandName = commandName, sequenceId = sequenceId) val handler = PacketListener(commandName = commandName, sequenceId = sequenceId)
...@@ -503,7 +539,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -503,7 +539,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) { withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
channel.send(delegate) channel.send(delegate)
} }
logger.info("Send: ${this.commandName}") logger.verbose("Send: ${this.commandName}")
return withTimeoutOrNull(timeoutMillis) { return withTimeoutOrNull(timeoutMillis) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
handler.await() as E handler.await() as E
...@@ -522,7 +558,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -522,7 +558,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) { withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
channel.send(data, 0, length) channel.send(data, 0, length)
} }
logger.info("Send: ${this.commandName}") logger.verbose("Send: ${this.commandName}")
return withTimeoutOrNull(timeoutMillis) { return withTimeoutOrNull(timeoutMillis) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
handler.await() as E handler.await() as E
...@@ -547,7 +583,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler ...@@ -547,7 +583,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
internal inner class PacketListener( // callback internal inner class PacketListener( // callback
val commandName: String, val commandName: String,
val sequenceId: Int val sequenceId: Int
) : CompletableDeferred<Packet> by CompletableDeferred(supervisor) { ) : CompletableDeferred<Packet?> by CompletableDeferred(supervisor) {
fun filter(commandName: String, sequenceId: Int) = this.commandName == commandName && this.sequenceId == sequenceId fun filter(commandName: String, sequenceId: Int) = this.commandName == commandName && this.sequenceId == sequenceId
} }
......
...@@ -15,12 +15,10 @@ import kotlinx.io.core.buildPacket ...@@ -15,12 +15,10 @@ import kotlinx.io.core.buildPacket
import kotlinx.io.core.writeFully import kotlinx.io.core.writeFully
import net.mamoe.mirai.qqandroid.network.QQAndroidClient import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.utils.cryptor.ECDH 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.encryptAndWrite
import net.mamoe.mirai.utils.io.writeShortLVByteArray import net.mamoe.mirai.utils.io.writeShortLVByteArray
/**
* Encryption method to be used for packet body.
*/
@UseExperimental(ExperimentalUnsignedTypes::class) @UseExperimental(ExperimentalUnsignedTypes::class)
internal interface EncryptMethod { internal interface EncryptMethod {
val id: Int val id: Int
...@@ -33,16 +31,6 @@ internal interface EncryptMethodSessionKey : EncryptMethod { ...@@ -33,16 +31,6 @@ internal interface EncryptMethodSessionKey : EncryptMethod {
val currentLoginState: Int val currentLoginState: Int
val sessionKey: ByteArray 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 = override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket =
buildPacket { buildPacket {
require(currentLoginState == 2 || currentLoginState == 3) { "currentLoginState must be either 2 or 3" } require(currentLoginState == 2 || currentLoginState == 3) { "currentLoginState must be either 2 or 3" }
...@@ -65,29 +53,27 @@ inline class EncryptMethodSessionKeyLoginState3(override val sessionKey: ByteArr ...@@ -65,29 +53,27 @@ inline class EncryptMethodSessionKeyLoginState3(override val sessionKey: ByteArr
override val currentLoginState: Int get() = 3 override val currentLoginState: Int get() = 3
} }
inline class EncryptMethodECDH135(override val ecdh: ECDH) : internal inline class EncryptMethodECDH135(override val ecdh: ECDH) :
EncryptMethodECDH { EncryptMethodECDH {
override val id: Int get() = 135 override val id: Int get() = 135
} }
inline class EncryptMethodECDH7(override val ecdh: ECDH) : internal inline class EncryptMethodECDH7(override val ecdh: ECDH) :
EncryptMethodECDH { EncryptMethodECDH {
override val id: Int get() = 7 override val id: Int get() = 7
} }
internal interface EncryptMethodECDH : EncryptMethod { 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 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 = override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket =
buildPacket { buildPacket {
writeByte(1) // const writeByte(1) // const
...@@ -95,15 +81,15 @@ internal interface EncryptMethodECDH : EncryptMethod { ...@@ -95,15 +81,15 @@ internal interface EncryptMethodECDH : EncryptMethod {
writeFully(client.randomKey) writeFully(client.randomKey)
writeShort(258) // const 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)
writeShortLVByteArray(ecdh.keyPair.publicKey.getEncoded().drop(23).take(49).toByteArray().also { encryptAndWrite(ECDHKeyPair.DefaultStub.defaultShareKey, body)
// it.toUHexString().debugPrint("PUBLIC KEY") } else {
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 {
//check(ecdh.calculateShareKeyByPeerPublicKey(it.adjustToPublicKey()).contentEquals(ecdh.keyPair.shareKey)) { "PublicKey Validation failed" } check(it[0].toInt() == 0x04) { "Bad publicKey generated. Expected first element=0x04, got${it[0]}" }
}) })
// 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
...@@ -33,7 +33,7 @@ import kotlin.contracts.contract ...@@ -33,7 +33,7 @@ import kotlin.contracts.contract
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
internal sealed class PacketFactory<TPacket : Packet> { internal sealed class PacketFactory<TPacket : Packet?> {
/** /**
* 筛选从服务器接收到的包时的 commandName * 筛选从服务器接收到的包时的 commandName
*/ */
...@@ -49,7 +49,7 @@ internal sealed class PacketFactory<TPacket : Packet> { ...@@ -49,7 +49,7 @@ internal sealed class PacketFactory<TPacket : Packet> {
* @param TPacket 服务器回复包解析结果 * @param TPacket 服务器回复包解析结果
*/ */
@UseExperimental(ExperimentalUnsignedTypes::class) @UseExperimental(ExperimentalUnsignedTypes::class)
internal abstract class OutgoingPacketFactory<TPacket : Packet>( internal abstract class OutgoingPacketFactory<TPacket : Packet?>(
/** /**
* 命令名. 如 `wtlogin.login`, `ConfigPushSvc.PushDomain` * 命令名. 如 `wtlogin.login`, `ConfigPushSvc.PushDomain`
*/ */
...@@ -73,7 +73,7 @@ internal abstract class OutgoingPacketFactory<TPacket : Packet>( ...@@ -73,7 +73,7 @@ internal abstract class OutgoingPacketFactory<TPacket : Packet>(
* 这个工厂可以在 [handle] 时回复一个 commandId 为 [responseCommandName] 的包, 也可以不回复. * 这个工厂可以在 [handle] 时回复一个 commandId 为 [responseCommandName] 的包, 也可以不回复.
* 必须先到 [KnownPacketFactories] 中注册工厂, 否则不能处理. * 必须先到 [KnownPacketFactories] 中注册工厂, 否则不能处理.
*/ */
internal abstract class IncomingPacketFactory<TPacket : Packet>( internal abstract class IncomingPacketFactory<TPacket : Packet?>(
/** /**
* 接收自服务器的包的 commandName * 接收自服务器的包的 commandName
*/ */
...@@ -97,10 +97,10 @@ internal abstract class IncomingPacketFactory<TPacket : Packet>( ...@@ -97,10 +97,10 @@ internal abstract class IncomingPacketFactory<TPacket : Packet>(
} }
@JvmName("decode0") @JvmName("decode0")
private suspend inline fun <P : Packet> OutgoingPacketFactory<P>.decode(bot: QQAndroidBot, packet: ByteReadPacket): P = packet.decode(bot) private suspend inline fun <P : Packet?> OutgoingPacketFactory<P>.decode(bot: QQAndroidBot, packet: ByteReadPacket): P = packet.decode(bot)
@JvmName("decode1") @JvmName("decode1")
private suspend inline fun <P : Packet> IncomingPacketFactory<P>.decode(bot: QQAndroidBot, packet: ByteReadPacket, sequenceId: Int): P = private suspend inline fun <P : Packet?> IncomingPacketFactory<P>.decode(bot: QQAndroidBot, packet: ByteReadPacket, sequenceId: Int): P =
packet.decode(bot, sequenceId) packet.decode(bot, sequenceId)
internal val DECRYPTER_16_ZERO = ByteArray(16) internal val DECRYPTER_16_ZERO = ByteArray(16)
...@@ -169,7 +169,7 @@ internal object KnownPacketFactories { ...@@ -169,7 +169,7 @@ internal object KnownPacketFactories {
// do not inline. Exceptions thrown will not be reported correctly // do not inline. Exceptions thrown will not be reported correctly
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
suspend fun <T : Packet> parseIncomingPacket(bot: QQAndroidBot, rawInput: Input, consumer: PacketConsumer<T>) = with(rawInput) { suspend fun <T : Packet?> parseIncomingPacket(bot: QQAndroidBot, rawInput: Input, consumer: PacketConsumer<T>) = with(rawInput) {
// login // login
val flag1 = readInt() val flag1 = readInt()
...@@ -229,7 +229,7 @@ internal object KnownPacketFactories { ...@@ -229,7 +229,7 @@ internal object KnownPacketFactories {
} }
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
internal suspend fun <T : Packet> handleIncomingPacket(it: IncomingPacket<T>, bot: QQAndroidBot, flag2: Int, consumer: PacketConsumer<T>) { internal suspend fun <T : Packet?> handleIncomingPacket(it: IncomingPacket<T>, bot: QQAndroidBot, flag2: Int, consumer: PacketConsumer<T>) {
if (it.packetFactory == null) { if (it.packetFactory == null) {
bot.network.logger.debug("Received commandName: ${it.commandName}") bot.network.logger.debug("Received commandName: ${it.commandName}")
PacketLogger.warning { "找不到 PacketFactory" } PacketLogger.warning { "找不到 PacketFactory" }
...@@ -263,7 +263,7 @@ internal object KnownPacketFactories { ...@@ -263,7 +263,7 @@ internal object KnownPacketFactories {
private inline fun <R> inline(block: () -> R): R = block() private inline fun <R> inline(block: () -> R): R = block()
class IncomingPacket<T : Packet>( class IncomingPacket<T : Packet?>(
val packetFactory: PacketFactory<T>?, val packetFactory: PacketFactory<T>?,
val sequenceId: Int, val sequenceId: Int,
val data: ByteReadPacket, val data: ByteReadPacket,
...@@ -337,7 +337,7 @@ internal object KnownPacketFactories { ...@@ -337,7 +337,7 @@ internal object KnownPacketFactories {
return IncomingPacket(packetFactory, ssoSequenceId, packet, commandName) return IncomingPacket(packetFactory, ssoSequenceId, packet, commandName)
} }
private suspend fun <T : Packet> ByteReadPacket.parseOicqResponse( private suspend fun <T : Packet?> ByteReadPacket.parseOicqResponse(
bot: QQAndroidBot, bot: QQAndroidBot,
packetFactory: OutgoingPacketFactory<T>, packetFactory: OutgoingPacketFactory<T>,
ssoSequenceId: Int, ssoSequenceId: Int,
......
...@@ -16,7 +16,6 @@ import net.mamoe.mirai.contact.MemberPermission ...@@ -16,7 +16,6 @@ import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.data.MultiPacket import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet 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.BotJoinGroupEvent
import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.MemberJoinEvent import net.mamoe.mirai.event.events.MemberJoinEvent
...@@ -103,23 +102,23 @@ internal class MessageSvc { ...@@ -103,23 +102,23 @@ internal class MessageSvc {
} }
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
internal class GetMsgSuccess(delegate: List<Packet>) : Response(MsgSvc.SyncFlag.STOP, delegate) open class GetMsgSuccess(delegate: List<Packet>) : Response(MsgSvc.SyncFlag.STOP, delegate) {
override fun toString(): String {
return "MessageSvc.PbGetMsg.GetMsgSuccess(messages=List(size=${this.size}))"
}
}
/** /**
* 不要直接 expect 这个 class. 它可能 * 不要直接 expect 这个 class. 它可能还没同步完成
*/ */
@MiraiInternalAPI @MiraiInternalAPI
open class Response(internal val syncFlagFromServer: MsgSvc.SyncFlag, delegate: List<Packet>) : MultiPacket<Packet>(delegate), open class Response(internal val syncFlagFromServer: MsgSvc.SyncFlag, delegate: List<Packet>) : MultiPacket<Packet>(delegate) {
BroadcastControllable {
override val shouldBroadcast: Boolean
get() = syncFlagFromServer == MsgSvc.SyncFlag.STOP
override fun toString(): String { override fun toString(): String {
return "MessageSvc.PbGetMsg.Response($syncFlagFromServer=$syncFlagFromServer, messages=List(size=${this.size}))" 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) @UseExperimental(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
...@@ -127,8 +126,8 @@ internal class MessageSvc { ...@@ -127,8 +126,8 @@ internal class MessageSvc {
val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer()) val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer())
if (resp.result != 0) { if (resp.result != 0) {
// println("!!! Result=${resp.result} !!!: " + resp.contentToString()) bot.network.logger.warning("MessageSvc.PushNotify: result != 0, result = ${resp.result}, errorMsg=${resp.errmsg}")
return GetMsgSuccess(mutableListOf()) return EmptyResponse
} }
bot.client.c2cMessageSync.syncCookie = resp.syncCookie bot.client.c2cMessageSync.syncCookie = resp.syncCookie
...@@ -149,7 +148,7 @@ internal class MessageSvc { ...@@ -149,7 +148,7 @@ internal class MessageSvc {
val group = bot.getGroupByUinOrNull(msg.msgHead.fromUin) val group = bot.getGroupByUinOrNull(msg.msgHead.fromUin)
if (msg.msgHead.authUin == bot.uin) { if (msg.msgHead.authUin == bot.uin) {
if (group != null) { 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 { ...@@ -159,6 +158,7 @@ internal class MessageSvc {
}.groups.first { it.groupUin == msg.msgHead.fromUin } }.groups.first { it.groupUin == msg.msgHead.fromUin }
@Suppress("DuplicatedCode")
val newGroup = GroupImpl( val newGroup = GroupImpl(
bot = bot, bot = bot,
coroutineContext = bot.coroutineContext, coroutineContext = bot.coroutineContext,
...@@ -194,6 +194,7 @@ internal class MessageSvc { ...@@ -194,6 +194,7 @@ internal class MessageSvc {
override val nameCard: String get() = "" override val nameCard: String get() = ""
override val permission: MemberPermission get() = MemberPermission.MEMBER override val permission: MemberPermission get() = MemberPermission.MEMBER
override val specialTitle: String get() = "" override val specialTitle: String get() = ""
override val muteTimestamp: Int get() = 0
override val uin: Long get() = msg.msgHead.authUin override val uin: Long get() = msg.msgHead.authUin
override val nick: String get() = msg.msgHead.authNick.takeIf { it.isNotEmpty() } ?: msg.msgHead.fromNick override val nick: String get() = msg.msgHead.authNick.takeIf { it.isNotEmpty() } ?: msg.msgHead.fromNick
}).also { group.members.delegate.addLast(it) }) }).also { group.members.delegate.addLast(it) })
...@@ -264,6 +265,7 @@ internal class MessageSvc { ...@@ -264,6 +265,7 @@ internal class MessageSvc {
/** /**
* 发送好友消息 * 发送好友消息
*/ */
@Suppress("FunctionName")
fun ToFriend( fun ToFriend(
client: QQAndroidClient, client: QQAndroidClient,
toUin: Long, toUin: Long,
...@@ -293,6 +295,7 @@ internal class MessageSvc { ...@@ -293,6 +295,7 @@ internal class MessageSvc {
/** /**
* 发送群消息 * 发送群消息
*/ */
@Suppress("FunctionName")
fun ToGroup( fun ToGroup(
client: QQAndroidClient, client: QQAndroidClient,
groupCode: Long, groupCode: Long,
......
...@@ -15,9 +15,9 @@ import kotlinx.io.core.ByteReadPacket ...@@ -15,9 +15,9 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes import kotlinx.io.core.readBytes
import kotlinx.io.core.readUInt import kotlinx.io.core.readUInt
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.NoPacket import net.mamoe.mirai.data.NoPacket
import net.mamoe.mirai.data.Packet import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.* import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.GroupMessage import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.qqandroid.GroupImpl import net.mamoe.mirai.qqandroid.GroupImpl
...@@ -41,58 +41,44 @@ import net.mamoe.mirai.utils.io.read ...@@ -41,58 +41,44 @@ import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.readString import net.mamoe.mirai.utils.io.readString
import net.mamoe.mirai.utils.io.toUHexString import net.mamoe.mirai.utils.io.toUHexString
internal inline class GroupMessageOrNull(val delegate: GroupMessage?) : Packet {
override fun toString(): String {
return delegate?.toString() ?: "<Receipt>"
}
}
internal class OnlinePush { internal class OnlinePush {
/** /**
* 接受群消息 * 接受群消息
*/ */
internal object PbPushGroupMsg : IncomingPacketFactory<GroupMessageOrNull>("OnlinePush.PbPushGroupMsg") { internal object PbPushGroupMsg : IncomingPacketFactory<GroupMessage?>("OnlinePush.PbPushGroupMsg") {
@UseExperimental(ExperimentalStdlibApi::class) @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 // 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 pbPushMsg = readProtoBuf(MsgOnlinePush.PbPushMsg.serializer())
val extraInfo: ImMsgBody.ExtraInfo? = pbPushMsg.msg.msgBody.richText.elems.firstOrNull { it.extraInfo != null }?.extraInfo val extraInfo: ImMsgBody.ExtraInfo? = pbPushMsg.msg.msgBody.richText.elems.firstOrNull { it.extraInfo != null }?.extraInfo
if (pbPushMsg.msg.msgHead.fromUin == bot.uin) { if (pbPushMsg.msg.msgHead.fromUin == bot.uin) {
return GroupMessageOrNull(null) return null
} }
val group = bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode) val group = bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode)
// println(pbPushMsg.msg.msgBody.richText.contentToString()) // println(pbPushMsg.msg.msgBody.richText.contentToString())
val flags = extraInfo?.flags ?: 0 val flags = extraInfo?.flags ?: 0
return GroupMessageOrNull( return GroupMessage(
GroupMessage( bot = bot,
bot = bot, group = group,
group = group, senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard,
senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard, sender = group[pbPushMsg.msg.msgHead.fromUin],
sender = group[pbPushMsg.msg.msgHead.fromUin], message = pbPushMsg.msg.toMessageChain(),
message = pbPushMsg.msg.toMessageChain(), permission = when {
permission = when { flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
flags and 16 != 0 -> MemberPermission.ADMINISTRATOR flags and 8 != 0 -> MemberPermission.OWNER
flags and 8 != 0 -> MemberPermission.OWNER flags == 0 -> MemberPermission.MEMBER
flags == 0 -> MemberPermission.MEMBER else -> {
else -> { bot.logger.warning("判断群员权限失败")
bot.logger.warning("判断群员权限失败") MemberPermission.MEMBER
MemberPermission.MEMBER
}
} }
) }
) )
} }
override suspend fun QQAndroidBot.handle(packet: GroupMessageOrNull, sequenceId: Int): OutgoingPacket? {
packet.delegate?.broadcast()
return null
}
} }
internal object PbPushTransMsg : IncomingPacketFactory<Packet>("OnlinePush.PbPushTransMsg", "OnlinePush.RespPush") { internal object PbPushTransMsg : IncomingPacketFactory<Packet>("OnlinePush.PbPushTransMsg", "OnlinePush.RespPush") {
...@@ -156,10 +142,10 @@ internal class OnlinePush { ...@@ -156,10 +142,10 @@ internal class OnlinePush {
@UseExperimental(ExperimentalStdlibApi::class) @UseExperimental(ExperimentalStdlibApi::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet {
val reqPushMsg = decodeUniPacket(OnlinePushPack.SvcReqPushMsg.serializer(), "req") 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<Packet> = reqPushMsg.vMsgInfos.mapNotNull { msgInfo: MsgInfo ->
msgInfo.vMsg!!.read {
when { when {
msgInfo.shMsgType.toInt() == 732 -> { msgInfo.shMsgType.toInt() == 732 -> {
val group = bot.getGroup(this.readUInt().toLong()) val group = bot.getGroup(this.readUInt().toLong())
...@@ -169,7 +155,7 @@ internal class OnlinePush { ...@@ -169,7 +155,7 @@ internal class OnlinePush {
3073 -> { // mute 3073 -> { // mute
val operatorUin = this.readUInt().toLong() val operatorUin = this.readUInt().toLong()
if (operatorUin == bot.uin) { if (operatorUin == bot.uin) {
return NoPacket return@mapNotNull null
} }
val operator = group[operatorUin] val operator = group[operatorUin]
this.readUInt().toLong() // time this.readUInt().toLong() // time
...@@ -177,42 +163,57 @@ internal class OnlinePush { ...@@ -177,42 +163,57 @@ internal class OnlinePush {
val target = this.readUInt().toLong() val target = this.readUInt().toLong()
val time = this.readInt() val time = this.readInt()
return if (target == 0L) { if (target == 0L) {
if (time == 0) { if (time == 0) {
GroupMuteAllEvent( return@mapNotNull GroupMuteAllEvent(
origin = group.isMuteAll.also { group._muteAll = false }, origin = group.isMuteAll.also { group._muteAll = false },
new = false, new = false,
operator = operator, operator = operator,
group = group group = group
) ) as Packet
} else { } else {
GroupMuteAllEvent( return@mapNotNull GroupMuteAllEvent(
origin = group.isMuteAll.also { group._muteAll = true }, origin = group.isMuteAll.also { group._muteAll = true },
new = true, new = true,
operator = operator, operator = operator,
group = group group = group
) ) as Packet
} }
} else { } else {
return if (target == bot.uin) { if (target == bot.uin) {
if (time == 0) { if (group._botMuteTimestamp != time) {
BotUnmuteEvent(operator) if (time == 0) {
} else group._botMuteTimestamp = 0
BotMuteEvent(durationSeconds = time, operator = operator) return@mapNotNull BotUnmuteEvent(operator) as Packet
} else {
group._botMuteTimestamp = time
return@mapNotNull BotMuteEvent(durationSeconds = time, operator = operator) as Packet
}
} else {
return@mapNotNull null
}
} else { } else {
val member = group[target] val member = group[target]
if (time == 0) { member as MemberImpl
MemberUnmuteEvent(operator = operator, member = member) 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 { } else {
MemberMuteEvent(operator = operator, member = member, durationSeconds = time) return@mapNotNull null
} }
} }
} }
} }
3585 -> { // 匿名 3585 -> {
// 匿名
val operator = group[this.readUInt().toLong()] val operator = group[this.readUInt().toLong()]
val switch = this.readInt() == 0 val switch = this.readInt() == 0
return GroupAllowAnonymousChatEvent( return@mapNotNull GroupAllowAnonymousChatEvent(
origin = group.isAnonymousChatEnabled.also { group._anonymousChat = switch }, origin = group.isAnonymousChatEnabled.also { group._anonymousChat = switch },
new = switch, new = switch,
operator = operator, operator = operator,
...@@ -225,7 +226,7 @@ internal class OnlinePush { ...@@ -225,7 +226,7 @@ internal class OnlinePush {
// println(dataBytes.toUHexString()) // println(dataBytes.toUHexString())
if (dataBytes[0].toInt() != 59) { if (dataBytes[0].toInt() != 59) {
return GroupNameChangeEvent( return@mapNotNull GroupNameChangeEvent(
origin = group.name.also { group._name = message }, origin = group.name.also { group._name = message },
new = message, new = message,
group = group, group = group,
...@@ -235,7 +236,7 @@ internal class OnlinePush { ...@@ -235,7 +236,7 @@ internal class OnlinePush {
//println(message + ":" + dataBytes.toUHexString()) //println(message + ":" + dataBytes.toUHexString())
when (message) { when (message) {
"管理员已关闭群聊坦白说" -> { "管理员已关闭群聊坦白说" -> {
return GroupAllowConfessTalkEvent( return@mapNotNull GroupAllowConfessTalkEvent(
origin = group.isConfessTalkEnabled.also { group._confessTalk = false }, origin = group.isConfessTalkEnabled.also { group._confessTalk = false },
new = false, new = false,
group = group, group = group,
...@@ -243,7 +244,7 @@ internal class OnlinePush { ...@@ -243,7 +244,7 @@ internal class OnlinePush {
) )
} }
"管理员已开启群聊坦白说" -> { "管理员已开启群聊坦白说" -> {
return GroupAllowConfessTalkEvent( return@mapNotNull GroupAllowConfessTalkEvent(
origin = group.isConfessTalkEnabled.also { group._confessTalk = true }, origin = group.isConfessTalkEnabled.also { group._confessTalk = true },
new = true, new = true,
group = group, group = group,
...@@ -252,7 +253,7 @@ internal class OnlinePush { ...@@ -252,7 +253,7 @@ internal class OnlinePush {
} }
else -> { else -> {
bot.network.logger.debug { "Unknown server messages $message" } bot.network.logger.debug { "Unknown server messages $message" }
return NoPacket return@mapNotNull null
} }
} }
} }
...@@ -263,6 +264,7 @@ internal class OnlinePush { ...@@ -263,6 +264,7 @@ internal class OnlinePush {
// } // }
else -> { else -> {
bot.network.logger.debug { "unknown group internal type $internalType , data: " + this.readBytes().toUHexString() + " " } bot.network.logger.debug { "unknown group internal type $internalType , data: " + this.readBytes().toUHexString() + " " }
return@mapNotNull null
} }
} }
} }
...@@ -270,18 +272,18 @@ internal class OnlinePush { ...@@ -270,18 +272,18 @@ internal class OnlinePush {
bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" } bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" }
// val content = msgInfo.vMsg.loadAs(OnlinePushPack.MsgType0x210.serializer()) // val content = msgInfo.vMsg.loadAs(OnlinePushPack.MsgType0x210.serializer())
// println(content.contentToString()) // println(content.contentToString())
return@mapNotNull null
} }
else -> { else -> {
bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" } bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" }
return@mapNotNull null
} }
} }
} }
} }
return MultiPacket(packets)
return NoPacket
} }
override suspend fun QQAndroidBot.handle(packet: Packet, sequenceId: Int): OutgoingPacket? { override suspend fun QQAndroidBot.handle(packet: Packet, sequenceId: Int): OutgoingPacket? {
return buildResponseUniPacket(client, sequenceId = sequenceId) { return buildResponseUniPacket(client, sequenceId = sequenceId) {
......
...@@ -47,7 +47,7 @@ internal class WtLogin { ...@@ -47,7 +47,7 @@ internal class WtLogin {
ticket: String ticket: String
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(2) // subCommand writeShort(2) // subCommand
writeShort(4) // count of TLVs writeShort(4) // count of TLVs
t193(ticket) t193(ticket)
...@@ -64,7 +64,7 @@ internal class WtLogin { ...@@ -64,7 +64,7 @@ internal class WtLogin {
captchaAnswer: String captchaAnswer: String
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(2) // subCommand writeShort(2) // subCommand
writeShort(4) // count of TLVs writeShort(4) // count of TLVs
t2(captchaAnswer, captchaSign, 0) t2(captchaAnswer, captchaSign, 0)
...@@ -83,7 +83,7 @@ internal class WtLogin { ...@@ -83,7 +83,7 @@ internal class WtLogin {
t402: ByteArray t402: ByteArray
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(20) // subCommand writeShort(20) // subCommand
writeShort(4) // count of TLVs, probably ignored by server? writeShort(4) // count of TLVs, probably ignored by server?
t8(2052) t8(2052)
...@@ -103,7 +103,7 @@ internal class WtLogin { ...@@ -103,7 +103,7 @@ internal class WtLogin {
client: QQAndroidClient client: QQAndroidClient
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> ): 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") { 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(8) // subCommand
writeShort(6) // count of TLVs, probably ignored by server?TODO writeShort(6) // count of TLVs, probably ignored by server?TODO
t8(2052) t8(2052)
...@@ -131,7 +131,7 @@ internal class WtLogin { ...@@ -131,7 +131,7 @@ internal class WtLogin {
client: QQAndroidClient client: QQAndroidClient
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(9) // subCommand writeShort(9) // subCommand
writeShort(17) // count of TLVs, probably ignored by server? writeShort(17) // count of TLVs, probably ignored by server?
//writeShort(LoginType.PASSWORD.value.toShort()) //writeShort(LoginType.PASSWORD.value.toShort())
...@@ -325,7 +325,7 @@ internal class WtLogin { ...@@ -325,7 +325,7 @@ internal class WtLogin {
2 -> onSolveLoginCaptcha(tlvMap, bot) 2 -> onSolveLoginCaptcha(tlvMap, bot)
160 /*-96*/ -> onUnsafeDeviceLogin(tlvMap) 160 /*-96*/ -> onUnsafeDeviceLogin(tlvMap)
204 /*-52*/ -> onSMSVerifyNeeded(tlvMap, bot) 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()}")
} }
} }
......
...@@ -7,9 +7,13 @@ ...@@ -7,9 +7,13 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.utils package net.mamoe.mirai.qqandroid.utils
import net.mamoe.mirai.utils.md5 import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic import kotlin.jvm.JvmStatic
/** /**
......
...@@ -7,8 +7,14 @@ ...@@ -7,8 +7,14 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.utils package net.mamoe.mirai.qqandroid.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
inline class MacOrAndroidIdChangeFlag(val value: Long = 0) { inline class MacOrAndroidIdChangeFlag(val value: Long = 0) {
fun macChanged(): MacOrAndroidIdChangeFlag = fun macChanged(): MacOrAndroidIdChangeFlag =
MacOrAndroidIdChangeFlag(this.value or 0x1) MacOrAndroidIdChangeFlag(this.value or 0x1)
......
...@@ -7,11 +7,16 @@ ...@@ -7,11 +7,16 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.utils package net.mamoe.mirai.qqandroid.utils
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
/** /**
* Inline the block * Inline the block
......
...@@ -7,10 +7,16 @@ ...@@ -7,10 +7,16 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.utils 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") @Suppress("NAME_SHADOWING")
var var0 = this.toLong() and 0xFFFFFFFF var var0 = this.toLong() and 0xFFFFFFFF
return buildString { return buildString {
......
...@@ -8,8 +8,6 @@ plugins { ...@@ -8,8 +8,6 @@ plugins {
id("com.jfrog.bintray") version "1.8.4-jetbrains-3" id("com.jfrog.bintray") version "1.8.4-jetbrains-3"
} }
apply(from = rootProject.file("gradle/publish.gradle"))
val kotlinVersion: String by rootProject.ext val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext val atomicFuVersion: String by rootProject.ext
val coroutinesVersion: String by rootProject.ext val coroutinesVersion: String by rootProject.ext
......
/*
* 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
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
...@@ -68,11 +68,11 @@ actual open class BotConfiguration actual constructor() { ...@@ -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
/** /**
* 验证码处理器 * 验证码处理器
*/ */
......
...@@ -9,8 +9,10 @@ ...@@ -9,8 +9,10 @@
package net.mamoe.mirai.utils.cryptor package net.mamoe.mirai.utils.cryptor
import android.annotation.SuppressLint
import net.mamoe.mirai.utils.md5 import net.mamoe.mirai.utils.md5
import java.security.* import java.security.*
import java.security.spec.ECGenParameterSpec
import java.security.spec.X509EncodedKeySpec import java.security.spec.X509EncodedKeySpec
import javax.crypto.KeyAgreement import javax.crypto.KeyAgreement
...@@ -18,13 +20,13 @@ import javax.crypto.KeyAgreement ...@@ -18,13 +20,13 @@ import javax.crypto.KeyAgreement
actual typealias ECDHPrivateKey = PrivateKey actual typealias ECDHPrivateKey = PrivateKey
actual typealias ECDHPublicKey = PublicKey actual typealias ECDHPublicKey = PublicKey
actual class ECDHKeyPair( internal actual class ECDHKeyPairImpl(
private val delegate: KeyPair private val delegate: KeyPair
) { ) : ECDHKeyPair {
actual val privateKey: ECDHPrivateKey get() = delegate.private override val privateKey: ECDHPrivateKey get() = delegate.private
actual val publicKey: ECDHPublicKey get() = delegate.public 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") @Suppress("FunctionName")
...@@ -32,8 +34,41 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair()) ...@@ -32,8 +34,41 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair())
actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual companion object { 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 { 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( actual fun calculateShareKey(
......
...@@ -30,9 +30,16 @@ actual class PlatformSocket : Closeable { ...@@ -30,9 +30,16 @@ actual class PlatformSocket : Closeable {
private lateinit var socket: Socket private lateinit var socket: Socket
actual val isOpen: Boolean 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 @PublishedApi
internal lateinit var writeChannel: BufferedOutputStream internal lateinit var writeChannel: BufferedOutputStream
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * 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 package net.mamoe.mirai
...@@ -35,7 +35,8 @@ import kotlin.jvm.JvmStatic ...@@ -35,7 +35,8 @@ import kotlin.jvm.JvmStatic
* *
* 注: Bot 为全协程实现, 没有其他任务时若不使用 [join], 主线程将会退出. * 注: Bot 为全协程实现, 没有其他任务时若不使用 [join], 主线程将会退出.
* *
* @see Contact * @see Contact 联系人
* @see kotlinx.coroutines.isActive 判断 [Bot] 是否正常运行中. (在线, 且没有被 [close])
*/ */
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
abstract class Bot : CoroutineScope { abstract class Bot : CoroutineScope {
...@@ -195,7 +196,9 @@ abstract class Bot : CoroutineScope { ...@@ -195,7 +196,9 @@ abstract class Bot : CoroutineScope {
/** /**
* 登录, 或重新登录. * 登录, 或重新登录.
* 重新登录时不会再次拉取联系人列表. * 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表.
*
* 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况.
* *
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin] * 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin]
* *
...@@ -231,24 +234,19 @@ abstract class Bot : CoroutineScope { ...@@ -231,24 +234,19 @@ abstract class Bot : CoroutineScope {
// endregion // endregion
/** /**
* 关闭这个 [Bot], 停止一切相关活动. 所有引用都会被释放. * 关闭这个 [Bot], 立即取消 [Bot] 的 [kotlinx.coroutines.SupervisorJob].
* 之后 [kotlinx.coroutines.isActive] 将会返回 `false`.
* *
* 注: 不可重新登录. 必须重新实例化一个 [Bot]. * **注意:** 不可重新登录. 必须重新实例化一个 [Bot].
* *
* @param cause 原因. 为 null 时视为正常关闭, 非 null 时视为异常关闭 * @param cause 原因. 为 null 时视为正常关闭, 非 null 时视为异常关闭
* *
* @see closeAndJoin * @see closeAndJoin 取消并 [Bot.join], 以确保 [Bot] 相关的活动被完全关闭
*/ */
abstract fun close(cause: Throwable? = null) abstract fun close(cause: Throwable? = null)
// region extensions // 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 { final override fun toString(): String {
return "Bot(${uin})" return "Bot(${uin})"
} }
......
...@@ -14,7 +14,6 @@ package net.mamoe.mirai ...@@ -14,7 +14,6 @@ package net.mamoe.mirai
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.event.Listener import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.broadcast 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.BotOfflineEvent
import net.mamoe.mirai.event.events.BotReloginEvent import net.mamoe.mirai.event.events.BotReloginEvent
import net.mamoe.mirai.event.subscribeAlways import net.mamoe.mirai.event.subscribeAlways
...@@ -73,11 +72,6 @@ abstract class BotImpl<N : BotNetworkHandler> constructor( ...@@ -73,11 +72,6 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
} }
} }
/**
* 可阻止事件广播
*/
abstract fun onEvent(event: BotEvent): Boolean
// region network // region network
final override val network: N get() = _network final override val network: N get() = _network
...@@ -89,21 +83,22 @@ abstract class BotImpl<N : BotNetworkHandler> constructor( ...@@ -89,21 +83,22 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
private val offlineListener: Listener<BotOfflineEvent> = this.subscribeAlways { event -> private val offlineListener: Listener<BotOfflineEvent> = this.subscribeAlways { event ->
when (event) { when (event) {
is BotOfflineEvent.Dropped -> { is BotOfflineEvent.Dropped -> {
bot.logger.info("Connection dropped or lost by server, retrying login") if (!_network.isActive) {
return@subscribeAlways
var lastFailedException: Throwable? = null }
repeat(configuration.reconnectionRetryTimes) { bot.logger.info("Connection dropped by server or lost, retrying login")
try {
network.relogin() tryNTimesOrException(configuration.reconnectionRetryTimes) { tryCount ->
logger.info("Reconnected successfully") if (tryCount != 0) {
return@subscribeAlways
} catch (e: Throwable) {
lastFailedException = e
delay(configuration.reconnectPeriodMillis) delay(configuration.reconnectPeriodMillis)
} }
} network.relogin(event.cause)
if (lastFailedException != null) { logger.info("Reconnected successfully")
throw lastFailedException!! BotReloginEvent(bot, event.cause).broadcast()
return@subscribeAlways
}?.let {
logger.info("Cannot reconnect")
throw it
} }
} }
is BotOfflineEvent.Active -> { is BotOfflineEvent.Active -> {
...@@ -112,17 +107,21 @@ abstract class BotImpl<N : BotNetworkHandler> constructor( ...@@ -112,17 +107,21 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
} else { } else {
" with exception: " + event.cause.message " with exception: " + event.cause.message
} }
bot.logger.info("Bot is closed manually$msg") bot.logger.info { "Bot is closed manually$msg" }
close(CancellationException(event.toString())) closeAndJoin(CancellationException(event.toString()))
} }
is BotOfflineEvent.Force -> { is BotOfflineEvent.Force -> {
bot.logger.info("Connection occupied by another android device: ${event.message}") bot.logger.info { "Connection occupied by another android device: ${event.message}" }
close(ForceOfflineException(event.toString())) 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( private suspend fun reinitializeNetworkHandler(
cause: Throwable? cause: Throwable?
...@@ -176,15 +175,19 @@ abstract class BotImpl<N : BotNetworkHandler> constructor( ...@@ -176,15 +175,19 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
override fun close(cause: Throwable?) { override fun close(cause: Throwable?) {
if (!this.botJob.isActive) {
// already cancelled
return
}
kotlin.runCatching { kotlin.runCatching {
if (cause == null) { if (cause == null) {
this.botJob.cancel()
network.close() network.close()
this.botJob.complete() offlineListener.cancel()
offlineListener.complete()
} else { } else {
this.botJob.cancel(CancellationException("bot cancelled", cause))
network.close(cause) network.close(cause)
this.botJob.completeExceptionally(cause) offlineListener.cancel(CancellationException("bot cancelled", cause))
offlineListener.completeExceptionally(cause)
} }
} }
groups.delegate.clear() groups.delegate.clear()
......
/*
* 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
...@@ -74,6 +74,16 @@ interface Contact : CoroutineScope { ...@@ -74,6 +74,16 @@ interface Contact : CoroutineScope {
* 而 [QQ] 含义为一个独立的人, 可以是好友, 也可以是陌生人. * 而 [QQ] 含义为一个独立的人, 可以是好友, 也可以是陌生人.
*/ */
override fun equals(other: Any?): Boolean 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()) suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain())
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.* import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
...@@ -89,19 +90,19 @@ interface Group : Contact, CoroutineScope { ...@@ -89,19 +90,19 @@ interface Group : Contact, CoroutineScope {
/** /**
* 机器人被禁言还剩余多少秒 * 机器人被禁言还剩余多少秒
* *
* @see BotMuteEvent * @see BotMuteEvent 机器人被禁言事件
* @see isBotMuted * @see isBotMuted 判断机器人是否正在被禁言
*/ */
val botMuteRemaining: Int val botMuteRemaining: Int
/** /**
* 机器人在这个群里的权限 * 机器人在这个群里的权限
* *
* **MiraiExperimentalAPI**: 在未来可能会被修改 * @see Group.checkBotPermission 检查 [Bot] 在这个群里的权限
* @see Group.checkBotPermissionOperator 要求 [Bot] 在这个群里的权限为 [管理员或群主][MemberPermission.isOperator]
* *
* @see BotGroupPermissionChangeEvent * @see BotGroupPermissionChangeEvent 机器人群员修改
*/ */
@MiraiExperimentalAPI
val botPermission: MemberPermission val botPermission: MemberPermission
...@@ -129,6 +130,7 @@ interface Group : Contact, CoroutineScope { ...@@ -129,6 +130,7 @@ interface Group : Contact, CoroutineScope {
/** /**
* 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败 * 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败
*/ */
@MiraiExperimentalAPI("还未支持")
suspend fun quit(): Boolean suspend fun quit(): Boolean
/** /**
......
/* /*
* Copyright 2020 Mamoe Technologies and contributors. * 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. * 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 * https://github.com/mamoe/mirai/blob/master/LICENSE
...@@ -29,33 +29,50 @@ interface Member : QQ, Contact { ...@@ -29,33 +29,50 @@ interface Member : QQ, Contact {
/** /**
* 成员的权限, 动态更新. * 成员的权限, 动态更新.
*
* @see MemberPermissionChangeEvent 权限变更事件. 由群主或机器人的操作触发.
*/ */
val permission: MemberPermission val permission: MemberPermission
/** /**
* 群名片. 可能为空. 修改时将会触发事件 * 群名片. 可能为空.
*
* 管理员和群主都可修改任何人(包括群主)的群名片.
* *
* 在修改时将会异步上传至服务器. * 在修改时将会异步上传至服务器.
* *
* @see [groupCardOrNick] 获取非空群名片或昵称 * @see [nameCardOrNick] 获取非空群名片或昵称
* *
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件 * @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时 * @throws PermissionDeniedException 无权限修改时
*/ */
var nameCard: String var nameCard: String
/** /**
* 群头衔 * 群头衔.
*
* 仅群主可以修改群头衔.
* *
* 在修改时将会异步上传至服务器. * 在修改时将会异步上传至服务器.
* *
* @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件 * @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时 * @throws PermissionDeniedException 无权限修改时
*/ */
var specialTitle: String var specialTitle: String
/** /**
* 禁言 * 被禁言剩余时长. 单位为秒.
*
* @see isMuted 判断改成员是否处于禁言状态
* @see mute 设置禁言
* @see unmute 取消禁言
*/
val muteTimeRemaining: Int
/**
* 禁言.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
* *
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常. * @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 机器人无权限时返回 `false` * @return 机器人无权限时返回 `false`
...@@ -72,6 +89,8 @@ interface Member : QQ, Contact { ...@@ -72,6 +89,8 @@ interface Member : QQ, Contact {
/** /**
* 解除禁言. * 解除禁言.
* *
* 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言.
*
* @see MemberUnmuteEvent 成员被取消禁言事件. * @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时 * @throws PermissionDeniedException 无权限修改时
*/ */
...@@ -80,6 +99,8 @@ interface Member : QQ, Contact { ...@@ -80,6 +99,8 @@ interface Member : QQ, Contact {
/** /**
* 踢出该成员. * 踢出该成员.
* *
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件. * @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时 * @throws PermissionDeniedException 无权限修改时
*/ */
...@@ -96,7 +117,14 @@ interface Member : QQ, Contact { ...@@ -96,7 +117,14 @@ interface Member : QQ, Contact {
* *
* 若 [群名片][Member.nameCard] 不为空则返回群名片, 为空则返回 [QQ.nick] * 若 [群名片][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 @ExperimentalTime
suspend inline fun Member.mute(duration: Duration) { suspend inline fun Member.mute(duration: Duration) {
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
...@@ -68,7 +69,6 @@ inline fun Member.isAdministrator(): Boolean = this.permission.isAdministrator() ...@@ -68,7 +69,6 @@ inline fun Member.isAdministrator(): Boolean = this.permission.isAdministrator()
inline fun Member.isOperator(): Boolean = this.permission.isOperator() inline fun Member.isOperator(): Boolean = this.permission.isOperator()
/** /**
* 权限不足 * 权限不足
*/ */
...@@ -77,6 +77,11 @@ expect class PermissionDeniedException : IllegalStateException { ...@@ -77,6 +77,11 @@ expect class PermissionDeniedException : IllegalStateException {
constructor(message: String?) constructor(message: String?)
} }
/**
* 要求 [Bot] 在这个群里的权限为 [required], 否则抛出异常 [PermissionDeniedException]
*
* @throws PermissionDeniedException
*/
@UseExperimental(MiraiExperimentalAPI::class) @UseExperimental(MiraiExperimentalAPI::class)
inline fun Group.checkBotPermission( inline fun Group.checkBotPermission(
required: MemberPermission, required: MemberPermission,
...@@ -89,6 +94,11 @@ inline fun Group.checkBotPermission( ...@@ -89,6 +94,11 @@ inline fun Group.checkBotPermission(
} }
} }
/**
* 要求 [Bot] 在这个群里的权限为 [管理员或群主][MemberPermission.isOperator], 否则抛出异常 [PermissionDeniedException]
*
* @throws PermissionDeniedException
*/
@UseExperimental(MiraiExperimentalAPI::class) @UseExperimental(MiraiExperimentalAPI::class)
inline fun Group.checkBotPermissionOperator( inline fun Group.checkBotPermissionOperator(
lazyMessage: () -> String = { lazyMessage: () -> String = {
......
/*
* 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
...@@ -17,4 +17,6 @@ interface MemberInfo : FriendInfo { ...@@ -17,4 +17,6 @@ interface MemberInfo : FriendInfo {
val permission: MemberPermission val permission: MemberPermission
val specialTitle: String val specialTitle: String
val muteTimestamp: Int
} }
\ No newline at end of file
...@@ -25,6 +25,8 @@ import net.mamoe.mirai.message.data.Message ...@@ -25,6 +25,8 @@ import net.mamoe.mirai.message.data.Message
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/** /**
* 订阅来自所有 [Bot] 的所有联系人的消息事件. 联系人可以是任意群或任意好友或临时会话. * 订阅来自所有 [Bot] 的所有联系人的消息事件. 联系人可以是任意群或任意好友或临时会话.
...@@ -33,7 +35,10 @@ import kotlin.contracts.contract ...@@ -33,7 +35,10 @@ import kotlin.contracts.contract
*/ */
@UseExperimental(ExperimentalContracts::class) @UseExperimental(ExperimentalContracts::class)
@MessageDsl @MessageDsl
inline fun <R> CoroutineScope.subscribeMessages(crossinline listeners: MessageSubscribersBuilder<MessagePacket<*, *>>.() -> R): R { inline fun <R> CoroutineScope.subscribeMessages(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline listeners: MessageSubscribersBuilder<MessagePacket<*, *>>.() -> R
): R {
// contract 可帮助 IDE 进行类型推断. 无实际代码作用. // contract 可帮助 IDE 进行类型推断. 无实际代码作用.
contract { contract {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
...@@ -42,7 +47,7 @@ inline fun <R> CoroutineScope.subscribeMessages(crossinline listeners: MessageSu ...@@ -42,7 +47,7 @@ inline fun <R> CoroutineScope.subscribeMessages(crossinline listeners: MessageSu
return MessageSubscribersBuilder { messageListener: MessageListener<MessagePacket<*, *>> -> return MessageSubscribersBuilder { messageListener: MessageListener<MessagePacket<*, *>> ->
// subscribeAlways 即注册一个监听器. 这个监听器收到消息后就传递给 [listener] // subscribeAlways 即注册一个监听器. 这个监听器收到消息后就传递给 [listener]
// listener 即为 DSL 里 `contains(...) { }`, `startsWith(...) { }` 的代码块. // listener 即为 DSL 里 `contains(...) { }`, `startsWith(...) { }` 的代码块.
subscribeAlways { subscribeAlways(coroutineContext) {
messageListener.invoke(this, this.message.toString()) messageListener.invoke(this, this.message.toString())
// this.message.toString() 即为 messageListener 中 it 接收到的值 // this.message.toString() 即为 messageListener 中 it 接收到的值
} }
...@@ -56,12 +61,15 @@ inline fun <R> CoroutineScope.subscribeMessages(crossinline listeners: MessageSu ...@@ -56,12 +61,15 @@ inline fun <R> CoroutineScope.subscribeMessages(crossinline listeners: MessageSu
*/ */
@UseExperimental(ExperimentalContracts::class) @UseExperimental(ExperimentalContracts::class)
@MessageDsl @MessageDsl
inline fun <R> CoroutineScope.subscribeGroupMessages(crossinline listeners: MessageSubscribersBuilder<GroupMessage>.() -> R): R { inline fun <R> CoroutineScope.subscribeGroupMessages(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline listeners: MessageSubscribersBuilder<GroupMessage>.() -> R
): R {
contract { contract {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
} }
return MessageSubscribersBuilder<GroupMessage> { listener -> return MessageSubscribersBuilder<GroupMessage> { listener ->
subscribeAlways { subscribeAlways(coroutineContext) {
listener(this, this.message.toString()) listener(this, this.message.toString())
} }
}.run(listeners) }.run(listeners)
...@@ -74,12 +82,15 @@ inline fun <R> CoroutineScope.subscribeGroupMessages(crossinline listeners: Mess ...@@ -74,12 +82,15 @@ inline fun <R> CoroutineScope.subscribeGroupMessages(crossinline listeners: Mess
*/ */
@UseExperimental(ExperimentalContracts::class) @UseExperimental(ExperimentalContracts::class)
@MessageDsl @MessageDsl
inline fun <R> CoroutineScope.subscribeFriendMessages(crossinline listeners: MessageSubscribersBuilder<FriendMessage>.() -> R): R { inline fun <R> CoroutineScope.subscribeFriendMessages(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline listeners: MessageSubscribersBuilder<FriendMessage>.() -> R
): R {
contract { contract {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
} }
return MessageSubscribersBuilder<FriendMessage> { listener -> return MessageSubscribersBuilder<FriendMessage> { listener ->
subscribeAlways { subscribeAlways(coroutineContext) {
listener(this, this.message.toString()) listener(this, this.message.toString())
} }
}.run(listeners) }.run(listeners)
...@@ -92,12 +103,15 @@ inline fun <R> CoroutineScope.subscribeFriendMessages(crossinline listeners: Mes ...@@ -92,12 +103,15 @@ inline fun <R> CoroutineScope.subscribeFriendMessages(crossinline listeners: Mes
*/ */
@UseExperimental(ExperimentalContracts::class) @UseExperimental(ExperimentalContracts::class)
@MessageDsl @MessageDsl
inline fun <R> Bot.subscribeMessages(crossinline listeners: MessageSubscribersBuilder<MessagePacket<*, *>>.() -> R): R { inline fun <R> Bot.subscribeMessages(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline listeners: MessageSubscribersBuilder<MessagePacket<*, *>>.() -> R
): R {
contract { contract {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
} }
return MessageSubscribersBuilder<MessagePacket<*, *>> { listener -> return MessageSubscribersBuilder<MessagePacket<*, *>> { listener ->
this.subscribeAlways { this.subscribeAlways(coroutineContext) {
listener(this, this.message.toString()) listener(this, this.message.toString())
} }
}.run(listeners) }.run(listeners)
...@@ -106,16 +120,21 @@ inline fun <R> Bot.subscribeMessages(crossinline listeners: MessageSubscribersBu ...@@ -106,16 +120,21 @@ inline fun <R> Bot.subscribeMessages(crossinline listeners: MessageSubscribersBu
/** /**
* 订阅来自这个 [Bot] 的所有群消息事件 * 订阅来自这个 [Bot] 的所有群消息事件
* *
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see CoroutineScope.incoming * @see CoroutineScope.incoming
*/ */
@UseExperimental(ExperimentalContracts::class) @UseExperimental(ExperimentalContracts::class)
@MessageDsl @MessageDsl
inline fun <R> Bot.subscribeGroupMessages(crossinline listeners: MessageSubscribersBuilder<GroupMessage>.() -> R): R { inline fun <R> Bot.subscribeGroupMessages(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline listeners: MessageSubscribersBuilder<GroupMessage>.() -> R
): R {
contract { contract {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
} }
return MessageSubscribersBuilder<GroupMessage> { listener -> return MessageSubscribersBuilder<GroupMessage> { listener ->
this.subscribeAlways { this.subscribeAlways(coroutineContext) {
listener(this, this.message.toString()) listener(this, this.message.toString())
} }
}.run(listeners) }.run(listeners)
...@@ -128,12 +147,15 @@ inline fun <R> Bot.subscribeGroupMessages(crossinline listeners: MessageSubscrib ...@@ -128,12 +147,15 @@ inline fun <R> Bot.subscribeGroupMessages(crossinline listeners: MessageSubscrib
*/ */
@UseExperimental(ExperimentalContracts::class) @UseExperimental(ExperimentalContracts::class)
@MessageDsl @MessageDsl
inline fun <R> Bot.subscribeFriendMessages(crossinline listeners: MessageSubscribersBuilder<FriendMessage>.() -> R): R { inline fun <R> Bot.subscribeFriendMessages(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline listeners: MessageSubscribersBuilder<FriendMessage>.() -> R
): R {
contract { contract {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
} }
return MessageSubscribersBuilder<FriendMessage> { listener -> return MessageSubscribersBuilder<FriendMessage> { listener ->
this.subscribeAlways { this.subscribeAlways(coroutineContext) {
listener(this, this.message.toString()) listener(this, this.message.toString())
} }
}.run(listeners) }.run(listeners)
...@@ -148,9 +170,12 @@ inline fun <R> Bot.subscribeFriendMessages(crossinline listeners: MessageSubscri ...@@ -148,9 +170,12 @@ inline fun <R> Bot.subscribeFriendMessages(crossinline listeners: MessageSubscri
* @see subscribeMessages * @see subscribeMessages
* @see subscribeGroupMessages * @see subscribeGroupMessages
*/ */
inline fun <reified E : Event> CoroutineScope.incoming(capacity: Int = Channel.RENDEZVOUS): ReceiveChannel<E> { inline fun <reified E : Event> CoroutineScope.incoming(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
capacity: Int = Channel.RENDEZVOUS
): ReceiveChannel<E> {
return Channel<E>(capacity).apply { return Channel<E>(capacity).apply {
subscribeAlways<E> { subscribeAlways<E>(coroutineContext) {
send(this) send(this)
} }
} }
......
...@@ -11,8 +11,6 @@ ...@@ -11,8 +11,6 @@
package net.mamoe.mirai.event 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.event.internal.broadcastInternal
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
...@@ -22,7 +20,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI ...@@ -22,7 +20,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
* 若监听这个类, 监听器将会接收所有事件的广播. * 若监听这个类, 监听器将会接收所有事件的广播.
* *
* @see subscribeAlways * @see subscribeAlways
* @see subscribeWhile * @see subscribeOnce
* *
* @see subscribeMessages * @see subscribeMessages
* *
...@@ -73,9 +71,6 @@ suspend fun <E : Event> E.broadcast(): E = apply { ...@@ -73,9 +71,6 @@ suspend fun <E : Event> E.broadcast(): E = apply {
if (this is BroadcastControllable && !this.shouldBroadcast) { if (this is BroadcastControllable && !this.shouldBroadcast) {
return@apply return@apply
} }
if (this is BotEvent && !(this.bot as BotImpl<*>).onEvent(this)) {
return@apply
}
this@broadcast.broadcastInternal() // inline, no extra cost this@broadcast.broadcastInternal() // inline, no extra cost
} }
......
...@@ -14,10 +14,16 @@ import kotlinx.coroutines.CoroutineExceptionHandler ...@@ -14,10 +14,16 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import net.mamoe.mirai.Bot 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.Handler
import net.mamoe.mirai.event.internal.subscribeInternal import net.mamoe.mirai.event.internal.subscribeInternal
import net.mamoe.mirai.utils.MiraiInternalAPI 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.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
/* /*
* 该文件为所有的订阅事件的方法. * 该文件为所有的订阅事件的方法.
...@@ -68,15 +74,16 @@ interface Listener<in E : Event> : CompletableJob { ...@@ -68,15 +74,16 @@ interface Listener<in E : Event> : CompletableJob {
* `runBlocking` 不会结束, 也就是下一行 `foo()` 不会被执行. 直到监听时创建的 `Listener` 被停止. * `runBlocking` 不会结束, 也就是下一行 `foo()` 不会被执行. 直到监听时创建的 `Listener` 被停止.
* *
* *
* 要创建一个全局都存在的监听, 即守护协程, 请在 [GlobalScope] 下调用本函数: * 要创建一个仅在某个机器人在线时的监听, 请在 [Bot] 下调用本函数 (因为 [Bot] 也实现 [CoroutineScope]).
* 这种方式创建的监听会自动筛选 [Bot].
* ```kotlin * ```kotlin
* GlobalScope.subscribe<Event> { /* 一些处理 */ } * bot1.subscribe<BotEvent> { /* 只会处理来自 bot1 的事件 */ }
* ``` * ```
* *
* *
* 要创建一个仅在某个机器人在线时的监听, 请在 [Bot] 下调用本函数 (因为 [Bot] 也实现 [CoroutineScope]): * 要创建一个全局都存在的监听, 即守护协程, 请在 [GlobalScope] 下调用本函数:
* ```kotlin * ```kotlin
* bot.subscribe<Subscribe> { /* 一些处理 */ } * GlobalScope.subscribe<Event> { /* 会收到来自全部 Bot 的事件和与 Bot 不相关的事件 */ }
* ``` * ```
* *
* *
...@@ -86,122 +93,137 @@ interface Listener<in E : Event> : CompletableJob { ...@@ -86,122 +93,137 @@ interface Listener<in E : Event> : CompletableJob {
* [this] 没有 [CoroutineExceptionHandler], 则在事件广播方的 [CoroutineExceptionHandler] 处理 * [this] 没有 [CoroutineExceptionHandler], 则在事件广播方的 [CoroutineExceptionHandler] 处理
* 若均找不到, 则会触发 logger warning. * 若均找不到, 则会触发 logger warning.
* - 事件处理时抛出异常不会停止监听器. * - 事件处理时抛出异常不会停止监听器.
* - 建议在事件处理中, [handler] 里处理异常, 或在 [this] 指定 [CoroutineExceptionHandler]. * - 建议在事件处理中 ( [handler] ) 处理异常,
* 或在 [this] [CoroutineScope.coroutineContext] 中添加 [CoroutineExceptionHandler].
*
* *
* **注意:** 事件处理是 `suspend` , 请规范处理 JVM 阻塞方法.
* *
* **注意:** 事件处理是 `suspend` , 请严格控制 JVM 阻塞方法的使用. 若致事件处理阻塞, 则会导致一些逻辑无法进行. * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
* *
* // TODO: 2020/2/13 在 bot 下监听时同时筛选对应 bot 实例 * @see subscribeAlways 一直监听
* @see subscribeOnce 只监听一次
* *
* @see subscribeMessages 监听消息 DSL * @see subscribeMessages 监听消息 DSL
* @see subscribeGroupMessages 监听群消息 DSL * @see subscribeGroupMessages 监听群消息 DSL
* @see subscribeFriendMessages 监听好友消息 DSL * @see subscribeFriendMessages 监听好友消息 DSL
*/ */
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event> CoroutineScope.subscribe(crossinline handler: suspend E.(E) -> ListeningStatus): Listener<E> = inline fun <reified E : Event> CoroutineScope.subscribe(
E::class.subscribeInternal(Handler { it.handler(it); }) coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline handler: suspend E.(E) -> ListeningStatus
): Listener<E> =
E::class.subscribeInternal(Handler(coroutineContext) { it.handler(it); })
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行. * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行.
* *
* 仅当 [Listener.complete] 或 [Listener.cancel] 时结束. * 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
* *
* @see subscribe 获取更多说明 * @see subscribe 获取更多说明
*/ */
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class, ExperimentalContracts::class)
inline fun <reified E : Event> CoroutineScope.subscribeAlways(crossinline listener: suspend E.(E) -> Unit): Listener<E> = inline fun <reified E : Event> CoroutineScope.subscribeAlways(
E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.LISTENING }) coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline listener: suspend E.(E) -> Unit
): Listener<E> {
contract {
callsInPlace(listener, InvocationKind.UNKNOWN)
}
return E::class.subscribeInternal(Handler(coroutineContext) { it.listener(it); ListeningStatus.LISTENING })
}
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 仅在第一次 [事件广播][Event.broadcast] 时, [listener] 会被执行. * 仅在第一次 [事件广播][Event.broadcast] 时, [listener] 会被执行.
* *
* 在这之前, 可通过 [Listener.complete] 来停止监听. * 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
* *
* @see subscribe 获取更多说明 * @see subscribe 获取更多说明
*/ */
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event> CoroutineScope.subscribeOnce(crossinline listener: suspend E.(E) -> Unit): Listener<E> = inline fun <reified E : Event> CoroutineScope.subscribeOnce(
E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.STOPPED }) coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline listener: suspend E.(E) -> Unit
): Listener<E> =
E::class.subscribeInternal(Handler(coroutineContext) { it.listener(it); ListeningStatus.STOPPED })
//
// 以下为带筛选 Bot 的监听
//
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在 [Bot] 的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行, 直到 [listener] 的返回值 [equals] 于 [valueIfStop] * 每当 [事件广播][Event.broadcast] 时, [handler] 都会被执行,
* 当 [handler] 返回 [ListeningStatus.STOPPED] 时停止监听
*
* 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
* *
* 可在任意时刻通过 [Listener.complete] 来停止监听. * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
* *
* @see subscribe 获取更多说明 * @see subscribe 获取更多说明
*/ */
@JvmName("subscribeAlwaysForBot")
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event, T> CoroutineScope.subscribeUntil(valueIfStop: T, crossinline listener: suspend E.(E) -> T): Listener<E> = inline fun <reified E : BotEvent> Bot.subscribe(
E::class.subscribeInternal(Handler { if (it.listener(it) == valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline handler: suspend E.(E) -> ListeningStatus
): Listener<E> =
E::class.subscribeInternal(Handler(coroutineContext) { if (it.bot === this) it.handler(it) else ListeningStatus.LISTENING })
/** /**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件. * 在 [Bot] 的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行, * 每当 [事件广播][Event.broadcast] 时, [listener] 都会被执行.
* 如果 [listener] 的返回值 [equals] 于 [valueIfContinue], 则继续监听, 否则停止 *
* 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
* *
* 可在任意时刻通过 [Listener.complete] 来停止监听. * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
* *
* @see subscribe 获取更多说明 * @see subscribe 获取更多说明
*/ */
@JvmName("subscribeAlwaysForBot1")
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event, T> CoroutineScope.subscribeWhile(valueIfContinue: T, crossinline listener: suspend E.(E) -> T): Listener<E> = inline fun <reified E : BotEvent> Bot.subscribeAlways(
E::class.subscribeInternal(Handler { if (it.listener(it) != valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING }) coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline listener: suspend E.(E) -> Unit
// endregion ): Listener<E> {
return E::class.subscribeInternal(Handler(coroutineContext) { if (it.bot === this) it.listener(it); ListeningStatus.LISTENING })
// region ListenerBuilder DSL }
/*
/** /**
* 监听构建器. 可同时进行多种方式的监听 * 在 [Bot] 的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 仅在第一次 [事件广播][Event.broadcast] 时, [listener] 会被执行.
* *
* ```kotlin * 可在任意时候通过 [Listener.complete] 来主动停止监听.
* FriendMessageEvent.subscribe { * [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
* always{ *
* it.reply("永远发生") * @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
* } *
* * @see subscribe 获取更多说明
* untilFalse {
* it.reply("你发送了 ${it.event}")
* it.event eq "停止"
* }
* }
* ```
*/ */
@ListenersBuilderDsl @JvmName("subscribeOnceForBot2")
@Suppress("MemberVisibilityCanBePrivate", "unused") @UseExperimental(MiraiInternalAPI::class)
inline class ListenerBuilder<out E : Event>( inline fun <reified E : BotEvent> Bot.subscribeOnce(
@PublishedApi internal inline val handlerConsumer: CoroutineCoroutineScope.(Listener<E>) -> Unit coroutineContext: CoroutineContext = EmptyCoroutineContext,
) { noinline listener: suspend E.(E) -> Unit
fun CoroutineCoroutineScope.handler(listener: suspend E.(E) -> ListeningStatus) { ): Listener<E> =
handlerConsumer(Handler { it.listener(it) }) E::class.subscribeInternal(Handler(coroutineContext) {
} if (it.bot === this) {
it.listener(it)
fun CoroutineCoroutineScope.always(listener: suspend E.(E) -> Unit) = handler { listener(it); ListeningStatus.LISTENING } ListeningStatus.STOPPED
} else ListeningStatus.LISTENING
fun <T> 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 <T> 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 }
}
@DslMarker
annotation class ListenersBuilderDsl
*/
// endregion // endregion
\ No newline at end of file
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("unused")
package net.mamoe.mirai.event.events package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
...@@ -57,7 +59,7 @@ sealed class BotOfflineEvent : BotEvent { ...@@ -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( ...@@ -194,7 +196,7 @@ data class GroupNameChangeEvent(
override val origin: String, override val origin: String,
override val new: String, override val new: String,
override val group: Group, override val group: Group,
val isByBot: Boolean val isByBot: Boolean // 无法获取 operator
) : GroupSettingChangeEvent<String>, Packet ) : GroupSettingChangeEvent<String>, Packet
/** /**
...@@ -210,6 +212,8 @@ data class GroupEntranceAnnouncementChangeEvent( ...@@ -210,6 +212,8 @@ data class GroupEntranceAnnouncementChangeEvent(
val operator: Member? val operator: Member?
) : GroupSettingChangeEvent<String>, Packet ) : GroupSettingChangeEvent<String>, Packet
val GroupEntranceAnnouncementChangeEvent.isByBot: Boolean get() = operator != null
/** /**
* 群 "全员禁言" 功能状态改变. 此事件广播前修改就已经完成. * 群 "全员禁言" 功能状态改变. 此事件广播前修改就已经完成.
...@@ -224,6 +228,8 @@ data class GroupMuteAllEvent( ...@@ -224,6 +228,8 @@ data class GroupMuteAllEvent(
val operator: Member? val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet ) : GroupSettingChangeEvent<Boolean>, Packet
val GroupMuteAllEvent.isByBot: Boolean get() = operator != null
/** /**
* 群 "匿名聊天" 功能状态改变. 此事件广播前修改就已经完成. * 群 "匿名聊天" 功能状态改变. 此事件广播前修改就已经完成.
*/ */
...@@ -237,6 +243,8 @@ data class GroupAllowAnonymousChatEvent( ...@@ -237,6 +243,8 @@ data class GroupAllowAnonymousChatEvent(
val operator: Member? val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet ) : GroupSettingChangeEvent<Boolean>, Packet
val GroupAllowAnonymousChatEvent.isByBot: Boolean get() = operator != null
/** /**
* 群 "坦白说" 功能状态改变. 此事件广播前修改就已经完成. * 群 "坦白说" 功能状态改变. 此事件广播前修改就已经完成.
*/ */
...@@ -260,6 +268,8 @@ data class GroupAllowMemberInviteEvent( ...@@ -260,6 +268,8 @@ data class GroupAllowMemberInviteEvent(
val operator: Member? val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet ) : GroupSettingChangeEvent<Boolean>, Packet
val GroupAllowMemberInviteEvent.isByBot: Boolean get() = operator != null
// endregion // endregion
...@@ -293,6 +303,8 @@ sealed class MemberLeaveEvent : GroupMemberEvent { ...@@ -293,6 +303,8 @@ sealed class MemberLeaveEvent : GroupMemberEvent {
data class Quit(override val member: Member) : MemberLeaveEvent() data class Quit(override val member: Member) : MemberLeaveEvent()
} }
val MemberLeaveEvent.Kick.isByBot: Boolean get() = operator != null
// endregion // endregion
// region 名片和头衔 // region 名片和头衔
...@@ -319,6 +331,8 @@ data class MemberCardChangeEvent( ...@@ -319,6 +331,8 @@ data class MemberCardChangeEvent(
val operator: Member? val operator: Member?
) : GroupMemberEvent ) : GroupMemberEvent
val MemberCardChangeEvent.isByBot: Boolean get() = operator != null
/** /**
* 群头衔改动. 一定为群主操作 * 群头衔改动. 一定为群主操作
*/ */
...@@ -333,9 +347,18 @@ data class MemberSpecialTitleChangeEvent( ...@@ -333,9 +347,18 @@ data class MemberSpecialTitleChangeEvent(
*/ */
val new: String, val new: String,
override val member: Member override val member: Member,
/**
* 操作人.
* 不为 null 时一定为群主. 可能与 [member] 引用相同, 此时为群员自己修改.
* 为 null 时则是机器人操作.
*/
val operator: Member?
) : GroupMemberEvent ) : GroupMemberEvent
val MemberSpecialTitleChangeEvent.isByBot: Boolean get() = operator != null
// endregion // endregion
...@@ -367,6 +390,8 @@ data class MemberMuteEvent( ...@@ -367,6 +390,8 @@ data class MemberMuteEvent(
val operator: Member? val operator: Member?
) : GroupMemberEvent, Packet ) : GroupMemberEvent, Packet
val MemberMuteEvent.isByBot: Boolean get() = operator != null
/** /**
* 群成员被取消禁言事件. 被禁言的成员都不可能是机器人本人 * 群成员被取消禁言事件. 被禁言的成员都不可能是机器人本人
*/ */
...@@ -378,6 +403,8 @@ data class MemberUnmuteEvent( ...@@ -378,6 +403,8 @@ data class MemberUnmuteEvent(
val operator: Member? val operator: Member?
) : GroupMemberEvent, Packet ) : GroupMemberEvent, Packet
val MemberUnmuteEvent.isByBot: Boolean get() = operator != null
// endregion // endregion
// endregion // endregion
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
package net.mamoe.mirai.event.internal package net.mamoe.mirai.event.internal
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.EventDisabled import net.mamoe.mirai.event.EventDisabled
...@@ -32,8 +31,12 @@ fun <L : Listener<E>, E : Event> KClass<out E>.subscribeInternal(listener: L): L ...@@ -32,8 +31,12 @@ fun <L : Listener<E>, E : Event> KClass<out E>.subscribeInternal(listener: L): L
@PublishedApi @PublishedApi
@Suppress("FunctionName") @Suppress("FunctionName")
internal fun <E : Event> CoroutineScope.Handler(handler: suspend (E) -> ListeningStatus): Handler<E> { internal fun <E : Event> CoroutineScope.Handler(
return Handler(coroutineContext[Job], coroutineContext, handler) coroutineContext: CoroutineContext,
handler: suspend (E) -> ListeningStatus
): Handler<E> {
val context = this.newCoroutineContext(coroutineContext)
return Handler(context[Job], context, handler)
} }
private inline fun inline(block: () -> Unit) = block() private inline fun inline(block: () -> Unit) = block()
...@@ -77,7 +80,32 @@ internal class Handler<in E : Event> ...@@ -77,7 +80,32 @@ internal class Handler<in E : Event>
*/ */
internal fun <E : Event> KClass<out E>.listeners(): EventListeners<E> = EventListenerManager.get(this) internal fun <E : Event> KClass<out E>.listeners(): EventListeners<E> = EventListenerManager.get(this)
internal class EventListeners<E : Event> : LockFreeLinkedList<Listener<E>>() internal class EventListeners<E : Event>(clazz: KClass<E>) : LockFreeLinkedList<Listener<E>>() {
@Suppress("UNCHECKED_CAST")
val supertypes: Set<KClass<out Event>> by lazy {
val supertypes = mutableSetOf<KClass<out Event>>()
fun addSupertypes(clazz: KClass<out Event>) {
clazz.supertypes.forEach {
val classifier = it.classifier as? KClass<out Event>
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]. * 管理每个事件 class 的 [EventListeners].
...@@ -88,16 +116,8 @@ internal object EventListenerManager { ...@@ -88,16 +116,8 @@ internal object EventListenerManager {
private val registries = LockFreeLinkedList<Registry<*>>() private val registries = LockFreeLinkedList<Registry<*>>()
private val lock = atomic(false) // 不要用 atomicfu. 在 publish 后会出现 VerifyError
private val lock: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
private fun setLockValue(value: Boolean) {
lock.value = value
}
@Suppress("BooleanLiteralArgument")
private fun trySetLockTrue(): Boolean {
return lock.compareAndSet(false, true)
}
@Suppress("UNCHECKED_CAST", "BooleanLiteralArgument") @Suppress("UNCHECKED_CAST", "BooleanLiteralArgument")
internal tailrec fun <E : Event> get(clazz: KClass<out E>): EventListeners<E> { internal tailrec fun <E : Event> get(clazz: KClass<out E>): EventListeners<E> {
...@@ -106,11 +126,11 @@ internal object EventListenerManager { ...@@ -106,11 +126,11 @@ internal object EventListenerManager {
return it.listeners as EventListeners<E> return it.listeners as EventListeners<E>
} }
} }
if (trySetLockTrue()) { if (lock.compareAndSet(false, true)) {
val registry = Registry(clazz, EventListeners()) val registry = Registry(clazz as KClass<E>, EventListeners(clazz))
registries.addLast(registry) registries.addLast(registry)
setLockValue(false) lock.value = false
return registry.listeners as EventListeners<E> return registry.listeners
} }
return get(clazz) return get(clazz)
} }
...@@ -123,19 +143,10 @@ internal suspend inline fun Event.broadcastInternal() { ...@@ -123,19 +143,10 @@ internal suspend inline fun Event.broadcastInternal() {
EventLogger.info { "Event broadcast: $this" } EventLogger.info { "Event broadcast: $this" }
callAndRemoveIfRequired(this::class.listeners()) val listeners = this::class.listeners()
callAndRemoveIfRequired(listeners)
var supertypes = this::class.supertypes listeners.supertypes.forEach {
while (true) { callAndRemoveIfRequired(it.listeners())
val superSubscribableType = supertypes.firstOrNull {
it.classifier as? KClass<out Event> != null
}
superSubscribableType?.let {
callAndRemoveIfRequired((it.classifier as KClass<out Event>).listeners())
}
supertypes = (superSubscribableType?.classifier as? KClass<*>)?.supertypes ?: return
} }
} }
......
...@@ -13,7 +13,6 @@ import net.mamoe.mirai.Bot ...@@ -13,7 +13,6 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.event.BroadcastControllable import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiInternalAPI
class FriendMessage( class FriendMessage(
bot: Bot, bot: Bot,
......
...@@ -14,7 +14,6 @@ import net.mamoe.mirai.contact.Group ...@@ -14,7 +14,6 @@ import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.Event 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.Message
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.getValue import net.mamoe.mirai.utils.getValue
...@@ -38,7 +37,6 @@ class GroupMessage( ...@@ -38,7 +37,6 @@ class GroupMessage(
override val subject: Group get() = group override val subject: Group get() = group
inline fun At.member(): Member = group[this.target]
inline fun Long.member(): Member = group[this] inline fun Long.member(): Member = group[this]
......
...@@ -19,7 +19,7 @@ import net.mamoe.mirai.contact.Contact ...@@ -19,7 +19,7 @@ import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.QQ 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.event.events.BotEvent
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
...@@ -37,7 +37,7 @@ expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot) ...@@ -37,7 +37,7 @@ expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot)
*/ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构 */ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
@MiraiInternalAPI @MiraiInternalAPI
abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : EventPacket, BotEvent { abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : Packet, BotEvent {
/** /**
* 接受到这条消息的 * 接受到这条消息的
*/ */
...@@ -115,6 +115,8 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : ...@@ -115,6 +115,8 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
*/ */
inline fun QQ.at(): At = At(this as? Member ?: error("`QQ.at` can only be used in GroupMessage")) 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 // endregion
// region 下载图片 // region 下载图片
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
package net.mamoe.mirai.message.data package net.mamoe.mirai.message.data
import net.mamoe.mirai.contact.Member 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 net.mamoe.mirai.utils.MiraiInternalAPI
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
...@@ -28,7 +28,7 @@ import kotlin.jvm.JvmName ...@@ -28,7 +28,7 @@ import kotlin.jvm.JvmName
*/ */
class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message { class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message {
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
constructor(member: Member) : this(member.id, "@${member.groupCardOrNick}") constructor(member: Member) : this(member.id, "@${member.nameCardOrNick}")
override fun toString(): String = display override fun toString(): String = display
......
...@@ -14,309 +14,161 @@ package net.mamoe.mirai.message.data ...@@ -14,309 +14,161 @@ package net.mamoe.mirai.message.data
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic
/** /**
* QQ 自带表情 * QQ 自带表情
*/ */
inline class Face(val id: FaceId) : Message { class Face(val id: Int) : Message {
override fun toString(): String = "[face${id.value}]" override fun toString(): String = "[mirai:face$id]"
companion object Key : Message.Key<Face> /**
* @author LamGC
*/
@Suppress("SpellCheckingInspection", "unused")
companion object IdList : Message.Key<Face> {
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 { override fun eq(other: Message): Boolean {
return other is Face && other.id == this.id return other is Face && other.id == this.id
} }
} }
\ No newline at end of file
/**
* @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)"
}
...@@ -47,7 +47,7 @@ sealed class Image : Message { ...@@ -47,7 +47,7 @@ sealed class Image : Message {
abstract val imageId: String abstract val imageId: String
final override fun toString(): String { final override fun toString(): String {
return "[image::$imageId]" return "[mirai:$imageId]"
} }
final override fun eq(other: Message): Boolean { final override fun eq(other: Message): Boolean {
......
...@@ -57,7 +57,7 @@ interface Message { ...@@ -57,7 +57,7 @@ interface Message {
*/ */
interface Key<M : Message> interface Key<M : Message>
infix fun eq(other: Message): Boolean = this == other infix fun eq(other: Message): Boolean = this.toString() == other.toString()
/** /**
* 将 [toString] 与 [other] 比较 * 将 [toString] 与 [other] 比较
......
...@@ -38,6 +38,7 @@ import kotlin.reflect.KProperty ...@@ -38,6 +38,7 @@ import kotlin.reflect.KProperty
interface MessageChain : Message, MutableList<Message> { interface MessageChain : Message, MutableList<Message> {
// region Message override // region Message override
override operator fun contains(sub: String): Boolean override operator fun contains(sub: String): Boolean
override fun followedBy(tail: Message): MessageChain override fun followedBy(tail: Message): MessageChain
// endregion // endregion
...@@ -67,6 +68,36 @@ interface MessageChain : Message, MutableList<Message> { ...@@ -67,6 +68,36 @@ interface MessageChain : Message, MutableList<Message> {
} }
} }
/**
* 遍历每一个有内容的消息, 即 [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] * 提供一个类型的值. 若不存在则会抛出异常 [NoSuchElementException]
*/ */
......
...@@ -31,6 +31,11 @@ interface MessageSource : Message { ...@@ -31,6 +31,11 @@ interface MessageSource : Message {
*/ */
val messageUid: Long val messageUid: Long
/**
* 发送时间, 单位为秒
*/
val time: Long
/** /**
* 发送人号码 * 发送人号码
*/ */
......
...@@ -68,7 +68,7 @@ abstract class BotNetworkHandler : CoroutineScope { ...@@ -68,7 +68,7 @@ abstract class BotNetworkHandler : CoroutineScope {
*/ */
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@MiraiInternalAPI @MiraiInternalAPI
abstract suspend fun relogin() abstract suspend fun relogin(cause: Throwable? = null)
/** /**
* 初始化获取好友列表等值. * 初始化获取好友列表等值.
......
...@@ -70,6 +70,13 @@ interface MiraiLogger { ...@@ -70,6 +70,13 @@ interface MiraiLogger {
*/ */
val identity: String? val identity: String?
/**
* 获取 [MiraiLogger] 是否已开启
*
* 除 [MiraiLoggerWithSwitch] 可控制开关外, 其他的所有 [MiraiLogger] 均一直开启.
*/
val isEnabled: Boolean
/** /**
* 随从. 在 this 中调用所有方法后都应继续往 [follower] 传递调用. * 随从. 在 this 中调用所有方法后都应继续往 [follower] 传递调用.
* [follower] 的存在可以让一次日志被多个日志记录器记录. * [follower] 的存在可以让一次日志被多个日志记录器记录.
...@@ -151,43 +158,43 @@ interface MiraiLogger { ...@@ -151,43 +158,43 @@ interface MiraiLogger {
inline fun MiraiLogger.verbose(lazyMessage: () -> String) { 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?) { 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?) { 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?) { 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?) { 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?) { 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?) { 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?) { 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?) { 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?) { 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 ...@@ -268,7 +275,7 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg
@PublishedApi @PublishedApi
internal var switch: Boolean = default internal var switch: Boolean = default
val isEnabled: Boolean get() = switch override val isEnabled: Boolean get() = switch
fun enable() { fun enable() {
switch = true switch = true
...@@ -278,16 +285,16 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg ...@@ -278,16 +285,16 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg
switch = false switch = false
} }
override fun verbose0(message: String?) = if (switch) delegate.verbose(message) else Unit override fun verbose0(message: String?) = delegate.verbose(message)
override fun verbose0(message: String?, e: Throwable?) = if (switch) delegate.verbose(message, e) else Unit override fun verbose0(message: String?, e: Throwable?) = delegate.verbose(message, e)
override fun debug0(message: String?) = if (switch) delegate.debug(message) else Unit override fun debug0(message: String?) = delegate.debug(message)
override fun debug0(message: String?, e: Throwable?) = if (switch) delegate.debug(message, e) else Unit override fun debug0(message: String?, e: Throwable?) = delegate.debug(message, e)
override fun info0(message: String?) = if (switch) delegate.info(message) else Unit override fun info0(message: String?) = delegate.info(message)
override fun info0(message: String?, e: Throwable?) = if (switch) delegate.info(message, e) else Unit override fun info0(message: String?, e: Throwable?) = delegate.info(message, e)
override fun warning0(message: String?) = if (switch) delegate.warning(message) else Unit override fun warning0(message: String?) = delegate.warning(message)
override fun warning0(message: String?, e: Throwable?) = if (switch) delegate.warning(message, e) else Unit override fun warning0(message: String?, e: Throwable?) = delegate.warning(message, e)
override fun error0(message: String?) = if (switch) delegate.error(message) else Unit override fun error0(message: String?) = delegate.error(message)
override fun error0(message: String?, e: Throwable?) = if (switch) delegate.error(message, e) else Unit override fun error0(message: String?, e: Throwable?) = delegate.error(message, e)
} }
/** /**
...@@ -298,54 +305,65 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg ...@@ -298,54 +305,65 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg
* 在定义 logger 变量时, 请一直使用 [MiraiLogger] 或者 [MiraiLoggerWithSwitch]. * 在定义 logger 变量时, 请一直使用 [MiraiLogger] 或者 [MiraiLoggerWithSwitch].
*/ */
abstract class MiraiLoggerPlatformBase : MiraiLogger { abstract class MiraiLoggerPlatformBase : MiraiLogger {
override val isEnabled: Boolean get() = true
final override var follower: MiraiLogger? = null final override var follower: MiraiLogger? = null
final override fun verbose(message: String?) { final override fun verbose(message: String?) {
if (!isEnabled) return
follower?.verbose(message) follower?.verbose(message)
verbose0(message) verbose0(message)
} }
final override fun verbose(message: String?, e: Throwable?) { final override fun verbose(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.verbose(message, e) follower?.verbose(message, e)
verbose0(message, e) verbose0(message, e)
} }
final override fun debug(message: String?) { final override fun debug(message: String?) {
if (!isEnabled) return
follower?.debug(message) follower?.debug(message)
debug0(message) debug0(message)
} }
final override fun debug(message: String?, e: Throwable?) { final override fun debug(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.debug(message, e) follower?.debug(message, e)
debug0(message, e) debug0(message, e)
} }
final override fun info(message: String?) { final override fun info(message: String?) {
if (!isEnabled) return
follower?.info(message) follower?.info(message)
info0(message) info0(message)
} }
final override fun info(message: String?, e: Throwable?) { final override fun info(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.info(message, e) follower?.info(message, e)
info0(message, e) info0(message, e)
} }
final override fun warning(message: String?) { final override fun warning(message: String?) {
if (!isEnabled) return
follower?.warning(message) follower?.warning(message)
warning0(message) warning0(message)
} }
final override fun warning(message: String?, e: Throwable?) { final override fun warning(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.warning(message, e) follower?.warning(message, e)
warning0(message, e) warning0(message, e)
} }
final override fun error(message: String?) { final override fun error(message: String?) {
if (!isEnabled) return
follower?.error(message) follower?.error(message)
error0(message) error0(message)
} }
final override fun error(message: String?, e: Throwable?) { final override fun error(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.error(message, e) follower?.error(message, e)
error0(message, e) error0(message, e)
} }
......
...@@ -19,7 +19,9 @@ expect interface ECDHPublicKey { ...@@ -19,7 +19,9 @@ expect interface ECDHPublicKey {
fun getEncoded(): ByteArray fun getEncoded(): ByteArray
} }
expect class ECDHKeyPair { internal expect class ECDHKeyPairImpl : ECDHKeyPair
interface ECDHKeyPair {
val privateKey: ECDHPrivateKey val privateKey: ECDHPrivateKey
val publicKey: ECDHPublicKey val publicKey: ECDHPublicKey
...@@ -27,6 +29,15 @@ expect class ECDHKeyPair { ...@@ -27,6 +29,15 @@ expect class ECDHKeyPair {
* 私匙和固定公匙([initialPublicKey]) 计算得到的 shareKey * 私匙和固定公匙([initialPublicKey]) 计算得到的 shareKey
*/ */
val initialShareKey: ByteArray 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) { ...@@ -41,6 +52,8 @@ expect class ECDH(keyPair: ECDHKeyPair) {
fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray
companion object { companion object {
val isECDHAvailable: Boolean
/** /**
* 由完整的 publicKey ByteArray 得到 [ECDHPublicKey] * 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
*/ */
...@@ -60,14 +73,11 @@ expect class ECDH(keyPair: ECDHKeyPair) { ...@@ -60,14 +73,11 @@ expect class ECDH(keyPair: ECDHKeyPair) {
override fun toString(): String override fun toString(): String
} }
/**
*
*/
@Suppress("FunctionName") @Suppress("FunctionName")
expect fun ECDH(): ECDH expect fun ECDH(): ECDH
val initialPublicKey = val initialPublicKey
ECDH.constructPublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8".chunkedHexToBytes()) get() = ECDH.constructPublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8".chunkedHexToBytes())
private val commonHeadFor02 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes() private val commonHeadFor02 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes()
private val commonHeadForNot02 = "3046301006072A8648CE3D020106052B8104001F033200".chunkedHexToBytes() private val commonHeadForNot02 = "3046301006072A8648CE3D020106052B8104001F033200".chunkedHexToBytes()
private const val constantHead = "3046301006072A8648CE3D020106052B8104001F03320004" private const val constantHead = "3046301006072A8648CE3D020106052B8104001F03320004"
......
...@@ -17,7 +17,6 @@ import kotlinx.io.core.readUInt ...@@ -17,7 +17,6 @@ import kotlinx.io.core.readUInt
import kotlinx.io.core.readULong import kotlinx.io.core.readULong
import net.mamoe.mirai.utils.MiraiDebugAPI import net.mamoe.mirai.utils.MiraiDebugAPI
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.io.* import net.mamoe.mirai.utils.io.*
import kotlin.jvm.JvmStatic import kotlin.jvm.JvmStatic
......
...@@ -21,6 +21,16 @@ import kotlin.random.nextInt ...@@ -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 * 255 -> 00 00 00 FF
*/ */
......
...@@ -14,7 +14,6 @@ package net.mamoe.mirai.utils ...@@ -14,7 +14,6 @@ package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
import kotlin.time.seconds
// 临时使用, 待 Kotlin Duration 稳定后使用 Duration. // 临时使用, 待 Kotlin Duration 稳定后使用 Duration.
// 内联属性, 则将来删除这些 API 将不会导致二进制不兼容. // 内联属性, 则将来删除这些 API 将不会导致二进制不兼容.
......
...@@ -15,17 +15,16 @@ expect fun Throwable.addSuppressed(e: Throwable) ...@@ -15,17 +15,16 @@ expect fun Throwable.addSuppressed(e: Throwable)
@MiraiInternalAPI @MiraiInternalAPI
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
inline fun <R> tryNTimes(repeat: Int, block: () -> R): R { inline fun <R> tryNTimes(repeat: Int, block: (Int) -> R): R {
var lastException: Throwable? = null var lastException: Throwable? = null
repeat(repeat) { repeat(repeat) {
try { try {
return block() return block(it)
} catch (e: Throwable) { } catch (e: Throwable) {
if (lastException == null) { if (lastException == null) {
lastException = e lastException = e
} } else lastException!!.addSuppressed(e)
lastException!!.addSuppressed(e)
} }
} }
...@@ -34,17 +33,16 @@ inline fun <R> tryNTimes(repeat: Int, block: () -> R): R { ...@@ -34,17 +33,16 @@ inline fun <R> tryNTimes(repeat: Int, block: () -> R): R {
@MiraiInternalAPI @MiraiInternalAPI
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
inline fun <R> tryNTimesOrNull(repeat: Int, block: () -> R): R? { inline fun <R> tryNTimesOrNull(repeat: Int, block: (Int) -> R): R? {
var lastException: Throwable? = null var lastException: Throwable? = null
repeat(repeat) { repeat(repeat) {
try { try {
return block() return block(it)
} catch (e: Throwable) { } catch (e: Throwable) {
if (lastException == null) { if (lastException == null) {
lastException = e lastException = e
} } else lastException!!.addSuppressed(e)
lastException!!.addSuppressed(e)
} }
} }
...@@ -53,18 +51,17 @@ inline fun <R> tryNTimesOrNull(repeat: Int, block: () -> R): R? { ...@@ -53,18 +51,17 @@ inline fun <R> tryNTimesOrNull(repeat: Int, block: () -> R): R? {
@MiraiInternalAPI @MiraiInternalAPI
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
inline fun <R> tryNTimesOrException(repeat: Int, block: () -> R): Throwable? { inline fun <R> tryNTimesOrException(repeat: Int, block: (Int) -> R): Throwable? {
var lastException: Throwable? = null var lastException: Throwable? = null
repeat(repeat) { repeat(repeat) {
try { try {
block() block(it)
return null return null
} catch (e: Throwable) { } catch (e: Throwable) {
if (lastException == null) { if (lastException == null) {
lastException = e lastException = e
} } else lastException!!.addSuppressed(e)
lastException!!.addSuppressed(e)
} }
} }
......
...@@ -29,13 +29,13 @@ internal val factory: BotFactory = run { ...@@ -29,13 +29,13 @@ internal val factory: BotFactory = run {
""" """
No BotFactory found. Please ensure that you've added dependency of protocol modules. No BotFactory found. Please ensure that you've added dependency of protocol modules.
Available modules: Available modules:
- net.mamoe:mirai-core-timpc - net.mamoe:mirai-core-timpc (stays at 0.12.0)
- net.mamoe:mirai-core-qqandroid (recommended) - net.mamoe:mirai-core-qqandroid (recommended)
You should have at lease one protocol module installed. You should have at lease one protocol module installed.
------------------------------------------------------- -------------------------------------------------------
找不到 BotFactory. 请确保你依赖了至少一个协议模块. 找不到 BotFactory. 请确保你依赖了至少一个协议模块.
可用的协议模块: 可用的协议模块:
- net.mamoe:mirai-core-timpc - net.mamoe:mirai-core-timpc (0.12.0 后停止更新)
- net.mamoe:mirai-core-qqandroid (推荐) - net.mamoe:mirai-core-qqandroid (推荐)
请添加上述任一模块的依赖(与 mirai-core 版本相同) 请添加上述任一模块的依赖(与 mirai-core 版本相同)
""".trimIndent() """.trimIndent()
......
/*
* 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
...@@ -16,15 +16,16 @@ import net.mamoe.mirai.event.ListeningStatus ...@@ -16,15 +16,16 @@ import net.mamoe.mirai.event.ListeningStatus
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Function import java.util.function.Function
import kotlin.coroutines.EmptyCoroutineContext
@MiraiInternalAPI @MiraiInternalAPI
@Suppress("FunctionName") @Suppress("FunctionName")
fun <E : Event> Class<E>._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Function<E, ListeningStatus>): Listener<E> { fun <E : Event> Class<E>._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Function<E, ListeningStatus>): Listener<E> {
return this.kotlin.subscribeInternal(scope.Handler { onEvent.apply(it) }) return this.kotlin.subscribeInternal(scope.Handler(EmptyCoroutineContext) { onEvent.apply(it) })
} }
@MiraiInternalAPI @MiraiInternalAPI
@Suppress("FunctionName") @Suppress("FunctionName")
fun <E : Event> Class<E>._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Consumer<E>): Listener<E> { fun <E : Event> Class<E>._subscribeEventForJaptOnly(scope: CoroutineScope, onEvent: Consumer<E>): Listener<E> {
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
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
...@@ -105,8 +105,8 @@ class DefaultLoginSolver( ...@@ -105,8 +105,8 @@ class DefaultLoginSolver(
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? = loginSolverLock.withLock { override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? = loginSolverLock.withLock {
val logger = getLogger(bot) val logger = getLogger(bot)
logger.info("需要进行账户安全认证") logger.info("需要进行账户安全认证")
logger.info("该账户有[设备锁]/[不常用登陆地点]/[不常用设备登陆]的问题") logger.info("该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题")
logger.info("完成以下账号认证即可成功登|理论本认证在mirai每个账户中最多出现1次") logger.info("完成以下账号认证即可成功登|理论本认证在mirai每个账户中最多出现1次")
logger.info("请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符") logger.info("请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符")
logger.info("这步操作将在后续的版本中优化") logger.info("这步操作将在后续的版本中优化")
logger.info(url) logger.info(url)
...@@ -221,11 +221,11 @@ actual open class BotConfiguration actual constructor() { ...@@ -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
/** /**
* 验证码处理器 * 验证码处理器
*/ */
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
package net.mamoe.mirai.utils.cryptor package net.mamoe.mirai.utils.cryptor
import net.mamoe.mirai.utils.io.chunkedHexToBytes
import net.mamoe.mirai.utils.md5 import net.mamoe.mirai.utils.md5
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.* import java.security.*
...@@ -21,20 +20,13 @@ import javax.crypto.KeyAgreement ...@@ -21,20 +20,13 @@ import javax.crypto.KeyAgreement
actual typealias ECDHPrivateKey = PrivateKey actual typealias ECDHPrivateKey = PrivateKey
actual typealias ECDHPublicKey = PublicKey actual typealias ECDHPublicKey = PublicKey
actual class ECDHKeyPair( internal actual class ECDHKeyPairImpl(
private val delegate: KeyPair? private val delegate: KeyPair
) { ) : ECDHKeyPair {
actual val privateKey: ECDHPrivateKey get() = delegate?.private ?: error("ECDH is not available") override val privateKey: ECDHPrivateKey get() = delegate.private
actual val publicKey: ECDHPublicKey get() = delegate?.public ?: defaultPublicKey override val publicKey: ECDHPublicKey get() = delegate.public
actual val initialShareKey: ByteArray = if (delegate == null) { override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
defaultShareKey
} else ECDH.calculateShareKey(privateKey, initialPublicKey)
companion object {
internal val defaultPublicKey = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128".chunkedHexToBytes().adjustToPublicKey()
internal val defaultShareKey = "4da0f614fc9f29c2054c77048a6566d7".chunkedHexToBytes()
}
} }
@Suppress("FunctionName") @Suppress("FunctionName")
...@@ -42,33 +34,34 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair()) ...@@ -42,33 +34,34 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair())
actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) { actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual companion object { 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 { actual val isECDHAvailable: Boolean get() = _isECDHAvailable
isECDHAvailable = kotlin.runCatching {
Security.addProvider(BouncyCastleProvider())
generateKeyPair() // try if it is working
}.isSuccess
}
actual fun generateKeyPair(): ECDHKeyPair { actual fun generateKeyPair(): ECDHKeyPair {
return if (!isECDHAvailable) { if (!isECDHAvailable) {
ECDHKeyPair(null) return ECDHKeyPair.DefaultStub
} else ECDHKeyPair(KeyPairGenerator.getInstance("EC", "BC").apply { initialize(ECGenParameterSpec("secp192k1")) }.genKeyPair()) }
return ECDHKeyPairImpl(KeyPairGenerator.getInstance("ECDH")
.also { it.initialize(ECGenParameterSpec("secp192k1")) }
.genKeyPair())
} }
actual fun calculateShareKey( actual fun calculateShareKey(
privateKey: ECDHPrivateKey, privateKey: ECDHPrivateKey,
publicKey: ECDHPublicKey publicKey: ECDHPublicKey
): ByteArray { ): ByteArray {
return if (!isECDHAvailable) { val instance = KeyAgreement.getInstance("ECDH", "BC")
ECDHKeyPair.defaultShareKey instance.init(privateKey)
} else { instance.doPhase(publicKey, true)
val instance = KeyAgreement.getInstance("ECDH", "BC") return md5(instance.generateSecret())
instance.init(privateKey)
instance.doPhase(publicKey, true)
md5(instance.generateSecret())
}
} }
actual fun constructPublicKey(key: ByteArray): ECDHPublicKey { actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {
......
...@@ -30,7 +30,10 @@ actual class PlatformSocket : Closeable { ...@@ -30,7 +30,10 @@ actual class PlatformSocket : Closeable {
private lateinit var socket: Socket private lateinit var socket: Socket
actual val isOpen: Boolean actual val isOpen: Boolean
get() = socket.isConnected get() =
if (::socket.isInitialized)
socket.isConnected
else false
actual override fun close() { actual override fun close() {
if (::socket.isInitialized) { if (::socket.isInitialized) {
......
...@@ -63,9 +63,6 @@ internal class LockFreeLinkedListTest { ...@@ -63,9 +63,6 @@ internal class LockFreeLinkedListTest {
val addJob = async { list.concurrentDo(2, 30000) { addLast(1) } } val addJob = async { list.concurrentDo(2, 30000) { addLast(1) } }
//delay(1) // let addJob fly //delay(1) // let addJob fly
if (addJob.isCompleted) {
println("Number of elements are not enough")
}
val foreachJob = async { val foreachJob = async {
list.concurrentDo(1, 10000) { list.concurrentDo(1, 10000) {
forEach { it + it } forEach { it + it }
......
...@@ -32,7 +32,7 @@ Mirai Java Apt ...@@ -32,7 +32,7 @@ Mirai Java Apt
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>net.mamoe</groupId> <groupId>net.mamoe</groupId>
<artifactId>mirai-core-qqandroid</artifactId> <artifactId>mirai-core-qqandroid-jvm</artifactId>
<version>CORE_VERSION</version> <!-- 替换版本为最新版本 --> <version>CORE_VERSION</version> <!-- 替换版本为最新版本 -->
</dependency> </dependency>
...@@ -51,7 +51,7 @@ repositories { ...@@ -51,7 +51,7 @@ repositories {
} }
dependencies { 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") implementation("net.mamoe:mirai-japt:JAPT_VERSION")
} }
``` ```
......
...@@ -59,7 +59,7 @@ fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$v ...@@ -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" fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
dependencies { 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 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") api(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-javafx", version = "1.3.2")
......
...@@ -184,5 +184,13 @@ public interface BlockingBot { ...@@ -184,5 +184,13 @@ public interface BlockingBot {
/** /**
* 关闭这个 [Bot], 停止一切相关活动. 不可重新登录. * 关闭这个 [Bot], 停止一切相关活动. 不可重新登录.
*/ */
void dispose(@Nullable Throwable throwable); void close(@Nullable Throwable throwable);
/**
* @deprecated 使用 {@link #close(Throwable)}
*/
@Deprecated
default void dispose(@Nullable Throwable throwable) {
close(throwable);
}
} }
...@@ -65,5 +65,5 @@ internal class BlockingBotImpl(private val bot: Bot) : BlockingBot { ...@@ -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 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 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment