Merge remote-tracking branch 'origin/master'

This commit is contained in:
jiahua.liu 2020-02-20 23:00:35 +08:00
commit 687a773e8e
102 changed files with 1770 additions and 1145 deletions

View File

@ -2,6 +2,53 @@
开发版本. 频繁更新, 不保证高稳定性
## `0.17.0` 2020/2/20
### mirai-core
- 支持原生表情 `Face`
- 修正 `groupCardOrNick``nameCardOrNick`
- 增加 `MessageChain.foreachContent(lambda)``Message.hasContent(): Boolean`
### mirai-core-qqandroid
- 提高重连速度
- 修复重连后某些情况不会心跳
- 修复收包时可能产生异常
## `0.16.0` 2020/2/19
### mirai-core
- 添加 `Bot.subscribe` 等筛选 Bot 实例的监听方法
- 其他一些小问题修复
### mirai-core-qqandroid
- 优化重连处理逻辑
- 确保好友消息和历史事件在初始化结束前同步完成
- 同步好友消息记录时不广播
## `0.15.5` 2020/2/19
### mirai-core
- 为 `MiraiLogger` 添加 common property `val isEnabled: Boolean`
- 修复 #62: 掉线重连后无 heartbeat
- 修复 #65: `Bot` close 后仍会重连
- 修复 #70: ECDH is not available on Android platform
### mirai-core-qqandroid
- 从服务器收到的事件将会额外使用 `bot.logger` 记录 (verbose).
- 降低包记录的等级: `info` -> `verbose`
- 改善 `Bot` 的 log 记录
- 加载好友列表失败时会重试
- 改善 `Bot``NetworkHandler` 关闭时取消 job 的逻辑
- 修复初始化(init)时同步历史好友消息时出错的问题
## `0.15.4` 2020/2/18
- 放弃使用 `atomicfu` 以解决其编译错误的问题. (#60)
## `0.15.3` 2020/2/18
- 修复无法引入依赖的问题.
## `0.15.2` 2020/2/18
### mirai-core

151
README.md
View File

@ -1,8 +1,12 @@
<div align="center">
<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">
----
[![Gitter](https://badges.gitter.im/mamoe/mirai.svg)](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Actions Status](https://github.com/mamoe/mirai/workflows/CI/badge.svg)](https://github.com/mamoe/mirai/actions)
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
@ -16,6 +20,7 @@ Mirai 是一个在全平台下运行,提供 QQ Android 和 TIM PC 协议支持
</div>
## Mirai
**[English](README-eng.md)**
多平台 **QQ Android 和 TimPC** 协议支持库与高效率的机器人框架.
@ -27,94 +32,59 @@ Mirai既可以作为你项目中的QQ协议支持Lib, 也可以作为单独的Ap
加入 Gitter, 或加入 QQ 群: 655057127
## 开始使用Mirai
Mirai支持以多种方式进行部署但是目前我们在集中对mirai-coremirai-japt以及mirai-api-http等核心模块进行特性的开发对于非开发者的使用暂时不做过多支持仅展示开发计划。
### 开发者
- 假如你熟悉Kotlin及包管理工具请参阅[Mirai Guide - Quick Start](/docs/guide_quick_start.md)
- 假如你不熟悉Kotlin希望一份较详细的起步教程请参阅[Mirai Guide - Getting Started](/docs/guide_getting_started.md)
- 假如你使用Java作为开发语言请参阅[mirai-japt](/mirai-japt/README.md)
- 假如你是其他平台开发者,可以通过了解 [mirai-api-http](https://github.com/mamoe/mirai/tree/master/mirai-api-http) 进行接入欢迎开发不同平台的mirai-sdk
- 此外,你还可以在 [Wiki](https://github.com/mamoe/mirai/wiki/Home) 中查看各类帮助,**如 API 示例**。
### 使用者
- [mirai-console](https://github.com/mamoe/mirai/tree/master/mirai-console) 支持插件, 在终端中启动 Mirai 并获得机器人服务,**本模块还未完善**,请耐心等待开发完成。
- mirai-webpanel Mirai的Web控制台支持在网页中管理机器人与插件。本模块目前在计划中。在其他模块稳定后将开始进行开发。
## CHANGELOG
在 [Project](https://github.com/mamoe/mirai/projects/3) 查看已支持功能和计划
在 [CHANGELOG](https://github.com/mamoe/mirai/blob/master/CHANGELOG.md) 查看版本更新记录 (仅发布的版本)
## Modules
### mirai-core
通用 API 模块,一套 API 适配两套协议。
**请参考此模块的 API**
### mirai-core-qqandroid
QQ for Android 8.2.0 版本2019 年 12 月)协议的实现,目前完成大部分。
- 高兼容性:协议仅含极少部分为硬编码,其余全部随官方方式动态生成
- 高安全性密匙随机ECDH 动态计算
- 已支持大部分使用场景, 详情请在[Project](https://github.com/mamoe/mirai/projects/3)查看
### mirai-core-timpc
TIM PC 2.3.2 版本2019 年 8 月)协议的实现,相较于 core仅新增少量 API. 详见 [README.md](mirai-core-timpc/)
TIM PC 2.3.2 版本2019 年 8 月)协议的实现
支持的功能:
- 消息收发:图片文字复合消息,图片消息
- 群管功能:群员列表,禁言
(目前不再更新此协议,请关注上文的安卓协议)
(目前不再更新此协议,请关注上文的安卓协议)
## Use directly
**直接使用 Mirai(终端环境/网页面板(将来)).**
[Mirai-Console](https://github.com/mamoe/mirai/tree/master/mirai-console) 插件支持, 在终端中启动 Mirai 并获得机器人服务
本模块还未完善。
## Use as a library
**mirai-core 为独立设计, 可以作为库内置于任意 Java(JVM)/Android 项目中使用.**
请将 `VERSION` 替换为最新的版本(如 `0.15.0`):
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
**Mirai 目前还处于实验性阶段, 我们无法保证任何稳定性, API 也可能会随时修改.**
### Maven
Kotlin 在 Maven 上只支持 JVM 平台.
```xml
<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
@ -125,41 +95,12 @@ JVM 上启动需 80M 内存, 每多一个机器人实例需要 30M 内存.
您的 star 是对我们最大的鼓励(点击项目右上角)
## Wiki
在 [Wiki](https://github.com/mamoe/mirai/wiki/Home) 中查看各类帮助,**如 API 示例**(可能过时,待 QQ Android 协议完成后会重写)。
## Try
### On JVM or Android
现在体验低付出高效率的 Mirai
```kotlin
val bot = Bot(qqId, password).alsoLogin()
bot.subscribeMessages {
"你好" reply "你好!"
"profile" reply { sender.queryProfile() }
contains("图片"){ File(imagePath).send() }
}
bot.subscribeAlways<MemberPermissionChangedEvent> {
if (it.kind == BECOME_OPERATOR)
reply("${it.member.id} 成为了管理员")
}
```
1. Clone
2. Import as Gradle project
3. 运行 Demo 程序: [mirai-demo](#mirai-demo) 示例和演示程序
## Build Requirements
## Libraries used
- Kotlin 1.3.61
- JDK 8 (required)
- JDK 11for protocol tools, optional
- Android SDK 29 (for Android target, optional)
#### Libraries used
感谢:
- [kotlin-stdlib](https://github.com/JetBrains/kotlin)
- [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines)
- [kotlinx-io](https://github.com/Kotlin/kotlinx-io)
@ -176,15 +117,21 @@ bot.subscribeAlways<MemberPermissionChangedEvent> {
- [toml4j](https://github.com/mwanji/toml4j)
- [snakeyaml](https://mvnrepository.com/artifact/org.yaml/snakeyaml)
## License
协议原版权归属腾讯科技股份有限公司所有,本项目其他代码遵守:
**GNU AFFERO GENERAL PUBLIC LICENSE version 3**
其中部分要求:
- (见 LICENSE 第 13 节) 尽管本许可协议有其他规定,但如果您修改本程序,则修改后的版本必须显着地为所有通过计算机网络与它进行远程交互的用户(如果您的版本支持这种交互)提供从网络服务器通过一些标准或惯用的软件复制方法**免费**访问相应的**源代码**的机会
- (见 LICENSE 第 4 节) 您可以免费或收费地传递这个项目的源代码或目标代码(即编译结果), **但前提是提供明显的版权声明** (您需要标注本 `GitHub` 项目地址)
## Acknowledgement
特别感谢 [JetBrains](https://www.jetbrains.com/?from=mirai) 为开源项目提供免费的 [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai) 等 IDE 的授权
[<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)

View File

@ -0,0 +1,129 @@
# Mirai Guide - Getting Started
由于Mirai项目在快速推进中因此内容时有变动本文档的最后更新日期为```2020-02-20```,对应版本```0.17.0```
假如仅仅使用Mirai不需要对整个项目进行Clone只需在项目内添加Gradle Dependency或使用即可。
下面介绍详细的入门步骤。
本页采用Kotlin作为开发语言**若你希望使用 Java 开发**, 请参阅: [mirai-japt](mirai-japt/README.md)
## Use Console
使用mirai-console以插件形式对服务器功能进行管理启动无需任何IDE。
**由于mirai-console还没有开发完成暂时不提供入门**
## Use Loader
通过编写Kotlin程序启动mirai-core并定义你的Mirai Bot行为。
假如已经对Gradle有一定了解可跳过12
### 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>
```

114
docs/guide_quick_start.md Normal file
View File

@ -0,0 +1,114 @@
# Mirai Guide - Quick Start
由于Mirai项目在快速推进中因此内容时有变动本文档的最后更新日期为```2020-02-20```,对应版本```0.17.0```
本文适用于对kotlin较熟悉的开发者
**若你希望一份更为基础且详细的guide**, 请参阅: [mirai-guide-getting-started](guide_getting_started.md)
**若你希望使用 Java 开发**, 请参阅: [mirai-japt](/mirai-japt/README.md)
## Build Requirements
- Kotlin 1.3.61
- JDK 6 (required)
- JDK 11for 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 内存.

View File

@ -1,7 +1,7 @@
# style guide
kotlin.code.style=official
# config
mirai_version=0.15.2
mirai_version=0.17.0
mirai_japt_version=1.0.1
kotlin.incremental.multiplatform=true
kotlin.parallel.tasks.in.project=true

View File

@ -94,7 +94,7 @@ publishing {
it.artifactId = "$project.name-common"
break
case 'jvm':
it.artifactId = "${project.name.replace("-jvm", "")}"
it.artifactId = "$project.name-jvm"
break
case 'js':
case 'native':

View File

@ -1,5 +1,5 @@
#Thu Feb 06 14:10:33 CST 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists

View File

@ -12,10 +12,12 @@ fun main() {
MiraiHttpAPIServer.start()
bot.network.awaitDisconnection()
bot.join()
}
```
## 认证相关
### 开始会话-认证(Authorize)
@ -141,6 +143,8 @@ fun main() {
> SessionKey与Bot 对应错误时将会返回状态码5指定对象不存在
## 消息相关
@ -261,10 +265,10 @@ fun main() {
### 发送图片消息通过URL
```
[POST] /sendGroupMessage
[POST] /sendImageMessage
```
使用此方法向指定群发送消息
使用此方法向指定对象(或好友)发送图片消息
#### 请求
@ -303,7 +307,7 @@ fun main() {
### 图片文件上传
```
[POST] /sendGroupMessage
[POST] /uploadImage
```
使用此方法上传图片文件至服务器并返回ImageId
@ -414,10 +418,10 @@ Content-Typemultipart/form-data
}
```
| 名字 | 类型 | 说明 |
| ------- | ------ | ------------------------- |
| target | Long | 群员QQ号 |
| display | String | @时显示的文本如"@Mirai" |
| 名字 | 类型 | 说明 |
| ------- | ------ | ---------------------------------------------- |
| target | Long | 群员QQ号 |
| dispaly | String | At时显示的文字发送消息时无效自动使用群名片 |
#### AtAll
@ -463,7 +467,7 @@ Content-Typemultipart/form-data
{
"type": "Image",
"imageId": "{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png" //群图片格式
"imageId": "/f8f1ab55-bf8e-4236-b55e-955848d7069f" //好友图片格式
//"imageId": "/f8f1ab55-bf8e-4236-b55e-955848d7069f" //好友图片格式
}
```
@ -517,6 +521,7 @@ Content-Typemultipart/form-data
```
### 获取群列表
使用此方法获取bot的群列表
@ -786,6 +791,8 @@ Content-Typemultipart/form-data
}
```
### 获取群设置
使用此方法获取群设置
@ -816,6 +823,7 @@ Content-Typemultipart/form-data
```
### 修改群员资料
使用此方法修改群员资料(需要有相关限权)
@ -856,6 +864,8 @@ Content-Typemultipart/form-data
}
```
### 获取群员资料
使用此方法获取群员资料

View File

@ -13,6 +13,8 @@ import kotlinx.coroutines.*
import net.mamoe.mirai.Bot
import net.mamoe.mirai.api.http.queue.MessageQueue
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.subscribeAlways
import net.mamoe.mirai.event.subscribeMessages
import net.mamoe.mirai.message.MessagePacket
import kotlin.coroutines.CoroutineContext
@ -102,12 +104,10 @@ class TempSession internal constructor(coroutineContext: CoroutineContext) : Ses
class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext) : Session(coroutineContext) {
val messageQueue = MessageQueue()
private val _listener: Listener<MessagePacket<*, *>>
private val _listener: Listener<BotEvent>
init {
bot.subscribeMessages {
_listener = always { this.run(messageQueue::add) } // this aka messagePacket
}
_listener = bot.subscribeAlways{ this.run(messageQueue::add) }
}
override fun close() {

View File

@ -0,0 +1,118 @@
package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.utils.MiraiExperimentalAPI
@Serializable
sealed class BotEventDTO : EventDTO()
@UseExperimental(MiraiExperimentalAPI::class)
fun BotEvent.toDTO() = when(this) {
is MessagePacket<*, *> -> toDTO()
else -> when(this) {
is BotOnlineEvent -> BotOnlineEventDTO(bot.uin)
is BotOfflineEvent.Active -> BotOfflineEventActiveDTO(bot.uin)
is BotOfflineEvent.Force -> BotOfflineEventForceDTO(bot.uin, title, message)
is BotOfflineEvent.Dropped -> BotOfflineEventDroppedDTO(bot.uin)
is BotReloginEvent -> BotReloginEventDTO(bot.uin)
// is MessageSendEvent.GroupMessageSendEvent -> {}
// is MessageSendEvent.FriendMessageSendEvent -> {}
// is BeforeImageUploadEvent -> {}
// is ImageUploadEvent.Succeed -> {}
is BotGroupPermissionChangeEvent -> BotGroupPermissionChangeEventDTO(origin, new, GroupDTO(group))
is BotMuteEvent -> BotMuteEventDTO(durationSeconds, MemberDTO(operator))
is BotUnmuteEvent -> BotUnmuteEventDTO(MemberDTO(operator))
is BotJoinGroupEvent -> BotJoinGroupEventDTO(GroupDTO(group))
// is GroupSettingChangeEvent<*> -> {} // 不知道会改什么
is GroupNameChangeEvent -> GroupNameChangeEventDTO(origin, new, GroupDTO(group), isByBot)
is GroupEntranceAnnouncementChangeEvent -> GroupEntranceAnnouncementChangeEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is GroupMuteAllEvent -> GroupMuteAllEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is GroupAllowAnonymousChatEvent -> GroupAllowAnonymousChatEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is GroupAllowConfessTalkEvent -> GroupAllowConfessTalkEventDTO(origin, new, GroupDTO(group), isByBot)
is GroupAllowMemberInviteEvent -> GroupAllowMemberInviteEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is MemberJoinEvent -> MemberJoinEventDTO(MemberDTO(member))
is MemberLeaveEvent.Kick -> MemberLeaveEventKickDTO(MemberDTO(member), operator?.let(::MemberDTO))
is MemberLeaveEvent.Quit -> MemberLeaveEventQuitDTO(MemberDTO(member))
is MemberCardChangeEvent -> MemberCardChangeEventDTO(origin, new, GroupDTO(group), operator?.let(::MemberDTO))
is MemberSpecialTitleChangeEvent -> MemberSpecialTitleChangeEventDTO(origin, new, MemberDTO(member))
is MemberPermissionChangeEvent -> MemberPermissionChangeEventDTO(origin, new, MemberDTO(member))
is MemberMuteEvent -> MemberMuteEventDTO(durationSeconds, MemberDTO(member), operator?.let(::MemberDTO))
is MemberUnmuteEvent -> MemberUnmuteEventDTO(MemberDTO(member), operator?.let(::MemberDTO))
else -> IgnoreEventDTO
}
}
@Serializable
@SerialName("BotOnlineEvent")
data class BotOnlineEventDTO(val qq: Long) : BotEventDTO()
@Serializable
@SerialName("BotOfflineEventActive")
data class BotOfflineEventActiveDTO(val qq: Long) : BotEventDTO()
@Serializable
@SerialName("BotOfflineEventForce")
data class BotOfflineEventForceDTO(val qq: Long, val title: String, val message: String) : BotEventDTO()
@Serializable
@SerialName("BotOfflineEventDropped")
data class BotOfflineEventDroppedDTO(val qq: Long) : BotEventDTO()
@Serializable
@SerialName("BotReloginEvent")
data class BotReloginEventDTO(val qq: Long) : BotEventDTO()
@Serializable
@SerialName("BotGroupPermissionChangeEvent")
data class BotGroupPermissionChangeEventDTO(val origin: MemberPermission, val new: MemberPermission, val group: GroupDTO) : BotEventDTO()
@Serializable
@SerialName("BotMuteEvent")
data class BotMuteEventDTO(val durationSeconds: Int, val operator: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("BotUnmuteEvent")
data class BotUnmuteEventDTO(val operator: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("BotJoinGroupEvent")
data class BotJoinGroupEventDTO(val group: GroupDTO) : BotEventDTO()
@Serializable
@SerialName("GroupNameChangeEvent")
data class GroupNameChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val isByBot: Boolean) : BotEventDTO()
@Serializable
@SerialName("GroupEntranceAnnouncementChangeEvent")
data class GroupEntranceAnnouncementChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("GroupMuteAllEvent")
data class GroupMuteAllEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("GroupAllowAnonymousChatEvent")
data class GroupAllowAnonymousChatEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("GroupAllowConfessTalkEvent")
data class GroupAllowConfessTalkEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val isByBot: Boolean) : BotEventDTO()
@Serializable
@SerialName("GroupAllowMemberInviteEvent")
data class GroupAllowMemberInviteEventDTO(val origin: Boolean, val new: Boolean, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("MemberJoinEvent")
data class MemberJoinEventDTO(val member: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("MemberLeaveEventKick")
data class MemberLeaveEventKickDTO(val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("MemberLeaveEventQuit")
data class MemberLeaveEventQuitDTO(val member: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("MemberCardChangeEvent")
data class MemberCardChangeEventDTO(val origin: String, val new: String, val group: GroupDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("MemberSpecialTitleChangeEvent")
data class MemberSpecialTitleChangeEventDTO(val origin: String, val new: String, val member: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("MemberPermissionChangeEvent")
data class MemberPermissionChangeEventDTO(val origin: MemberPermission, val new: MemberPermission, val member: MemberDTO) : BotEventDTO()
@Serializable
@SerialName("MemberMuteEvent")
data class MemberMuteEventDTO(val durationSeconds: Int, val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()
@Serializable
@SerialName("MemberUnmuteEvent")
data class MemberUnmuteEventDTO(val member: MemberDTO, val operator: MemberDTO?) : BotEventDTO()

View File

@ -37,7 +37,7 @@ data class MemberDTO(
val group: GroupDTO
) : ContactDTO() {
constructor(member: Member) : this(
member.id, member.groupCardOrNick, member.permission,
member.id, member.nameCardOrNick, member.permission,
GroupDTO(member.group)
)
}

View File

@ -1,8 +1,7 @@
package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.mamoe.mirai.api.http.AuthedSession
interface DTO
@ -16,3 +15,8 @@ abstract class VerifyDTO : DTO {
@Transient
lateinit var session: AuthedSession // 反序列化验证成功后传入
}
@Serializable
abstract class EventDTO : DTO
object IgnoreEventDTO : EventDTO()

View File

@ -11,6 +11,8 @@ package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessagePacket
@ -30,32 +32,36 @@ data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO()
@SerialName("GroupMessage")
data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO()
@Serializable
@SerialName("UnKnownMessage")
data class UnKnownMessagePacketDTO(val msg: String) : MessagePacketDTO()
// Message
@Serializable
@SerialName("Source")
data class MessageSourceDTO(val uid: Long) : MessageDTO()
@Serializable
@SerialName("At")
data class AtDTO(val target: Long, val display: String) : MessageDTO()
data class AtDTO(val target: Long, val display: String = "") : MessageDTO()
@Serializable
@SerialName("AtAll")
data class AtAllDTO(val target: Long = 0) : MessageDTO() // target为保留字段
@Serializable
@SerialName("Face")
data class FaceDTO(val faceId: Int) : MessageDTO()
@Serializable
@SerialName("Plain")
data class PlainDTO(val text: String) : MessageDTO()
@Serializable
@SerialName("Image")
data class ImageDTO(val imageId: String) : MessageDTO()
@Serializable
@SerialName("Xml")
data class XmlDTO(val xml: String) : MessageDTO()
@Serializable
@SerialName("Unknown")
data class UnknownMessageDTO(val text: String) : MessageDTO()
@ -64,11 +70,11 @@ data class UnknownMessageDTO(val text: String) : MessageDTO()
* Abstract Class
* */
@Serializable
sealed class MessagePacketDTO : DTO {
lateinit var messageChain : MessageChainDTO
sealed class MessagePacketDTO : EventDTO() {
lateinit var messageChain: MessageChainDTO
}
typealias MessageChainDTO = Array<MessageDTO>
typealias MessageChainDTO = List<MessageDTO>
@Serializable
sealed class MessageDTO : DTO
@ -77,21 +83,25 @@ sealed class MessageDTO : DTO
/*
Extend function
*/
suspend fun MessagePacket<*, *>.toDTO(): MessagePacketDTO = when (this) {
fun MessagePacket<*, *>.toDTO() = when (this) {
is FriendMessage -> FriendMessagePacketDTO(QQDTO(sender))
is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender))
else -> UnKnownMessagePacketDTO("UnKnown Message Packet")
}.apply { messageChain = Array(message.size){ message[it].toDTO() }}
else -> IgnoreEventDTO
}.apply {
if (this is MessagePacketDTO) {
messageChain = mutableListOf<MessageDTO>().also { ls -> message.foreachContent { ls.add(it.toDTO()) } }
}
}
fun MessageChainDTO.toMessageChain() =
MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage()) } }
fun MessageChainDTO.toMessageChain(contact: Contact) =
MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage(contact)) } }
@UseExperimental(ExperimentalUnsignedTypes::class)
fun Message.toDTO() = when (this) {
is MessageSource -> MessageSourceDTO(messageUid)
is At -> AtDTO(target, display)
is AtAll -> AtAllDTO(0L)
is Face -> FaceDTO(id.value.toInt())
is Face -> FaceDTO(id)
is PlainText -> PlainDTO(stringValue)
is Image -> ImageDTO(imageId)
is XMLMessage -> XmlDTO(stringValue)
@ -99,15 +109,13 @@ fun Message.toDTO() = when (this) {
}
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
fun MessageDTO.toMessage() = when (this) {
is AtDTO -> At(target, display)
fun MessageDTO.toMessage(contact: Contact) = when (this) {
is AtDTO -> At((contact as Group)[target])
is AtAllDTO -> AtAll
is FaceDTO -> Face(FaceId(faceId.toUByte()))
is FaceDTO -> Face(faceId)
is PlainDTO -> PlainText(text)
is ImageDTO -> Image(imageId)
is XmlDTO -> XMLMessage(xml)
is MessageSourceDTO, is UnknownMessageDTO -> PlainText("assert cannot reach")
}

View File

@ -9,26 +9,35 @@
package net.mamoe.mirai.api.http.queue
import net.mamoe.mirai.api.http.data.common.EventDTO
import net.mamoe.mirai.api.http.data.common.IgnoreEventDTO
import net.mamoe.mirai.api.http.data.common.toDTO
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.message.data.MessageSource
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
class MessageQueue : ConcurrentLinkedDeque<MessagePacket<*, *>>() {
class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
val quoteCache = ConcurrentHashMap<Long, GroupMessage>()
fun fetch(size: Int): List<MessagePacket<*, *>> {
fun fetch(size: Int): List<EventDTO> {
var count = size
quoteCache.clear()
val ret = ArrayList<MessagePacket<*, *>>(count)
while (!this.isEmpty() && count-- > 0) {
val packet = pop()
ret.add(packet)
val ret = ArrayList<EventDTO>(count)
while (!this.isEmpty() && count > 0) {
val event = pop()
if (packet is GroupMessage) {
addCache(packet)
event.toDTO().also {
if (it != IgnoreEventDTO) {
ret.add(it)
count--
}
}
if (event is GroupMessage) {
addCache(event)
}
}
return ret

View File

@ -18,6 +18,7 @@ import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.api.http.data.NoSuchBotException
import net.mamoe.mirai.api.http.data.StateCode
import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import kotlin.coroutines.EmptyCoroutineContext
@ -28,7 +29,7 @@ fun Application.authModule() {
if (it.authKey != SessionManager.authKey) {
call.respondStateCode(StateCode(1, "Auth Key错误"))
} else {
call.respondStateCode(StateCode(0, SessionManager.createTempSession().key))
call.respondDTO(AuthRetDTO(0, SessionManager.createTempSession().key))
}
}
@ -55,6 +56,9 @@ fun Application.authModule() {
}
}
@Serializable
private data class AuthRetDTO(val code: Int, val session: String) : DTO
@Serializable
private data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO()

View File

@ -37,24 +37,28 @@ fun Application.messageModule() {
miraiGet("/fetchMessage") {
val count: Int = paramOrNull("count")
val fetch = it.messageQueue.fetch(count)
val ls = Array(fetch.size) { index -> fetch[index].toDTO() }
call.respondJson(ls.toList().toJson())
call.respondJson(fetch.toJson())
}
miraiVerify<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)
}
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)
}
miraiVerify<SendDTO>("/quoteMessage") {
it.session.messageQueue.quoteCache[it.target]?.quoteReply(it.messageChain.toMessageChain())
?: throw NoSuchElementException()
it.session.messageQueue.quoteCache[it.target]?.apply {
quoteReply(it.messageChain.toMessageChain(group))
} ?: throw NoSuchElementException()
call.respondStateCode(StateCode.Success)
}

View File

@ -13,7 +13,6 @@ import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import net.mamoe.mirai.api.http.data.common.*
import net.mamoe.mirai.message.data.MessageSource
// 解析失败时直接返回null由路由判断响应400状态
@UseExperimental(ImplicitReflectionSerializer::class)
@ -45,20 +44,46 @@ else MiraiJson.json.stringify(serializer, this)
*/
object MiraiJson {
val json = Json(context = SerializersModule {
polymorphic(MessagePacketDTO.serializer()) {
polymorphic(EventDTO.serializer()) {
GroupMessagePacketDTO::class with GroupMessagePacketDTO.serializer()
FriendMessagePacketDTO::class with FriendMessagePacketDTO.serializer()
UnKnownMessagePacketDTO::class with UnKnownMessagePacketDTO.serializer()
}
polymorphic(MessageDTO.serializer()) {
MessageSourceDTO::class with MessageSourceDTO.serializer()
AtDTO::class with AtDTO.serializer()
AtAllDTO::class with AtAllDTO.serializer()
FaceDTO::class with FaceDTO.serializer()
PlainDTO::class with PlainDTO.serializer()
ImageDTO::class with ImageDTO.serializer()
XmlDTO::class with XmlDTO.serializer()
UnknownMessageDTO::class with UnknownMessageDTO.serializer()
BotOnlineEventDTO::class with BotOnlineEventDTO.serializer()
BotOfflineEventActiveDTO::class with BotOfflineEventActiveDTO.serializer()
BotOfflineEventForceDTO::class with BotOfflineEventForceDTO.serializer()
BotOfflineEventDroppedDTO::class with BotOfflineEventDroppedDTO.serializer()
BotReloginEventDTO::class with BotReloginEventDTO.serializer()
BotGroupPermissionChangeEventDTO::class with BotGroupPermissionChangeEventDTO.serializer()
BotMuteEventDTO::class with BotMuteEventDTO.serializer()
BotUnmuteEventDTO::class with BotUnmuteEventDTO.serializer()
BotJoinGroupEventDTO::class with BotJoinGroupEventDTO.serializer()
GroupNameChangeEventDTO::class with GroupNameChangeEventDTO.serializer()
GroupEntranceAnnouncementChangeEventDTO::class with GroupEntranceAnnouncementChangeEventDTO.serializer()
GroupMuteAllEventDTO::class with GroupMuteAllEventDTO.serializer()
GroupAllowAnonymousChatEventDTO::class with GroupAllowAnonymousChatEventDTO.serializer()
GroupAllowConfessTalkEventDTO::class with GroupAllowConfessTalkEventDTO.serializer()
GroupAllowMemberInviteEventDTO::class with GroupAllowMemberInviteEventDTO.serializer()
MemberJoinEventDTO::class with MemberJoinEventDTO.serializer()
MemberLeaveEventKickDTO::class with MemberLeaveEventKickDTO.serializer()
MemberLeaveEventQuitDTO::class with MemberLeaveEventQuitDTO.serializer()
MemberCardChangeEventDTO::class with MemberCardChangeEventDTO.serializer()
MemberSpecialTitleChangeEventDTO::class with MemberSpecialTitleChangeEventDTO.serializer()
MemberPermissionChangeEventDTO::class with MemberPermissionChangeEventDTO.serializer()
MemberMuteEventDTO::class with MemberMuteEventDTO.serializer()
MemberUnmuteEventDTO::class with MemberUnmuteEventDTO.serializer()
}
// Message Polymorphic
// polymorphic(MessageDTO.serializer()) {
// MessageSourceDTO::class with MessageSourceDTO.serializer()
// AtDTO::class with AtDTO.serializer()
// AtAllDTO::class with AtAllDTO.serializer()
// FaceDTO::class with FaceDTO.serializer()
// PlainDTO::class with PlainDTO.serializer()
// ImageDTO::class with ImageDTO.serializer()
// XmlDTO::class with XmlDTO.serializer()
// UnknownMessageDTO::class with UnknownMessageDTO.serializer()
// }
})
}

View File

@ -2,7 +2,8 @@ package net.mamoe.mirai.console.graphical
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
import net.mamoe.mirai.console.graphical.view.PrimaryView
import net.mamoe.mirai.console.graphical.styleSheet.PrimaryStyleSheet
import net.mamoe.mirai.console.graphical.view.Decorator
import tornadofx.App
import tornadofx.find
import tornadofx.launch
@ -11,7 +12,7 @@ fun main(args: Array<String>) {
launch<MiraiGraphicalUI>(args)
}
class MiraiGraphicalUI: App(PrimaryView::class) {
class MiraiGraphicalUI : App(Decorator::class, PrimaryStyleSheet::class) {
override fun init() {
super.init()
@ -23,4 +24,4 @@ class MiraiGraphicalUI: App(PrimaryView::class) {
super.stop()
MiraiConsole.stop()
}
}
}

View File

@ -11,3 +11,9 @@ class BotModel(val uin: Long) {
val logHistory = observableListOf<String>()
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)
}

View File

@ -0,0 +1,47 @@
package net.mamoe.mirai.console.graphical.styleSheet
import javafx.scene.Cursor
import javafx.scene.effect.BlurType
import javafx.scene.effect.DropShadow
import javafx.scene.paint.Color
import javafx.scene.text.FontWeight
import tornadofx.*
class LoginViewStyleSheet : Stylesheet() {
companion object {
val vBox by csselement("VBox")
}
init {
vBox {
maxWidth = 500.px
maxHeight = 500.px
backgroundColor += c("39c5BB", 0.3)
backgroundRadius += box(15.px)
padding = box(50.px, 100.px)
spacing = 25.px
borderRadius += box(15.px)
effect = DropShadow(BlurType.THREE_PASS_BOX, Color.GRAY, 10.0, 0.0, 15.0, 15.0)
}
textField {
prefHeight = 30.px
textFill = Color.BLACK
fontWeight = FontWeight.BOLD
}
button {
backgroundColor += c("00BCD4", 0.8)
padding = box(10.px, 0.px)
prefWidth = 500.px
textFill = Color.WHITE
fontWeight = FontWeight.BOLD
cursor = Cursor.HAND
}
}
}

View File

@ -0,0 +1,21 @@
package net.mamoe.mirai.console.graphical.styleSheet
import tornadofx.*
class PrimaryStyleSheet : Stylesheet() {
companion object {
val jfxTitle by cssclass("jfx-decorator-buttons-container")
val container by cssclass("jfx-decorator-content-container")
}
init {
jfxTitle {
backgroundColor += c("00BCD4")
}
container {
borderColor += box(c("00BCD4"))
borderWidth += box(0.px, 4.px, 4.px, 4.px)
}
}
}

View File

@ -17,20 +17,20 @@ internal fun EventTarget.jfxButton(text: String = "", graphic: Node? = null, op:
if (graphic != null) it.graphic = graphic
}
fun EventTarget.jfxTextfield(value: String? = null, op: TextField.() -> Unit = {}) = JFXTextField().attachTo(this, op) {
fun EventTarget.jfxTextfield(value: String? = null, op: JFXTextField.() -> Unit = {}) = JFXTextField().attachTo(this, op) {
if (value != null) it.text = value
}
fun EventTarget.jfxTextfield(property: ObservableValue<String>, op: TextField.() -> Unit = {}) = jfxTextfield().apply {
fun EventTarget.jfxTextfield(property: ObservableValue<String>, op: JFXTextField.() -> Unit = {}) = jfxTextfield().apply {
bind(property)
op(this)
}
fun EventTarget.jfxPasswordfield(value: String? = null, op: TextField.() -> Unit = {}) = JFXPasswordField().attachTo(this, op) {
fun EventTarget.jfxPasswordfield(value: String? = null, op: JFXPasswordField.() -> Unit = {}) = JFXPasswordField().attachTo(this, op) {
if (value != null) it.text = value
}
fun EventTarget.jfxPasswordfield(property: ObservableValue<String>, op: TextField.() -> Unit = {}) = jfxPasswordfield().apply {
fun EventTarget.jfxPasswordfield(property: ObservableValue<String>, op: JFXPasswordField.() -> Unit = {}) = jfxPasswordfield().apply {
bind(property)
op(this)
}

View File

@ -0,0 +1,9 @@
package net.mamoe.mirai.console.graphical.view
import com.jfoenix.controls.JFXDecorator
import tornadofx.View
class Decorator: View() {
override val root = JFXDecorator(primaryStage, find<PrimaryView>().root)
}

View File

@ -1,33 +0,0 @@
package net.mamoe.mirai.console.graphical.view
import javafx.beans.property.SimpleStringProperty
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
import net.mamoe.mirai.console.graphical.util.jfxButton
import net.mamoe.mirai.console.graphical.util.jfxPasswordfield
import net.mamoe.mirai.console.graphical.util.jfxTextfield
import tornadofx.*
class LoginFragment : Fragment() {
private val controller = find<MiraiGraphicalUIController>(FX.defaultScope)
private val qq = SimpleStringProperty("0")
private val psd = SimpleStringProperty("")
override val root = form {
fieldset("登录") {
field("QQ") {
jfxTextfield(qq)
}
field("密码") {
jfxPasswordfield(psd)
}
}
jfxButton("登录").action {
runBlocking {
controller.login(qq.value, psd.value)
}
close()
}
}
}

View File

@ -0,0 +1,50 @@
package net.mamoe.mirai.console.graphical.view
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Pos
import javafx.scene.image.Image
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
import net.mamoe.mirai.console.graphical.styleSheet.LoginViewStyleSheet
import net.mamoe.mirai.console.graphical.util.jfxButton
import net.mamoe.mirai.console.graphical.util.jfxPasswordfield
import net.mamoe.mirai.console.graphical.util.jfxTextfield
import tornadofx.*
class LoginView : View("CNM") {
private val controller = find<MiraiGraphicalUIController>()
private val qq = SimpleStringProperty("")
private val psd = SimpleStringProperty("")
override val root = borderpane {
addStylesheet(LoginViewStyleSheet::class)
center = vbox {
imageview(Image(LoginView::class.java.classLoader.getResourceAsStream("character.png"))) {
alignment = Pos.CENTER
}
jfxTextfield(qq) {
promptText = "QQ"
isLabelFloat = true
}
jfxPasswordfield(psd) {
promptText = "Password"
isLabelFloat = true
}
jfxButton("Login").action {
runAsync {
runBlocking { controller.login(qq.value, psd.value) }
}.ui {
qq.value = ""
psd.value = ""
}
}
}
}
}

View File

@ -1,20 +1,15 @@
package net.mamoe.mirai.console.graphical.view
import com.jfoenix.controls.JFXListCell
import javafx.geometry.Insets
import javafx.geometry.Pos
import com.jfoenix.controls.*
import javafx.collections.ObservableList
import javafx.scene.control.Tab
import javafx.scene.control.TabPane
import javafx.scene.image.Image
import javafx.scene.paint.Color
import javafx.scene.text.FontWeight
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
import net.mamoe.mirai.console.graphical.model.BotModel
import net.mamoe.mirai.console.graphical.util.jfxButton
import net.mamoe.mirai.console.graphical.util.jfxListView
import net.mamoe.mirai.console.graphical.util.jfxTabPane
import tornadofx.*
import java.io.FileInputStream
class PrimaryView : View() {
@ -35,20 +30,12 @@ class PrimaryView : View() {
setCellFactory {
object : JFXListCell<BotModel>() {
var tab: Tab? = null
init {
onDoubleClick {
if (tab == null) {
(center as TabPane).tab(item.uin.toString()) {
listview(item.logHistory)
onDoubleClick { close() }
tab = this
}
} else {
(center as TabPane).tabs.add(tab)
}
tab?.select()
(center as TabPane).logTab(
text = item.uin.toString(),
logs = item.logHistory
).select()
}
}
@ -65,44 +52,37 @@ class PrimaryView : View() {
}
}
}
hbox {
padding = Insets(10.0)
spacing = 10.0
alignment = Pos.CENTER
jfxButton("L").action {
find<LoginFragment>().openModal()
}
jfxButton("P")
jfxButton("S")
style { backgroundColor += c("00BCD4") }
children.style(true) {
backgroundColor += c("00BCD4")
fontSize = 15.px
fontWeight = FontWeight.BOLD
textFill = Color.WHITE
borderRadius += box(25.px)
backgroundRadius += box(25.px)
}
}
}
center = jfxTabPane {
tab("Main") {
listview(controller.mainLog) {
fitToParentSize()
cellFormat {
graphic = label(it) {
maxWidthProperty().bind(this@listview.widthProperty())
isWrapText = true
}
}
}
tab("Login") {
this += find<LoginView>().root
}
tab("Plugin")
tab("Settings")
logTab("Main", controller.mainLog)
}
}
}
private fun TabPane.logTab(
text: String? = null,
logs: ObservableList<String>,
op: Tab.() -> Unit = {}
)= tab(text) {
listview(logs) {
fitToParentSize()
cellFormat {
graphic = label(it) {
maxWidthProperty().bind(this@listview.widthProperty())
isWrapText = true
}
}
}
also(op)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -187,8 +187,8 @@ object MiraiConsoleTerminalUI : MiraiConsoleUI {
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
pushLog(0, "[Login Solver]需要进行账户安全认证")
pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登陆地点]/[不常用设备登陆]的问题")
pushLog(0, "[Login Solver]完成以下账号认证即可成功登|理论本认证在mirai每个账户中最多出现1次")
pushLog(0, "[Login Solver]该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题")
pushLog(0, "[Login Solver]完成以下账号认证即可成功登|理论本认证在mirai每个账户中最多出现1次")
pushLog(0, "[Login Solver]请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符")
pushLog(0, "[Login Solver]这步操作将在后续的版本中优化")
pushLog(0, url)

View File

@ -77,7 +77,7 @@ object MiraiConsole {
logger("Mirai-console 启动完成")
logger("\"/login qqnumber qqpassword \" to login a bot")
logger("\"/login qq号 qq密码 \" 来登一个BOT")
logger("\"/login qq号 qq密码 \" 来登一个BOT")
}
fun stop() {
@ -208,7 +208,7 @@ object MiraiConsole {
}
val bot: Bot? = if (it.size == 2) {
if (bots.size == 0) {
logger("还没有BOT登")
logger("还没有BOT登")
return@onCommand false
}
bots[0].get()

View File

@ -162,16 +162,11 @@ internal class QQImpl(
TODO("not implemented")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return other is QQ && other.id == this.id
}
override fun hashCode(): Int = super.hashCode()
override fun toString(): String = "QQ($id)"
}
@Suppress("MemberVisibilityCanBePrivate")
@Suppress("MemberVisibilityCanBePrivate", "DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
internal class MemberImpl(
qq: QQImpl,
group: GroupImpl,
@ -182,9 +177,21 @@ internal class MemberImpl(
val qq: QQImpl by qq.unsafeWeakRef()
override var permission: MemberPermission = memberInfo.permission
@Suppress("PropertyName")
internal var _nameCard: String = memberInfo.nameCard
@Suppress("PropertyName")
internal var _specialTitle: String = memberInfo.specialTitle
@Suppress("PropertyName")
var _muteTimestamp: Int = memberInfo.muteTimestamp
override val muteTimeRemaining: Int =
if (_muteTimestamp == 0 || _muteTimestamp == 0xFFFFFFFF.toInt()) {
0
} else {
_muteTimestamp - currentTimeSeconds.toInt() - bot.client.timeDifference.toInt()
}
override var nameCard: String
get() = _nameCard
set(newValue) {
@ -220,7 +227,7 @@ internal class MemberImpl(
newValue
).sendWithoutExpect()
}
MemberSpecialTitleChangeEvent(oldValue, newValue, this@MemberImpl).broadcast()
MemberSpecialTitleChangeEvent(oldValue, newValue, this@MemberImpl, null).broadcast()
}
}
}
@ -279,12 +286,9 @@ internal class MemberImpl(
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return other is Member && other.id == this.id
override fun toString(): String {
return "Member($id)"
}
override fun hashCode(): Int = super.hashCode()
}
internal class MemberInfoImpl(
@ -301,6 +305,7 @@ internal class MemberInfoImpl(
else -> MemberPermission.MEMBER
}
override val specialTitle: String get() = jceInfo.sSpecialTitle ?: ""
override val muteTimestamp: Int get() = jceInfo.dwShutupTimestap?.toInt() ?: 0
}
/**
@ -323,13 +328,13 @@ internal class GroupImpl(
@UseExperimental(MiraiExperimentalAPI::class)
override lateinit var botPermission: MemberPermission
var _botMuteRemaining: Int = groupInfo.botMuteRemaining
var _botMuteTimestamp: Int = groupInfo.botMuteRemaining
override val botMuteRemaining: Int =
if (_botMuteRemaining == 0 || _botMuteRemaining == 0xFFFFFFFF.toInt()) {
if (_botMuteTimestamp == 0 || _botMuteTimestamp == 0xFFFFFFFF.toInt()) {
0
} else {
_botMuteRemaining - currentTimeSeconds.toInt() - bot.client.timeDifference.toInt()
_botMuteTimestamp - currentTimeSeconds.toInt() - bot.client.timeDifference.toInt()
}
override val members: ContactList<Member> = ContactList(members.mapNotNull {
@ -600,10 +605,7 @@ internal class GroupImpl(
image.input.close()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return other is Group && other.id == this.id
override fun toString(): String {
return "Group($id)"
}
override fun hashCode(): Int = super.hashCode()
}

View File

@ -20,7 +20,6 @@ import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.data.FriendInfo
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
@ -116,10 +115,6 @@ internal abstract class QQAndroidBotBase constructor(
return sequence
}
override fun onEvent(event: BotEvent): Boolean {
return firstLoginSucceed
}
override suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult {
TODO("not implemented")
}

View File

@ -7,6 +7,9 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmName("SerializationUtils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.io.serialization
import kotlinx.io.core.*
@ -20,7 +23,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestDataVersion3
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket
import net.mamoe.mirai.utils.firstValue
import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.toUHexString
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
fun <T : JceStruct> ByteArray.loadAs(deserializer: DeserializationStrategy<T>, c: JceCharset = JceCharset.UTF8): T {

View File

@ -21,6 +21,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.SourceMsg
internal inline class MessageSourceFromServer(
val delegate: ImMsgBody.SourceMsg
) : MessageSource {
override val time: Long get() = delegate.time.toLong() and 0xFFFFFFFF
override val messageUid: Long get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!
override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.senderUin
@ -32,6 +33,7 @@ internal inline class MessageSourceFromServer(
internal inline class MessageSourceFromMsg(
val delegate: MsgComm.Msg
) : MessageSource {
override val time: Long get() = delegate.msgHead.msgTime.toLong() and 0xFFFFFFFF
override val messageUid: Long get() = delegate.msgBody.richText.attr!!.random.toLong()
override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.msgHead.fromUin

View File

@ -9,6 +9,8 @@
package net.mamoe.mirai.qqandroid.message
import kotlinx.io.core.buildPacket
import kotlinx.io.core.readBytes
import kotlinx.io.core.readUInt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
@ -20,13 +22,18 @@ import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.toByteArray
private val AT_BUF_1 = byteArrayOf(0x00, 0x01, 0x00, 0x00, 0x00, 0x0A, 0x00)
private val AT_BUF_2 = ByteArray(2)
internal fun At.toJceData(): ImMsgBody.Text {
val text = this.toString()
return ImMsgBody.Text(
str = this.toString(),
attr6Buf = AT_BUF_1 + this.target.toInt().toByteArray() + AT_BUF_2
str = text,
attr6Buf = buildPacket {
writeShort(1)
writeShort(0)
writeShort(text.length.toShort())
writeByte(1)
writeInt(target.toInt())
writeShort(0)
}.readBytes()
)
}
@ -86,6 +93,16 @@ _400Height=0x000000EB(235)
pbReserve=<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 {
return ImMsgBody.CustomFace(
filePath = this.filepath,
@ -213,6 +230,7 @@ internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
is NotOnlineImageFromServer -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
is NotOnlineImageFromFile -> elements.add(ImMsgBody.Elem(notOnlineImage = it.toJceData()))
is AtAll -> elements.add(atAllData)
is Face -> elements.add(ImMsgBody.Elem(face = it.toJceData()))
is QuoteReply,
is MessageSource -> {
@ -312,18 +330,20 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChain) {
it.srcMsg != null -> message.add(QuoteReply(MessageSourceFromServer(it.srcMsg)))
it.notOnlineImage != null -> message.add(NotOnlineImageFromServer(it.notOnlineImage))
it.customFace != null -> message.add(CustomFaceFromServer(it.customFace))
it.face != null -> message.add(Face(it.face.index))
it.text != null -> {
if (it.text.attr6Buf.isEmpty()) {
message.add(it.text.str.toMessage())
} else {
//00 01 00 00 00 05 01 00 00 00 00 00 00 all
//00 01 00 00 00 0A 00 3E 03 3F A2 00 00 one
// 00 01 00 00 00 05 01 00 00 00 00 00 00 all
// 00 01 00 00 00 0A 00 3E 03 3F A2 00 00 one/nick
// 00 01 00 00 00 07 00 44 71 47 90 00 00 one/groupCard
val id: Long
it.text.attr6Buf.read {
discardExact(7)
id = readUInt().toLong()
}
if (id == 0L){
if (id == 0L) {
message.add(AtAll)
} else {
message.add(At(id, it.text.str))

View File

@ -20,10 +20,7 @@ import kotlinx.io.core.buildPacket
import kotlinx.io.core.use
import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.event.CancellableEvent
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.*
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.BotOnlineEvent
import net.mamoe.mirai.network.BotNetworkHandler
@ -78,7 +75,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
} catch (e: CancellationException) {
return@launch
} catch (e: Throwable) {
BotOfflineEvent.Dropped(bot).broadcast()
if (this@QQAndroidBotNetworkHandler.isActive) {
BotOfflineEvent.Dropped(bot, e).broadcast()
}
return@launch
}
packetReceiveLock.withLock {
@ -88,25 +87,40 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}.also { _packetReceiverJob = it }
}
override suspend fun relogin() {
heartbeatJob?.cancel()
private fun startHeartbeatJobOrKill(cancelCause: CancellationException? = null): Job {
heartbeatJob?.cancel(cancelCause)
return this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) {
while (this.isActive) {
delay(bot.configuration.heartbeatPeriodMillis)
val failException = doHeartBeat()
if (failException != null) {
delay(bot.configuration.firstReconnectDelayMillis)
close(failException)
BotOfflineEvent.Dropped(bot, failException).broadcast()
}
}
}.also { heartbeatJob = it }
}
override suspend fun relogin(cause: Throwable?) {
heartbeatJob?.cancel(CancellationException("relogin", cause))
if (::channel.isInitialized) {
if (channel.isOpen) {
kotlin.runCatching {
registerClientOnline()
registerClientOnline(500)
}.exceptionOrNull() ?: return
logger.info("Cannot do fast relogin. Trying slow relogin")
}
channel.close()
}
channel = PlatformSocket()
// TODO: 2020/2/14 连接多个服务器
// TODO: 2020/2/14 连接多个服务器, #52
withTimeoutOrNull(3000) {
channel.connect("113.96.13.208", 8080)
} ?: error("timeout connecting server")
startPacketReceiverJobOrKill(CancellationException("reconnect"))
startPacketReceiverJobOrKill(CancellationException("relogin", cause))
// logger.info("Trying login")
var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
mainloop@ while (true) {
when (response) {
@ -157,10 +171,11 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
// println("d2key=${bot.client.wLoginSigInfo.d2Key.toUHexString()}")
registerClientOnline()
startHeartbeatJobOrKill()
}
private suspend fun registerClientOnline() {
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>()
private suspend fun registerClientOnline(timeoutMillis: Long = 3000) {
StatSvc.Register(bot.client).sendAndExpect<StatSvc.Register.Response>(timeoutMillis)
}
// caches
@ -170,25 +185,34 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
@UseExperimental(MiraiExperimentalAPI::class, ExperimentalTime::class)
override suspend fun init(): Unit = coroutineScope {
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendWithoutExpect()
check(bot.isActive) { "bot is dead therefore network can't init" }
check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't init" }
bot.qqs.delegate.clear()
bot.groups.delegate.clear()
val friendListJob = launch {
try {
lateinit var loadFriends: suspend () -> Unit
// 不要用 fun, 不要 join declaration, 不要用 val, 编译失败警告
loadFriends = suspend loadFriends@{
logger.info("开始加载好友信息")
var currentFriendCount = 0
var totalFriendCount: Short
while (true) {
val data = FriendList.GetFriendGroupList(
bot.client,
currentFriendCount,
150,
0,
0
).sendAndExpect<FriendList.GetFriendGroupList.Response>(timeoutMillis = 5000, retry = 2)
val data = runCatching {
FriendList.GetFriendGroupList(
bot.client,
currentFriendCount,
150,
0,
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
data.friendList.forEach {
// atomic add
@ -196,16 +220,16 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
currentFriendCount++
}
}
logger.verbose("正在加载好友列表 ${currentFriendCount}/${totalFriendCount}")
logger.verbose { "正在加载好友列表 ${currentFriendCount}/${totalFriendCount}" }
if (currentFriendCount >= totalFriendCount) {
break
}
// delay(200)
}
logger.info("好友列表加载完成, 共 ${currentFriendCount}")
} catch (e: Exception) {
logger.error("加载好友列表失败|一般这是由于加载过于频繁导致/将以热加载方式加载好友列表")
logger.info { "好友列表加载完成, 共 ${currentFriendCount}" }
}
loadFriends()
}
val groupJob = launch {
@ -247,7 +271,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
))
)
}?.let {
logger.error("${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试")
logger.error { "${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试" }
logger.error(it)
this@QQAndroidBotNetworkHandler.launch {
delay(10_000)
@ -260,26 +284,25 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
loadGroup()
}
}
logger.info("群组列表与群成员加载完成, 共 ${troopListData.groups.size}")
logger.info { "群组列表与群成员加载完成, 共 ${troopListData.groups.size}" }
} catch (e: Exception) {
logger.error("加载组信息失败|一般这是由于加载过于频繁导致/将以热加载方式加载群列表")
logger.error { "加载组信息失败|一般这是由于加载过于频繁导致/将以热加载方式加载群列表" }
logger.error(e)
}
}
joinAll(friendListJob, groupJob)
heartbeatJob = this@QQAndroidBotNetworkHandler.launch(CoroutineName("Heartbeat")) {
while (this.isActive) {
delay(bot.configuration.heartbeatPeriodMillis)
val failException = doHeartBeat()
if (failException != null) {
delay(bot.configuration.firstReconnectDelayMillis)
close()
BotOfflineEvent.Dropped(bot).broadcast()
withTimeoutOrNull(5000) {
lateinit var listener: Listener<PacketReceivedEvent>
listener = this.subscribeAlways {
if (it.packet is MessageSvc.PbGetMsg.GetMsgSuccess) {
listener.complete()
}
}
}
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendWithoutExpect()
} ?: error("timeout syncing friend message history")
bot.firstLoginSucceed = true
@ -288,10 +311,12 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
@Suppress("UNCHECKED_CAST")
KnownPacketFactories.handleIncomingPacket(it as KnownPacketFactories.IncomingPacket<Packet>, bot, it.flag2, it.consumer)
}
pendingIncomingPackets = null // release
val list = pendingIncomingPackets
pendingIncomingPackets = null // release, help gc
list?.clear() // help gc
BotOnlineEvent(bot).broadcast()
Unit
Unit // dont remove. can help type inference
}
suspend fun doHeartBeat(): Exception? {
@ -347,7 +372,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
// with generic type, less mistakes
private suspend inline fun <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 ->
handlePacket(packetFactory, packet, commandName, sequenceId)
if (packet is MultiPacket<*>) {
@ -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).
packetListeners.forEach { listener ->
if (listener.filter(commandName, sequenceId) && packetListeners.remove(listener)) {
@ -370,7 +395,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
}
// check top-level cancelling
if (PacketReceivedEvent(packet).broadcast().isCancelled) {
if (packet != null && PacketReceivedEvent(packet).broadcast().isCancelled) {
return
}
@ -386,7 +411,13 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
if (packet is CancellableEvent && packet.isCancelled) return
}
logger.info("Received: ${packet.toString().replace("\n", """\n""").replace("\r", "")}")
if (packet != null && (bot.logger.isEnabled || logger.isEnabled)) {
val logMessage = "Received: ${packet.toString().replace("\n", """\n""").replace("\r", "")}"
if (packet is Event) {
bot.logger.verbose(logMessage)
} else logger.verbose(logMessage)
}
packetFactory?.run {
when (this) {
@ -480,7 +511,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
* 发送一个包, 但不期待任何返回.
*/
suspend fun OutgoingPacket.sendWithoutExpect() {
logger.info("Send: ${this.commandName}")
check(bot.isActive) { "bot is dead therefore can't send any packet" }
check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" }
logger.verbose("Send: ${this.commandName}")
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
channel.send(delegate)
}
@ -495,6 +528,9 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
require(timeoutMillis > 0) { "timeoutMillis must > 0" }
require(retry >= 0) { "retry must >= 0" }
check(bot.isActive) { "bot is dead therefore can't send any packet" }
check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" }
var lastException: Exception? = null
if (retry == 0) {
val handler = PacketListener(commandName = commandName, sequenceId = sequenceId)
@ -503,7 +539,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
channel.send(delegate)
}
logger.info("Send: ${this.commandName}")
logger.verbose("Send: ${this.commandName}")
return withTimeoutOrNull(timeoutMillis) {
@Suppress("UNCHECKED_CAST")
handler.await() as E
@ -522,7 +558,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
channel.send(data, 0, length)
}
logger.info("Send: ${this.commandName}")
logger.verbose("Send: ${this.commandName}")
return withTimeoutOrNull(timeoutMillis) {
@Suppress("UNCHECKED_CAST")
handler.await() as E
@ -547,7 +583,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
internal inner class PacketListener( // callback
val commandName: String,
val sequenceId: Int
) : CompletableDeferred<Packet> by CompletableDeferred(supervisor) {
) : CompletableDeferred<Packet?> by CompletableDeferred(supervisor) {
fun filter(commandName: String, sequenceId: Int) = this.commandName == commandName && this.sequenceId == sequenceId
}

View File

@ -15,12 +15,10 @@ import kotlinx.io.core.buildPacket
import kotlinx.io.core.writeFully
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.utils.cryptor.ECDH
import net.mamoe.mirai.utils.cryptor.ECDHKeyPair
import net.mamoe.mirai.utils.io.encryptAndWrite
import net.mamoe.mirai.utils.io.writeShortLVByteArray
/**
* Encryption method to be used for packet body.
*/
@UseExperimental(ExperimentalUnsignedTypes::class)
internal interface EncryptMethod {
val id: Int
@ -33,16 +31,6 @@ internal interface EncryptMethodSessionKey : EncryptMethod {
val currentLoginState: Int
val sessionKey: ByteArray
/**
* buildPacket{
* byte 1
* byte if (currentLoginState == 2) 3 else 2
* fully key
* short 258
* short 0
* fully encrypted
* }
*/
override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket =
buildPacket {
require(currentLoginState == 2 || currentLoginState == 3) { "currentLoginState must be either 2 or 3" }
@ -65,29 +53,27 @@ inline class EncryptMethodSessionKeyLoginState3(override val sessionKey: ByteArr
override val currentLoginState: Int get() = 3
}
inline class EncryptMethodECDH135(override val ecdh: ECDH) :
internal inline class EncryptMethodECDH135(override val ecdh: ECDH) :
EncryptMethodECDH {
override val id: Int get() = 135
}
inline class EncryptMethodECDH7(override val ecdh: ECDH) :
internal inline class EncryptMethodECDH7(override val ecdh: ECDH) :
EncryptMethodECDH {
override val id: Int get() = 7
}
internal interface EncryptMethodECDH : EncryptMethod {
companion object {
operator fun invoke(ecdh: ECDH): EncryptMethodECDH {
return if (ecdh.keyPair === ECDHKeyPair.DefaultStub) {
EncryptMethodECDH135(ecdh)
} else EncryptMethodECDH7(ecdh)
}
}
val ecdh: ECDH
/**
* **Packet Structure**
* byte 1
* byte 1
* byte[] [ECDH.privateKey]
* short 258
* short [ECDH.publicKey].size
* byte[] [ECDH.publicKey]
* byte[] encrypted `body()` by [ECDH.shareKey]
*/
override fun makeBody(client: QQAndroidClient, body: BytePacketBuilder.() -> Unit): ByteReadPacket =
buildPacket {
writeByte(1) // const
@ -95,15 +81,15 @@ internal interface EncryptMethodECDH : EncryptMethod {
writeFully(client.randomKey)
writeShort(258) // const
// writeShortLVByteArray("04 CB 36 66 98 56 1E 93 6E 80 C1 57 E0 74 CA B1 3B 0B B6 8D DE B2 82 45 48 A1 B1 8D D4 FB 61 22 AF E1 2F E4 8C 52 66 D8 D7 26 9D 76 51 A8 EB 6F E7".hexToBytes())
if (ecdh.keyPair === ECDHKeyPair.DefaultStub) {
writeShortLVByteArray(ECDHKeyPair.DefaultStub.defaultPublicKey)
encryptAndWrite(ECDHKeyPair.DefaultStub.defaultShareKey, body)
} else {
writeShortLVByteArray(ecdh.keyPair.publicKey.getEncoded().drop(23).take(49).toByteArray().also {
check(it[0].toInt() == 0x04) { "Bad publicKey generated. Expected first element=0x04, got${it[0]}" }
})
writeShortLVByteArray(ecdh.keyPair.publicKey.getEncoded().drop(23).take(49).toByteArray().also {
// it.toUHexString().debugPrint("PUBLIC KEY")
check(it[0].toInt() == 0x04) { "Bad publicKey generated. Expected first element=0x04, got${it[0]}" }
//check(ecdh.calculateShareKeyByPeerPublicKey(it.adjustToPublicKey()).contentEquals(ecdh.keyPair.shareKey)) { "PublicKey Validation failed" }
})
// encryptAndWrite("26 33 BA EC 86 EB 79 E6 BC E0 20 06 5E A9 56 6C".hexToBytes(), body)
encryptAndWrite(ecdh.keyPair.initialShareKey, body)
encryptAndWrite(ecdh.keyPair.initialShareKey, body)
}
}
}

View File

@ -33,7 +33,7 @@ import kotlin.contracts.contract
import kotlin.jvm.JvmName
internal sealed class PacketFactory<TPacket : Packet> {
internal sealed class PacketFactory<TPacket : Packet?> {
/**
* 筛选从服务器接收到的包时的 commandName
*/
@ -49,7 +49,7 @@ internal sealed class PacketFactory<TPacket : Packet> {
* @param TPacket 服务器回复包解析结果
*/
@UseExperimental(ExperimentalUnsignedTypes::class)
internal abstract class OutgoingPacketFactory<TPacket : Packet>(
internal abstract class OutgoingPacketFactory<TPacket : Packet?>(
/**
* 命令名. `wtlogin.login`, `ConfigPushSvc.PushDomain`
*/
@ -73,7 +73,7 @@ internal abstract class OutgoingPacketFactory<TPacket : Packet>(
* 这个工厂可以在 [handle] 时回复一个 commandId [responseCommandName] 的包, 也可以不回复.
* 必须先到 [KnownPacketFactories] 中注册工厂, 否则不能处理.
*/
internal abstract class IncomingPacketFactory<TPacket : Packet>(
internal abstract class IncomingPacketFactory<TPacket : Packet?>(
/**
* 接收自服务器的包的 commandName
*/
@ -97,10 +97,10 @@ internal abstract class IncomingPacketFactory<TPacket : Packet>(
}
@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")
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)
internal val DECRYPTER_16_ZERO = ByteArray(16)
@ -169,7 +169,7 @@ internal object KnownPacketFactories {
// do not inline. Exceptions thrown will not be reported correctly
@UseExperimental(MiraiInternalAPI::class)
@Suppress("UNCHECKED_CAST")
suspend fun <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
val flag1 = readInt()
@ -229,7 +229,7 @@ internal object KnownPacketFactories {
}
@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) {
bot.network.logger.debug("Received commandName: ${it.commandName}")
PacketLogger.warning { "找不到 PacketFactory" }
@ -263,7 +263,7 @@ internal object KnownPacketFactories {
private inline fun <R> inline(block: () -> R): R = block()
class IncomingPacket<T : Packet>(
class IncomingPacket<T : Packet?>(
val packetFactory: PacketFactory<T>?,
val sequenceId: Int,
val data: ByteReadPacket,
@ -337,7 +337,7 @@ internal object KnownPacketFactories {
return IncomingPacket(packetFactory, ssoSequenceId, packet, commandName)
}
private suspend fun <T : Packet> ByteReadPacket.parseOicqResponse(
private suspend fun <T : Packet?> ByteReadPacket.parseOicqResponse(
bot: QQAndroidBot,
packetFactory: OutgoingPacketFactory<T>,
ssoSequenceId: Int,

View File

@ -16,7 +16,6 @@ import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.event.events.BotJoinGroupEvent
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.MemberJoinEvent
@ -103,23 +102,23 @@ internal class MessageSvc {
}
@UseExperimental(MiraiInternalAPI::class)
internal class GetMsgSuccess(delegate: List<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
open class Response(internal val syncFlagFromServer: MsgSvc.SyncFlag, delegate: List<Packet>) : MultiPacket<Packet>(delegate),
BroadcastControllable {
override val shouldBroadcast: Boolean
get() = syncFlagFromServer == MsgSvc.SyncFlag.STOP
open class Response(internal val syncFlagFromServer: MsgSvc.SyncFlag, delegate: List<Packet>) : MultiPacket<Packet>(delegate) {
override fun toString(): String {
return "MessageSvc.PbGetMsg.Response($syncFlagFromServer=$syncFlagFromServer, messages=List(size=${this.size}))"
}
}
object EmptyResponse : Response(MsgSvc.SyncFlag.STOP, emptyList())
object EmptyResponse : GetMsgSuccess(emptyList())
@UseExperimental(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
@ -127,8 +126,8 @@ internal class MessageSvc {
val resp = readProtoBuf(MsgSvc.PbGetMsgResp.serializer())
if (resp.result != 0) {
// println("!!! Result=${resp.result} !!!: " + resp.contentToString())
return GetMsgSuccess(mutableListOf())
bot.network.logger.warning("MessageSvc.PushNotify: result != 0, result = ${resp.result}, errorMsg=${resp.errmsg}")
return EmptyResponse
}
bot.client.c2cMessageSync.syncCookie = resp.syncCookie
@ -149,7 +148,7 @@ internal class MessageSvc {
val group = bot.getGroupByUinOrNull(msg.msgHead.fromUin)
if (msg.msgHead.authUin == bot.uin) {
if (group != null) {
error("group is not null while bot is invited to the group")
return@mapNotNull null
}
// 新群
@ -159,6 +158,7 @@ internal class MessageSvc {
}.groups.first { it.groupUin == msg.msgHead.fromUin }
@Suppress("DuplicatedCode")
val newGroup = GroupImpl(
bot = bot,
coroutineContext = bot.coroutineContext,
@ -194,6 +194,7 @@ internal class MessageSvc {
override val nameCard: String get() = ""
override val permission: MemberPermission get() = MemberPermission.MEMBER
override val specialTitle: String get() = ""
override val muteTimestamp: Int get() = 0
override val uin: Long get() = msg.msgHead.authUin
override val nick: String get() = msg.msgHead.authNick.takeIf { it.isNotEmpty() } ?: msg.msgHead.fromNick
}).also { group.members.delegate.addLast(it) })
@ -264,6 +265,7 @@ internal class MessageSvc {
/**
* 发送好友消息
*/
@Suppress("FunctionName")
fun ToFriend(
client: QQAndroidClient,
toUin: Long,
@ -293,6 +295,7 @@ internal class MessageSvc {
/**
* 发送群消息
*/
@Suppress("FunctionName")
fun ToGroup(
client: QQAndroidClient,
groupCode: Long,

View File

@ -15,9 +15,9 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes
import kotlinx.io.core.readUInt
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.MultiPacket
import net.mamoe.mirai.data.NoPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.qqandroid.GroupImpl
@ -41,58 +41,44 @@ import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.readString
import net.mamoe.mirai.utils.io.toUHexString
internal inline class GroupMessageOrNull(val delegate: GroupMessage?) : Packet {
override fun toString(): String {
return delegate?.toString() ?: "<Receipt>"
}
}
internal class OnlinePush {
/**
* 接受群消息
*/
internal object PbPushGroupMsg : IncomingPacketFactory<GroupMessageOrNull>("OnlinePush.PbPushGroupMsg") {
internal object PbPushGroupMsg : IncomingPacketFactory<GroupMessage?>("OnlinePush.PbPushGroupMsg") {
@UseExperimental(ExperimentalStdlibApi::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): GroupMessageOrNull {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): GroupMessage? {
// 00 00 02 E4 0A D5 05 0A 4F 08 A2 FF 8C F0 03 10 DD F1 92 B7 07 18 52 20 00 28 BC 3D 30 8C 82 AB F1 05 38 D2 80 E0 8C 80 80 80 80 02 4A 21 08 E7 C1 AD B8 02 10 01 18 BA 05 22 09 48 69 6D 31 38 38 6D 6F 65 30 06 38 02 42 05 4D 69 72 61 69 50 01 58 01 60 00 88 01 08 12 06 08 01 10 00 18 00 1A F9 04 0A F6 04 0A 26 08 00 10 87 82 AB F1 05 18 B7 B4 BF 30 20 00 28 0C 30 00 38 86 01 40 22 4A 0C E5 BE AE E8 BD AF E9 9B 85 E9 BB 91 12 E6 03 42 E3 03 12 2A 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 22 00 2A 04 03 00 00 00 32 60 15 36 20 39 36 6B 45 31 41 38 35 32 32 39 64 63 36 39 38 34 37 39 37 37 62 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 34 45 31 38 35 38 32 32 2D 30 45 37 42 2D 46 38 30 46 2D 43 35 42 31 2D 33 34 34 38 38 33 37 34 44 33 39 43 7D 2E 6A 70 67 31 32 31 32 41 38 C6 BB 8A A9 08 40 FB AE 9E C2 09 48 50 50 41 5A 00 60 01 6A 10 4E 18 58 22 0E 7B F8 0F C5 B1 34 48 83 74 D3 9C 72 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 31 39 38 3F 74 65 72 6D 3D 32 82 01 57 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 30 3F 74 65 72 6D 3D 32 B0 01 4D B8 01 2E C8 01 FF 05 D8 01 4D E0 01 2E FA 01 59 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 31 30 34 30 34 30 30 32 39 30 2F 36 35 35 30 35 37 31 32 37 2D 32 32 33 33 36 33 38 33 34 32 2D 34 45 31 38 35 38 32 32 30 45 37 42 46 38 30 46 43 35 42 31 33 34 34 38 38 33 37 34 44 33 39 43 2F 34 30 30 3F 74 65 72 6D 3D 32 80 02 4D 88 02 2E 12 45 AA 02 42 50 03 60 00 68 00 9A 01 39 08 09 20 BF 50 80 01 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 08 01 90 04 80 80 80 10 B8 04 00 C0 04 00 12 06 4A 04 08 00 40 01 12 14 82 01 11 0A 09 48 69 6D 31 38 38 6D 6F 65 18 06 20 08 28 03 10 8A CA 9D A1 07 1A 00
if (!bot.firstLoginSucceed) return GroupMessageOrNull(null)
if (!bot.firstLoginSucceed) return null
val pbPushMsg = readProtoBuf(MsgOnlinePush.PbPushMsg.serializer())
val extraInfo: ImMsgBody.ExtraInfo? = pbPushMsg.msg.msgBody.richText.elems.firstOrNull { it.extraInfo != null }?.extraInfo
if (pbPushMsg.msg.msgHead.fromUin == bot.uin) {
return GroupMessageOrNull(null)
return null
}
val group = bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode)
// println(pbPushMsg.msg.msgBody.richText.contentToString())
val flags = extraInfo?.flags ?: 0
return GroupMessageOrNull(
GroupMessage(
bot = bot,
group = group,
senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard,
sender = group[pbPushMsg.msg.msgHead.fromUin],
message = pbPushMsg.msg.toMessageChain(),
permission = when {
flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
flags and 8 != 0 -> MemberPermission.OWNER
flags == 0 -> MemberPermission.MEMBER
else -> {
bot.logger.warning("判断群员权限失败")
MemberPermission.MEMBER
}
return GroupMessage(
bot = bot,
group = group,
senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard,
sender = group[pbPushMsg.msg.msgHead.fromUin],
message = pbPushMsg.msg.toMessageChain(),
permission = when {
flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
flags and 8 != 0 -> MemberPermission.OWNER
flags == 0 -> MemberPermission.MEMBER
else -> {
bot.logger.warning("判断群员权限失败")
MemberPermission.MEMBER
}
)
}
)
}
override suspend fun QQAndroidBot.handle(packet: GroupMessageOrNull, sequenceId: Int): OutgoingPacket? {
packet.delegate?.broadcast()
return null
}
}
internal object PbPushTransMsg : IncomingPacketFactory<Packet>("OnlinePush.PbPushTransMsg", "OnlinePush.RespPush") {
@ -156,10 +142,10 @@ internal class OnlinePush {
@UseExperimental(ExperimentalStdlibApi::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet {
val reqPushMsg = decodeUniPacket(OnlinePushPack.SvcReqPushMsg.serializer(), "req")
reqPushMsg.vMsgInfos.forEach { msgInfo: MsgInfo ->
msgInfo.vMsg!!.read {
// TODO: 2020/2/13 可能会同时收到多个事件. 使用 map 而不要直接 return
@Suppress("USELESS_CAST") // 不要信任 kotlin 类型推断
val packets: List<Packet> = reqPushMsg.vMsgInfos.mapNotNull { msgInfo: MsgInfo ->
msgInfo.vMsg!!.read {
when {
msgInfo.shMsgType.toInt() == 732 -> {
val group = bot.getGroup(this.readUInt().toLong())
@ -169,7 +155,7 @@ internal class OnlinePush {
3073 -> { // mute
val operatorUin = this.readUInt().toLong()
if (operatorUin == bot.uin) {
return NoPacket
return@mapNotNull null
}
val operator = group[operatorUin]
this.readUInt().toLong() // time
@ -177,42 +163,57 @@ internal class OnlinePush {
val target = this.readUInt().toLong()
val time = this.readInt()
return if (target == 0L) {
if (target == 0L) {
if (time == 0) {
GroupMuteAllEvent(
return@mapNotNull GroupMuteAllEvent(
origin = group.isMuteAll.also { group._muteAll = false },
new = false,
operator = operator,
group = group
)
) as Packet
} else {
GroupMuteAllEvent(
return@mapNotNull GroupMuteAllEvent(
origin = group.isMuteAll.also { group._muteAll = true },
new = true,
operator = operator,
group = group
)
) as Packet
}
} else {
return if (target == bot.uin) {
if (time == 0) {
BotUnmuteEvent(operator)
} else
BotMuteEvent(durationSeconds = time, operator = operator)
if (target == bot.uin) {
if (group._botMuteTimestamp != time) {
if (time == 0) {
group._botMuteTimestamp = 0
return@mapNotNull BotUnmuteEvent(operator) as Packet
} else {
group._botMuteTimestamp = time
return@mapNotNull BotMuteEvent(durationSeconds = time, operator = operator) as Packet
}
} else {
return@mapNotNull null
}
} else {
val member = group[target]
if (time == 0) {
MemberUnmuteEvent(operator = operator, member = member)
member as MemberImpl
if (member._muteTimestamp != time) {
if (time == 0) {
member._muteTimestamp = 0
return@mapNotNull MemberUnmuteEvent(member, operator) as Packet
} else {
member._muteTimestamp = time
return@mapNotNull MemberMuteEvent(member, time, operator) as Packet
}
} else {
MemberMuteEvent(operator = operator, member = member, durationSeconds = time)
return@mapNotNull null
}
}
}
}
3585 -> { // 匿名
3585 -> {
// 匿名
val operator = group[this.readUInt().toLong()]
val switch = this.readInt() == 0
return GroupAllowAnonymousChatEvent(
return@mapNotNull GroupAllowAnonymousChatEvent(
origin = group.isAnonymousChatEnabled.also { group._anonymousChat = switch },
new = switch,
operator = operator,
@ -225,7 +226,7 @@ internal class OnlinePush {
// println(dataBytes.toUHexString())
if (dataBytes[0].toInt() != 59) {
return GroupNameChangeEvent(
return@mapNotNull GroupNameChangeEvent(
origin = group.name.also { group._name = message },
new = message,
group = group,
@ -235,7 +236,7 @@ internal class OnlinePush {
//println(message + ":" + dataBytes.toUHexString())
when (message) {
"管理员已关闭群聊坦白说" -> {
return GroupAllowConfessTalkEvent(
return@mapNotNull GroupAllowConfessTalkEvent(
origin = group.isConfessTalkEnabled.also { group._confessTalk = false },
new = false,
group = group,
@ -243,7 +244,7 @@ internal class OnlinePush {
)
}
"管理员已开启群聊坦白说" -> {
return GroupAllowConfessTalkEvent(
return@mapNotNull GroupAllowConfessTalkEvent(
origin = group.isConfessTalkEnabled.also { group._confessTalk = true },
new = true,
group = group,
@ -252,7 +253,7 @@ internal class OnlinePush {
}
else -> {
bot.network.logger.debug { "Unknown server messages $message" }
return NoPacket
return@mapNotNull null
}
}
}
@ -263,6 +264,7 @@ internal class OnlinePush {
// }
else -> {
bot.network.logger.debug { "unknown group internal type $internalType , data: " + this.readBytes().toUHexString() + " " }
return@mapNotNull null
}
}
}
@ -270,18 +272,18 @@ internal class OnlinePush {
bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" }
// val content = msgInfo.vMsg.loadAs(OnlinePushPack.MsgType0x210.serializer())
// println(content.contentToString())
return@mapNotNull null
}
else -> {
bot.network.logger.debug { "unknown shtype ${msgInfo.shMsgType.toInt()}" }
return@mapNotNull null
}
}
}
}
return NoPacket
return MultiPacket(packets)
}
override suspend fun QQAndroidBot.handle(packet: Packet, sequenceId: Int): OutgoingPacket? {
return buildResponseUniPacket(client, sequenceId = sequenceId) {

View File

@ -47,7 +47,7 @@ internal class WtLogin {
ticket: String
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(2) // subCommand
writeShort(4) // count of TLVs
t193(ticket)
@ -64,7 +64,7 @@ internal class WtLogin {
captchaAnswer: String
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(2) // subCommand
writeShort(4) // count of TLVs
t2(captchaAnswer, captchaSign, 0)
@ -83,7 +83,7 @@ internal class WtLogin {
t402: ByteArray
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(20) // subCommand
writeShort(4) // count of TLVs, probably ignored by server?
t8(2052)
@ -103,7 +103,7 @@ internal class WtLogin {
client: QQAndroidClient
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId, unknownHex = "01 00 00 00 00 00 00 00 00 00 01 00") {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(8) // subCommand
writeShort(6) // count of TLVs, probably ignored by server?TODO
t8(2052)
@ -131,7 +131,7 @@ internal class WtLogin {
client: QQAndroidClient
): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) {
writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) {
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
writeShort(9) // subCommand
writeShort(17) // count of TLVs, probably ignored by server?
//writeShort(LoginType.PASSWORD.value.toShort())
@ -325,7 +325,7 @@ internal class WtLogin {
2 -> onSolveLoginCaptcha(tlvMap, bot)
160 /*-96*/ -> onUnsafeDeviceLogin(tlvMap)
204 /*-52*/ -> onSMSVerifyNeeded(tlvMap, bot)
else -> tlvMap[0x149]?.let { analysisTlv149(it) } ?: error("unknown login result type: $type")
else -> tlvMap[0x149]?.let { analysisTlv149(it) } ?: error("unknown login result type: $type, TLVMap = ${tlvMap.contentToString()}")
}
}

View File

@ -7,9 +7,13 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.utils
import net.mamoe.mirai.utils.md5
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic
/**

View File

@ -7,8 +7,14 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
inline class MacOrAndroidIdChangeFlag(val value: Long = 0) {
fun macChanged(): MacOrAndroidIdChangeFlag =
MacOrAndroidIdChangeFlag(this.value or 0x1)

View File

@ -7,11 +7,16 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.utils
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
/**
* Inline the block

View File

@ -7,10 +7,16 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:JvmName("Utils")
@file:JvmMultifileClass
package net.mamoe.mirai.qqandroid.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
fun Int.toIpV4AddressString(): String {
internal fun Int.toIpV4AddressString(): String {
@Suppress("NAME_SHADOWING")
var var0 = this.toLong() and 0xFFFFFFFF
return buildString {

View File

@ -8,8 +8,6 @@ plugins {
id("com.jfrog.bintray") version "1.8.4-jetbrains-3"
}
apply(from = rootProject.file("gradle/publish.gradle"))
val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext
val coroutinesVersion: String by rootProject.ext

View File

@ -1,14 +0,0 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai
actual object MiraiEnvironment {
actual val platform: Platform get() = Platform.ANDROID
}

View File

@ -0,0 +1,17 @@
package net.mamoe.mirai.event.internal
import java.util.concurrent.atomic.AtomicBoolean
internal actual class MiraiAtomicBoolean actual constructor(initial: Boolean) {
private val delegate: AtomicBoolean = AtomicBoolean(initial)
actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean {
return delegate.compareAndSet(expect, update)
}
actual var value: Boolean
get() = delegate.get()
set(value) {
delegate.set(value)
}
}

View File

@ -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
/**
* 验证码处理器
*/

View File

@ -9,8 +9,10 @@
package net.mamoe.mirai.utils.cryptor
import android.annotation.SuppressLint
import net.mamoe.mirai.utils.md5
import java.security.*
import java.security.spec.ECGenParameterSpec
import java.security.spec.X509EncodedKeySpec
import javax.crypto.KeyAgreement
@ -18,13 +20,13 @@ import javax.crypto.KeyAgreement
actual typealias ECDHPrivateKey = PrivateKey
actual typealias ECDHPublicKey = PublicKey
actual class ECDHKeyPair(
internal actual class ECDHKeyPairImpl(
private val delegate: KeyPair
) {
actual val privateKey: ECDHPrivateKey get() = delegate.private
actual val publicKey: ECDHPublicKey get() = delegate.public
) : ECDHKeyPair {
override val privateKey: ECDHPrivateKey get() = delegate.private
override val publicKey: ECDHPublicKey get() = delegate.public
actual val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
}
@Suppress("FunctionName")
@ -32,8 +34,41 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair())
actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual companion object {
@Suppress("ObjectPropertyName")
private var _isECDHAvailable: Boolean = false // because `runCatching` has no contract.
actual val isECDHAvailable: Boolean get() = _isECDHAvailable
init {
kotlin.runCatching {
@SuppressLint("PrivateApi")
val clazz = Class.forName(
"com.android.org.bouncycastle.jce.provider.BouncyCastleProvider",
true,
ClassLoader.getSystemClassLoader()
)
val providerName = clazz.getDeclaredField("PROVIDER_NAME").get(null) as String
if (Security.getProvider(providerName) != null) {
Security.removeProvider(providerName)
}
Security.addProvider(clazz.newInstance() as Provider)
generateKeyPair()
_isECDHAvailable = true
}.exceptionOrNull()?.let {
throw IllegalStateException("cannot init BouncyCastle", it)
}
_isECDHAvailable = false
}
actual fun generateKeyPair(): ECDHKeyPair {
return ECDHKeyPair(KeyPairGenerator.getInstance("ECDH").genKeyPair())
if (!isECDHAvailable) {
return ECDHKeyPair.DefaultStub
}
return ECDHKeyPairImpl(KeyPairGenerator.getInstance("ECDH")
.also { it.initialize(ECGenParameterSpec("secp192k1")) }
.genKeyPair())
}
actual fun calculateShareKey(

View File

@ -30,9 +30,16 @@ actual class PlatformSocket : Closeable {
private lateinit var socket: Socket
actual val isOpen: Boolean
get() = socket.isConnected
get() =
if (::socket.isInitialized)
socket.isConnected
else false
actual override fun close() = socket.close()
actual override fun close() {
if (::socket.isInitialized) {
socket.close()
}
}
@PublishedApi
internal lateinit var writeChannel: BufferedOutputStream

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE")
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "FunctionName", "NOTHING_TO_INLINE", "UnusedImport")
package net.mamoe.mirai
@ -35,7 +35,8 @@ import kotlin.jvm.JvmStatic
*
* : Bot 为全协程实现, 没有其他任务时若不使用 [join], 主线程将会退出.
*
* @see Contact
* @see Contact 联系人
* @see kotlinx.coroutines.isActive 判断 [Bot] 是否正常运行中. (在线, 且没有被 [close])
*/
@UseExperimental(MiraiInternalAPI::class)
abstract class Bot : CoroutineScope {
@ -195,7 +196,9 @@ abstract class Bot : CoroutineScope {
/**
* 登录, 或重新登录.
* 重新登录时不会再次拉取联系人列表.
* 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表.
*
* 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况.
*
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin]
*
@ -231,24 +234,19 @@ abstract class Bot : CoroutineScope {
// endregion
/**
* 关闭这个 [Bot], 停止一切相关活动. 所有引用都会被释放.
* 关闭这个 [Bot], 立即取消 [Bot] [kotlinx.coroutines.SupervisorJob].
* 之后 [kotlinx.coroutines.isActive] 将会返回 `false`.
*
* : 不可重新登录. 必须重新实例化一个 [Bot].
* **:** 不可重新登录. 必须重新实例化一个 [Bot].
*
* @param cause 原因. null 时视为正常关闭, null 时视为异常关闭
*
* @see closeAndJoin
* @see closeAndJoin 取消并 [Bot.join], 以确保 [Bot] 相关的活动被完全关闭
*/
abstract fun close(cause: Throwable? = null)
// region extensions
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("getFriend(this.toLong())"))
fun Int.qq(): QQ = getFriend(this.toLong())
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("getFriend(this)"))
fun Long.qq(): QQ = getFriend(this)
final override fun toString(): String {
return "Bot(${uin})"
}

View File

@ -14,7 +14,6 @@ package net.mamoe.mirai
import kotlinx.coroutines.*
import net.mamoe.mirai.event.Listener
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.BotReloginEvent
import net.mamoe.mirai.event.subscribeAlways
@ -73,11 +72,6 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
}
}
/**
* 可阻止事件广播
*/
abstract fun onEvent(event: BotEvent): Boolean
// region network
final override val network: N get() = _network
@ -89,21 +83,22 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
private val offlineListener: Listener<BotOfflineEvent> = this.subscribeAlways { event ->
when (event) {
is BotOfflineEvent.Dropped -> {
bot.logger.info("Connection dropped or lost by server, retrying login")
if (!_network.isActive) {
return@subscribeAlways
}
bot.logger.info("Connection dropped by server or lost, retrying login")
var lastFailedException: Throwable? = null
repeat(configuration.reconnectionRetryTimes) {
try {
network.relogin()
logger.info("Reconnected successfully")
return@subscribeAlways
} catch (e: Throwable) {
lastFailedException = e
tryNTimesOrException(configuration.reconnectionRetryTimes) { tryCount ->
if (tryCount != 0) {
delay(configuration.reconnectPeriodMillis)
}
}
if (lastFailedException != null) {
throw lastFailedException!!
network.relogin(event.cause)
logger.info("Reconnected successfully")
BotReloginEvent(bot, event.cause).broadcast()
return@subscribeAlways
}?.let {
logger.info("Cannot reconnect")
throw it
}
}
is BotOfflineEvent.Active -> {
@ -112,17 +107,21 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
} else {
" with exception: " + event.cause.message
}
bot.logger.info("Bot is closed manually$msg")
close(CancellationException(event.toString()))
bot.logger.info { "Bot is closed manually$msg" }
closeAndJoin(CancellationException(event.toString()))
}
is BotOfflineEvent.Force -> {
bot.logger.info("Connection occupied by another android device: ${event.message}")
close(ForceOfflineException(event.toString()))
bot.logger.info { "Connection occupied by another android device: ${event.message}" }
closeAndJoin(ForceOfflineException(event.toString()))
}
}
}
final override suspend fun login() = reinitializeNetworkHandler(null)
final override suspend fun login() {
logger.info("Logging in...")
reinitializeNetworkHandler(null)
logger.info("Login successful")
}
private suspend fun reinitializeNetworkHandler(
cause: Throwable?
@ -176,15 +175,19 @@ abstract class BotImpl<N : BotNetworkHandler> constructor(
@UseExperimental(MiraiInternalAPI::class)
override fun close(cause: Throwable?) {
if (!this.botJob.isActive) {
// already cancelled
return
}
kotlin.runCatching {
if (cause == null) {
this.botJob.cancel()
network.close()
this.botJob.complete()
offlineListener.complete()
offlineListener.cancel()
} else {
this.botJob.cancel(CancellationException("bot cancelled", cause))
network.close(cause)
this.botJob.completeExceptionally(cause)
offlineListener.completeExceptionally(cause)
offlineListener.cancel(CancellationException("bot cancelled", cause))
}
}
groups.delegate.clear()

View File

@ -1,25 +0,0 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai
/**
* 平台相关环境属性
*/
expect object MiraiEnvironment {
val platform: Platform
}
/**
* 可用平台列表
*/
enum class Platform {
ANDROID,
JVM
}

View File

@ -74,6 +74,16 @@ interface Contact : CoroutineScope {
* [QQ] 含义为一个独立的人, 可以是好友, 也可以是陌生人.
*/
override fun equals(other: Any?): Boolean
/**
* @return `bot.hashCode() * 31 + id.hashCode()`
*/
override fun hashCode(): Int
/**
* @return "QQ($id)" or "Group($id)" or "Member($id)"
*/
override fun toString(): String
}
suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain())

View File

@ -12,6 +12,7 @@
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI
@ -89,19 +90,19 @@ interface Group : Contact, CoroutineScope {
/**
* 机器人被禁言还剩余多少秒
*
* @see BotMuteEvent
* @see isBotMuted
* @see BotMuteEvent 机器人被禁言事件
* @see isBotMuted 判断机器人是否正在被禁言
*/
val botMuteRemaining: Int
/**
* 机器人在这个群里的权限
*
* **MiraiExperimentalAPI**: 在未来可能会被修改
* @see Group.checkBotPermission 检查 [Bot] 在这个群里的权限
* @see Group.checkBotPermissionOperator 要求 [Bot] 在这个群里的权限为 [管理员或群主][MemberPermission.isOperator]
*
* @see BotGroupPermissionChangeEvent
* @see BotGroupPermissionChangeEvent 机器人群员修改
*/
@MiraiExperimentalAPI
val botPermission: MemberPermission
@ -129,6 +130,7 @@ interface Group : Contact, CoroutineScope {
/**
* 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败
*/
@MiraiExperimentalAPI("还未支持")
suspend fun quit(): Boolean
/**

View File

@ -1,7 +1,7 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 在以下链接找到该许可证.
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
@ -29,33 +29,50 @@ interface Member : QQ, Contact {
/**
* 成员的权限, 动态更新.
*
* @see MemberPermissionChangeEvent 权限变更事件. 由群主或机器人的操作触发.
*/
val permission: MemberPermission
/**
* 群名片. 可能为空. 修改时将会触发事件
* 群名片. 可能为空.
*
* 管理员和群主都可修改任何人包括群主的群名片.
*
* 在修改时将会异步上传至服务器.
*
* @see [groupCardOrNick] 获取非空群名片或昵称
* @see [nameCardOrNick] 获取非空群名片或昵称
*
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时
*/
var nameCard: String
/**
* 群头衔
* 群头衔.
*
* 仅群主可以修改群头衔.
*
* 在修改时将会异步上传至服务器.
*
* @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件
* @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时
*/
var specialTitle: String
/**
* 禁言
* 被禁言剩余时长. 单位为秒.
*
* @see isMuted 判断改成员是否处于禁言状态
* @see mute 设置禁言
* @see unmute 取消禁言
*/
val muteTimeRemaining: Int
/**
* 禁言.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 机器人无权限时返回 `false`
@ -72,6 +89,8 @@ interface Member : QQ, Contact {
/**
* 解除禁言.
*
* 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言.
*
* @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/
@ -80,6 +99,8 @@ interface Member : QQ, Contact {
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@ -96,7 +117,14 @@ interface Member : QQ, Contact {
*
* [群名片][Member.nameCard] 不为空则返回群名片, 为空则返回 [QQ.nick]
*/
val Member.groupCardOrNick: String get() = this.nameCard.takeIf { it.isNotEmpty() } ?: this.nick
val Member.nameCardOrNick: String get() = this.nameCard.takeIf { it.isNotEmpty() } ?: this.nick
/**
* 判断改成员是否处于禁言状态.
*/
fun Member.isMuted(): Boolean {
return muteTimeRemaining != 0 && muteTimeRemaining != 0xFFFFFFFF.toInt()
}
@ExperimentalTime
suspend inline fun Member.mute(duration: Duration) {

View File

@ -9,6 +9,7 @@
package net.mamoe.mirai.contact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.MiraiExperimentalAPI
@ -68,7 +69,6 @@ inline fun Member.isAdministrator(): Boolean = this.permission.isAdministrator()
inline fun Member.isOperator(): Boolean = this.permission.isOperator()
/**
* 权限不足
*/
@ -77,6 +77,11 @@ expect class PermissionDeniedException : IllegalStateException {
constructor(message: String?)
}
/**
* 要求 [Bot] 在这个群里的权限为 [required], 否则抛出异常 [PermissionDeniedException]
*
* @throws PermissionDeniedException
*/
@UseExperimental(MiraiExperimentalAPI::class)
inline fun Group.checkBotPermission(
required: MemberPermission,
@ -89,6 +94,11 @@ inline fun Group.checkBotPermission(
}
}
/**
* 要求 [Bot] 在这个群里的权限为 [管理员或群主][MemberPermission.isOperator], 否则抛出异常 [PermissionDeniedException]
*
* @throws PermissionDeniedException
*/
@UseExperimental(MiraiExperimentalAPI::class)
inline fun Group.checkBotPermissionOperator(
lazyMessage: () -> String = {

View File

@ -1,19 +0,0 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.data
import net.mamoe.mirai.event.Event
/**
* 事件包. 可被监听.
*
* @see Event
*/
interface EventPacket : Event, Packet

View File

@ -17,4 +17,6 @@ interface MemberInfo : FriendInfo {
val permission: MemberPermission
val specialTitle: String
val muteTimestamp: Int
}

View File

@ -25,6 +25,8 @@ import net.mamoe.mirai.message.data.Message
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* 订阅来自所有 [Bot] 的所有联系人的消息事件. 联系人可以是任意群或任意好友或临时会话.
@ -33,7 +35,10 @@ import kotlin.contracts.contract
*/
@UseExperimental(ExperimentalContracts::class)
@MessageDsl
inline fun <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 {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
@ -42,7 +47,7 @@ inline fun <R> CoroutineScope.subscribeMessages(crossinline listeners: MessageSu
return MessageSubscribersBuilder { messageListener: MessageListener<MessagePacket<*, *>> ->
// subscribeAlways 即注册一个监听器. 这个监听器收到消息后就传递给 [listener]
// listener 即为 DSL 里 `contains(...) { }`, `startsWith(...) { }` 的代码块.
subscribeAlways {
subscribeAlways(coroutineContext) {
messageListener.invoke(this, this.message.toString())
// this.message.toString() 即为 messageListener 中 it 接收到的值
}
@ -56,12 +61,15 @@ inline fun <R> CoroutineScope.subscribeMessages(crossinline listeners: MessageSu
*/
@UseExperimental(ExperimentalContracts::class)
@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 {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
}
return MessageSubscribersBuilder<GroupMessage> { listener ->
subscribeAlways {
subscribeAlways(coroutineContext) {
listener(this, this.message.toString())
}
}.run(listeners)
@ -74,12 +82,15 @@ inline fun <R> CoroutineScope.subscribeGroupMessages(crossinline listeners: Mess
*/
@UseExperimental(ExperimentalContracts::class)
@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 {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
}
return MessageSubscribersBuilder<FriendMessage> { listener ->
subscribeAlways {
subscribeAlways(coroutineContext) {
listener(this, this.message.toString())
}
}.run(listeners)
@ -92,12 +103,15 @@ inline fun <R> CoroutineScope.subscribeFriendMessages(crossinline listeners: Mes
*/
@UseExperimental(ExperimentalContracts::class)
@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 {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
}
return MessageSubscribersBuilder<MessagePacket<*, *>> { listener ->
this.subscribeAlways {
this.subscribeAlways(coroutineContext) {
listener(this, this.message.toString())
}
}.run(listeners)
@ -106,16 +120,21 @@ inline fun <R> Bot.subscribeMessages(crossinline listeners: MessageSubscribersBu
/**
* 订阅来自这个 [Bot] 的所有群消息事件
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see CoroutineScope.incoming
*/
@UseExperimental(ExperimentalContracts::class)
@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 {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
}
return MessageSubscribersBuilder<GroupMessage> { listener ->
this.subscribeAlways {
this.subscribeAlways(coroutineContext) {
listener(this, this.message.toString())
}
}.run(listeners)
@ -128,12 +147,15 @@ inline fun <R> Bot.subscribeGroupMessages(crossinline listeners: MessageSubscrib
*/
@UseExperimental(ExperimentalContracts::class)
@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 {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
}
return MessageSubscribersBuilder<FriendMessage> { listener ->
this.subscribeAlways {
this.subscribeAlways(coroutineContext) {
listener(this, this.message.toString())
}
}.run(listeners)
@ -148,9 +170,12 @@ inline fun <R> Bot.subscribeFriendMessages(crossinline listeners: MessageSubscri
* @see subscribeMessages
* @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 {
subscribeAlways<E> {
subscribeAlways<E>(coroutineContext) {
send(this)
}
}

View File

@ -11,8 +11,6 @@
package net.mamoe.mirai.event
import net.mamoe.mirai.BotImpl
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.internal.broadcastInternal
import net.mamoe.mirai.utils.MiraiInternalAPI
@ -22,7 +20,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
* 若监听这个类, 监听器将会接收所有事件的广播.
*
* @see subscribeAlways
* @see subscribeWhile
* @see subscribeOnce
*
* @see subscribeMessages
*
@ -73,9 +71,6 @@ suspend fun <E : Event> E.broadcast(): E = apply {
if (this is BroadcastControllable && !this.shouldBroadcast) {
return@apply
}
if (this is BotEvent && !(this.bot as BotImpl<*>).onEvent(this)) {
return@apply
}
this@broadcast.broadcastInternal() // inline, no extra cost
}

View File

@ -14,10 +14,16 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.internal.Handler
import net.mamoe.mirai.event.internal.subscribeInternal
import net.mamoe.mirai.utils.MiraiInternalAPI
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
/*
* 该文件为所有的订阅事件的方法.
@ -68,15 +74,16 @@ interface Listener<in E : Event> : CompletableJob {
* `runBlocking` 不会结束, 也就是下一行 `foo()` 不会被执行. 直到监听时创建的 `Listener` 被停止.
*
*
* 要创建一个全局都存在的监听, 即守护协程, 请在 [GlobalScope] 下调用本函数:
* 要创建一个仅在某个机器人在线时的监听, 请在 [Bot] 下调用本函数 (因为 [Bot] 也实现 [CoroutineScope]).
* 这种方式创建的监听会自动筛选 [Bot].
* ```kotlin
* GlobalScope.subscribe<Event> { /* 一些处理 */ }
* bot1.subscribe<BotEvent> { /* 只会处理来自 bot1 的事件 */ }
* ```
*
*
* 要创建一个仅在某个机器人在线时的监听, 请在 [Bot] 下调用本函数 (因为 [Bot] 也实现 [CoroutineScope]):
* 要创建一个全局都存在的监听, 即守护协程, 请在 [GlobalScope] 下调用本函数:
* ```kotlin
* bot.subscribe<Subscribe> { /* 一些处理 */ }
* GlobalScope.subscribe<Event> { /* 会收到来自全部 Bot 的事件和与 Bot 不相关的事件 */ }
* ```
*
*
@ -86,122 +93,137 @@ interface Listener<in E : Event> : CompletableJob {
* [this] 没有 [CoroutineExceptionHandler], 则在事件广播方的 [CoroutineExceptionHandler] 处理
* 若均找不到, 则会触发 logger warning.
* - 事件处理时抛出异常不会停止监听器.
* - 建议在事件处理中, [handler] 里处理异常, 或在 [this] 指定 [CoroutineExceptionHandler].
* - 建议在事件处理中 ( [handler] ) 处理异常,
* 或在 [this] [CoroutineScope.coroutineContext] 中添加 [CoroutineExceptionHandler].
*
*
* **注意:** 事件处理是 `suspend` , 严格控制 JVM 阻塞方法的使用. 若致事件处理阻塞, 则会导致一些逻辑无法进行.
* **注意:** 事件处理是 `suspend` , 规范处理 JVM 阻塞方法.
*
* // TODO: 2020/2/13 在 bot 下监听时同时筛选对应 bot 实例
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see subscribeMessages 监听消息 DSL
* @see subscribeGroupMessages 监听群消息 DSL
* @see subscribeAlways 一直监听
* @see subscribeOnce 只监听一次
*
* @see subscribeMessages 监听消息 DSL
* @see subscribeGroupMessages 监听群消息 DSL
* @see subscribeFriendMessages 监听好友消息 DSL
*/
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event> CoroutineScope.subscribe(crossinline handler: suspend E.(E) -> ListeningStatus): Listener<E> =
E::class.subscribeInternal(Handler { it.handler(it); })
inline fun <reified E : Event> CoroutineScope.subscribe(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline handler: suspend E.(E) -> ListeningStatus
): Listener<E> =
E::class.subscribeInternal(Handler(coroutineContext) { it.handler(it); })
/**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Event.broadcast] , [listener] 都会被执行.
*
* 仅当 [Listener.complete] [Listener.cancel] 时结束.
* 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see subscribe 获取更多说明
*/
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event> CoroutineScope.subscribeAlways(crossinline listener: suspend E.(E) -> Unit): Listener<E> =
E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.LISTENING })
@UseExperimental(MiraiInternalAPI::class, ExperimentalContracts::class)
inline fun <reified E : Event> CoroutineScope.subscribeAlways(
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] 及其子类事件.
* 仅在第一次 [事件广播][Event.broadcast] , [listener] 会被执行.
*
* 在这之前, 可通过 [Listener.complete] 来停止监听.
* 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see subscribe 获取更多说明
*/
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event> CoroutineScope.subscribeOnce(crossinline listener: suspend E.(E) -> Unit): Listener<E> =
E::class.subscribeInternal(Handler { it.listener(it); ListeningStatus.STOPPED })
inline fun <reified E : Event> CoroutineScope.subscribeOnce(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline listener: suspend E.(E) -> Unit
): Listener<E> =
E::class.subscribeInternal(Handler(coroutineContext) { it.listener(it); ListeningStatus.STOPPED })
//
// 以下为带筛选 Bot 的监听
//
/**
* 在指定的 [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Event.broadcast] , [listener] 都会被执行, 直到 [listener] 的返回值 [equals] [valueIfStop]
* [Bot] [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Event.broadcast] , [handler] 都会被执行,
* [handler] 返回 [ListeningStatus.STOPPED] 时停止监听
*
* 可在任意时刻通过 [Listener.complete] 来停止监听.
* 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see subscribe 获取更多说明
*/
@JvmName("subscribeAlwaysForBot")
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event, T> CoroutineScope.subscribeUntil(valueIfStop: T, crossinline listener: suspend E.(E) -> T): Listener<E> =
E::class.subscribeInternal(Handler { if (it.listener(it) == valueIfStop) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
inline fun <reified E : BotEvent> Bot.subscribe(
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] 及其子类事件.
* 每当 [事件广播][Event.broadcast] , [listener] 都会被执行,
* 如果 [listener] 的返回值 [equals] [valueIfContinue], 则继续监听, 否则停止
* [Bot] [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 每当 [事件广播][Event.broadcast] , [listener] 都会被执行.
*
* 可在任意时刻通过 [Listener.complete] 来停止监听.
* 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see subscribe 获取更多说明
*/
@JvmName("subscribeAlwaysForBot1")
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : Event, T> CoroutineScope.subscribeWhile(valueIfContinue: T, crossinline listener: suspend E.(E) -> T): Listener<E> =
E::class.subscribeInternal(Handler { if (it.listener(it) != valueIfContinue) ListeningStatus.STOPPED else ListeningStatus.LISTENING })
// endregion
// region ListenerBuilder DSL
/*
/**
* 监听构建器. 可同时进行多种方式的监听
*
* ```kotlin
* FriendMessageEvent.subscribe {
* always{
* it.reply("永远发生")
* }
*
* untilFalse {
* it.reply("你发送了 ${it.event}")
* it.event eq "停止"
* }
* }
* ```
*/
@ListenersBuilderDsl
@Suppress("MemberVisibilityCanBePrivate", "unused")
inline class ListenerBuilder<out E : Event>(
@PublishedApi internal inline val handlerConsumer: CoroutineCoroutineScope.(Listener<E>) -> Unit
) {
fun CoroutineCoroutineScope.handler(listener: suspend E.(E) -> ListeningStatus) {
handlerConsumer(Handler { it.listener(it) })
}
fun CoroutineCoroutineScope.always(listener: suspend E.(E) -> Unit) = handler { listener(it); ListeningStatus.LISTENING }
fun <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 }
inline fun <reified E : BotEvent> Bot.subscribeAlways(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline listener: suspend E.(E) -> Unit
): Listener<E> {
return E::class.subscribeInternal(Handler(coroutineContext) { if (it.bot === this) it.listener(it); ListeningStatus.LISTENING })
}
@DslMarker
annotation class ListenersBuilderDsl
*/
/**
* [Bot] [CoroutineScope] 下订阅所有 [E] 及其子类事件.
* 仅在第一次 [事件广播][Event.broadcast] , [listener] 会被执行.
*
* 可在任意时候通过 [Listener.complete] 来主动停止监听.
* [Bot] 被关闭后事件监听会被 [取消][Listener.cancel].
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see subscribe 获取更多说明
*/
@JvmName("subscribeOnceForBot2")
@UseExperimental(MiraiInternalAPI::class)
inline fun <reified E : BotEvent> Bot.subscribeOnce(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
noinline listener: suspend E.(E) -> Unit
): Listener<E> =
E::class.subscribeInternal(Handler(coroutineContext) {
if (it.bot === this) {
it.listener(it)
ListeningStatus.STOPPED
} else ListeningStatus.LISTENING
})
// endregion

View File

@ -7,6 +7,8 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.event.events
import net.mamoe.mirai.Bot
@ -57,7 +59,7 @@ sealed class BotOfflineEvent : BotEvent {
/**
* 被服务器断开或因网络问题而掉线
*/
data class Dropped(override val bot: Bot) : BotOfflineEvent(), Packet, BotPassiveEvent
data class Dropped(override val bot: Bot, val cause: Throwable?) : BotOfflineEvent(), Packet, BotPassiveEvent
}
/**
@ -194,7 +196,7 @@ data class GroupNameChangeEvent(
override val origin: String,
override val new: String,
override val group: Group,
val isByBot: Boolean
val isByBot: Boolean // 无法获取 operator
) : GroupSettingChangeEvent<String>, Packet
/**
@ -210,6 +212,8 @@ data class GroupEntranceAnnouncementChangeEvent(
val operator: Member?
) : GroupSettingChangeEvent<String>, Packet
val GroupEntranceAnnouncementChangeEvent.isByBot: Boolean get() = operator != null
/**
* "全员禁言" 功能状态改变. 此事件广播前修改就已经完成.
@ -224,6 +228,8 @@ data class GroupMuteAllEvent(
val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet
val GroupMuteAllEvent.isByBot: Boolean get() = operator != null
/**
* "匿名聊天" 功能状态改变. 此事件广播前修改就已经完成.
*/
@ -237,6 +243,8 @@ data class GroupAllowAnonymousChatEvent(
val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet
val GroupAllowAnonymousChatEvent.isByBot: Boolean get() = operator != null
/**
* "坦白说" 功能状态改变. 此事件广播前修改就已经完成.
*/
@ -260,6 +268,8 @@ data class GroupAllowMemberInviteEvent(
val operator: Member?
) : GroupSettingChangeEvent<Boolean>, Packet
val GroupAllowMemberInviteEvent.isByBot: Boolean get() = operator != null
// endregion
@ -293,6 +303,8 @@ sealed class MemberLeaveEvent : GroupMemberEvent {
data class Quit(override val member: Member) : MemberLeaveEvent()
}
val MemberLeaveEvent.Kick.isByBot: Boolean get() = operator != null
// endregion
// region 名片和头衔
@ -319,6 +331,8 @@ data class MemberCardChangeEvent(
val operator: Member?
) : GroupMemberEvent
val MemberCardChangeEvent.isByBot: Boolean get() = operator != null
/**
* 群头衔改动. 一定为群主操作
*/
@ -333,9 +347,18 @@ data class MemberSpecialTitleChangeEvent(
*/
val new: String,
override val member: Member
override val member: Member,
/**
* 操作人.
* 不为 null 时一定为群主. 可能与 [member] 引用相同, 此时为群员自己修改.
* null 时则是机器人操作.
*/
val operator: Member?
) : GroupMemberEvent
val MemberSpecialTitleChangeEvent.isByBot: Boolean get() = operator != null
// endregion
@ -367,6 +390,8 @@ data class MemberMuteEvent(
val operator: Member?
) : GroupMemberEvent, Packet
val MemberMuteEvent.isByBot: Boolean get() = operator != null
/**
* 群成员被取消禁言事件. 被禁言的成员都不可能是机器人本人
*/
@ -378,6 +403,8 @@ data class MemberUnmuteEvent(
val operator: Member?
) : GroupMemberEvent, Packet
val MemberUnmuteEvent.isByBot: Boolean get() = operator != null
// endregion
// endregion

View File

@ -9,7 +9,6 @@
package net.mamoe.mirai.event.internal
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.EventDisabled
@ -32,8 +31,12 @@ fun <L : Listener<E>, E : Event> KClass<out E>.subscribeInternal(listener: L): L
@PublishedApi
@Suppress("FunctionName")
internal fun <E : Event> CoroutineScope.Handler(handler: suspend (E) -> ListeningStatus): Handler<E> {
return Handler(coroutineContext[Job], coroutineContext, handler)
internal fun <E : Event> CoroutineScope.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()
@ -77,7 +80,32 @@ internal class Handler<in E : Event>
*/
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].
@ -88,16 +116,8 @@ internal object EventListenerManager {
private val registries = LockFreeLinkedList<Registry<*>>()
private val lock = atomic(false)
private fun setLockValue(value: Boolean) {
lock.value = value
}
@Suppress("BooleanLiteralArgument")
private fun trySetLockTrue(): Boolean {
return lock.compareAndSet(false, true)
}
// 不要用 atomicfu. 在 publish 后会出现 VerifyError
private val lock: MiraiAtomicBoolean = MiraiAtomicBoolean(false)
@Suppress("UNCHECKED_CAST", "BooleanLiteralArgument")
internal tailrec fun <E : Event> get(clazz: KClass<out E>): EventListeners<E> {
@ -106,11 +126,11 @@ internal object EventListenerManager {
return it.listeners as EventListeners<E>
}
}
if (trySetLockTrue()) {
val registry = Registry(clazz, EventListeners())
if (lock.compareAndSet(false, true)) {
val registry = Registry(clazz as KClass<E>, EventListeners(clazz))
registries.addLast(registry)
setLockValue(false)
return registry.listeners as EventListeners<E>
lock.value = false
return registry.listeners
}
return get(clazz)
}
@ -123,19 +143,10 @@ internal suspend inline fun Event.broadcastInternal() {
EventLogger.info { "Event broadcast: $this" }
callAndRemoveIfRequired(this::class.listeners())
var supertypes = this::class.supertypes
while (true) {
val superSubscribableType = supertypes.firstOrNull {
it.classifier as? KClass<out Event> != null
}
superSubscribableType?.let {
callAndRemoveIfRequired((it.classifier as KClass<out Event>).listeners())
}
supertypes = (superSubscribableType?.classifier as? KClass<*>)?.supertypes ?: return
val listeners = this::class.listeners()
callAndRemoveIfRequired(listeners)
listeners.supertypes.forEach {
callAndRemoveIfRequired(it.listeners())
}
}

View File

@ -13,7 +13,6 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiInternalAPI
class FriendMessage(
bot: Bot,

View File

@ -14,7 +14,6 @@ import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.message.data.At
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.getValue
@ -38,7 +37,6 @@ class GroupMessage(
override val subject: Group get() = group
inline fun At.member(): Member = group[this.target]
inline fun Long.member(): Member = group[this]

View File

@ -19,7 +19,7 @@ import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.EventPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.*
@ -37,7 +37,7 @@ expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot)
*/ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构
@Suppress("NOTHING_TO_INLINE")
@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) :
*/
inline fun QQ.at(): At = At(this as? Member ?: error("`QQ.at` can only be used in GroupMessage"))
inline fun At.member(): Member = (this@MessagePacketBase as? GroupMessage)?.group?.get(this.target) ?: error("`At.member` can only be used in GroupMessage")
// endregion
// region 下载图片

View File

@ -15,7 +15,7 @@
package net.mamoe.mirai.message.data
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.groupCardOrNick
import net.mamoe.mirai.contact.nameCardOrNick
import net.mamoe.mirai.utils.MiraiInternalAPI
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
@ -28,7 +28,7 @@ import kotlin.jvm.JvmName
*/
class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message {
@UseExperimental(MiraiInternalAPI::class)
constructor(member: Member) : this(member.id, "@${member.groupCardOrNick}")
constructor(member: Member) : this(member.id, "@${member.nameCardOrNick}")
override fun toString(): String = display

View File

@ -14,309 +14,161 @@ package net.mamoe.mirai.message.data
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic
/**
* QQ 自带表情
*/
inline class Face(val id: FaceId) : Message {
override fun toString(): String = "[face${id.value}]"
class Face(val id: Int) : Message {
override fun toString(): String = "[mirai:face$id]"
companion object Key : Message.Key<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 {
return other is Face && other.id == this.id
}
}
/**
* @author LamGC
*/
@Suppress("SpellCheckingInspection", "unused")
@UseExperimental(ExperimentalUnsignedTypes::class)
inline class FaceId constructor(inline val value: UByte) {
companion object {
@JvmStatic
val unknown: FaceId = FaceId(0xffu)
@JvmStatic
val jingya: FaceId = FaceId(0u)
@JvmStatic
val piezui: FaceId = FaceId(1u)
@JvmStatic
val se: FaceId = FaceId(2u)
@JvmStatic
val fadai: FaceId = FaceId(3u)
@JvmStatic
val deyi: FaceId = FaceId(4u)
@JvmStatic
val liulei: FaceId = FaceId(5u)
@JvmStatic
val haixiu: FaceId = FaceId(6u)
@JvmStatic
val bizui: FaceId = FaceId(7u)
@JvmStatic
val shui: FaceId = FaceId(8u)
@JvmStatic
val daku: FaceId = FaceId(9u)
@JvmStatic
val ganga: FaceId = FaceId(10u)
@JvmStatic
val fanu: FaceId = FaceId(11u)
@JvmStatic
val tiaopi: FaceId = FaceId(12u)
@JvmStatic
val ciya: FaceId = FaceId(13u)
@JvmStatic
val weixiao: FaceId = FaceId(14u)
@JvmStatic
val nanguo: FaceId = FaceId(15u)
@JvmStatic
val ku: FaceId = FaceId(16u)
@JvmStatic
val zhuakuang: FaceId = FaceId(18u)
@JvmStatic
val tu: FaceId = FaceId(19u)
@JvmStatic
val touxiao: FaceId = FaceId(20u)
@JvmStatic
val keai: FaceId = FaceId(21u)
@JvmStatic
val baiyan: FaceId = FaceId(22u)
@JvmStatic
val aoman: FaceId = FaceId(23u)
@JvmStatic
val ji_e: FaceId = FaceId(24u)
@JvmStatic
val kun: FaceId = FaceId(25u)
@JvmStatic
val jingkong: FaceId = FaceId(26u)
@JvmStatic
val liuhan: FaceId = FaceId(27u)
@JvmStatic
val hanxiao: FaceId = FaceId(28u)
@JvmStatic
val dabing: FaceId = FaceId(29u)
@JvmStatic
val fendou: FaceId = FaceId(30u)
@JvmStatic
val zhouma: FaceId = FaceId(31u)
@JvmStatic
val yiwen: FaceId = FaceId(32u)
@JvmStatic
val yun: FaceId = FaceId(34u)
@JvmStatic
val zhemo: FaceId = FaceId(35u)
@JvmStatic
val shuai: FaceId = FaceId(36u)
@JvmStatic
val kulou: FaceId = FaceId(37u)
@JvmStatic
val qiaoda: FaceId = FaceId(38u)
@JvmStatic
val zaijian: FaceId = FaceId(39u)
@JvmStatic
val fadou: FaceId = FaceId(41u)
@JvmStatic
val aiqing: FaceId = FaceId(42u)
@JvmStatic
val tiaotiao: FaceId = FaceId(43u)
@JvmStatic
val zhutou: FaceId = FaceId(46u)
@JvmStatic
val yongbao: FaceId = FaceId(49u)
@JvmStatic
val dan_gao: FaceId = FaceId(53u)
@JvmStatic
val shandian: FaceId = FaceId(54u)
@JvmStatic
val zhadan: FaceId = FaceId(55u)
@JvmStatic
val dao: FaceId = FaceId(56u)
@JvmStatic
val zuqiu: FaceId = FaceId(57u)
@JvmStatic
val bianbian: FaceId = FaceId(59u)
@JvmStatic
val kafei: FaceId = FaceId(60u)
@JvmStatic
val fan: FaceId = FaceId(61u)
@JvmStatic
val meigui: FaceId = FaceId(63u)
@JvmStatic
val diaoxie: FaceId = FaceId(64u)
@JvmStatic
val aixin: FaceId = FaceId(66u)
@JvmStatic
val xinsui: FaceId = FaceId(67u)
@JvmStatic
val liwu: FaceId = FaceId(69u)
@JvmStatic
val taiyang: FaceId = FaceId(74u)
@JvmStatic
val yueliang: FaceId = FaceId(75u)
@JvmStatic
val qiang: FaceId = FaceId(76u)
@JvmStatic
val ruo: FaceId = FaceId(77u)
@JvmStatic
val woshou: FaceId = FaceId(78u)
@JvmStatic
val shengli: FaceId = FaceId(79u)
@JvmStatic
val feiwen: FaceId = FaceId(85u)
@JvmStatic
val naohuo: FaceId = FaceId(86u)
@JvmStatic
val xigua: FaceId = FaceId(89u)
@JvmStatic
val lenghan: FaceId = FaceId(96u)
@JvmStatic
val cahan: FaceId = FaceId(97u)
@JvmStatic
val koubi: FaceId = FaceId(98u)
@JvmStatic
val guzhang: FaceId = FaceId(99u)
@JvmStatic
val qiudale: FaceId = FaceId(100u)
@JvmStatic
val huaixiao: FaceId = FaceId(101u)
@JvmStatic
val zuohengheng: FaceId = FaceId(102u)
@JvmStatic
val youhengheng: FaceId = FaceId(103u)
@JvmStatic
val haqian: FaceId = FaceId(104u)
@JvmStatic
val bishi: FaceId = FaceId(105u)
@JvmStatic
val weiqu: FaceId = FaceId(106u)
@JvmStatic
val kuaikule: FaceId = FaceId(107u)
@JvmStatic
val yinxian: FaceId = FaceId(108u)
@JvmStatic
val qinqin: FaceId = FaceId(109u)
@JvmStatic
val xia: FaceId = FaceId(110u)
@JvmStatic
val kelian: FaceId = FaceId(111u)
@JvmStatic
val caidao: FaceId = FaceId(112u)
@JvmStatic
val pijiu: FaceId = FaceId(113u)
@JvmStatic
val lanqiu: FaceId = FaceId(114u)
@JvmStatic
val pingpang: FaceId = FaceId(115u)
@JvmStatic
val shiai: FaceId = FaceId(116u)
@JvmStatic
val piaochong: FaceId = FaceId(117u)
@JvmStatic
val baoquan: FaceId = FaceId(118u)
@JvmStatic
val gouyin: FaceId = FaceId(119u)
@JvmStatic
val quantou: FaceId = FaceId(120u)
@JvmStatic
val chajin: FaceId = FaceId(121u)
@JvmStatic
val aini: FaceId = FaceId(122u)
@JvmStatic
val bu: FaceId = FaceId(123u)
@JvmStatic
val hao: FaceId = FaceId(124u)
@JvmStatic
val zhuanquan: FaceId = FaceId(125u)
@JvmStatic
val ketou: FaceId = FaceId(126u)
@JvmStatic
val huitou: FaceId = FaceId(127u)
@JvmStatic
val tiaosheng: FaceId = FaceId(128u)
@JvmStatic
val huishou: FaceId = FaceId(129u)
@JvmStatic
val jidong: FaceId = FaceId(130u)
@JvmStatic
val jiewu: FaceId = FaceId(131u)
@JvmStatic
val xianwen: FaceId = FaceId(132u)
@JvmStatic
val zuotaiji: FaceId = FaceId(133u)
@JvmStatic
val youtaiji: FaceId = FaceId(134u)
@JvmStatic
val shuangxi: FaceId = FaceId(136u)
@JvmStatic
val bianpao: FaceId = FaceId(137u)
@JvmStatic
val denglong: FaceId = FaceId(138u)
@JvmStatic
val facai: FaceId = FaceId(139u)
@JvmStatic
val K_ge: FaceId = FaceId(140u)
@JvmStatic
val gouwu: FaceId = FaceId(141u)
@JvmStatic
val youjian: FaceId = FaceId(142u)
@JvmStatic
val shuai_qi: FaceId = FaceId(143u)
@JvmStatic
val hecai: FaceId = FaceId(144u)
@JvmStatic
val qidao: FaceId = FaceId(145u)
@JvmStatic
val baojin: FaceId = FaceId(146u)
@JvmStatic
val bangbangtang: FaceId = FaceId(147u)
@JvmStatic
val he_nai: FaceId = FaceId(148u)
@JvmStatic
val xiamian: FaceId = FaceId(149u)
@JvmStatic
val xiangjiao: FaceId = FaceId(150u)
@JvmStatic
val feiji: FaceId = FaceId(151u)
@JvmStatic
val kaiche: FaceId = FaceId(152u)
@JvmStatic
val gaotiezuochetou: FaceId = FaceId(153u)
@JvmStatic
val chexiang: FaceId = FaceId(154u)
@JvmStatic
val gaotieyouchetou: FaceId = FaceId(155u)
@JvmStatic
val duoyun: FaceId = FaceId(156u)
@JvmStatic
val xiayu: FaceId = FaceId(157u)
@JvmStatic
val chaopiao: FaceId = FaceId(158u)
@JvmStatic
val xiongmao: FaceId = FaceId(159u)
@JvmStatic
val dengpao: FaceId = FaceId(160u)
@JvmStatic
val fengche: FaceId = FaceId(161u)
@JvmStatic
val naozhong: FaceId = FaceId(162u)
@JvmStatic
val dasan: FaceId = FaceId(163u)
@JvmStatic
val caiqiu: FaceId = FaceId(164u)
@JvmStatic
val zuanjie: FaceId = FaceId(165u)
@JvmStatic
val shafa: FaceId = FaceId(166u)
@JvmStatic
val zhijin: FaceId = FaceId(167u)
@JvmStatic
val yao: FaceId = FaceId(168u)
@JvmStatic
val shouqiang: FaceId = FaceId(169u)
@JvmStatic
val qingwa: FaceId = FaceId(170u)
}
override fun toString(): String = "$FaceId($value)"
}
}

View File

@ -47,7 +47,7 @@ sealed class Image : Message {
abstract val imageId: String
final override fun toString(): String {
return "[image::$imageId]"
return "[mirai:$imageId]"
}
final override fun eq(other: Message): Boolean {

View File

@ -57,7 +57,7 @@ interface 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] 比较

View File

@ -38,6 +38,7 @@ import kotlin.reflect.KProperty
interface MessageChain : Message, MutableList<Message> {
// region Message override
override operator fun contains(sub: String): Boolean
override fun followedBy(tail: Message): MessageChain
// endregion
@ -67,6 +68,36 @@ interface MessageChain : Message, MutableList<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]
*/

View File

@ -31,6 +31,11 @@ interface MessageSource : Message {
*/
val messageUid: Long
/**
* 发送时间, 单位为秒
*/
val time: Long
/**
* 发送人号码
*/

View File

@ -68,7 +68,7 @@ abstract class BotNetworkHandler : CoroutineScope {
*/
@Suppress("SpellCheckingInspection")
@MiraiInternalAPI
abstract suspend fun relogin()
abstract suspend fun relogin(cause: Throwable? = null)
/**
* 初始化获取好友列表等值.

View File

@ -70,6 +70,13 @@ interface MiraiLogger {
*/
val identity: String?
/**
* 获取 [MiraiLogger] 是否已开启
*
* [MiraiLoggerWithSwitch] 可控制开关外, 其他的所有 [MiraiLogger] 均一直开启.
*/
val isEnabled: Boolean
/**
* 随从. this 中调用所有方法后都应继续往 [follower] 传递调用.
* [follower] 的存在可以让一次日志被多个日志记录器记录.
@ -151,43 +158,43 @@ interface MiraiLogger {
inline fun MiraiLogger.verbose(lazyMessage: () -> String) {
if (this is MiraiLoggerWithSwitch && switch) verbose(lazyMessage())
if (isEnabled) verbose(lazyMessage())
}
inline fun MiraiLogger.verbose(lazyMessage: () -> String, e: Throwable?) {
if (this is MiraiLoggerWithSwitch && switch) verbose(lazyMessage(), e)
if (isEnabled) verbose(lazyMessage(), e)
}
inline fun MiraiLogger.debug(lazyMessage: () -> String?) {
if (this is MiraiLoggerWithSwitch && switch) debug(lazyMessage())
if (isEnabled) debug(lazyMessage())
}
inline fun MiraiLogger.debug(lazyMessage: () -> String?, e: Throwable?) {
if (this is MiraiLoggerWithSwitch && switch) debug(lazyMessage(), e)
if (isEnabled) debug(lazyMessage(), e)
}
inline fun MiraiLogger.info(lazyMessage: () -> String?) {
if (this is MiraiLoggerWithSwitch && switch) info(lazyMessage())
if (isEnabled) info(lazyMessage())
}
inline fun MiraiLogger.info(lazyMessage: () -> String?, e: Throwable?) {
if (this is MiraiLoggerWithSwitch && switch) info(lazyMessage(), e)
if (isEnabled) info(lazyMessage(), e)
}
inline fun MiraiLogger.warning(lazyMessage: () -> String?) {
if (this is MiraiLoggerWithSwitch && switch) warning(lazyMessage())
if (isEnabled) warning(lazyMessage())
}
inline fun MiraiLogger.warning(lazyMessage: () -> String?, e: Throwable?) {
if (this is MiraiLoggerWithSwitch && switch) warning(lazyMessage(), e)
if (isEnabled) warning(lazyMessage(), e)
}
inline fun MiraiLogger.error(lazyMessage: () -> String?) {
if (this is MiraiLoggerWithSwitch && switch) error(lazyMessage())
if (isEnabled) error(lazyMessage())
}
inline fun MiraiLogger.error(lazyMessage: () -> String?, e: Throwable?) {
if (this is MiraiLoggerWithSwitch && switch) error(lazyMessage(), e)
if (isEnabled) error(lazyMessage(), e)
}
/**
@ -268,7 +275,7 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg
@PublishedApi
internal var switch: Boolean = default
val isEnabled: Boolean get() = switch
override val isEnabled: Boolean get() = switch
fun enable() {
switch = true
@ -278,16 +285,16 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg
switch = false
}
override fun verbose0(message: String?) = if (switch) delegate.verbose(message) else Unit
override fun verbose0(message: String?, e: Throwable?) = if (switch) delegate.verbose(message, e) else Unit
override fun debug0(message: String?) = if (switch) delegate.debug(message) else Unit
override fun debug0(message: String?, e: Throwable?) = if (switch) delegate.debug(message, e) else Unit
override fun info0(message: String?) = if (switch) delegate.info(message) else Unit
override fun info0(message: String?, e: Throwable?) = if (switch) delegate.info(message, e) else Unit
override fun warning0(message: String?) = if (switch) delegate.warning(message) else Unit
override fun warning0(message: String?, e: Throwable?) = if (switch) delegate.warning(message, e) else Unit
override fun error0(message: String?) = if (switch) delegate.error(message) else Unit
override fun error0(message: String?, e: Throwable?) = if (switch) delegate.error(message, e) else Unit
override fun verbose0(message: String?) = delegate.verbose(message)
override fun verbose0(message: String?, e: Throwable?) = delegate.verbose(message, e)
override fun debug0(message: String?) = delegate.debug(message)
override fun debug0(message: String?, e: Throwable?) = delegate.debug(message, e)
override fun info0(message: String?) = delegate.info(message)
override fun info0(message: String?, e: Throwable?) = delegate.info(message, e)
override fun warning0(message: String?) = delegate.warning(message)
override fun warning0(message: String?, e: Throwable?) = delegate.warning(message, e)
override fun error0(message: String?) = delegate.error(message)
override fun error0(message: String?, e: Throwable?) = delegate.error(message, e)
}
/**
@ -298,54 +305,65 @@ class MiraiLoggerWithSwitch internal constructor(private val delegate: MiraiLogg
* 在定义 logger 变量时, 请一直使用 [MiraiLogger] 或者 [MiraiLoggerWithSwitch].
*/
abstract class MiraiLoggerPlatformBase : MiraiLogger {
override val isEnabled: Boolean get() = true
final override var follower: MiraiLogger? = null
final override fun verbose(message: String?) {
if (!isEnabled) return
follower?.verbose(message)
verbose0(message)
}
final override fun verbose(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.verbose(message, e)
verbose0(message, e)
}
final override fun debug(message: String?) {
if (!isEnabled) return
follower?.debug(message)
debug0(message)
}
final override fun debug(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.debug(message, e)
debug0(message, e)
}
final override fun info(message: String?) {
if (!isEnabled) return
follower?.info(message)
info0(message)
}
final override fun info(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.info(message, e)
info0(message, e)
}
final override fun warning(message: String?) {
if (!isEnabled) return
follower?.warning(message)
warning0(message)
}
final override fun warning(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.warning(message, e)
warning0(message, e)
}
final override fun error(message: String?) {
if (!isEnabled) return
follower?.error(message)
error0(message)
}
final override fun error(message: String?, e: Throwable?) {
if (!isEnabled) return
follower?.error(message, e)
error0(message, e)
}

View File

@ -19,7 +19,9 @@ expect interface ECDHPublicKey {
fun getEncoded(): ByteArray
}
expect class ECDHKeyPair {
internal expect class ECDHKeyPairImpl : ECDHKeyPair
interface ECDHKeyPair {
val privateKey: ECDHPrivateKey
val publicKey: ECDHPublicKey
@ -27,6 +29,15 @@ expect class ECDHKeyPair {
* 私匙和固定公匙([initialPublicKey]) 计算得到的 shareKey
*/
val initialShareKey: ByteArray
object DefaultStub : ECDHKeyPair {
val defaultPublicKey = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128".chunkedHexToBytes()
val defaultShareKey = "4da0f614fc9f29c2054c77048a6566d7".chunkedHexToBytes()
override val privateKey: Nothing get() = error("stub!")
override val publicKey: Nothing get() = error("stub!")
override val initialShareKey: ByteArray get() = defaultShareKey
}
}
/**
@ -41,6 +52,8 @@ expect class ECDH(keyPair: ECDHKeyPair) {
fun calculateShareKeyByPeerPublicKey(peerPublicKey: ECDHPublicKey): ByteArray
companion object {
val isECDHAvailable: Boolean
/**
* 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
*/
@ -60,14 +73,11 @@ expect class ECDH(keyPair: ECDHKeyPair) {
override fun toString(): String
}
/**
*
*/
@Suppress("FunctionName")
expect fun ECDH(): ECDH
val initialPublicKey =
ECDH.constructPublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8".chunkedHexToBytes())
val initialPublicKey
get() = ECDH.constructPublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8".chunkedHexToBytes())
private val commonHeadFor02 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes()
private val commonHeadForNot02 = "3046301006072A8648CE3D020106052B8104001F033200".chunkedHexToBytes()
private const val constantHead = "3046301006072A8648CE3D020106052B8104001F03320004"

View File

@ -17,7 +17,6 @@ import kotlinx.io.core.readUInt
import kotlinx.io.core.readULong
import net.mamoe.mirai.utils.MiraiDebugAPI
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.io.*
import kotlin.jvm.JvmStatic

View File

@ -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
*/

View File

@ -14,7 +14,6 @@ package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.time.seconds
// 临时使用, 待 Kotlin Duration 稳定后使用 Duration.
// 内联属性, 则将来删除这些 API 将不会导致二进制不兼容.

View File

@ -15,17 +15,16 @@ expect fun Throwable.addSuppressed(e: Throwable)
@MiraiInternalAPI
@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
repeat(repeat) {
try {
return block()
return block(it)
} catch (e: Throwable) {
if (lastException == null) {
lastException = e
}
lastException!!.addSuppressed(e)
} else lastException!!.addSuppressed(e)
}
}
@ -34,17 +33,16 @@ inline fun <R> tryNTimes(repeat: Int, block: () -> R): R {
@MiraiInternalAPI
@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
repeat(repeat) {
try {
return block()
return block(it)
} catch (e: Throwable) {
if (lastException == null) {
lastException = e
}
lastException!!.addSuppressed(e)
} else lastException!!.addSuppressed(e)
}
}
@ -53,18 +51,17 @@ inline fun <R> tryNTimesOrNull(repeat: Int, block: () -> R): R? {
@MiraiInternalAPI
@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
repeat(repeat) {
try {
block()
block(it)
return null
} catch (e: Throwable) {
if (lastException == null) {
lastException = e
}
lastException!!.addSuppressed(e)
} else lastException!!.addSuppressed(e)
}
}

View File

@ -29,13 +29,13 @@ internal val factory: BotFactory = run {
"""
No BotFactory found. Please ensure that you've added dependency of protocol modules.
Available modules:
- net.mamoe:mirai-core-timpc
- net.mamoe:mirai-core-timpc (stays at 0.12.0)
- net.mamoe:mirai-core-qqandroid (recommended)
You should have at lease one protocol module installed.
-------------------------------------------------------
找不到 BotFactory. 请确保你依赖了至少一个协议模块.
可用的协议模块:
- net.mamoe:mirai-core-timpc
- net.mamoe:mirai-core-timpc (0.12.0 后停止更新)
- net.mamoe:mirai-core-qqandroid (推荐)
请添加上述任一模块的依赖( mirai-core 版本相同)
""".trimIndent()

View File

@ -1,18 +0,0 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("MayBeConstant", "unused")
package net.mamoe.mirai
actual object MiraiEnvironment {
@JvmStatic
actual val platform: Platform
get() = Platform.JVM
}

View File

@ -16,15 +16,16 @@ import net.mamoe.mirai.event.ListeningStatus
import net.mamoe.mirai.utils.MiraiInternalAPI
import java.util.function.Consumer
import java.util.function.Function
import kotlin.coroutines.EmptyCoroutineContext
@MiraiInternalAPI
@Suppress("FunctionName")
fun <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
@Suppress("FunctionName")
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; })
}

View File

@ -0,0 +1,18 @@
package net.mamoe.mirai.event.internal
import java.util.concurrent.atomic.AtomicBoolean
internal actual class MiraiAtomicBoolean actual constructor(initial: Boolean) {
private val delegate: AtomicBoolean = AtomicBoolean(initial)
actual fun compareAndSet(expect: Boolean, update: Boolean): Boolean {
return delegate.compareAndSet(expect, update)
}
actual var value: Boolean
get() = delegate.get()
set(value) {
delegate.set(value)
}
}

View File

@ -105,8 +105,8 @@ class DefaultLoginSolver(
override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? = loginSolverLock.withLock {
val logger = getLogger(bot)
logger.info("需要进行账户安全认证")
logger.info("该账户有[设备锁]/[不常用登陆地点]/[不常用设备登陆]的问题")
logger.info("完成以下账号认证即可成功登|理论本认证在mirai每个账户中最多出现1次")
logger.info("该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题")
logger.info("完成以下账号认证即可成功登|理论本认证在mirai每个账户中最多出现1次")
logger.info("请将该链接在QQ浏览器中打开并完成认证, 成功后输入任意字符")
logger.info("这步操作将在后续的版本中优化")
logger.info(url)
@ -221,11 +221,11 @@ actual open class BotConfiguration actual constructor() {
/**
* 重连失败后, 继续尝试的每次等待时间
*/
actual var reconnectPeriodMillis: Long = 60.secondsToMillis
actual var reconnectPeriodMillis: Long = 5.secondsToMillis
/**
* 最多尝试多少次重连
*/
actual var reconnectionRetryTimes: Int = 3
actual var reconnectionRetryTimes: Int = Int.MAX_VALUE
/**
* 验证码处理器
*/

View File

@ -9,7 +9,6 @@
package net.mamoe.mirai.utils.cryptor
import net.mamoe.mirai.utils.io.chunkedHexToBytes
import net.mamoe.mirai.utils.md5
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.*
@ -21,20 +20,13 @@ import javax.crypto.KeyAgreement
actual typealias ECDHPrivateKey = PrivateKey
actual typealias ECDHPublicKey = PublicKey
actual class ECDHKeyPair(
private val delegate: KeyPair?
) {
actual val privateKey: ECDHPrivateKey get() = delegate?.private ?: error("ECDH is not available")
actual val publicKey: ECDHPublicKey get() = delegate?.public ?: defaultPublicKey
internal actual class ECDHKeyPairImpl(
private val delegate: KeyPair
) : ECDHKeyPair {
override val privateKey: ECDHPrivateKey get() = delegate.private
override val publicKey: ECDHPublicKey get() = delegate.public
actual val initialShareKey: ByteArray = if (delegate == null) {
defaultShareKey
} else ECDH.calculateShareKey(privateKey, initialPublicKey)
companion object {
internal val defaultPublicKey = "020b03cf3d99541f29ffec281bebbd4ea211292ac1f53d7128".chunkedHexToBytes().adjustToPublicKey()
internal val defaultShareKey = "4da0f614fc9f29c2054c77048a6566d7".chunkedHexToBytes()
}
override val initialShareKey: ByteArray = ECDH.calculateShareKey(privateKey, initialPublicKey)
}
@Suppress("FunctionName")
@ -42,33 +34,34 @@ actual fun ECDH() = ECDH(ECDH.generateKeyPair())
actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
actual companion object {
private var isECDHAvailable = true
@Suppress("ObjectPropertyName")
private val _isECDHAvailable: Boolean = kotlin.runCatching {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) != null) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
}
Security.addProvider(BouncyCastleProvider())
generateKeyPair() // try if it is working
}.isSuccess
init {
isECDHAvailable = kotlin.runCatching {
Security.addProvider(BouncyCastleProvider())
generateKeyPair() // try if it is working
}.isSuccess
}
actual val isECDHAvailable: Boolean get() = _isECDHAvailable
actual fun generateKeyPair(): ECDHKeyPair {
return if (!isECDHAvailable) {
ECDHKeyPair(null)
} else ECDHKeyPair(KeyPairGenerator.getInstance("EC", "BC").apply { initialize(ECGenParameterSpec("secp192k1")) }.genKeyPair())
if (!isECDHAvailable) {
return ECDHKeyPair.DefaultStub
}
return ECDHKeyPairImpl(KeyPairGenerator.getInstance("ECDH")
.also { it.initialize(ECGenParameterSpec("secp192k1")) }
.genKeyPair())
}
actual fun calculateShareKey(
privateKey: ECDHPrivateKey,
publicKey: ECDHPublicKey
): ByteArray {
return if (!isECDHAvailable) {
ECDHKeyPair.defaultShareKey
} else {
val instance = KeyAgreement.getInstance("ECDH", "BC")
instance.init(privateKey)
instance.doPhase(publicKey, true)
md5(instance.generateSecret())
}
val instance = KeyAgreement.getInstance("ECDH", "BC")
instance.init(privateKey)
instance.doPhase(publicKey, true)
return md5(instance.generateSecret())
}
actual fun constructPublicKey(key: ByteArray): ECDHPublicKey {

View File

@ -30,7 +30,10 @@ actual class PlatformSocket : Closeable {
private lateinit var socket: Socket
actual val isOpen: Boolean
get() = socket.isConnected
get() =
if (::socket.isInitialized)
socket.isConnected
else false
actual override fun close() {
if (::socket.isInitialized) {

View File

@ -63,9 +63,6 @@ internal class LockFreeLinkedListTest {
val addJob = async { list.concurrentDo(2, 30000) { addLast(1) } }
//delay(1) // let addJob fly
if (addJob.isCompleted) {
println("Number of elements are not enough")
}
val foreachJob = async {
list.concurrentDo(1, 10000) {
forEach { it + it }

View File

@ -32,7 +32,7 @@ Mirai Java Apt
<dependencies>
<dependency>
<groupId>net.mamoe</groupId>
<artifactId>mirai-core-qqandroid</artifactId>
<artifactId>mirai-core-qqandroid-jvm</artifactId>
<version>CORE_VERSION</version> <!-- 替换版本为最新版本 -->
</dependency>
@ -51,7 +51,7 @@ repositories {
}
dependencies {
implementation("net.mamoe:mirai-core-qqandroid:CORE_VERSION")
implementation("net.mamoe:mirai-core-qqandroid-jvm:CORE_VERSION")
implementation("net.mamoe:mirai-japt:JAPT_VERSION")
}
```

View File

@ -59,7 +59,7 @@ fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$v
fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
dependencies {
api(project(":mirai-core"))
implementation(project(":mirai-core"))
runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main")) // classpath is not added correctly by IDE
api(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-javafx", version = "1.3.2")

Some files were not shown because too many files have changed in this diff Show More