Commit b8f49b50 authored by Cyenoch's avatar Cyenoch Committed by GitHub

Merge pull request #1 from mamoe/master

re
parents baa3e56d 292388a1
[*.{kt, kts}]
max_line_length = 120
---
name: 疑问 / 帮助
about: 询问一个问题
title: ''
labels: question
assignees: ''
---
<!--请详细描述你的问题. 我们会尊重每一个问题, 你不必担心问题是否过于简单-->
---
name: 特性申请
about: 申请 mirai 添加新的特性
title: ''
labels: feature
assignees: ''
---
<!--
以下相关功能将会被直接拒绝:
- 资金相关: 红包, 转账
- 主动加好友, 主动加群, 主动邀请加入群
可以提交的内容:
- 有较高使用频率的协议 (低频功能不接受提议)
- 架构 / 功能上的建议 (非常欢迎,我们会尊重你的建议)
-->
<!--请在下一行开始描述你的问题-->
---
name: Bug 报告
about: 提交一个 bug
title: ''
labels: " bug "
assignees: ''
---
## 问题
<!--在这里简略描述你遇到的问题-->
<!--如果有控制台报错,请尽量附加全面的日志或截图-->
## 如何复现
<!--在这里简略说明如何让这个问题再次发生-->
<!--可使用 1. 2. 3. 的列表格式,或其他任意恰当的格式>
<!--如有必要,你可以在下文继续添加其他信息-->
name: CI name: Gradle CI
on: [push] on: [push, pull_request]
jobs: jobs:
build: build:
...@@ -8,10 +8,12 @@ jobs: ...@@ -8,10 +8,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- name: setup-android - name: Set up JDK 1.8
uses: msfjarvis/setup-android@0.2 uses: actions/setup-java@v1
with: with:
# Gradle tasks to run - If you want to run ./gradlew assemble, specify assemble here. java-version: 1.8
gradleTasks: build -x mirai-core:jvmTest - name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
# This is a basic workflow to help you get started with Actions
name: Shadow
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
release:
types:
- created
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle and shadowJar
run: ./gradlew :mirai-core:shadowJar :mirai-core-qqandroid:shadowJar
- name: Upload artifact
uses: actions/upload-artifact@v1.0.0
with:
# Artifact name
name: mirai-core-all
# Directory containing files to upload
path: "mirai-core/build/libs/mirai-core-*-all.jar"
- name: Upload artifact
uses: actions/upload-artifact@v1.0.0
with:
# Artifact name
name: mirai-core-qqandroid-all
# Directory containing files to upload
path: "mirai-core-qqandroid/build/libs/mirai-core-qqandroid-*-all.jar"
...@@ -2,6 +2,200 @@ ...@@ -2,6 +2,200 @@
开发版本. 频繁更新, 不保证高稳定性 开发版本. 频繁更新, 不保证高稳定性
## `0.27.0` 2020/3/8
- 支持 `XML`, `Json`, `LightApp``RichMessage`
## `0.26.2` 2020/3/8
- 新增 `MessageChain.repeat``MessageChain.times`
- JVM 平台下 `PlatformLogger` 可重定向输出
- 修复 `NullMessageChain.equals` 判断不正确的问题
- 新增 `PlainText.of` 以应对一些特殊情况
## `0.26.1` 2020/3/8
- 重写 Jce 序列化, 提升反序列性能
- 更新 `Kotlin` 版本到 1.3.70
- 更新 `kotlinx.coroutines`, `atomicfu`, `kotlinx.coroutines` 依赖版本
## `0.26.0` 2020/3/7
- 使用 `kotlinx.io` 而不是 `ktor.io`
- 修复 #111, #108, #116, #112
## `0.25.0` 2020/3/6
- 适配 8.2.7 版本(2020 年 3 月)协议
- 全面的 `Image` 类型: Online/Offline Image, Friend/Group Image
- 修复查询图片链接时好友图片链接错误的问题
- 修复 bugs: #105, #106, #107
## `0.24.1` 2020/3/3
- 修复 `Member` 的委托 `QQ` 弱引用被释放的问题
-`Bot.friends` 替代 `Bot.qqs`
-`Bot.containsFriend`, `Bot.containsGroup` 替代 `Bot.contains`
- 新增 `BotFactory.Bot(String, ByteArray)` 用 md5 密码登录
-`BotFactory` 等类型的一些扩展指定 `JvmName`
- 移动 `Bot.QQ` 到低级 API
## `0.24.0` 2020/3/1
- Java 完全友好: Java 使用者可以同 Kotlin 方式直接阻塞式或异步(Future)调用 API
- 新增 `MessageSource.originalMessage: MessageChain` 以获取源消息内容
- 群消息的撤回现在已稳定 (`Bot.recall`)
- 现在可以引用回复机器人自己发送的消息: `MessageReceipt.quoteReply`
- 新增 `MessageRecallEvent`
- 整理 `MessageChain` 的构造, 优化性能
- 整理所有网络层代码, 弃用 `kotlinx.io` 而使用 `io.ktor.utils.io`
- 其他杂项优化
## `0.23.0` 2020/2/28
### mirai-core
- 修复上传图片
- 一些问题修复
- 大量杂项优化
### mirai-core-qqandroid
- `MessageReceipt.source` 现在为 public. 可获取源消息 id
- 修复上传好友图片失败的问题
- 上传群图片现在分包缓存, 优化性能
## `0.22.0` 2020/2/24
### mirai-core
- 重构 `MessageChain`, 引入 `CombinedMessage`. (兼容大部分原 API)
- 新增 `MessageChainBuilder`, `buildMessageChain`
- `ExternalImage` 现在接收多种输入参数
### mirai-core-qqandroid
- 修复访问好友消息回执 `.sequenceId` 时抛出异常的问题
## `0.21.0` 2020/2/23
- 支持好友消息的引用回复
- 更加结构化的 `QuoteReply` 架构, 支持引用任意群/好友消息回复给任意群/好友.
## `0.20.0` 2020/2/23
### mirai-core
- 支持图片下载: `image.channel(): ByteReadChannel`, `image.url()`
- 添加 `LockFreeLinkedList<E>.iterator`
- 添加 `LockFreeLinkedList<E>.forEachNode`
- 并行处理事件监听
- 添加 `nextMessageContaining` 和相关可空版本
- '撤回' 从 `Contact` 移动到 `Bot`
- 删除 `MessageSource.sourceMessage`
- 让 MessageSource 拥有唯一的 long 类型 id, 删除原 `uid``sequence` 结构.
- 修复 `Message.eq` 歧义
## `0.19.1` 2020/2/21
### mirai-core
- 支持机器人撤回群消息 (含自己发送的消息): `Group.recall`, `MessageReceipt.recall`
- 支持一定时间后自动撤回: `Group.recallIn`, `MessageReceipt.recallIn`
- `sendMessage` 返回 `MessageReceipt` 以实现撤回功能
- 添加 `MessageChain.addOrRemove`
- 添加 `ContactList.firstOrNull`, `ContactList.first`
- 新的异步事件监听方式: `subscribingGetAsync` 启动一个协程并从一个事件从获取返回值到 `Deferred`.
- 新的线性事件监听方式: `subscribingGet` 挂起当前协程并从一个事件从获取返回值.
##### 新的线性消息连续处理: `nextMessage` 挂起当前协程并等待下一条消息:
使用该示例, 发送两条消息, 一条为 "禁言", 另一条包含一个 At
```kotlin
case("禁言") {
val value: At = nextMessage { message.any(At) }[At]
value.member().mute(10)
}
```
示例 2:
```kotlin
case("复读下一条") {
reply(nextMessage().message)
}
```
### mirai-core-qqandroid
- 修复一些情况下 `At` 无法发送的问题
- 统一 ImageId: 群消息收到的 ImageId 均为 `{xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx}.jpg` 形式(固定长度 37)
- 支持成员主动离开事件的解析 (#51)
## `0.18.0` 2020/2/20
### mirai-core
- 添加 `MessageSource.time`
- 添加事件监听时额外的 `coroutineContext`
- 为一些带有 `operator` 的事件添加 `.isByBot` 的属性扩展
- 优化事件广播逻辑, 修复可能无法触发监听的问题
- 为所有 `Contact` 添加 `toString()` (#80)
### mirai-core-qqandroid
- 支持成员禁言状态和时间查询 `Member.muteTimeRemaining`
- 修复 `At``display` (#73), 同时修复 `QuoteReply` 无法显示问题 (#54).
- 广播 `BotReloginEvent` (#78)
- 支持机器人自身禁言时间的更新和查询 (#82)
## `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
- 尝试修复 `atomicfu` 编译错误的问题
### mirai-core-qqandroid
- 查询群信息失败后重试
## `0.15.1` 2020/2/15
### mirai-core
- 统一异常处理: 所有群成员相关操作无权限时均抛出异常而不返回 `false`.
### mirai-core-qqandroid
- 初始化未完成时缓存接收的所有事件包 (#46)
- 解析群踢人事件时忽略找不到的群成员
- 登录完成后广播事件 `BotOnlineEvent`
## `0.15.0` 2020/2/14 ## `0.15.0` 2020/2/14
### mirai-core ### mirai-core
...@@ -132,7 +326,7 @@ TIMPC ...@@ -132,7 +326,7 @@ TIMPC
## `0.10.0` *2019/12/23* ## `0.10.0` *2019/12/23*
**事件优化** **事件优化**
更快的监听过程 更快的监听过程
现在监听不再是 `suspend`, 而必须显式指定 `CoroutineScope`. 详见 [`Subscribers.kt`](mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt#L69) 现在监听不再是 `suspend`, 而必须显式指定 `CoroutineScope`. 详见 `Subscribers.kt`
删除原本的 bot.subscribe 等监听模式. 删除原本的 bot.subscribe 等监听模式.
**其他** **其他**
...@@ -144,16 +338,16 @@ TIMPC ...@@ -144,16 +338,16 @@ TIMPC
这些模块都继承自 `mirai-core`. 这些模块都继承自 `mirai-core`.
现在, 要使用 mirai, 必须依赖于特定的协议模块, 如 `mirai-core-timpc`. 现在, 要使用 mirai, 必须依赖于特定的协议模块, 如 `mirai-core-timpc`.
查阅 API 时请查看 `mirai-core`. 查阅 API 时请查看 `mirai-core`.
每个模块只提供少量的额外方法. 我们会给出详细列表. 每个模块只提供少量的额外方法. 我们会给出详细列表.
在目前的开发中您无需考虑多协议兼容. 在目前的开发中您无需考虑多协议兼容.
**Bot 构造** **Bot 构造**
协议抽象后构造 Bot 需指定协议的 `BotFactory`. 协议抽象后构造 Bot 需指定协议的 `BotFactory`.
在 JVM 平台, Mirai 通过 classname 自动加载协议模块的 `BotFactory`, 因此若您只使用一套协议, 则无需修改现行源码 在 JVM 平台, Mirai 通过 classname 自动加载协议模块的 `BotFactory`, 因此若您只使用一套协议, 则无需修改现行源码
**事件** **事件**
大部分事件包名修改. 大部分事件包名修改.
**UInt -> Long** **UInt -> Long**
修改全部 QQ ID, Group ID 的类型由 UInt 为 Long. 修改全部 QQ ID, Group ID 的类型由 UInt 为 Long.
...@@ -203,21 +397,21 @@ TIMPC ...@@ -203,21 +397,21 @@ TIMPC
- 禁言的扩展函数现在会传递实际函数的返回值 - 禁言的扩展函数现在会传递实际函数的返回值
## `0.7.0` *2019/12/04* ## `0.7.0` *2019/12/04*
协议 协议
- 重新分析验证码包, 解决一些无法解析的情况. (这可能会产生新的问题, 遇到后请提交 issue) - 重新分析验证码包, 解决一些无法解析的情况. (这可能会产生新的问题, 遇到后请提交 issue)
- 重新分析提交密码包 - 重新分析提交密码包
- *提交验证码仍可能出现问题 (已在 `0.7.5` 修复)* - *提交验证码仍可能出现问题 (已在 `0.7.5` 修复)*
功能 功能
- XML 消息 DSL 构造支持 (实验性) (暂不支持发送) - XML 消息 DSL 构造支持 (实验性) (暂不支持发送)
- 群成员列表现在包含群主 (原本就应该包含) - 群成员列表现在包含群主 (原本就应该包含)
- 在消息事件处理中添加获取 `.qq()``.group()` 的扩展函数. - 在消息事件处理中添加获取 `.qq()``.group()` 的扩展函数.
- 现在处理群消息时 sender 为 Member (以前为 QQ) - 现在处理群消息时 sender 为 Member (以前为 QQ)
- 修改 `Message.concat``Message.followedBy` - 修改 `Message.concat``Message.followedBy`
- 修改成员权限 `OPERATOR``ADMINISTRATOR` - 修改成员权限 `OPERATOR``ADMINISTRATOR`
- **bot.subscribeAll<>() 等函数的 handler lambda 的 receiver 由 Bot 改变为 BotSession**; 此变动不会造成现有代码的修改, 但并不兼容旧版本编译的代码 - **bot.subscribeAll<>() 等函数的 handler lambda 的 receiver 由 Bot 改变为 BotSession**; 此变动不会造成现有代码的修改, 但并不兼容旧版本编译的代码
性能优化 性能优化
- 内联 ContactList - 内联 ContactList
- 2 个 Contact.sendMessage 重载改为内联扩展函数 **(需要添加 import)** - 2 个 Contact.sendMessage 重载改为内联扩展函数 **(需要添加 import)**
- 其他小优化 - 其他小优化
......
# 贡献
感谢你来到这里和你对 mirai 做的所有贡献。
mirai 欢迎一切形式的代码贡献。你可以通过以下几种途径向 mirai 贡献。
## 主仓库 `mirai-core`
### 代码优化
优化功能设计或实现, 或是引入一个新的设计(建议先通过 issue 与我们达成共识)
### 协议更新
为 mirai 添加更广泛的协议支持。
### 注意事项
- mirai 框架已经把实现协议需要做的工作最小化. 为避免工作重复, 请务必熟悉 `net.mamoe.mirai.utils``net.mamoe.mirai.qqandroid.utils` 中工具类
- mirai 使用 [`kotlinx.io`](https://github.com/Kotlin/kotlinx-io) IO 库
- mirai 为多平台项目, 请务必考虑多平台兼容性
- mirai 为全协程实现, 请在有必要的时候考虑并发安全性
- 尽量不要引用新的库
- 遵守 Kotlin 代码规范(提交前使用 IDE 格式化代码)
- 熟悉 [`PacketFactory`](https://github.com/mamoe/mirai/blob/master/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt) 架构
- 不要手动拆解数据包. 请一定使用 `kotlinx.serialization` 拆解 ProtoBuf 和使用 mirai 的 `Jce` 序列化器拆解 Jce 数据包
- 必须保证高代码效率(使用 `ByteArrayPool``WeakRef` 等)
## 社区
插件社区不要求太高的代码质量,任何人都可以帮助 mirai。
可以为 [mirai-console](https://github.com/mamoe/mirai-console) 编写插件, 并发布到社区网站: (建设中)
...@@ -6,14 +6,16 @@ Some of the protocol came from the other open-source projects. ...@@ -6,14 +6,16 @@ Some of the protocol came from the other open-source projects.
**The development is only for learning, DO NOT use it for illegal purposes.** **The development is only for learning, DO NOT use it for illegal purposes.**
## UpdateLog ## Changelog
You can inspect supported protocols at [Project](https://github.com/mamoe/mirai/projects/1) You can inspect supported protocols at [Project](https://github.com/mamoe/mirai/projects/1)
and logs of updates at [UpdateLog](https://github.com/mamoe/mirai/blob/master/UpdateLog.md) and logs of updates at [CHANGELOG](https://github.com/mamoe/mirai/blob/master/CHANGELOG.md)
## Use as a library ## Use as a library
You can install mirai as a library into your project. You can install mirai as a library into your project.
Mirai is only published on `jcenter`, therefore please ensure you have the `jcenter()` repository in your `build.gradle`, like: Mirai is only published on `jcenter`, therefore please ensure you have the `jcenter()` repository in your `build.gradle`.
```kotlin ```kotlin
repositories{ repositories{
jcenter() jcenter()
...@@ -23,7 +25,8 @@ repositories{ ...@@ -23,7 +25,8 @@ repositories{
If your project is a multiplatform project, you should add dependencies for each platform respectively. If your project is a multiplatform project, you should add dependencies for each platform respectively.
If your project is not a multiplatform project, you just need to add the platform-specific dependency. If your project is not a multiplatform project, you just need to add the platform-specific dependency.
`VERSION` should be replaced with the newest version, say [![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/) `VERSION` should be replaced with the newest version, say [![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
Mirai is still under experimental stage, it is suggested to keep the version newest. Mirai is still under experimental stage, it is suggested to keep the version newest.
**common** **common**
...@@ -71,14 +74,20 @@ If you meet any problem or have any questions, be free to open a issue. Our goal ...@@ -71,14 +74,20 @@ If you meet any problem or have any questions, be free to open a issue. Our goal
Kotlin 1.3.61 Kotlin 1.3.61
On JVM: Java 6 On JVM: Java 6
On Android: SDK 15 On Android: SDK 15
### Using java ### Using java
Q: Can I use Mirai without Kotlin? Q: Can I use Mirai without Kotlin?
A: Calling from java is not yet supported. Coroutines, extensions and inlines, which are difficult to use from Java, are generally used in Mirai. Therefore you should have the skill of Kotlin before you use Mirai. A: Calling from java is not yet supported. Coroutines, extensions and inlines, which are difficult to use from Java, are generally used in Mirai. Therefore you should have the skill of Kotlin before you use Mirai.
#### Libraries used ## Acknowledgements
Mirai uses these open-source libraries.
Thanks to [JetBrains](https://www.jetbrains.com/?from=mirai) for allocating free open-source licences for IDEs such as [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai).
[<img src=".github/jetbrains-variant-3.png" width="200"/>](https://www.jetbrains.com/?from=mirai)
### Third Party Libraries
- [kotlin-stdlib](https://github.com/JetBrains/kotlin) - [kotlin-stdlib](https://github.com/JetBrains/kotlin)
- [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines) - [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines)
...@@ -92,7 +101,19 @@ Mirai uses these open-source libraries. ...@@ -92,7 +101,19 @@ Mirai uses these open-source libraries.
- [javafx](https://github.com/openjdk/jfx) - [javafx](https://github.com/openjdk/jfx)
- [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) - [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization)
## License
## Acknowledgement Copyright (C) 2019-2020 mamoe and Mirai contributors
Thanks to [JetBrains](https://www.jetbrains.com/?from=mirai) for allocating free open-source licences for IDEs such as [IntelliJ IDEA](https://www.jetbrains.com/idea/?from=mirai).
[<img src=".github/jetbrains-variant-3.png" width="200"/>](https://www.jetbrains.com/?from=mirai) This program is free software: you can redistribute it and/or modify
\ No newline at end of file it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
This diff is collapsed.
import java.lang.System.getProperty
import java.util.*
buildscript { buildscript {
repositories { repositories {
mavenLocal() mavenLocal()
maven(url = "https://mirrors.huaweicloud.com/repository/maven")
jcenter() jcenter()
mavenCentral() // mavenCentral()
google() google()
maven { url 'https://dl.bintray.com/kotlin/kotlin-dev/'} // maven (url="https://dl.bintray.com/kotlin/kotlin-eap")
} }
dependencies { dependencies {
// Do try to waste your time. val kotlinVersion: String by project
classpath 'com.android.tools.build:gradle:3.5.3' val atomicFuVersion: String by project
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath("com.android.tools.build:gradle:3.5.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("com.github.jengelman.gradle.plugins:shadow:5.2.0") classpath("com.github.jengelman.gradle.plugins:shadow:5.2.0")
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")
classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicFuVersion" classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicFuVersion")
} }
} }
try { runCatching {
def keyProps = new Properties() val keyProps = Properties().apply {
def keyFile = file("local.properties") file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
if (keyFile.exists()) keyFile.withInputStream { keyProps.load(it) } }
if (!keyProps.getProperty("sdk.dir", "").isEmpty()) { if (keyProps.getProperty("sdk.dir", "").isNotEmpty()) {
project.ext.set("isAndroidSDKAvailable", true) project.ext.set("isAndroidSDKAvailable", true)
} else { } else {
project.ext.set("isAndroidSDKAvailable", false) project.ext.set("isAndroidSDKAvailable", false)
} }
}catch(Exception e){} }
allprojects { allprojects {
group = "net.mamoe" group = "net.mamoe"
version = getProperty("mirai_version") version = getProperty("miraiVersion")
// tasks.withType(KotlinCompile).all { task ->
// task.kotlinOptions{
// jvmTarget = '1.6'
// }
// }
repositories { repositories {
mavenLocal() mavenLocal()
maven(url = "https://mirrors.huaweicloud.com/repository/maven")
jcenter() jcenter()
mavenCentral() // mavenCentral()
google() google()
maven { url "https://dl.bintray.com/kotlin/kotlin-eap" } // maven (url="https://dl.bintray.com/kotlin/kotlin-eap")
maven { url "https://dl.bintray.com/kotlin/kotlin-dev" }
} }
} }
\ No newline at end of file
# Mirai Guide - Build For Mirai
由于Mirai项目在快速推进中,因此内容时有变动,本文档的最后更新日期为```2020-02-29```,对应版本```0.23.0```
本页面采用Kotlin作为开发语言,**若你希望使用 Java 开发**, 请参阅: [mirai-japt](https://github.com/mamoe/mirai-japt)
本页面是[Mirai Guide - Subscribe Events](/docs/guide_subscribe_events.md)的后续Guide
## build.gradle
我们首先来看一下完整的```build.gradle```文件
```groovy
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.3.61'
}
group 'test'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
jcenter()
}
dependencies {
implementation 'net.mamoe:mirai-core-qqandroid-jvm:0.19.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile group: 'junit', name: 'junit', version: '4.12'
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
```
使用gradle直接打包,不会将依赖也打包进去
因此,我们引入一些插件进行打包
### ShadowJar
shadowJar支持将依赖也打包到Jar内,下面介绍用法。
#### 1.buildscript
首先声明buildScript
```groovy
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
}
}
```
在plugin前加入以上语句
#### 2.在plugins中进行插件的使用
将原本的plugins
```groovy
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.3.61'
}
```
覆盖为
```groovy
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.3.61'
id 'com.github.johnrengelman.shadow' version '5.2.0'//使用shadow对依赖进行打包
}
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'java'
```
#### 3.添加shadowJar
在文件底部添加
```groovy
shadowJar {
// 生成包的命名规则: baseName-version-classifier.jar
manifest {
attributes(
'Main-Class': 'net.mamoe.mirai.simpleloader.MyLoaderKt'//入口点
)
}
// 将 build.gradle 打入到 jar 中, 方便查看依赖包版本
from("./"){
include 'build.gradle'
}
}
```
#### 4.运行build
在IDEA中点击```ShadowJar```左侧的run按钮(绿色小三角),build完成后在```build\libs```中找到jar
至此,build.gradle内的内容是
```groovy
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
}
}
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.3.61'
id 'com.github.johnrengelman.shadow' version '5.2.0'//使用shadow对依赖进行打包
}
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'java'
group 'test'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
jcenter()
}
dependencies {
implementation 'net.mamoe:mirai-core-qqandroid-jvm:0.23.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile group: 'junit', name: 'junit', version: '4.12'
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
shadowJar {
// 生成包的命名规则: baseName-version-classifier.jar
manifest {
attributes(
'Main-Class': 'net.mamoe.mirai.simpleloader.MyLoaderKt'
)
}
// 将 build.gradle 打入到 jar 中, 方便查看依赖包版本
from("./"){
include 'build.gradle'
}
}
```
# Mirai Guide - Getting Started
由于Mirai项目在快速推进中,因此内容时有变动,本文档的最后更新日期为```2020-02-29```,对应版本```0.23.0```
假如仅仅使用Mirai,不需要对整个项目进行Clone,只需在项目内添加Gradle Dependency或使用即可。
下面介绍详细的入门步骤。
本页采用Kotlin作为开发语言,**若你希望使用 Java 开发**, 请参阅: [mirai-japt](https://github.com/mamoe/mirai-japt)
## 起步步骤
通过编写Kotlin程序,以第三方库的形式调用```mirai-core```,并定义你的Mirai Bot行为。
假如已经对Gradle有一定了解,可跳过1,2
### 1 安装IDEA与JDK
JDK要求6以上
### 2 新建Gradle项目
*使用gradle项目可能需要代理,在IDEA的```settings```->```proxy settings```中可以设置
-```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.23.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```-```Package```) 包名为```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进行功能的添加。
下面,可以尝试对不同事件进行监听[Mirai Guide - Subscribe Events](/docs/guide_subscribe_events.md)
### 此外,还可以使用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.23.0</version> <!-- 替换版本为最新版本 -->
</dependency>
</dependencies>
```
# Mirai Guide - Quick Start
由于 mirai 项目在快速推进中,因此内容时有变动,本文档的最后更新日期为```2020/3/1```,对应版本```0.23.0```
本文适用于对 Kotlin 较熟悉的开发者,
使用 mirai 作为第三方依赖库引用任意一个 Kotlin, Java 或其他 JVM 平台的项目
**若你希望一份更基础的教程**, 请参阅: [mirai-guide-getting-started](guide_getting_started.md)
**若你希望使用 Java 开发**, 请参阅: [mirai-japt](https://github.com/mamoe/mirai-japt)
## 构建需求
- Kotlin 1.3.61 (必须)
- JDK 6 或更高 (必须)
- Android SDK 29 (可选, 用于编译安卓目标)
## 获取 Demo
可在 [mirai-demos](https://github.com/mamoe/mirai-demos) 中获取已经配置好依赖的示例项目.
## Quick Start
请将 `VERSION` 替换为 `mirai-core` 的最新版本号(如 `0.23.0`):
[![Download](https://api.bintray.com/packages/him188moe/mirai/mirai-core/images/download.svg)](https://bintray.com/him188moe/mirai/mirai-core/)
### 添加依赖
#### 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.23.0</version> <!-- 替换版本为最新版本 -->
</dependency>
</dependencies>
```
#### Gradle
Mirai 只发布在 `jcenter`, 因此请确保添加 `jcenter()` 仓库:
```kotlin
repositories{
jcenter()
}
```
**注意:**
Mirai 核心由 API 模块(`mirai-core`)和协议模块组成。
只添加 API 模块将无法正常工作。
现在只推荐使用 QQAndroid 协议,请参照下文选择对应目标平台的依赖添加。
**jvm** (JVM 平台源集)
```kotlin
implementation("net.mamoe:mirai-core-qqandroid-jvm:VERSION")
```
**common** (Kotlin 多平台项目的通用源集)
```kotlin
implementation("net.mamoe:mirai-core-qqandroid-common:VERSION")
```
**android** (Android 平台源集)
```kotlin
implementation("net.mamoe:mirai-core-qqandroid-android:VERSION")
```
### 开始使用
```kotlin
val bot = Bot(qqId, password).alsoLogin()
bot.subscribeMessages {
"你好" reply "你好!"
contains("图片"){ File("C:\\image.png").sendAsImage() }
}
bot.subscribeAlways<MemberPermissionChangedEvent> {
if (it.kind == BECOME_OPERATOR)
reply("${it.member.id} 成为了管理员")
}
```
\ No newline at end of file
# Mirai Guide - Subscribe Events
由于Mirai项目在快速推进中,因此内容时有变动,本文档的最后更新日期为```2020-02-29```,对应版本```0.23.0```
本页面采用Kotlin作为开发语言,**若你希望使用 Java 开发**, 请参阅: [mirai-japt](https://github.com/mamoe/mirai-japt)
本页面是[Mirai Guide - Getting Started](/docs/guide_getting_started.md)的后续Guide
## 消息事件-Message Event
首先我们来回顾上一个Guide的源码
```kotlin
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 离线, 避免主线程退出
}
```
在本例中,```miraiBot```是一个Bot对象,让其登录,然后对```Message Event```进行了监听。
对于``````Message Event`````````Mirai```提供了较其他Event更强大的[MessageSubscribersBuilder](https://github.com/mamoe/mirai/wiki/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/MessageSubscribers.kt#L140),本例也采用了[MessageSubscribersBuilder](https://github.com/mamoe/mirai/wiki/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/MessageSubscribers.kt#L140)。其他具体使用方法可以参考[Wiki:Message Event](https://github.com/mamoe/mirai/wiki/Development-Guide---Kotlin#Message-Event)部分。
## 事件-Event
上一节中提到的```Message Event```仅仅是众多```Event```的这一种,其他```Event```有群员加入群,离开群,私聊等等...
具体doc暂不提供,现可翻阅源码[**BotEvents.kt**](https://github.com/mamoe/mirai/blob/master/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt),查看注释。当前事件仍在扩充中,可能有一定不足。
下面我们开始示例对一些事件进行监听。
## 尝试监听事件-Try Subscribing Events
### 监听加群事件
在代码中的```miraiBot.join()```前添加
```kotlin
miraiBot.subscribeAlways<MemberJoinEvent> {
it.group.sendMessage("欢迎 ${it.member.nameCardOrNick} 加入本群!")
}
```
本段语句监听了加入群的事件。
### 监听禁言事件
在代码中添加
```kotlin
miraiBot.subscribeAlways<MemberMuteEvent> (){
it.group.sendMessage("恭喜老哥 ${it.member.nameCardOrNick} 喜提禁言套餐一份")
}
```
在被禁言后,Bot将发送恭喜语句。
### 添加后的可执行代码
至此,当前的代码为
```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
import net.mamoe.mirai.contact.nameCardOrNick
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.event.events.MemberJoinEvent
import net.mamoe.mirai.event.events.MemberMuteEvent
import net.mamoe.mirai.event.subscribeAlways
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.subscribeAlways<MemberJoinEvent> {
it.group.sendMessage("欢迎 ${it.member.nameCardOrNick} 加入本群!")
}
miraiBot.subscribeAlways<MemberMuteEvent> (){
it.group.sendMessage("恭喜老哥 ${it.member.nameCardOrNick} 喜提禁言套餐一份")
}
miraiBot.join() // 等待 Bot 离线, 避免主线程退出
}
```
下面可以参阅[Mirai Guide - Build For Mirai](/docs/guide_build_for_mirai.md),对你的Mirai应用进行打包
# style guide # style guide
kotlin.code.style=official kotlin.code.style=official
# config # config
mirai_version=0.15.0 miraiVersion=0.27.0
mirai_japt_version=1.0.0
kotlin.incremental.multiplatform=true kotlin.incremental.multiplatform=true
kotlin.parallel.tasks.in.project=true kotlin.parallel.tasks.in.project=true
# kotlin # kotlin
kotlinVersion=1.3.61 kotlinVersion=1.3.70
# kotlin libraries # kotlin libraries
serializationVersion=0.14.0 serializationVersion=0.20.0
coroutinesVersion=1.3.3 coroutinesVersion=1.3.4
atomicFuVersion=0.14.1 atomicFuVersion=0.14.1
kotlinXIoVersion=0.1.16 kotlinXIoVersion=0.1.16
coroutinesIoVersion=0.1.16 coroutinesIoVersion=0.1.16
# utility # utility
ktorVersion=1.3.1 ktorVersion=1.3.1
klockVersion=1.7.0 \ No newline at end of file
# gradle plugin
protobufJavaVersion=3.10.0
\ No newline at end of file
// 部分源码来自 kotlinx.coroutines
def pomConfig = {
licenses {
license {
name "AGPL-V3"
url "https://www.gnu.org/licenses/agpl-3.0.txt"
distribution "repo"
}
}
developers {
developer {
id "mamoe"
name "Mamoe Technologies"
}
}
scm {
url "https://github.com/mamoe/mirai"
}
}
bintray {
def keyProps = new Properties()
def keyFile = file("../keys.properties")
if (keyFile.exists()) keyFile.withInputStream { keyProps.load(it) }
user = keyProps.getProperty("bintrayUser")
key = keyProps.getProperty("bintrayKey")
pkg {
repo = 'mirai'
name = "mirai-japt"
licenses = ['AGPL']
vcsUrl = 'https://github.com/mamoe/mirai'
}
}
afterEvaluate {
project.publishing.publications.forEach { publication ->
publication.pom.withXml {
def root = asNode()
//root.appendNode('groupId', project.group)
//root.appendNode('artifactId', project.name)
//root.appendNode('version', project.version)
root.appendNode('name', project.name)
root.appendNode('description', project.description)
root.appendNode('url', 'https://github.com/mamoe/mirai')
root.children().last() + pomConfig
}
}
}
bintrayUpload.doFirst {
publications = project.publishing.publications
}
bintrayUpload.dependsOn {
def list = new LinkedList<Task>()
list.add(tasks.getByName("build"))
list.addAll(tasks.findAll { task -> task.name.contains('Jar') })
list.addAll(tasks.findAll { task -> task.name.startsWith('generateMetadataFileFor') })
list.addAll(tasks.findAll { task -> task.name.startsWith('generatePomFileFor') })
list
}
// empty xxx-javadoc.jar
task javadocJar(type: Jar) {
archiveClassifier = 'javadoc'
}
publishing {
publications.all {
// add empty javadocs (no need for MPP root publication which publishes only pom file)
if (it.name != 'kotlinMultiplatform') {
it.artifact(javadocJar)
}
// Rename MPP artifacts for backward compatibility
def type = it.name
switch (type) {
case 'kotlinMultiplatform':
it.artifactId = "$project.name"
break
case 'metadata':
it.artifactId = "$project.name-common"
break
case 'jvm':
it.artifactId = "$project.name"
break
case 'js':
case 'native':
it.artifactId = "$project.name-$type"
break
}
// disable metadata everywhere, but in native modules
if (type == 'maven' || type == 'metadata' || type == 'jvm' || type == 'js') {
moduleDescriptorGenerator = null
}
}
}
\ No newline at end of file
// 部分源码来自 kotlinx.coroutines // 部分源码来自 kotlinx.coroutines
// Source code from kotlinx.coroutines
def pomConfig = { def pomConfig = {
licenses { licenses {
...@@ -12,6 +13,7 @@ def pomConfig = { ...@@ -12,6 +13,7 @@ def pomConfig = {
developer { developer {
id "mamoe" id "mamoe"
name "Mamoe Technologies" name "Mamoe Technologies"
email "support@mamoe.net"
} }
} }
scm { scm {
...@@ -65,12 +67,16 @@ bintrayUpload.dependsOn { ...@@ -65,12 +67,16 @@ bintrayUpload.dependsOn {
list list
} }
try{
// empty xxx-javadoc.jar // empty xxx-javadoc.jar
task javadocJar(type: Jar) { task javadocJar(type: Jar) {
archiveClassifier = 'javadoc' archiveClassifier = 'javadoc'
} }
} catch (Exception e){
}
publishing { publishing {
publications.all { publications.all {
// add empty javadocs (no need for MPP root publication which publishes only pom file) // add empty javadocs (no need for MPP root publication which publishes only pom file)
...@@ -88,7 +94,7 @@ publishing { ...@@ -88,7 +94,7 @@ publishing {
it.artifactId = "$project.name-common" it.artifactId = "$project.name-common"
break break
case 'jvm': case 'jvm':
it.artifactId = "$project.name" it.artifactId = "$project.name-jvm"
break break
case 'js': case 'js':
case 'native': case 'native':
......
#Thu Feb 06 14:10:33 CST 2020 #Thu Feb 06 14:10:33 CST 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-bin.zip
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
......
# mirai-api-http
<b>
Mirai-API-http provides adapter for ALL langugae to access mirai via HTTP protocol.<br>
</b>
**[中文](README_CH.md)**
### Start Session-Authorize
```php
Path: /auth
Method: POST
```
this verify your session to one bot and you could have full access to that bot<br>
NOTE that only 1 bot could be control under 1 session, you could have multiple session to control all bots.
#### Request:<br>
| name | type | optional|example|note|
| --- | --- | --- | --- | --- |
| key | String |false|U9HSaDXl39ksd918273hU|MIRAI API HTTP key, this could be found after initialize|
| qq | String |false|1040400290|bot QQ number you want to access|
#### Response if success:<br>
| name | type | example|note|
| --- | --- | --- | --- |
| success |Boolean |true|if this session is authorized|
| session |String |UANSHDKSLAOISN|your session key|
#### Response if failed:<br>
| name | type | example|note|
| --- | --- | --- | --- |
| success |Boolean |false|if this session is authorized|
| session |String |null|your session key|
| error |int |0|error code|
#### Error:<br>
| code | reason|
| --- | --- |
| 0 | wrong MIRAI API HTTP key |
| 1 | unknown bot number |
without session key, you are not able to access any method below.</br>
session key should be attached to your <b>cookies</b> like this:
| name | value |
| --- | --- |
| session |your session key here |
if you were getting HTTP error code 403, you should ask for a new session key.
\ No newline at end of file
This diff is collapsed.
@file:Suppress("UNUSED_VARIABLE")
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
plugins {
id("kotlinx-atomicfu")
kotlin("jvm")
id("kotlinx-serialization")
}
group = "net.mamoe.mirai"
version = rootProject.ext["mirai_version"].toString()
description = "Mirai Http Api"
val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext
val coroutinesVersion: String by rootProject.ext
val kotlinXIoVersion: String by rootProject.ext
val coroutinesIoVersion: String by rootProject.ext
val klockVersion: String by rootProject.ext
val ktorVersion: String by rootProject.ext
val serializationVersion: String by rootProject.ext
fun KotlinDependencyHandler.kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
fun KotlinDependencyHandler.ktor(id: String, version: String = ktorVersion) = "io.ktor:ktor-$id:$version"
kotlin {
sourceSets["main"].apply {
dependencies {
implementation(project(":mirai-core-qqandroid"))
implementation(kotlin("stdlib-jdk8", kotlinVersion))
implementation(kotlin("stdlib-jdk7", kotlinVersion))
implementation(kotlin("reflect", kotlinVersion))
implementation(ktor("server-cio"))
implementation(kotlinx("io-jvm", kotlinXIoVersion))
implementation(ktor("http-jvm"))
implementation("org.slf4j:slf4j-simple:1.7.26")
}
}
sourceSets["test"].apply {
dependencies {
}
kotlin.outputDir = file("build/classes/kotlin/jvm/test")
kotlin.setSrcDirs(listOf("src/$name/kotlin"))
}
sourceSets.all {
languageSettings.enableLanguageFeature("InlineClasses")
languageSettings.useExperimentalAnnotation("kotlin.Experimental")
dependencies {
implementation(kotlin("stdlib", kotlinVersion))
implementation(kotlin("serialization", kotlinVersion))
implementation("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
implementation(kotlinx("io", kotlinXIoVersion))
implementation(kotlinx("coroutines-io", coroutinesIoVersion))
implementation(kotlinx("coroutines-core", coroutinesVersion))
implementation(kotlinx("serialization-runtime", serializationVersion))
implementation(ktor("server-core"))
implementation(ktor("http"))
}
}
}
/*
* 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.api.http
import io.ktor.application.Application
import io.ktor.server.cio.CIO
import io.ktor.server.engine.applicationEngineEnvironment
import io.ktor.server.engine.connector
import io.ktor.server.engine.embeddedServer
import io.ktor.util.KtorExperimentalAPI
import net.mamoe.mirai.api.http.route.mirai
import net.mamoe.mirai.utils.DefaultLogger
import org.slf4j.LoggerFactory
import org.slf4j.helpers.NOPLogger
import org.slf4j.helpers.NOPLoggerFactory
object MiraiHttpAPIServer {
private val logger = DefaultLogger("Mirai HTTP API")
init {
SessionManager.authKey = generateSessionKey()//用于验证的key, 使用和SessionKey相同的方法生成, 但意义不同
}
fun setAuthKey(key: String) {
SessionManager.authKey = key
}
@UseExperimental(KtorExperimentalAPI::class)
fun start(
port: Int = 8080,
authKey: String,
callback: (() -> Unit)? = null
) {
require(authKey.length in 8..128) { "Expected authKey length is between 8 to 128" }
SessionManager.authKey = authKey
// TODO: start是无阻塞的,理应获取启动状态后再执行后续代码
try {
embeddedServer(CIO, environment = applicationEngineEnvironment {
this.log = NOPLoggerFactory().getLogger("NMYSL")
this.module(Application::mirai)
connector {
this.port = port
}
}).start()
logger.info("Http api server is running with authKey: ${SessionManager.authKey}")
callback?.invoke()
} catch (e: Exception) {
logger.error("Http api server launch error")
}
}
}
\ No newline at end of file
/*
* 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.api.http
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.subscribeMessages
import net.mamoe.mirai.message.MessagePacket
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
tailrec fun generateSessionKey(): String {
fun generateRandomSessionKey(): String {
val all = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm"
return buildString(capacity = 8) {
repeat(8) {
append(all.random())
}
}
}
val key = generateRandomSessionKey()
if (!SessionManager.allSession.containsKey(key)) {
return key
}
return generateSessionKey()
}
internal object SessionManager {
val allSession: MutableMap<String, Session> = mutableMapOf()
lateinit var authKey: String
fun createTempSession(): TempSession = TempSession(EmptyCoroutineContext).also { newTempSession ->
allSession[newTempSession.key] = newTempSession
//设置180000ms后检测并回收
newTempSession.launch {
delay(180000)
allSession[newTempSession.key]?.run {
if (this is TempSession)
closeSession(newTempSession.key)
}
}
}
operator fun get(sessionKey: String) = allSession[sessionKey]
fun containSession(sessionKey: String): Boolean = allSession.containsKey(sessionKey)
fun closeSession(sessionKey: String) = allSession.remove(sessionKey)?.also { it.close() }
fun closeSession(session: Session) = closeSession(session.key)
}
/**
* @author NaturalHG
* 这个用于管理不同Client与Mirai HTTP的会话
*
* [Session]均为内部操作用类
* 需使用[SessionManager]
*/
abstract class Session internal constructor(
coroutineContext: CoroutineContext
) : CoroutineScope {
val supervisorJob = SupervisorJob(coroutineContext[Job])
final override val coroutineContext: CoroutineContext = supervisorJob + coroutineContext
val key: String = generateSessionKey()
internal open fun close() {
supervisorJob.complete()
}
}
/**
* 任何新链接建立后分配一个[TempSession]
*
* TempSession在建立180s内没有转变为[AuthedSession]应被清除
*/
class TempSession internal constructor(coroutineContext: CoroutineContext) : Session(coroutineContext)
/**
* 任何[TempSession]认证后转化为一个[AuthedSession]
* 在这一步[AuthedSession]应该已经有assigned的bot
*/
class AuthedSession internal constructor(val bot: Bot, coroutineContext: CoroutineContext) : Session(coroutineContext) {
val messageQueue = MessageQueue()
private val _listener: Listener<MessagePacket<*, *>>
init {
bot.subscribeMessages {
_listener = always { this.run(messageQueue::add) } // this aka messagePacket
}
}
override fun close() {
_listener.complete()
super.close()
}
}
package net.mamoe.mirai.api.http.data
/**
* 错误请求. 抛出这个异常后将会返回错误给一个请求
*/
@Suppress("unused")
open class IllegalAccessException : Exception {
override val message: String get() = super.message!!
constructor(message: String) : super(message, null)
constructor(cause: Throwable) : super(cause.toString(), cause)
constructor(message: String, cause: Throwable?) : super(message, cause)
}
/**
* Session失效或不存在
*/
object IllegalSessionException : IllegalAccessException("Session失效或不存在")
/**
* Session未激活
*/
object NotVerifiedSessionException : IllegalAccessException("Session未激活")
/**
* 指定Bot不存在
*/
object NoSuchBotException: IllegalAccessException("指定Bot不存在")
/**
* 指定Bot不存在
*/
object PermissionDeniedException: IllegalAccessException("无操作限权")
/**
* 错误参数
*/
class IllegalParamException(message: String) : IllegalAccessException(message)
\ No newline at end of file
package net.mamoe.mirai.api.http.data
import kotlinx.serialization.Serializable
@Serializable
open class StateCode(val code: Int, var msg: String) {
object Success : StateCode(0, "success") // 成功
object NoBot : StateCode(2, "指定Bot不存在")
object IllegalSession : StateCode(3, "Session失效或不存在")
object NotVerifySession : StateCode(4, "Session未认证")
object NoElement : StateCode(5, "指定对象不存在")
object PermissionDenied : StateCode(10, "无操作权限")
// KS bug: 主构造器中不能有非字段参数 https://github.com/Kotlin/kotlinx.serialization/issues/575
@Serializable
class IllegalAccess() : StateCode(400, "") { // 非法访问
constructor(msg: String) : this() {
this.msg = msg
}
}
}
\ No newline at end of file
/*
* 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.api.http.data.common
import kotlinx.serialization.Serializable
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI
@Serializable
abstract class ContactDTO : DTO {
abstract val id: Long
}
@Serializable
data class QQDTO(
override val id: Long,
val nickName: String,
val remark: String
) : ContactDTO() {
// TODO: queryProfile.nickname & queryRemark.value not support now
constructor(qq: QQ) : this(qq.id, "", "")
}
@Serializable
data class MemberDTO(
override val id: Long,
val memberName: String,
val permission: MemberPermission,
val group: GroupDTO
) : ContactDTO() {
constructor(member: Member) : this(
member.id, member.groupCardOrNick, member.permission,
GroupDTO(member.group)
)
}
@Serializable
data class GroupDTO(
override val id: Long,
val name: String,
val permission: MemberPermission
) : ContactDTO() {
@UseExperimental(MiraiExperimentalAPI::class)
constructor(group: Group) : this(group.id, group.name, group.botPermission)
}
package net.mamoe.mirai.api.http.data.common
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import net.mamoe.mirai.api.http.AuthedSession
interface DTO
@Serializable
data class AuthDTO(val authKey: String) : DTO
@Serializable
abstract class VerifyDTO : DTO {
abstract val sessionKey: String
@Transient
lateinit var session: AuthedSession // 反序列化验证成功后传入
}
/*
* 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.api.http.data.common
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.MessagePacket
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.MiraiInternalAPI
/*
* DTO data class
* */
// MessagePacket
@Serializable
@SerialName("FriendMessage")
data class FriendMessagePacketDTO(val sender: QQDTO) : MessagePacketDTO()
@Serializable
@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()
@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()
/*
* Abstract Class
* */
@Serializable
sealed class MessagePacketDTO : DTO {
lateinit var messageChain : MessageChainDTO
}
typealias MessageChainDTO = Array<MessageDTO>
@Serializable
sealed class MessageDTO : DTO
/*
Extend function
*/
suspend fun MessagePacket<*, *>.toDTO(): MessagePacketDTO = 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() }}
fun MessageChainDTO.toMessageChain() =
MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage()) } }
@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 PlainText -> PlainDTO(stringValue)
is Image -> ImageDTO(imageId)
is XMLMessage -> XmlDTO(stringValue)
else -> UnknownMessageDTO("未知消息类型")
}
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
fun MessageDTO.toMessage() = when (this) {
is AtDTO -> At(target, display)
is AtAllDTO -> AtAll
is FaceDTO -> Face(FaceId(faceId.toUByte()))
is PlainDTO -> PlainText(text)
is ImageDTO -> Image(imageId)
is XmlDTO -> XMLMessage(xml)
is MessageSourceDTO, is UnknownMessageDTO -> PlainText("assert cannot reach")
}
/*
* 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.api.http.queue
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<*, *>>() {
val quoteCache = ConcurrentHashMap<Long, GroupMessage>()
fun fetch(size: Int): List<MessagePacket<*, *>> {
var count = size
quoteCache.clear()
val ret = ArrayList<MessagePacket<*, *>>(count)
while (!this.isEmpty() && count-- > 0) {
val packet = pop()
ret.add(packet)
if (packet is GroupMessage) {
addCache(packet)
}
}
return ret
}
private fun addCache(msg: GroupMessage) {
quoteCache[msg.message[MessageSource].messageUid] = msg
}
}
\ No newline at end of file
/*
* 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.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.routing.routing
import kotlinx.serialization.Serializable
import net.mamoe.mirai.Bot
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.VerifyDTO
import kotlin.coroutines.EmptyCoroutineContext
fun Application.authModule() {
routing {
miraiAuth("/auth") {
if (it.authKey != SessionManager.authKey) {
call.respondStateCode(StateCode(1, "Auth Key错误"))
} else {
call.respondStateCode(StateCode(0, SessionManager.createTempSession().key))
}
}
miraiVerify<BindDTO>("/verify", verifiedSessionKey = false) {
val bot = getBotOrThrow(it.qq)
with(SessionManager) {
closeSession(it.sessionKey)
allSession[it.sessionKey] = AuthedSession(bot, EmptyCoroutineContext)
}
call.respondStateCode(StateCode.Success)
}
miraiVerify<BindDTO>("/release") {
val bot = getBotOrThrow(it.qq)
val session = SessionManager[it.sessionKey] as AuthedSession
if (bot.uin == session.bot.uin) {
SessionManager.closeSession(it.sessionKey)
call.respondStateCode(StateCode.Success)
} else {
throw NoSuchElementException()
}
}
}
}
@Serializable
private data class BindDTO(override val sessionKey: String, val qq: Long) : VerifyDTO()
private fun getBotOrThrow(qq: Long) = try {
Bot.instanceWhose(qq)
} catch (e: NoSuchElementException) {
throw NoSuchBotException
}
\ No newline at end of file
/*
* 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.api.http.route
import io.ktor.application.Application
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.request.receive
import io.ktor.response.defaultTextContentType
import io.ktor.response.respondText
import io.ktor.routing.Route
import io.ktor.routing.route
import io.ktor.util.pipeline.ContextDsl
import io.ktor.util.pipeline.PipelineContext
import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.api.http.TempSession
import net.mamoe.mirai.api.http.data.*
import net.mamoe.mirai.api.http.data.common.AuthDTO
import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.api.http.util.jsonParseOrNull
import net.mamoe.mirai.api.http.util.toJson
import org.slf4j.Logger
import org.slf4j.helpers.NOPLogger
import org.slf4j.helpers.NOPLoggerFactory
import org.slf4j.impl.SimpleLogger
import org.slf4j.impl.SimpleLoggerFactory
fun Application.mirai() {
install(DefaultHeaders)
install(CallLogging) {
logger = NOPLoggerFactory().getLogger("NMSL")
}
authModule()
messageModule()
infoModule()
groupManageModule()
}
/**
* Auth,处理http server的验证
* 为闭包传入一个AuthDTO对象
*/
@ContextDsl
internal fun Route.miraiAuth(
path: String,
body: suspend PipelineContext<Unit, ApplicationCall>.(AuthDTO) -> Unit
): Route {
return route(path, HttpMethod.Post) {
intercept {
val dto = context.receiveDTO<AuthDTO>() ?: throw IllegalParamException("参数格式错误")
this.body(dto)
}
}
}
/**
* Get,用于获取bot的属性
* 验证请求参数中sessionKey参数的有效性
*/
@ContextDsl
internal fun Route.miraiGet(
path: String,
body: suspend PipelineContext<Unit, ApplicationCall>.(AuthedSession) -> Unit
): Route {
return route(path, HttpMethod.Get) {
intercept {
val sessionKey = call.parameters["sessionKey"] ?: throw IllegalParamException("参数格式错误")
if (!SessionManager.containSession(sessionKey)) throw IllegalSessionException
when(val session = SessionManager[sessionKey]) {
is TempSession -> throw NotVerifiedSessionException
is AuthedSession -> this.body(session)
}
}
}
}
/**
* Verify,用于处理bot的行为请求
* 验证数据传输对象(DTO)中是否包含sessionKey字段
* 且验证sessionKey的有效性
*
* @param verifiedSessionKey 是否验证sessionKey是否被激活
*
* it 为json解析出的DTO对象
*/
@ContextDsl
internal inline fun <reified T : VerifyDTO> Route.miraiVerify(
path: String,
verifiedSessionKey: Boolean = true,
crossinline body: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit
): Route {
return route(path, HttpMethod.Post) {
intercept {
val dto = context.receiveDTO<T>() ?: throw IllegalParamException("参数格式错误")
SessionManager[dto.sessionKey]?.let {
when {
it is TempSession && verifiedSessionKey -> throw NotVerifiedSessionException
it is AuthedSession -> dto.session = it
}
} ?: throw IllegalSessionException
this.body(dto)
}
}
}
/**
* 统一捕获并处理异常
*/
internal inline fun Route.intercept(crossinline blk: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit) = handle {
try {
blk(this)
} catch (e: IllegalSessionException) {
call.respondStateCode(StateCode.IllegalSession)
} catch (e: NotVerifiedSessionException) {
call.respondStateCode(StateCode.NotVerifySession)
} catch (e: NoSuchBotException) {
call.respondStateCode(StateCode.NoBot)
} catch (e: NoSuchElementException) {
call.respondStateCode(StateCode.NoElement)
} catch (e: PermissionDeniedException) {
call.respondStateCode(StateCode.PermissionDenied)
} catch (e: IllegalAccessException) {
call.respondStateCode(StateCode(400, e.message), HttpStatusCode.BadRequest)
}
}
/*
extend function
*/
internal suspend inline fun <reified T : StateCode> ApplicationCall.respondStateCode(code: T, status: HttpStatusCode = HttpStatusCode.OK) = respondJson(code.toJson(StateCode.serializer()), status)
internal suspend inline fun <reified T : DTO> ApplicationCall.respondDTO(dto: T, status: HttpStatusCode = HttpStatusCode.OK) = respondJson(dto.toJson(), status)
internal suspend fun ApplicationCall.respondJson(json: String, status: HttpStatusCode = HttpStatusCode.OK) =
respondText(json, defaultTextContentType(ContentType("application", "json")), status)
internal suspend inline fun <reified T : DTO> ApplicationCall.receiveDTO(): T? = receive<String>().jsonParseOrNull()
fun PipelineContext<Unit, ApplicationCall>.illegalParam(
expectingType: String?,
paramName: String,
actualValue: String? = call.parameters[paramName]
): Nothing = throw IllegalParamException("Illegal param. A $expectingType is required for `$paramName` while `$actualValue` is given")
@Suppress("IMPLICIT_CAST_TO_ANY")
@UseExperimental(ExperimentalUnsignedTypes::class)
internal inline fun <reified R> PipelineContext<Unit, ApplicationCall>.paramOrNull(name: String): R =
when (R::class) {
Byte::class -> call.parameters[name]?.toByte()
Int::class -> call.parameters[name]?.toInt()
Short::class -> call.parameters[name]?.toShort()
Float::class -> call.parameters[name]?.toFloat()
Long::class -> call.parameters[name]?.toLong()
Double::class -> call.parameters[name]?.toDouble()
Boolean::class -> when (call.parameters[name]) {
"true" -> true
"false" -> false
"0" -> false
"1" -> true
null -> null
else -> illegalParam("boolean", name)
}
String::class -> call.parameters[name]
UByte::class -> call.parameters[name]?.toUByte()
UInt::class -> call.parameters[name]?.toUInt()
UShort::class -> call.parameters[name]?.toUShort()
else -> error(name::class.simpleName + " is not supported")
} as R ?: illegalParam(R::class.simpleName, name)
/**
* multi part
*/
internal fun List<PartData>.value(name: String) =
try {
(filter { it.name == name }[0] as PartData.FormItem).value
} catch (e: Exception) {
throw IllegalParamException("参数格式错误")
}
internal fun List<PartData>.file(name: String) =
try {
filter { it.name == name }[0] as? PartData.FileItem
} catch (e: Exception) {
throw IllegalParamException("参数格式错误")
}
\ No newline at end of file
/*
* 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.api.http.route
package net.mamoe.mirai.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.routing.routing
import kotlinx.serialization.Serializable
import net.mamoe.mirai.api.http.data.PermissionDeniedException
import net.mamoe.mirai.api.http.data.StateCode
import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
fun Application.groupManageModule() {
routing {
/**
* 禁言(需要相关权限)
*/
miraiVerify<MuteDTO>("/muteAll") {
it.session.bot.getGroup(it.target).isMuteAll = true
call.respondStateCode(StateCode.Success)
}
miraiVerify<MuteDTO>("/unmuteAll") {
it.session.bot.getGroup(it.target).isMuteAll = false
call.respondStateCode(StateCode.Success)
}
miraiVerify<MuteDTO>("/mute") {
when (it.session.bot.getGroup(it.target)[it.memberId].mute(it.time)) {
true -> call.respondStateCode(StateCode.Success)
else -> throw PermissionDeniedException
}
}
miraiVerify<MuteDTO>("/unmute") {
when (it.session.bot.getGroup(it.target).members[it.memberId].unmute()) {
true -> call.respondStateCode(StateCode.Success)
else -> throw PermissionDeniedException
}
}
/**
* 移出群聊(需要相关权限)
*/
miraiVerify<KickDTO>("/kick") {
when (it.session.bot.getGroup(it.target)[it.memberId].kick(it.msg)) {
true -> call.respondStateCode(StateCode.Success)
else -> throw PermissionDeniedException
}
}
/**
* 群设置(需要相关权限)
*/
miraiGet("/groupConfig") {
val group = it.bot.getGroup(paramOrNull("target"))
call.respondDTO(GroupDetailDTO(group))
}
miraiVerify<GroupConfigDTO>("/groupConfig") { dto ->
val group = dto.session.bot.getGroup(dto.target)
with(dto.config) {
name?.let { group.name = it }
announcement?.let { group.entranceAnnouncement = it }
confessTalk?.let { group.isConfessTalkEnabled = it }
allowMemberInvite?.let { group.isAllowMemberInvite = it }
// TODO: 待core接口实现设置可改
// autoApprove?.let { group.autoApprove = it }
// anonymousChat?.let { group.anonymousChat = it }
}
call.respondStateCode(StateCode.Success)
}
/**
* 群员信息管理(需要相关权限)
*/
miraiGet("/memberInfo") {
val member = it.bot.getGroup(paramOrNull("target"))[paramOrNull("memberId")]
call.respondDTO(MemberDetailDTO(member))
}
miraiVerify<MemberInfoDTO>("/memberInfo") { dto ->
val member = dto.session.bot.getGroup(dto.target)[dto.memberId]
with(dto.info) {
name?.let { member.nameCard = it }
specialTitle?.let { member.specialTitle = it }
}
call.respondStateCode(StateCode.Success)
}
}
}
@Serializable
private data class MuteDTO(
override val sessionKey: String,
val target: Long,
val memberId: Long = 0,
val time: Int = 0
) : VerifyDTO()
@Serializable
private data class KickDTO(
override val sessionKey: String,
val target: Long,
val memberId: Long,
val msg: String = ""
) : VerifyDTO()
@Serializable
private data class GroupConfigDTO(
override val sessionKey: String,
val target: Long,
val config: GroupDetailDTO
) : VerifyDTO()
@Serializable
private data class GroupDetailDTO(
val name: String? = null,
val announcement: String? = null,
val confessTalk: Boolean? = null,
val allowMemberInvite: Boolean? = null,
val autoApprove: Boolean? = null,
val anonymousChat: Boolean? = null
) : DTO {
constructor(group: Group) : this(
group.name, group.entranceAnnouncement, group.isConfessTalkEnabled, group.isAllowMemberInvite,
group.isAutoApproveEnabled, group.isAnonymousChatEnabled
)
}
@Serializable
private data class MemberInfoDTO(
override val sessionKey: String,
val target: Long,
val memberId: Long,
val info: MemberDetailDTO
) : VerifyDTO()
@Serializable
private data class MemberDetailDTO(
val name: String? = null,
val specialTitle: String? = null
) : DTO {
constructor(member: Member) : this(member.nameCard, member.specialTitle)
}
/*
* 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.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.routing.routing
import net.mamoe.mirai.api.http.data.common.GroupDTO
import net.mamoe.mirai.api.http.data.common.MemberDTO
import net.mamoe.mirai.api.http.data.common.QQDTO
import net.mamoe.mirai.api.http.util.toJson
import net.mamoe.mirai.contact.toMutableList
fun Application.infoModule() {
routing {
miraiGet("/friendList") {
val ls = it.bot.qqs.toMutableList().map { qq -> QQDTO(qq) }
call.respondJson(ls.toJson())
}
miraiGet("/groupList") {
val ls = it.bot.groups.toMutableList().map { group -> GroupDTO(group) }
call.respondJson(ls.toJson())
}
miraiGet("/memberList") {
val ls = it.bot.getGroup(paramOrNull("target")).members.toMutableList().map { member -> MemberDTO(member) }
call.respondJson(ls.toJson())
}
}
}
\ No newline at end of file
/*
* 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.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.http.content.readAllParts
import io.ktor.http.content.streamProvider
import io.ktor.request.receiveMultipart
import io.ktor.response.respondText
import io.ktor.routing.post
import io.ktor.routing.routing
import kotlinx.serialization.Serializable
import net.mamoe.mirai.api.http.AuthedSession
import net.mamoe.mirai.api.http.SessionManager
import net.mamoe.mirai.api.http.data.*
import net.mamoe.mirai.api.http.data.common.MessageChainDTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.api.http.data.common.toDTO
import net.mamoe.mirai.api.http.data.common.toMessageChain
import net.mamoe.mirai.api.http.util.toJson
import net.mamoe.mirai.contact.toList
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.uploadImage
import java.net.URL
fun Application.messageModule() {
routing {
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())
}
miraiVerify<SendDTO>("/sendFriendMessage") {
it.session.bot.getFriend(it.target).sendMessage(it.messageChain.toMessageChain())
call.respondStateCode(StateCode.Success)
}
miraiVerify<SendDTO>("/sendGroupMessage") {
it.session.bot.getGroup(it.target).sendMessage(it.messageChain.toMessageChain())
call.respondStateCode(StateCode.Success)
}
miraiVerify<SendDTO>("/quoteMessage") {
it.session.messageQueue.quoteCache[it.target]?.quoteReply(it.messageChain.toMessageChain())
?: throw NoSuchElementException()
call.respondStateCode(StateCode.Success)
}
miraiVerify<SendImageDTO>("sendImageMessage") {
val bot = it.session.bot
val contact = when {
it.target != null -> bot[it.target]
it.qq != null -> bot.getFriend(it.qq)
it.group != null -> bot.getGroup(it.group)
else -> throw IllegalParamException("target、qq、group不可全为null")
}
val ls = it.urls.map { url -> contact.uploadImage(URL(url)) }
contact.sendMessage(MessageChain(ls))
call.respondJson(ls.map { image -> image.imageId }.toJson())
}
// TODO: 重构
post("uploadImage") {
val parts = call.receiveMultipart().readAllParts()
val sessionKey = parts.value("sessionKey")
if (!SessionManager.containSession(sessionKey)) throw IllegalSessionException
val session = try {
SessionManager[sessionKey] as AuthedSession
} catch (e: TypeCastException) {
throw NotVerifiedSessionException
}
val type = parts.value("type")
parts.file("img")?.apply {
val image = streamProvider().use {
when (type) {
"group" -> session.bot.groups.toList().random().uploadImage(it)
"friend" -> session.bot.qqs.toList().random().uploadImage(it)
else -> null
}
}
image?.apply {
call.respondText(imageId)
} ?: throw IllegalAccessException("图片上传错误")
} ?: throw IllegalAccessException("未知错误")
}
}
}
@Serializable
private data class SendDTO(
override val sessionKey: String,
val target: Long,
val messageChain: MessageChainDTO
) : VerifyDTO()
@Serializable
private data class SendImageDTO(
override val sessionKey: String,
val target: Long? = null,
val qq: Long? = null,
val group: Long? = null,
val urls: List<String>
) : VerifyDTO()
/*
* 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.api.http.util
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)
inline fun <reified T : Any> String.jsonParseOrNull(
serializer: DeserializationStrategy<T>? = null
): T? = try {
if(serializer == null) MiraiJson.json.parse(this) else Json.parse(this)
} catch (e: Exception) { null }
@UseExperimental(ImplicitReflectionSerializer::class, UnstableDefault::class)
inline fun <reified T : Any> T.toJson(
serializer: SerializationStrategy<T>? = null
): String = if (serializer == null) MiraiJson.json.stringify(this)
else MiraiJson.json.stringify(serializer, this)
// 序列化列表时,stringify需要使用的泛型是T,而非List<T>
// 因为使用的stringify的stringify(objs: List<T>)重载
@UseExperimental(ImplicitReflectionSerializer::class, UnstableDefault::class)
inline fun <reified T : Any> List<T>.toJson(
serializer: SerializationStrategy<List<T>>? = null
): String = if (serializer == null) MiraiJson.json.stringify(this)
else MiraiJson.json.stringify(serializer, this)
/**
* Json解析规则,需要注册支持的多态的类
*/
object MiraiJson {
val json = Json(context = SerializersModule {
polymorphic(MessagePacketDTO.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()
}
})
}
\ No newline at end of file
# Mirai Console
#### Mirai Console allows you to run Mirai in command lines/terminal.
#### 你可以终端中或命令行环境下运行在Mirai
<br>
#### More Importantly, Mirai Console support <b>Plugins</b>, tells the bot what to do
#### Mirai Console 支持插件系统, 你可以自己开发或使用公开的插件来逻辑化机器人, 如群管
<br>
#### download 下载
#### how to get/write plugins 如何获取/写插件
<br>
<br>
### how to use(如何使用)
#### how to run Mirai Console
<ul>
<li>download mirai-console.jar</li>
<li>open command line/terminal</li>
<li>create a folder and put mirai-console.jar in</li>
<li>cd that folder</li>
<li>"java -jar mirai-console.jar"</li>
</ul>
<ul>
<li>下载mirai-console.jar</li>
<li>打开终端</li>
<li>在任何地方创建一个文件夹, 并放入mirai-console.jar</li>
<li>在终端中打开该文件夹"cd"</li>
<li>输入"java -jar mirai-console.jar"</li>
</ul>
#### how to add plugins
<ul>
<li>After first time of running mirai console</li>
<li>/plugins/folder will be created next to mirai-console.jar</li>
<li>put plugin(.jar) into /plugins/</li>
<li>restart mirai console</li>
<li>checking logger and check if the plugin is loaded successfully</li>
<li>if the plugin has it own Config file, it normally appears in /plugins/{pluginName}/</li>
</ul>
<ul>
<li>在首次运行mirai console后</li>
<li>mirai-console.jar 的同级会出现/plugins/文件夹</li>
<li>将插件(.jar)放入/plugins/文件夹</li>
<li>重启mirai console</li>
<li>在开启后检查日志, 是否成功加载</li>
<li>如该插件有配置文件, 配置文件一般会创建在/plugins/插件名字/ 文件夹下</li>
</ul>
plugins {
id("kotlinx-serialization")
id("kotlin")
id("java")
}
apply(plugin = "com.github.johnrengelman.shadow")
apply(plugin = "java-library")
tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>() {
manifest {
attributes["Main-Class"] = "net.mamoe.mirai.MiraiConsoleLoader"
}
}
val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext
val coroutinesVersion: String by rootProject.ext
val kotlinXIoVersion: String by rootProject.ext
val coroutinesIoVersion: String by rootProject.ext
val klockVersion: String by rootProject.ext
val ktorVersion: String by rootProject.ext
val serializationVersion: String by rootProject.ext
fun kotlinx(id: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$id:$version"
fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
dependencies {
api(project(":mirai-core"))
api(project(":mirai-core-qqandroid"))
api(project(":mirai-api-http"))
runtimeOnly(files("../mirai-core-qqandroid/build/classes/kotlin/jvm/main"))
runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main"))
api(kotlin("serialization"))
api(group = "com.alibaba", name = "fastjson", version = "1.2.62")
api(group = "org.yaml", name = "snakeyaml", version = "1.25")
api(group = "com.moandjiezana.toml", name = "toml4j", version = "0.7.2")
api(group = "com.googlecode.lanterna", name = "lanterna", version = "3.0.2")
// classpath is not set correctly by IDE
}
\ No newline at end of file
package net.mamoe.mirai
/*
* 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
*/
import net.mamoe.mirai.plugins.PluginManager
object CommandManager {
private val registeredCommand: MutableMap<String, ICommand> = mutableMapOf()
fun getCommands(): Collection<ICommand> {
return registeredCommand.values
}
fun register(command: ICommand) {
val allNames = mutableListOf(command.name).also { it.addAll(command.alias) }
allNames.forEach {
if (registeredCommand.containsKey(it)) {
error("net.mamoe.mirai.Command Name(or Alias) $it is already registered, consider if same function plugin was installed")
}
}
allNames.forEach {
registeredCommand[it] = command
}
}
fun unregister(command: ICommand) {
val allNames = mutableListOf<String>(command.name).also { it.addAll(command.alias) }
allNames.forEach {
registeredCommand.remove(it)
}
}
fun unregister(commandName: String) {
registeredCommand.remove(commandName)
}
fun runCommand(fullCommand: String): Boolean {
val blocks = fullCommand.split(" ")
val commandHead = blocks[0].replace("/", "")
if (!registeredCommand.containsKey(commandHead)) {
return false
}
val args = blocks.subList(1, blocks.size)
registeredCommand[commandHead]?.run {
if (onCommand(
blocks.subList(1, blocks.size)
)
) {
PluginManager.onCommand(this, args)
}
}
return true
}
}
interface ICommand {
val name: String
val alias: List<String>
val description: String
fun onCommand(args: List<String>): Boolean
fun register()
}
abstract class Command(
override val name: String,
override val alias: List<String> = listOf(),
override val description: String = ""
) : ICommand {
/**
* 最高优先级监听器
* 如果return [false] 这次指令不会被[PluginBase]的全局onCommand监听器监听
* */
open override fun onCommand(args: List<String>): Boolean {
return true
}
override fun register() {
CommandManager.register(this)
}
}
class AnonymousCommand internal constructor(
override val name: String,
override val alias: List<String>,
override val description: String,
val onCommand: ICommand.(args: List<String>) -> Boolean
) : ICommand {
override fun onCommand(args: List<String>): Boolean {
return onCommand.invoke(this, args)
}
override fun register() {
CommandManager.register(this)
}
}
class CommandBuilder internal constructor() {
var name: String? = null
var alias: List<String>? = null
var description: String = ""
var onCommand: (ICommand.(args: List<String>) -> Boolean)? = null
fun onCommand(commandProcess: ICommand.(args: List<String>) -> Boolean) {
onCommand = commandProcess
}
fun register(): ICommand {
if (name == null || onCommand == null) {
error("net.mamoe.mirai.CommandBuilder not complete")
}
if (alias == null) {
alias = listOf()
}
return AnonymousCommand(name!!, alias!!, description, onCommand!!).also { it.register() }
}
}
fun buildCommand(builder: CommandBuilder.() -> Unit): ICommand {
return CommandBuilder().apply(builder).register()
}
# mirai-core-qqandroid # mirai-core-qqandroid
Protocol support for QQ for Android for Mirai. Protocol support for QQ for Android for Mirai.
Not yet available to work.
## Protocol Structure
See [README.md](src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/README.md)
\ No newline at end of file
...@@ -5,10 +5,10 @@ plugins { ...@@ -5,10 +5,10 @@ plugins {
id("kotlinx-atomicfu") id("kotlinx-atomicfu")
id("kotlinx-serialization") id("kotlinx-serialization")
`maven-publish` `maven-publish`
id("com.jfrog.bintray") version "1.8.4-jetbrains-3" // DO NOT CHANGE THIS VERSION UNLESS YOU WANT TO WASTE YOUR TIME id("com.jfrog.bintray") version "1.8.4-jetbrains-3"
} }
apply(from = rootProject.file("gradle/publish.gradle")) apply(plugin = "com.github.johnrengelman.shadow")
val kotlinVersion: String by rootProject.ext val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext val atomicFuVersion: String by rootProject.ext
...@@ -16,7 +16,7 @@ val coroutinesVersion: String by rootProject.ext ...@@ -16,7 +16,7 @@ val coroutinesVersion: String by rootProject.ext
val kotlinXIoVersion: String by rootProject.ext val kotlinXIoVersion: String by rootProject.ext
val coroutinesIoVersion: String by rootProject.ext val coroutinesIoVersion: String by rootProject.ext
val klockVersion: String by rootProject.ext
val ktorVersion: String by rootProject.ext val ktorVersion: String by rootProject.ext
val serializationVersion: String by rootProject.ext val serializationVersion: String by rootProject.ext
...@@ -27,10 +27,12 @@ fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version" ...@@ -27,10 +27,12 @@ fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version"
description = "QQ protocol library" description = "QQ protocol library"
version = rootProject.ext.get("mirai_version")!!.toString()
val isAndroidSDKAvailable: Boolean by project val isAndroidSDKAvailable: Boolean by project
val miraiVersion: String by project
version = miraiVersion
kotlin { kotlin {
if (isAndroidSDKAvailable) { if (isAndroidSDKAvailable) {
apply(from = rootProject.file("gradle/android.gradle")) apply(from = rootProject.file("gradle/android.gradle"))
...@@ -75,6 +77,7 @@ kotlin { ...@@ -75,6 +77,7 @@ kotlin {
commonMain { commonMain {
dependencies { dependencies {
api(kotlinx("serialization-runtime-common", serializationVersion)) api(kotlinx("serialization-runtime-common", serializationVersion))
api(kotlinx("serialization-protobuf-common", serializationVersion))
} }
} }
commonTest { commonTest {
...@@ -88,6 +91,7 @@ kotlin { ...@@ -88,6 +91,7 @@ kotlin {
if (isAndroidSDKAvailable) { if (isAndroidSDKAvailable) {
val androidMain by getting { val androidMain by getting {
dependencies { dependencies {
api(kotlinx("serialization-protobuf", serializationVersion))
} }
} }
...@@ -105,6 +109,7 @@ kotlin { ...@@ -105,6 +109,7 @@ kotlin {
dependencies { dependencies {
runtimeOnly(files("build/classes/kotlin/jvm/main")) // classpath is not properly set by IDE runtimeOnly(files("build/classes/kotlin/jvm/main")) // classpath is not properly set by IDE
api(kotlinx("serialization-runtime", serializationVersion)) api(kotlinx("serialization-runtime", serializationVersion))
//api(kotlinx("serialization-protobuf", serializationVersion))
} }
} }
...@@ -119,4 +124,10 @@ kotlin { ...@@ -119,4 +124,10 @@ kotlin {
} }
} }
} }
} }
\ No newline at end of file //
//tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
// kotlinOptions.jvmTarget = "1.8"
//}
apply(from = rootProject.file("gradle/publish.gradle"))
package net.mamoe.mirai.qqandroid.io.serialization
import kotlinx.io.core.Input
import kotlinx.io.core.Output
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerialFormat
import kotlinx.serialization.SerializationStrategy
interface IOFormat : SerialFormat {
fun <T> dumpTo(serializer: SerializationStrategy<T>, ojb: T, output: Output)
fun <T> load(deserializer: DeserializationStrategy<T>, input: Input): T
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment