mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-27 17:00:14 +08:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
687a773e8e
47
CHANGELOG.md
47
CHANGELOG.md
@ -2,6 +2,53 @@
|
||||
|
||||
开发版本. 频繁更新, 不保证高稳定性
|
||||
|
||||
## `0.17.0` 2020/2/20
|
||||
|
||||
### mirai-core
|
||||
- 支持原生表情 `Face`
|
||||
- 修正 `groupCardOrNick` 为 `nameCardOrNick`
|
||||
- 增加 `MessageChain.foreachContent(lambda)` 和 `Message.hasContent(): Boolean`
|
||||
|
||||
### mirai-core-qqandroid
|
||||
- 提高重连速度
|
||||
- 修复重连后某些情况不会心跳
|
||||
- 修复收包时可能产生异常
|
||||
|
||||
## `0.16.0` 2020/2/19
|
||||
|
||||
### mirai-core
|
||||
- 添加 `Bot.subscribe` 等筛选 Bot 实例的监听方法
|
||||
- 其他一些小问题修复
|
||||
|
||||
### mirai-core-qqandroid
|
||||
- 优化重连处理逻辑
|
||||
- 确保好友消息和历史事件在初始化结束前同步完成
|
||||
- 同步好友消息记录时不广播
|
||||
|
||||
## `0.15.5` 2020/2/19
|
||||
|
||||
### mirai-core
|
||||
- 为 `MiraiLogger` 添加 common property `val isEnabled: Boolean`
|
||||
- 修复 #62: 掉线重连后无 heartbeat
|
||||
- 修复 #65: `Bot` close 后仍会重连
|
||||
- 修复 #70: ECDH is not available on Android platform
|
||||
|
||||
### mirai-core-qqandroid
|
||||
- 从服务器收到的事件将会额外使用 `bot.logger` 记录 (verbose).
|
||||
- 降低包记录的等级: `info` -> `verbose`
|
||||
- 改善 `Bot` 的 log 记录
|
||||
- 加载好友列表失败时会重试
|
||||
- 改善 `Bot` 或 `NetworkHandler` 关闭时取消 job 的逻辑
|
||||
- 修复初始化(init)时同步历史好友消息时出错的问题
|
||||
|
||||
## `0.15.4` 2020/2/18
|
||||
|
||||
- 放弃使用 `atomicfu` 以解决其编译错误的问题. (#60)
|
||||
|
||||
## `0.15.3` 2020/2/18
|
||||
|
||||
- 修复无法引入依赖的问题.
|
||||
|
||||
## `0.15.2` 2020/2/18
|
||||
|
||||
### mirai-core
|
||||
|
151
README.md
151
README.md
@ -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-core,mirai-japt以及mirai-api-http等核心模块进行特性的开发,对于非开发者的使用暂时不做过多支持,仅展示开发计划。
|
||||
|
||||
### 开发者
|
||||
|
||||
- 假如你熟悉Kotlin及包管理工具,请参阅[Mirai Guide - Quick Start](/docs/guide_quick_start.md)
|
||||
- 假如你不熟悉Kotlin,希望一份较详细的起步教程,请参阅[Mirai Guide - Getting Started](/docs/guide_getting_started.md)
|
||||
- 假如你使用Java作为开发语言,请参阅[mirai-japt](/mirai-japt/README.md)
|
||||
- 假如你是其他平台开发者,可以通过了解 [mirai-api-http](https://github.com/mamoe/mirai/tree/master/mirai-api-http) 进行接入,欢迎开发不同平台的mirai-sdk
|
||||
- 此外,你还可以在 [Wiki](https://github.com/mamoe/mirai/wiki/Home) 中查看各类帮助,**如 API 示例**。
|
||||
|
||||
### 使用者
|
||||
|
||||
- [mirai-console](https://github.com/mamoe/mirai/tree/master/mirai-console) 支持插件, 在终端中启动 Mirai 并获得机器人服务,**本模块还未完善**,请耐心等待开发完成。
|
||||
- mirai-webpanel Mirai的Web控制台,支持在网页中管理机器人与插件。本模块目前在计划中。在其他模块稳定后,将开始进行开发。
|
||||
|
||||
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
在 [Project](https://github.com/mamoe/mirai/projects/3) 查看已支持功能和计划
|
||||
在 [CHANGELOG](https://github.com/mamoe/mirai/blob/master/CHANGELOG.md) 查看版本更新记录 (仅发布的版本)
|
||||
|
||||
|
||||
|
||||
## Modules
|
||||
|
||||
### mirai-core
|
||||
|
||||
通用 API 模块,一套 API 适配两套协议。
|
||||
**请参考此模块的 API**
|
||||
|
||||
|
||||
### mirai-core-qqandroid
|
||||
|
||||
QQ for Android (8.2.0 版本,2019 年 12 月)协议的实现,目前完成大部分。
|
||||
|
||||
- 高兼容性:协议仅含极少部分为硬编码,其余全部随官方方式动态生成
|
||||
- 高安全性:密匙随机,ECDH 动态计算
|
||||
- 已支持大部分使用场景, 详情请在[Project](https://github.com/mamoe/mirai/projects/3)查看
|
||||
|
||||
### mirai-core-timpc
|
||||
TIM PC (2.3.2 版本,2019 年 8 月)协议的实现,相较于 core,仅新增少量 API. 详见 [README.md](mirai-core-timpc/)
|
||||
|
||||
TIM PC (2.3.2 版本,2019 年 8 月)协议的实现
|
||||
支持的功能:
|
||||
|
||||
- 消息收发:图片文字复合消息,图片消息
|
||||
- 群管功能:群员列表,禁言
|
||||
(目前不再更新此协议,请关注上文的安卓协议)
|
||||
(目前不再更新此协议,请关注上文的安卓协议)
|
||||
|
||||
## Use directly
|
||||
**直接使用 Mirai(终端环境/网页面板(将来)).**
|
||||
[Mirai-Console](https://github.com/mamoe/mirai/tree/master/mirai-console) 插件支持, 在终端中启动 Mirai 并获得机器人服务
|
||||
本模块还未完善。
|
||||
|
||||
## Use as a library
|
||||
**mirai-core 为独立设计, 可以作为库内置于任意 Java(JVM)/Android 项目中使用.**
|
||||
|
||||
请将 `VERSION` 替换为最新的版本(如 `0.15.0`):
|
||||
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
|
||||
**Mirai 目前还处于实验性阶段, 我们无法保证任何稳定性, API 也可能会随时修改.**
|
||||
|
||||
### Maven
|
||||
Kotlin 在 Maven 上只支持 JVM 平台.
|
||||
```xml
|
||||
<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 11(for protocol tools, optional)
|
||||
- Android SDK 29 (for Android target, optional)
|
||||
|
||||
#### Libraries used
|
||||
感谢:
|
||||
|
||||
- [kotlin-stdlib](https://github.com/JetBrains/kotlin)
|
||||
- [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines)
|
||||
- [kotlinx-io](https://github.com/Kotlin/kotlinx-io)
|
||||
@ -176,15 +117,21 @@ bot.subscribeAlways<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)
|
129
docs/guide_getting_started.md
Normal file
129
docs/guide_getting_started.md
Normal 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有一定了解,可跳过1,2
|
||||
|
||||
### 1 安装IDEA与JDK
|
||||
|
||||
JDK要求6以上
|
||||
|
||||
### 2 新建Gradle项目
|
||||
|
||||
- 在```File->new project```中选择```Gradle```
|
||||
- 在面板中的```Additional Libraries and Frameworks```中勾选```Java```以及```Kotlin/JVM```
|
||||
- 点击```next```,填入```GroupId```与```ArtifactId```(对于测试项目来说,可随意填写)
|
||||
- 点击```next```,点击```Use default gradle wrapper(recommended)```
|
||||
- 创建项目完成
|
||||
|
||||
### 3 添加依赖
|
||||
|
||||
- 打开项目的```Project```面板,点击编辑```build.gradle```
|
||||
|
||||
- 首先添加repositories
|
||||
|
||||
```groovy
|
||||
//添加jcenter仓库
|
||||
/*
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
原文内容,更新为下文
|
||||
*/
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
```
|
||||
|
||||
- 添加依赖,将dependencies部分覆盖。 `mirai-core` 的最新版本为: [![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'net.mamoe:mirai-core-qqandroid-jvm:0.17.0'//此处版本应替换为当前最新
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
testCompile group: 'junit', name: 'junit', version: '4.12'
|
||||
}
|
||||
```
|
||||
|
||||
- 打开右侧Gradle面板,点击刷新按钮
|
||||
- 至此,依赖添加完成
|
||||
|
||||
### 4 Try Bot
|
||||
|
||||
- 在src/main文件夹下新建文件夹,命名为```kotlin```
|
||||
- 在```kotlin```下新建包(在```kotlin```文件夹上右键-```New```-```Packages```) 包名为```net.mamoe.mirai.simpleloader```
|
||||
|
||||
- 在包下新建kotlin文件```MyLoader.kt```
|
||||
|
||||
```kotlin
|
||||
package net.mamoe.mirai.simpleloader
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.alsoLogin
|
||||
import net.mamoe.mirai.event.subscribeMessages
|
||||
|
||||
suspend fun main() {
|
||||
val qqId = 10000L//Bot的QQ号,需为Long类型,在结尾处添加大写L
|
||||
val password = "your_password"//Bot的密码
|
||||
val miraiBot = Bot(qqId, password).alsoLogin()//新建Bot并登录
|
||||
miraiBot.subscribeMessages {
|
||||
"你好" reply "你好!"
|
||||
case("at me") {
|
||||
reply(sender.at() + " 给爷爬 ")
|
||||
}
|
||||
|
||||
(contains("舔") or contains("刘老板")) {
|
||||
"刘老板太强了".reply()
|
||||
}
|
||||
}
|
||||
miraiBot.join() // 等待 Bot 离线, 避免主线程退出
|
||||
}
|
||||
```
|
||||
|
||||
- 单击编辑器内第8行(```suspend fun main```)左侧的run按钮(绿色三角),等待,MiraiBot成功登录。
|
||||
- 本例的功能中,在任意群内任意成员发送包含“舔”字或“刘老板”字样的消息,MiraiBot会回复“刘老板太强了”
|
||||
|
||||
|
||||
|
||||
至此,简单的入门已经结束,下面可根据不同的需求参阅wiki进行功能的添加。
|
||||
|
||||
|
||||
### 此外,还可以使用Maven作为包管理工具
|
||||
本项目推荐使用gradle,因此不提供详细入门指导
|
||||
|
||||
```xml
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jcenter</id>
|
||||
<url>https://jcenter.bintray.com/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.mamoe</groupId>
|
||||
<artifactId>mirai-core-qqandroid-jvm</artifactId>
|
||||
<version>0.17.0</version> <!-- 替换版本为最新版本 -->
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
114
docs/guide_quick_start.md
Normal file
114
docs/guide_quick_start.md
Normal 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 11(for protocol tools, optional)
|
||||
- Android SDK 29 (for Android target, optional)
|
||||
|
||||
## Use directly
|
||||
|
||||
**直接使用 Mirai(终端环境/网页面板(将来)).**
|
||||
[Mirai-Console](https://github.com/mamoe/mirai/tree/master/mirai-console) 插件支持, 在终端中启动 Mirai 并获得机器人服务
|
||||
本模块还未完善。
|
||||
|
||||
## Use as a library
|
||||
|
||||
**mirai-core 为独立设计, 可以作为库内置于任意 Java(JVM)/Android 项目中使用.**
|
||||
|
||||
请将 `VERSION` 替换为最新的版本(如 `0.15.0`):
|
||||
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
|
||||
**Mirai 目前还处于实验性阶段, 我们无法保证任何稳定性, API 也可能会随时修改.**
|
||||
|
||||
### Maven
|
||||
|
||||
Kotlin 在 Maven 上只支持 JVM 平台.
|
||||
|
||||
```xml
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jcenter</id>
|
||||
<url>https://jcenter.bintray.com/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.mamoe</groupId>
|
||||
<artifactId>mirai-core-qqandroid-jvm</artifactId>
|
||||
<version>0.15.1</version> <!-- 替换版本为最新版本 -->
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
### Gradle
|
||||
|
||||
Mirai 只发布在 `jcenter`, 因此请确保添加 `jcenter()` 仓库:
|
||||
|
||||
```kotlin
|
||||
repositories{
|
||||
jcenter()
|
||||
}
|
||||
```
|
||||
|
||||
若您需要使用在跨平台项目, 则要对各个目标平台添加不同的依赖,这与 kotlin 相关多平台库的依赖是类似的。
|
||||
**若您只需要使用在单一平台, 则只需要添加一项该平台的依赖.**
|
||||
|
||||
**注意:**
|
||||
Mirai 核心由 API 模块(`mirai-core`)和协议模块组成。
|
||||
只添加 API 模块将无法正常工作。
|
||||
现在只推荐使用 QQAndroid 协议,请参照下文选择对应目标平台的依赖添加。
|
||||
|
||||
**jvm** (JVM 平台)
|
||||
|
||||
```kotlin
|
||||
implementation("net.mamoe:mirai-core-qqandroid-jvm:VERSION")
|
||||
```
|
||||
|
||||
**common** (通用平台)
|
||||
|
||||
```kotlin
|
||||
implementation("net.mamoe:mirai-core-qqandroid-common:VERSION")
|
||||
```
|
||||
|
||||
**android** (Android 平台)
|
||||
|
||||
```kotlin
|
||||
implementation("net.mamoe:mirai-core-qqandroid-android:VERSION")
|
||||
```
|
||||
|
||||
## Try
|
||||
|
||||
### On JVM or Android
|
||||
|
||||
现在体验低付出高效率的 Mirai
|
||||
|
||||
```kotlin
|
||||
val bot = Bot(qqId, password).alsoLogin()
|
||||
bot.subscribeMessages {
|
||||
"你好" reply "你好!"
|
||||
"profile" reply { sender.queryProfile() }
|
||||
contains("图片"){ File(imagePath).send() }
|
||||
}
|
||||
bot.subscribeAlways<MemberPermissionChangedEvent> {
|
||||
if (it.kind == BECOME_OPERATOR)
|
||||
reply("${it.member.id} 成为了管理员")
|
||||
}
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
Android 上, Mirai 运行需使用 80M 内存.
|
||||
JVM 上启动需 80M 内存, 每多一个机器人实例需要 30M 内存.
|
||||
|
@ -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
|
||||
|
@ -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':
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
@ -12,10 +12,12 @@ fun main() {
|
||||
|
||||
MiraiHttpAPIServer.start()
|
||||
|
||||
bot.network.awaitDisconnection()
|
||||
bot.join()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 认证相关
|
||||
|
||||
### 开始会话-认证(Authorize)
|
||||
@ -141,6 +143,8 @@ fun main() {
|
||||
> SessionKey与Bot 对应错误时将会返回状态码5:指定对象不存在
|
||||
|
||||
|
||||
|
||||
|
||||
## 消息相关
|
||||
|
||||
|
||||
@ -261,10 +265,10 @@ fun main() {
|
||||
### 发送图片消息(通过URL)
|
||||
|
||||
```
|
||||
[POST] /sendGroupMessage
|
||||
[POST] /sendImageMessage
|
||||
```
|
||||
|
||||
使用此方法向指定群发送消息
|
||||
使用此方法向指定对象(群或好友)发送图片消息
|
||||
|
||||
#### 请求
|
||||
|
||||
@ -303,7 +307,7 @@ fun main() {
|
||||
### 图片文件上传
|
||||
|
||||
```
|
||||
[POST] /sendGroupMessage
|
||||
[POST] /uploadImage
|
||||
```
|
||||
|
||||
使用此方法上传图片文件至服务器并返回ImageId
|
||||
@ -414,10 +418,10 @@ Content-Type:multipart/form-data
|
||||
}
|
||||
```
|
||||
|
||||
| 名字 | 类型 | 说明 |
|
||||
| ------- | ------ | ------------------------- |
|
||||
| target | Long | 群员QQ号 |
|
||||
| display | String | @时显示的文本如:"@Mirai" |
|
||||
| 名字 | 类型 | 说明 |
|
||||
| ------- | ------ | ---------------------------------------------- |
|
||||
| target | Long | 群员QQ号 |
|
||||
| dispaly | String | At时显示的文字,发送消息时无效,自动使用群名片 |
|
||||
|
||||
#### AtAll
|
||||
|
||||
@ -463,7 +467,7 @@ Content-Type:multipart/form-data
|
||||
{
|
||||
"type": "Image",
|
||||
"imageId": "{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png" //群图片格式
|
||||
"imageId": "/f8f1ab55-bf8e-4236-b55e-955848d7069f" //好友图片格式
|
||||
//"imageId": "/f8f1ab55-bf8e-4236-b55e-955848d7069f" //好友图片格式
|
||||
}
|
||||
```
|
||||
|
||||
@ -517,6 +521,7 @@ Content-Type:multipart/form-data
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 获取群列表
|
||||
|
||||
使用此方法获取bot的群列表
|
||||
@ -786,6 +791,8 @@ Content-Type:multipart/form-data
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 获取群设置
|
||||
|
||||
使用此方法获取群设置
|
||||
@ -816,6 +823,7 @@ Content-Type:multipart/form-data
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 修改群员资料
|
||||
|
||||
使用此方法修改群员资料(需要有相关限权)
|
||||
@ -856,6 +864,8 @@ Content-Type:multipart/form-data
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 获取群员资料
|
||||
|
||||
使用此方法获取群员资料
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
// }
|
||||
})
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
BIN
mirai-console-graphical/src/main/resources/character.png
Normal file
BIN
mirai-console-graphical/src/main/resources/character.png
Normal file
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 |
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
@ -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
|
||||
|
@ -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))
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -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()}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
/**
|
@ -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)
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -68,11 +68,11 @@ actual open class BotConfiguration actual constructor() {
|
||||
/**
|
||||
* 重连失败后, 继续尝试的每次等待时间
|
||||
*/
|
||||
actual var reconnectPeriodMillis: Long = 60.secondsToMillis
|
||||
actual var reconnectPeriodMillis: Long = 5.secondsToMillis
|
||||
/**
|
||||
* 最多尝试多少次重连
|
||||
*/
|
||||
actual var reconnectionRetryTimes: Int = 3
|
||||
actual var reconnectionRetryTimes: Int = Int.MAX_VALUE
|
||||
/**
|
||||
* 验证码处理器
|
||||
*/
|
||||
|
@ -9,8 +9,10 @@
|
||||
|
||||
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(
|
||||
|
@ -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
|
||||
|
@ -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})"
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
@ -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())
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
@ -17,4 +17,6 @@ interface MemberInfo : FriendInfo {
|
||||
val permission: MemberPermission
|
||||
|
||||
val specialTitle: String
|
||||
|
||||
val muteTimestamp: Int
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
||||
|
||||
|
@ -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 下载图片
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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] 比较
|
||||
|
@ -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]
|
||||
*/
|
||||
|
@ -31,6 +31,11 @@ interface MessageSource : Message {
|
||||
*/
|
||||
val messageUid: Long
|
||||
|
||||
/**
|
||||
* 发送时间, 单位为秒
|
||||
*/
|
||||
val time: Long
|
||||
|
||||
/**
|
||||
* 发送人号码
|
||||
*/
|
||||
|
@ -68,7 +68,7 @@ abstract class BotNetworkHandler : CoroutineScope {
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
@MiraiInternalAPI
|
||||
abstract suspend fun relogin()
|
||||
abstract suspend fun relogin(cause: Throwable? = null)
|
||||
|
||||
/**
|
||||
* 初始化获取好友列表等值.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
@ -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 将不会导致二进制不兼容.
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
@ -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; })
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
/**
|
||||
* 验证码处理器
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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 }
|
||||
|
@ -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")
|
||||
}
|
||||
```
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user