Merge remote-tracking branch 'origin/master'

This commit is contained in:
jiahua.liu 2020-02-22 20:41:26 +08:00
commit e9c794296d
64 changed files with 1524 additions and 368 deletions

View File

@ -2,6 +2,37 @@
开发版本. 频繁更新, 不保证高稳定性
## `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
@ -212,7 +243,7 @@ TIMPC
## `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 等监听模式.
**其他**

View File

@ -1,6 +1,7 @@
buildscript {
repositories {
mavenLocal()
maven { url "https://mirrors.huaweicloud.com/repository/maven" }
jcenter()
mavenCentral()
google()
@ -9,7 +10,6 @@ buildscript {
}
dependencies {
// Do try to waste your time.
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")
@ -32,14 +32,10 @@ allprojects {
group = "net.mamoe"
version = getProperty("mirai_version")
// tasks.withType(KotlinCompile).all { task ->
// task.kotlinOptions{
// jvmTarget = '1.6'
// }
// }
repositories {
mavenLocal()
maven { url "https://mirrors.huaweicloud.com/repository/maven" }
jcenter()
mavenCentral()
google()

View File

@ -0,0 +1,186 @@
# Mirai Guide - Build For Mirai
由于Mirai项目在快速推进中因此内容时有变动本文档的最后更新日期为```2020-02-22```,对应版本```0.19.1```
本页面采用Kotlin作为开发语言**若你希望使用 Java 开发**, 请参阅: [mirai-japt](mirai-japt/README.md)
本页面是[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.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"
}
shadowJar {
// 生成包的命名规则: baseName-version-classifier.jar
manifest {
attributes(
'Main-Class': 'net.mamoe.mirai.simpleloader.MyLoaderKt'
)
}
// 将 build.gradle 打入到 jar 中, 方便查看依赖包版本
from("./"){
include 'build.gradle'
}
}
```

View File

@ -26,6 +26,8 @@ JDK要求6以上
### 2 新建Gradle项目
*使用gradle项目可能需要代理在IDEA的```settings```->```proxy settings```中可以设置
- 在```File->new project```中选择```Gradle```
- 在面板中的```Additional Libraries and Frameworks```中勾选```Java```以及```Kotlin/JVM```
- 点击```next```,填入```GroupId```与```ArtifactId```(对于测试项目来说,可随意填写)
@ -69,7 +71,7 @@ JDK要求6以上
### 4 Try Bot
- 在src/main文件夹下新建文件夹命名为```kotlin```
- 在```kotlin```下新建包(在```kotlin```文件夹上右键-```New```-```Packages```) 包名为```net.mamoe.mirai.simpleloader```
- 在```kotlin```下新建包(在```kotlin```文件夹上右键-```New```-```Package```) 包名为```net.mamoe.mirai.simpleloader```
- 在包下新建kotlin文件```MyLoader.kt```
@ -103,9 +105,9 @@ suspend fun main() {
- 本例的功能中在任意群内任意成员发送包含“舔”字或“刘老板”字样的消息MiraiBot会回复“刘老板太强了”
至此简单的入门已经结束下面可根据不同的需求参阅wiki进行功能的添加。
下面,可以尝试对不同事件进行监听[Mirai Guide - Subscribe Events](/docs/guide_subscribe_events.md)
### 此外还可以使用Maven作为包管理工具
本项目推荐使用gradle因此不提供详细入门指导

View File

@ -0,0 +1,116 @@
# Mirai Guide - Subscribe Events
由于Mirai项目在快速推进中因此内容时有变动本文档的最后更新日期为```2020-02-21```,对应版本```0.17.0```
本页面采用Kotlin作为开发语言**若你希望使用 Java 开发**, 请参阅: [mirai-japt](mirai-japt/README.md)
本页面是[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应用进行打包

View File

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

View File

@ -408,7 +408,7 @@ Content-Typemultipart/form-data
### 事件类型一览
[事件类型一览](./EventType_CN.md)
[事件类型一览](EventType_CH.md)
> 事件为Bot被动接收的信息无法主动构建
@ -430,13 +430,13 @@ Content-Typemultipart/form-data
```json5
{
"type": "Source",
"uid": 123456
"id": 123456
}
```
| 名字 | 类型 | 说明 |
| ---- | ---- | ------------------------------------------------------------ |
| uid | Long | 消息的识别号用于引用回复Source类型只在群消息中返回且永远为chain的第一个元素 |
| id | Long | 消息的识别号用于引用回复Source类型只在群消息中返回且永远为chain的第一个元素 |
#### At

View File

@ -1,9 +1,10 @@
package net.mamoe.mirai.api.http.data
import kotlinx.serialization.Serializable
import net.mamoe.mirai.api.http.data.common.DTO
@Serializable
open class StateCode(val code: Int, var msg: String) {
open class StateCode(val code: Int, var msg: String) : DTO {
object Success : StateCode(0, "success") // 成功
object NoBot : StateCode(2, "指定Bot不存在")
object IllegalSession : StateCode(3, "Session失效或不存在")

View File

@ -36,7 +36,7 @@ data class GroupMessagePacketDTO(val sender: MemberDTO) : MessagePacketDTO()
// Message
@Serializable
@SerialName("Source")
data class MessageSourceDTO(val uid: Long) : MessageDTO()
data class MessageSourceDTO(val id: Long) : MessageDTO()
@Serializable
@SerialName("At")
@ -64,7 +64,7 @@ data class XmlDTO(val xml: String) : MessageDTO()
@Serializable
@SerialName("Unknown")
data class UnknownMessageDTO(val text: String) : MessageDTO()
object UnknownMessageDTO : MessageDTO()
/*
* Abstract Class
@ -88,24 +88,29 @@ fun MessagePacket<*, *>.toDTO() = when (this) {
is GroupMessage -> GroupMessagePacketDTO(MemberDTO(sender))
else -> IgnoreEventDTO
}.apply {
if (this is MessagePacketDTO) {
messageChain = mutableListOf<MessageDTO>().also { ls -> message.foreachContent { ls.add(it.toDTO()) } }
}
if (this is MessagePacketDTO) { messageChain = message.toDTOChain() }
// else: `this` is bot event
}
fun MessageChain.toDTOChain() = mutableListOf(this[MessageSource].toDTO()).apply {
foreachContent { content -> content.toDTO().takeUnless { it == UnknownMessageDTO }?.let(::add) }
}
fun MessageChainDTO.toMessageChain(contact: Contact) =
MessageChain().apply { this@toMessageChain.forEach { add(it.toMessage(contact)) } }
internal fun MessageSource.calMessageId() = (messageUid.toLong() shl 32) or (sequenceId.toLong() and 0xFFFFFFFF)
@UseExperimental(ExperimentalUnsignedTypes::class)
fun Message.toDTO() = when (this) {
is MessageSource -> MessageSourceDTO(messageUid)
is MessageSource -> MessageSourceDTO(calMessageId())
is At -> AtDTO(target, display)
is AtAll -> AtAllDTO(0L)
is Face -> FaceDTO(id)
is PlainText -> PlainDTO(stringValue)
is Image -> ImageDTO(imageId)
is XMLMessage -> XmlDTO(stringValue)
else -> UnknownMessageDTO("未知消息类型")
else -> UnknownMessageDTO
}
@UseExperimental(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)

View File

@ -11,20 +11,22 @@ 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.calMessageId
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.data.MessageSource
import java.util.concurrent.ConcurrentHashMap
import net.mamoe.mirai.utils.firstKey
import java.util.concurrent.ConcurrentLinkedDeque
class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
val quoteCache = ConcurrentHashMap<Long, GroupMessage>()
val quoteCacheSize = 4096
val quoteCache = LinkedHashMap<Long, GroupMessage>()
fun fetch(size: Int): List<EventDTO> {
var count = size
quoteCache.clear()
val ret = ArrayList<EventDTO>(count)
while (!this.isEmpty() && count > 0) {
val event = pop()
@ -36,14 +38,18 @@ class MessageQueue : ConcurrentLinkedDeque<BotEvent>() {
}
}
// TODO: 等FriendMessage支持quote
if (event is GroupMessage) {
addCache(event)
addQuoteCache(event)
}
}
return ret
}
private fun addCache(msg: GroupMessage) {
quoteCache[msg.message[MessageSource].messageUid] = msg
private fun addQuoteCache(msg: GroupMessage) {
quoteCache[msg.message[MessageSource].calMessageId()] = msg
if (quoteCache.size > quoteCacheSize) {
quoteCache.remove(quoteCache.firstKey())
}
}
}

View File

@ -21,6 +21,7 @@ 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.respond
import io.ktor.response.respondText
import io.ktor.routing.Route
import io.ktor.routing.route
@ -141,6 +142,9 @@ internal inline fun Route.intercept(crossinline blk: suspend PipelineContext<Uni
call.respondStateCode(StateCode.PermissionDenied)
} catch (e: IllegalAccessException) {
call.respondStateCode(StateCode(400, e.message), HttpStatusCode.BadRequest)
} catch (e: Throwable) {
e.printStackTrace()
call.respond(HttpStatusCode.InternalServerError, e.message!!)
}
}

View File

@ -11,19 +11,21 @@ package net.mamoe.mirai.api.http.route
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.readAllParts
import io.ktor.http.content.streamProvider
import io.ktor.request.receiveMultipart
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.post
import io.ktor.routing.routing
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
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
@ -45,14 +47,12 @@ fun Application.messageModule() {
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).apply {
sendMessage(it.messageChain.toMessageChain(this)) // this aka Group
}
call.respondStateCode(StateCode.Success)
}
miraiVerify<SendDTO>("/quoteMessage") {
@ -100,6 +100,11 @@ fun Application.messageModule() {
} ?: throw IllegalAccessException("图片上传错误")
} ?: throw IllegalAccessException("未知错误")
}
miraiVerify<RecallDTO>("recall") {
// TODO
call.respond(HttpStatusCode.NotFound, "未完成")
}
}
}
@ -119,3 +124,15 @@ private data class SendImageDTO(
val urls: List<String>
) : VerifyDTO()
@Serializable
private class SendRetDTO(
val messageId: Long,
@Transient val stateCode: StateCode = Success
) : StateCode(stateCode.code, stateCode.msg)
@Serializable
private data class RecallDTO(
override val sessionKey: String,
val target: Long,
val sender: Long
) : VerifyDTO()

View File

@ -1,6 +1,7 @@
package net.mamoe.mirai.console.graphical.controller
import javafx.application.Platform
import javafx.collections.ObservableList
import javafx.stage.Modality
import kotlinx.io.core.IoBuffer
import net.mamoe.mirai.Bot
@ -8,6 +9,7 @@ import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.MiraiConsoleUI
import net.mamoe.mirai.console.graphical.model.BotModel
import net.mamoe.mirai.console.graphical.model.ConsoleInfo
import net.mamoe.mirai.console.graphical.model.PluginModel
import net.mamoe.mirai.console.graphical.model.VerificationCodeModel
import net.mamoe.mirai.console.graphical.view.VerificationCodeFragment
import net.mamoe.mirai.utils.LoginSolver
@ -21,13 +23,21 @@ class MiraiGraphicalUIController : Controller(), MiraiConsoleUI {
private val loginSolver = GraphicalLoginSolver()
private val cache = mutableMapOf<Long, BotModel>()
val mainLog = observableListOf<String>()
val botList = observableListOf<BotModel>()
val pluginList: ObservableList<PluginModel> by lazy(::getPluginsFromConsole)
val consoleConfig : Map<String, Any> by lazy(::getConfigFromConsole)
val consoleInfo = ConsoleInfo()
suspend fun login(qq: String, psd: String) {
MiraiConsole.CommandListener.commandChannel.send("/login $qq $psd")
}
suspend fun sendCommand(command: String) = MiraiConsole.CommandListener.commandChannel.send(command)
override fun pushLog(identity: Long, message: String) = Platform.runLater {
when (identity) {
0L -> mainLog.add(message)
@ -68,6 +78,13 @@ class MiraiGraphicalUIController : Controller(), MiraiConsoleUI {
}
override fun createLoginSolver(): LoginSolver = loginSolver
private fun getPluginsFromConsole(): ObservableList<PluginModel> {
// TODO
return observableListOf<PluginModel>()
}
private fun getConfigFromConsole() = MiraiConsole.MiraiProperties.config.asMap()
}
class GraphicalLoginSolver : LoginSolver() {

View File

@ -0,0 +1,19 @@
package net.mamoe.mirai.console.graphical.model
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.getValue
import tornadofx.setValue
class PluginModel : RecursiveTreeObject<PluginModel>() {
val nameProperty = SimpleStringProperty(this, "nameProperty")
val name by nameProperty
val descriptionProperty = SimpleStringProperty(this, "descriptionProperty")
val description by descriptionProperty
val enabledProperty = SimpleBooleanProperty(this, "enabledProperty")
var enabled by enabledProperty
}

View File

@ -1,6 +1,7 @@
package net.mamoe.mirai.console.graphical.util
import com.jfoenix.controls.*
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject
import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList
import javafx.event.EventTarget
@ -42,3 +43,6 @@ internal fun <T> EventTarget.jfxListView(values: ObservableList<T>? = null, op:
else it.items = values
}
}
fun <T : RecursiveTreeObject<T>?> EventTarget.jfxTreeTableView(items: ObservableList<T>? = null, op: JFXTreeTableView<T>.() -> Unit = {})
= JFXTreeTableView<T>(RecursiveTreeItem(items, RecursiveTreeObject<T>::getChildren)).attachTo(this, op)

View File

@ -0,0 +1,22 @@
package net.mamoe.mirai.console.graphical.view
import com.jfoenix.controls.JFXTreeTableColumn
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
import net.mamoe.mirai.console.graphical.model.PluginModel
import net.mamoe.mirai.console.graphical.util.jfxTreeTableView
import tornadofx.View
class PluginsView : View() {
private val controller = find<MiraiGraphicalUIController>()
val plugins = controller.pluginList
override val root = jfxTreeTableView(plugins) {
columns.addAll(
JFXTreeTableColumn<PluginModel, String>("插件名").apply { },
JFXTreeTableColumn<PluginModel, String>("版本").apply { },
JFXTreeTableColumn<PluginModel, String>("作者").apply { },
JFXTreeTableColumn<PluginModel, String>("介绍").apply { }
)
}
}

View File

@ -5,6 +5,8 @@ import javafx.collections.ObservableList
import javafx.scene.control.Tab
import javafx.scene.control.TabPane
import javafx.scene.image.Image
import javafx.scene.input.KeyCode
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
import net.mamoe.mirai.console.graphical.model.BotModel
import net.mamoe.mirai.console.graphical.util.jfxListView
@ -52,17 +54,26 @@ class PrimaryView : View() {
}
}
}
// command input
textfield {
setOnKeyPressed {
if (it.code == KeyCode.ENTER) {
runAsync {
runBlocking { controller.sendCommand(text) }
}.ui { text = "" }
}
}
}
}
center = jfxTabPane {
tab("Login") {
this += find<LoginView>().root
}
tab("Login").content = find<LoginView>().root
tab("Plugin")
tab("Plugins").content = find<PluginsView>().root
tab("Settings")
tab("Settings").content = find<SettingsView>().root
logTab("Main", controller.mainLog)
}

View File

@ -0,0 +1,23 @@
package net.mamoe.mirai.console.graphical.view
import net.mamoe.mirai.console.graphical.controller.MiraiGraphicalUIController
import net.mamoe.mirai.console.graphical.util.jfxTextfield
import tornadofx.View
import tornadofx.field
import tornadofx.fieldset
import tornadofx.form
class SettingsView : View() {
private val controller = find<MiraiGraphicalUIController>()
override val root = form {
controller.consoleConfig.forEach {
fieldset {
field(it.key) {
jfxTextfield(it.value.toString()) { isEditable = false }
}
}
}
}
}

View File

@ -16,14 +16,13 @@ import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.data.CustomFaceFromFile
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.NotOnlineImageFromFile
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
import net.mamoe.mirai.qqandroid.network.highway.postImage
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopMemberInfo
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
@ -66,20 +65,22 @@ internal class QQImpl(
override val nick: String
get() = friendInfo.nick
override suspend fun sendMessage(message: MessageChain) {
override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ> {
val event = FriendMessageSendEvent(this, message).broadcast()
if (event.isCancelled) {
throw EventCancelledException("cancelled by FriendMessageSendEvent")
}
lateinit var source: MessageSource
bot.network.run {
check(
MessageSvc.PbSendMsg.ToFriend(
bot.client,
id,
event.message
).sendAndExpect<MessageSvc.PbSendMsg.Response>() is MessageSvc.PbSendMsg.Response.SUCCESS
) { source = it }.sendAndExpect<MessageSvc.PbSendMsg.Response>() is MessageSvc.PbSendMsg.Response.SUCCESS
) { "send message failed" }
}
return MessageReceipt(message, source, this)
}
override suspend fun uploadImage(image: ExternalImage): Image = try {
@ -166,7 +167,7 @@ internal class QQImpl(
}
@Suppress("MemberVisibilityCanBePrivate", "DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
@Suppress("MemberVisibilityCanBePrivate")
internal class MemberImpl(
qq: QQImpl,
group: GroupImpl,
@ -286,26 +287,39 @@ internal class MemberImpl(
}
}
override fun hashCode(): Int {
var result = bot.hashCode()
result = 31 * result + id.hashCode()
return result
}
@Suppress("DuplicatedCode")
override fun equals(other: Any?): Boolean { // 不要删除. trust me
if (this === other) return true
if (other !is Contact) return false
if (this::class != other::class) return false
return this.id == other.id && this.bot == other.bot
}
override fun toString(): String {
return "Member($id)"
}
}
internal class MemberInfoImpl(
private val jceInfo: StTroopMemberInfo,
private val groupOwnerId: Long
jceInfo: StTroopMemberInfo,
groupOwnerId: Long
) : MemberInfo {
override val uin: Long get() = jceInfo.memberUin
override val nameCard: String get() = jceInfo.sName ?: ""
override val nick: String get() = jceInfo.nick
override val permission: MemberPermission
get() = when {
jceInfo.memberUin == groupOwnerId -> MemberPermission.OWNER
jceInfo.dwFlag == 1L -> MemberPermission.ADMINISTRATOR
else -> MemberPermission.MEMBER
}
override val specialTitle: String get() = jceInfo.sSpecialTitle ?: ""
override val muteTimestamp: Int get() = jceInfo.dwShutupTimestap?.toInt() ?: 0
override val uin: Long = jceInfo.memberUin
override val nameCard: String = jceInfo.sName ?: ""
override val nick: String = jceInfo.nick
override val permission: MemberPermission = when {
jceInfo.memberUin == groupOwnerId -> MemberPermission.OWNER
jceInfo.dwFlag == 1L -> MemberPermission.ADMINISTRATOR
else -> MemberPermission.MEMBER
}
override val specialTitle: String = jceInfo.sSpecialTitle ?: ""
override val muteTimestamp: Int = jceInfo.dwShutupTimestap?.toInt() ?: 0
}
/**
@ -325,6 +339,24 @@ internal class GroupImpl(
override lateinit var owner: Member
@UseExperimental(MiraiExperimentalAPI::class)
override val botAsMember: Member by lazy {
Member(object : MemberInfo {
override val nameCard: String
get() = bot.nick // TODO: 2020/2/21 机器人在群内的昵称获取
override val permission: MemberPermission
get() = botPermission
override val specialTitle: String
get() = "" // TODO: 2020/2/21 获取机器人在群里的头衔
override val muteTimestamp: Int
get() = botMuteRemaining
override val uin: Long
get() = bot.uin
override val nick: String
get() = bot.nick
})
}
@UseExperimental(MiraiExperimentalAPI::class)
override lateinit var botPermission: MemberPermission
@ -340,6 +372,9 @@ internal class GroupImpl(
override val members: ContactList<Member> = ContactList(members.mapNotNull {
if (it.uin == bot.uin) {
botPermission = it.permission
if (it.permission == MemberPermission.OWNER) {
owner = botAsMember
}
null
} else Member(it).also { member ->
if (member.permission == MemberPermission.OWNER) {
@ -475,6 +510,20 @@ internal class GroupImpl(
TODO("not implemented")
}
override suspend fun recall(source: MessageSource) {
if (source.senderId != bot.uin) {
checkBotPermissionOperator()
}
source.ensureSequenceIdAvailable()
bot.network.run {
val response = PbMessageSvc.PbMsgWithDraw.Group(bot.client, this@GroupImpl.id, source.sequenceId, source.messageUid.toInt())
.sendAndExpect<PbMessageSvc.PbMsgWithDraw.Response>()
check(response is PbMessageSvc.PbMsgWithDraw.Response.Success) { "Failed to recall message #${source.sequenceId}: $response" }
}
}
@UseExperimental(MiraiExperimentalAPI::class)
override fun Member(memberInfo: MemberInfo): Member {
return MemberImpl(
@ -498,22 +547,27 @@ internal class GroupImpl(
return members.delegate.filteringGetOrNull { it.id == id }
}
override suspend fun sendMessage(message: MessageChain) {
override suspend fun sendMessage(message: MessageChain): MessageReceipt<Group> {
check(!isBotMuted) { "bot is muted. Remaining seconds=$botMuteRemaining" }
val event = GroupMessageSendEvent(this, message).broadcast()
if (event.isCancelled) {
throw EventCancelledException("cancelled by FriendMessageSendEvent")
}
lateinit var source: MessageSvc.PbSendMsg.MessageSourceFromSend
bot.network.run {
val response = MessageSvc.PbSendMsg.ToGroup(
val response: MessageSvc.PbSendMsg.Response = MessageSvc.PbSendMsg.ToGroup(
bot.client,
id,
event.message
).sendAndExpect<MessageSvc.PbSendMsg.Response>()
) { source = it }.sendAndExpect()
check(
response is MessageSvc.PbSendMsg.Response.SUCCESS
) { "send message failed: $response" }
}
source.startWaitingSequenceId(this)
return MessageReceipt(message, source, this)
}
override suspend fun uploadImage(image: ExternalImage): Image = try {

View File

@ -10,7 +10,6 @@
package net.mamoe.mirai.qqandroid.message
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.qqandroid.io.serialization.loadAs
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
@ -22,8 +21,14 @@ 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 sequenceId: Int get() = delegate.origSeqs?.firstOrNull() ?: error("cannot find sequenceId from ImMsgBody.SourceMsg")
override suspend fun ensureSequenceIdAvailable() {
// nothing to do
}
override val messageUid: Int get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids!!.toInt()
// override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.senderUin
override val groupId: Long get() = Group.calculateGroupCodeByGroupUin(delegate.toUin)
@ -34,8 +39,13 @@ 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 sequenceId: Int get() = delegate.msgHead.msgSeq
override suspend fun ensureSequenceIdAvailable() {
// nothing to do
}
override val messageUid: Int get() = delegate.msgBody.richText.attr!!.random
// override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val senderId: Long get() = delegate.msgHead.fromUin
override val groupId: Long get() = delegate.msgHead.groupInfo!!.groupCode
@ -52,7 +62,7 @@ internal inline class MessageSourceFromMsg(
type = 0,
time = delegate.msgHead.msgTime,
pbReserve = SourceMsg.ResvAttr(
origUids = messageUid
origUids = messageUid.toLong() and 0xffFFffFF
).toByteArray(SourceMsg.ResvAttr.serializer()),
srcMsg = MsgComm.Msg(
msgHead = MsgComm.MsgHead(
@ -62,7 +72,8 @@ internal inline class MessageSourceFromMsg(
c2cCmd = delegate.msgHead.c2cCmd,
msgSeq = delegate.msgHead.msgSeq,
msgTime = delegate.msgHead.msgTime,
msgUid = messageUid, // ok
msgUid = messageUid.toLong() and 0xffFFffFF
, // ok
groupInfo = MsgComm.GroupInfo(groupCode = delegate.msgHead.groupInfo.groupCode),
isSrcMsg = true
),

View File

@ -15,6 +15,7 @@ import kotlinx.io.core.readUInt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiDebugAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.io.discardExact
@ -27,12 +28,13 @@ internal fun At.toJceData(): ImMsgBody.Text {
return ImMsgBody.Text(
str = text,
attr6Buf = buildPacket {
writeShort(1)
writeShort(0)
writeShort(text.length.toShort())
writeByte(1)
writeInt(target.toInt())
writeShort(0)
// MessageForText$AtTroopMemberInfo
writeShort(1) // const
writeShort(0) // startPos
writeShort(text.length.toShort()) // textLen
writeByte(0) // flag, may=1
writeInt(target.toInt()) // uin
writeShort(0) // const
}.readBytes()
)
}
@ -206,7 +208,15 @@ notOnlineImage=NotOnlineImage#2050019814 {
private val atAllData = ImMsgBody.Elem(
text = ImMsgBody.Text(
str = "@全体成员",
attr6Buf = "00 01 00 00 00 05 01 00 00 00 00 00 00".hexToBytes()
attr6Buf = buildPacket {
// MessageForText$AtTroopMemberInfo
writeShort(1) // const
writeShort(0) // startPos
writeShort("@全体成员".length.toShort()) // textLen
writeByte(1) // flag, may=1
writeInt(0) // uin
writeShort(0) // const
}.readBytes()
)
)
@ -224,7 +234,7 @@ internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
this.forEach {
when (it) {
is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
is At -> elements.add(ImMsgBody.Elem(text = it.toJceData()))
is At -> elements.add(ImMsgBody.Elem(text = it.toJceData())).also { elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " "))) }
is CustomFaceFromFile -> elements.add(ImMsgBody.Elem(customFace = it.toJceData()))
is CustomFaceFromServer -> elements.add(ImMsgBody.Elem(customFace = it.delegate))
is NotOnlineImageFromServer -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
@ -249,7 +259,7 @@ internal fun MessageChain.toRichTextElems(): MutableList<ImMsgBody.Elem> {
internal class CustomFaceFromServer(
internal val delegate: ImMsgBody.CustomFace
) : CustomFace() {
override val filepath: String get() = delegate.filePath
override val filepath: String = delegate.filePath
override val fileId: Int get() = delegate.fileId
override val serverIp: Int get() = delegate.serverIp
override val serverPort: Int get() = delegate.serverPort
@ -265,14 +275,14 @@ internal class CustomFaceFromServer(
override val size: Int get() = delegate.size
override val original: Int get() = delegate.origin
override val pbReserve: ByteArray get() = delegate.pbReserve
override val imageId: String get() = delegate.filePath
override val imageId: String = ExternalImage.generateImageId(delegate.md5, imageType)
override fun equals(other: Any?): Boolean {
return other is CustomFaceFromServer && other.filepath == this.filepath && other.md5.contentEquals(this.md5)
}
override fun hashCode(): Int {
return filepath.hashCode() + 31 * md5.hashCode()
return imageId.hashCode() + 31 * md5.hashCode()
}
}
@ -296,7 +306,7 @@ internal class NotOnlineImageFromServer(
}
override fun hashCode(): Int {
return resourceId.hashCode() + 31 * md5.hashCode()
return imageId.hashCode() + 31 * md5.hashCode()
}
}

View File

@ -374,12 +374,12 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
// with generic type, less mistakes
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<*>) {
packet.forEach {
handlePacket(null, it, commandName, sequenceId)
}
}
handlePacket(packetFactory, packet, commandName, sequenceId)
}
}
@ -388,29 +388,6 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
*/
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)) {
listener.complete(packet)
}
}
// check top-level cancelling
if (packet != null && PacketReceivedEvent(packet).broadcast().isCancelled) {
return
}
// broadcast
if (packet is Event) {
if (packet is BroadcastControllable) {
if (packet.shouldBroadcast) packet.broadcast()
} else {
packet.broadcast()
}
if (packet is CancellableEvent && packet.isCancelled) return
}
if (packet != null && (bot.logger.isEnabled || logger.isEnabled)) {
val logMessage = "Received: ${packet.toString().replace("\n", """\n""").replace("\r", "")}"
@ -419,12 +396,32 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
} else logger.verbose(logMessage)
}
packetListeners.forEach { listener ->
if (listener.filter(commandName, sequenceId) && packetListeners.remove(listener)) {
listener.complete(packet)
}
}
packetFactory?.run {
when (this) {
is OutgoingPacketFactory<P> -> bot.handle(packet)
is IncomingPacketFactory<P> -> bot.handle(packet, sequenceId)?.sendWithoutExpect()
}
}
if (packet != null && PacketReceivedEvent(packet).broadcast().isCancelled) {
return
}
if (packet is Event) {
if (packet is BroadcastControllable) {
if (packet.shouldBroadcast) packet.broadcast()
} else {
packet.broadcast()
}
if (packet is CancellableEvent && packet.isCancelled) return
}
}
/**

View File

@ -0,0 +1,38 @@
/*
* 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.qqandroid.network.protocol.data.proto
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
import net.mamoe.mirai.qqandroid.io.ProtoBuf
class MsgRevokeUserDef : ProtoBuf {
@Serializable
class MsgInfoUserDef(
@SerialId(1) val longMessageFlag: Int = 0,
@SerialId(2) val longMsgInfo: List<MsgInfoDef>? = null,
@SerialId(3) val fileUuid: List<String> = listOf()
) : ProtoBuf {
@Serializable
class MsgInfoDef(
@SerialId(1) val msgSeq: Int = 0,
@SerialId(2) val longMsgId: Int = 0,
@SerialId(3) val longMsgNum: Int = 0,
@SerialId(4) val longMsgIndex: Int = 0
) : ProtoBuf
}
@Serializable
class UinTypeUserDef(
@SerialId(1) val fromUinType: Int = 0,
@SerialId(2) val fromGroupCode: Long = 0L,
@SerialId(3) val fileUuid: List<String> = listOf()
) : ProtoBuf
}

View File

@ -26,7 +26,7 @@ internal class MsgSvc : ProtoBuf {
internal class PbGetMsgResp(
@SerialId(1) val result: Int = 0,
@SerialId(2) val errmsg: String = "",
@SerialId(3) val syncCookie: ByteArray = EMPTY_BYTE_ARRAY,
@SerialId(3) val syncCookie: ByteArray? = EMPTY_BYTE_ARRAY,
@SerialId(4) val syncFlag: SyncFlag,
@SerialId(5) val uinPairMsgs: List<MsgComm.UinPairMsg>? = null,
@SerialId(6) val bindUin: Long = 0L,

View File

@ -14,6 +14,7 @@ import kotlinx.io.pool.useInstance
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.PbMessageSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
@ -138,7 +139,8 @@ internal object KnownPacketFactories {
TroopManagement.GetGroupInfo,
TroopManagement.EditGroupNametag,
TroopManagement.Kick,
Heartbeat.Alive
Heartbeat.Alive,
PbMessageSvc.PbMsgWithDraw
)
object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf(

View File

@ -0,0 +1,91 @@
/*
* 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.qqandroid.network.protocol.packet.chat
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.io.serialization.writeProtoBuf
import net.mamoe.mirai.qqandroid.network.QQAndroidClient
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgRevokeUserDef
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
internal class PbMessageSvc {
object PbMsgWithDraw : OutgoingPacketFactory<PbMsgWithDraw.Response>(
"PbMessageSvc.PbMsgWithDraw"
) {
sealed class Response : Packet {
object Success : Response() {
override fun toString(): String {
return "PbMessageSvc.PbMsgWithDraw.Response.Success"
}
}
data class Failed(
val result: Int,
val errorMessage: String
) : Response()
}
// 12 1A 08 01 10 00 18 E7 C1 AD B8 02 22 0A 08 BF BA 03 10 BF 81 CB B7 03 2A 02 08 00
fun Group(
client: QQAndroidClient,
groupCode: Long,
messageSequenceId: Int, // 56639
messageRandom: Int, // 921878719
messageType: Int = 0
): OutgoingPacket = buildOutgoingUniPacket(client) {
writeProtoBuf(
MsgSvc.PbMsgWithDrawReq.serializer(),
MsgSvc.PbMsgWithDrawReq(
groupWithDraw = listOf(
MsgSvc.PbGroupMsgWithDrawReq(
subCmd = 1,
groupType = 0, // 普通群
groupCode = groupCode,
msgList = listOf(
MsgSvc.PbGroupMsgWithDrawReq.MessageInfo(
msgSeq = messageSequenceId,
msgRandom = messageRandom,
msgType = messageType
)
),
userdef = MsgRevokeUserDef.MsgInfoUserDef(
longMessageFlag = 0
).toByteArray(MsgRevokeUserDef.MsgInfoUserDef.serializer())
)
)
)
)
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val resp = readProtoBuf(MsgSvc.PbMsgWithDrawResp.serializer())
resp.groupWithDraw?.firstOrNull()?.let {
if (it.result != 0) {
return Response.Failed(it.result, it.errmsg)
}
return Response.Success
}
resp.c2cWithDraw?.firstOrNull()?.let {
if (it.result != 0) {
return Response.Failed(it.result, it.errmsg)
}
return Response.Success
}
return Response.Failed(-1, "No response")
}
}
}

View File

@ -9,8 +9,11 @@
package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.data.MemberInfo
@ -19,8 +22,11 @@ import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.events.BotJoinGroupEvent
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.MemberJoinEvent
import net.mamoe.mirai.event.subscribingGetAsync
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.addOrRemove
import net.mamoe.mirai.qqandroid.GroupImpl
import net.mamoe.mirai.qqandroid.QQAndroidBot
import net.mamoe.mirai.qqandroid.io.serialization.decodeUniPacket
@ -256,22 +262,73 @@ internal class MessageSvc {
override fun toString(): String = "MessageSvc.PbSendMsg.Response.SUCCESS"
}
/**
* 121: 被限制? 个别号才不能发
*/
data class Failed(val resultType: Int, val errorCode: Int, val errorMessage: String) : Response() {
override fun toString(): String =
"MessageSvc.PbSendMsg.Response.Failed(resultType=$resultType, errorCode=$errorCode, errorMessage=$errorMessage)"
}
}
internal class MessageSourceFromSend(
override val messageUid: Int,
override val time: Long,
override val senderId: Long,
override val groupId: Long// ,
// override val sourceMessage: MessageChain
) : MessageSource {
private lateinit var sequenceIdDeferred: Deferred<Int>
@UseExperimental(MiraiExperimentalAPI::class)
fun startWaitingSequenceId(contact: Contact) {
sequenceIdDeferred = contact.subscribingGetAsync<OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt, Int> {
if (it.messageRandom == messageUid) {
it.sequenceId
} else null
}
}
@UseExperimental(ExperimentalCoroutinesApi::class)
override val sequenceId: Int
get() = sequenceIdDeferred.getCompleted()
override suspend fun ensureSequenceIdAvailable() {
sequenceIdDeferred.join()
}
override fun toString(): String {
return ""
}
}
inline fun ToFriend(
client: QQAndroidClient,
toUin: Long,
message: MessageChain,
crossinline sourceCallback: (MessageSource) -> Unit
): OutgoingPacket {
val source = MessageSourceFromSend(
messageUid = Random.nextInt().absoluteValue,
senderId = client.uin,
time = currentTimeSeconds + client.timeDifference,
groupId = 0//
// sourceMessage = message
)
sourceCallback(source)
return ToFriend(client, toUin, message, source)
}
/**
* 发送好友消息
*/
@Suppress("FunctionName")
fun ToFriend(
private fun ToFriend(
client: QQAndroidClient,
toUin: Long,
message: MessageChain
message: MessageChain,
source: MessageSource
): OutgoingPacket = buildOutgoingUniPacket(client) {
///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes())
///return@buildOutgoingUniPacket
@ -285,13 +342,32 @@ internal class MessageSvc {
)
),
msgSeq = client.atomicNextMessageSequenceId(),
msgRand = Random.nextInt().absoluteValue,
syncCookie = SyncCookie(time = currentTimeSeconds).toByteArray(SyncCookie.serializer())
msgRand = source.messageUid,
syncCookie = SyncCookie(time = source.time).toByteArray(SyncCookie.serializer())
// msgVia = 1
)
)
}
inline fun ToGroup(
client: QQAndroidClient,
groupCode: Long,
message: MessageChain,
sourceCallback: (MessageSourceFromSend) -> Unit
): OutgoingPacket {
val source = MessageSourceFromSend(
messageUid = Random.nextInt().absoluteValue,
senderId = client.uin,
time = currentTimeSeconds + client.timeDifference,
groupId = groupCode//,
// sourceMessage = message
)
sourceCallback(source)
return ToGroup(client, groupCode, message, source)
}
/**
* 发送群消息
*/
@ -299,14 +375,13 @@ internal class MessageSvc {
fun ToGroup(
client: QQAndroidClient,
groupCode: Long,
message: MessageChain
message: MessageChain,
source: MessageSource
): OutgoingPacket = buildOutgoingUniPacket(client) {
///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes())
// DebugLogger.debug("sending group message: " + message.toRichTextElems().contentToString())
val seq = client.atomicNextMessageSequenceId()
///return@buildOutgoingUniPacket
writeProtoBuf(
MsgSvc.PbSendMsgReq.serializer(), MsgSvc.PbSendMsgReq(
@ -317,12 +392,14 @@ internal class MessageSvc {
elems = message.toRichTextElems()
)
),
msgSeq = seq,
msgRand = Random.nextInt().absoluteValue,
msgSeq = client.atomicNextMessageSequenceId(),
msgRand = source.messageUid,
syncCookie = EMPTY_BYTE_ARRAY,
msgVia = 1
)
)
message.addOrRemove(source)
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {

View File

@ -19,6 +19,7 @@ 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.Event
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.qqandroid.GroupImpl
@ -46,9 +47,18 @@ internal class OnlinePush {
/**
* 接受群消息
*/
internal object PbPushGroupMsg : IncomingPacketFactory<GroupMessage?>("OnlinePush.PbPushGroupMsg") {
internal object PbPushGroupMsg : IncomingPacketFactory<Packet?>("OnlinePush.PbPushGroupMsg") {
internal class SendGroupMessageReceipt(
val messageRandom: Int,
val sequenceId: Int
) : Packet, Event {
override fun toString(): String {
return "OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt(messageRandom=$messageRandom, sequenceId=$sequenceId)"
}
}
@UseExperimental(ExperimentalStdlibApi::class)
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): GroupMessage? {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet? {
// 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 null
val pbPushMsg = readProtoBuf(MsgOnlinePush.PbPushMsg.serializer())
@ -56,7 +66,7 @@ internal class OnlinePush {
val extraInfo: ImMsgBody.ExtraInfo? = pbPushMsg.msg.msgBody.richText.elems.firstOrNull { it.extraInfo != null }?.extraInfo
if (pbPushMsg.msg.msgHead.fromUin == bot.uin) {
return null
return SendGroupMessageReceipt(pbPushMsg.msg.msgBody.richText.attr!!.random, pbPushMsg.msg.msgHead.msgSeq)
}
val group = bot.getGroup(pbPushMsg.msg.msgHead.groupInfo!!.groupCode)
@ -64,8 +74,6 @@ internal class OnlinePush {
// println(pbPushMsg.msg.msgBody.richText.contentToString())
val flags = extraInfo?.flags ?: 0
return GroupMessage(
bot = bot,
group = group,
senderName = pbPushMsg.msg.msgHead.groupInfo.groupCard,
sender = group[pbPushMsg.msg.msgHead.fromUin],
message = pbPushMsg.msg.toMessageChain(),

View File

@ -107,6 +107,8 @@ fun main() {
* 顶层方法. TCP 切掉头后直接来这里
*/
fun ByteReadPacket.decodeMultiClientToServerPackets() {
DebugLogger.enable()
PacketLogger.enable()
println("=======================处理客户端到服务器=======================")
var count = 0
while (remaining != 0L) {

View File

@ -25,7 +25,7 @@ fun main() {
println(
File(
"""
E:\Projects\QQAndroidFF\app\src\main\java\tencent\im\statsvc\getonline
E:\Projects\QQAndroidFF\app\src\main\java\tencent\im\msgrevoke
""".trimIndent()
)
.generateUnarrangedClasses().toMutableList().arrangeClasses().joinToString("\n\n")

View File

@ -9,7 +9,6 @@
package net.mamoe.mirai.message
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.utils.MiraiInternalAPI
@ -18,7 +17,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
* 平台相关扩展
*/
@UseExperimental(MiraiInternalAPI::class)
actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual constructor(bot: Bot) : MessagePacketBase<TSender, TSubject>(bot) {
actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual constructor() : MessagePacketBase<TSender, TSubject>() {
// suspend inline fun uploadImage(image: Bitmap): Image = subject.uploadImage(image)
//suspend inline fun uploadImage(image: URL): Image = subject.uploadImage(image)
//suspend inline fun uploadImage(image: Input): Image = subject.uploadImage(image)

View File

@ -84,7 +84,7 @@ abstract class Bot : CoroutineScope {
*/
@MiraiExperimentalAPI("还未支持")
val nick: String
get() = TODO("bot 昵称获取")
get() = ""// TODO("bot 昵称获取")
/**
* 日志记录器
@ -175,8 +175,6 @@ abstract class Bot : CoroutineScope {
*/
abstract suspend fun queryGroupMemberList(groupUin: Long, groupCode: Long, ownerId: Long): Sequence<MemberInfo>
// TODO 目前还不能构造群对象. 这将在以后支持
// endregion
// region network

View File

@ -18,6 +18,7 @@ import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.ImageUploadEvent
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.WeakRefProperty
@ -40,6 +41,9 @@ interface Contact : CoroutineScope {
*
* 对于 [QQ], `uin` `id` 是相同的意思.
* 对于 [Group], `groupCode` `id` 是相同的意思.
*
* @see QQ.id
* @see Group.id
*/
val id: Long
@ -51,8 +55,10 @@ interface Contact : CoroutineScope {
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. [引用回复][MessageReceipt.quote]仅群聊 [撤回][MessageReceipt.recall] 这条消息.
*/
suspend fun sendMessage(message: MessageChain)
suspend fun sendMessage(message: MessageChain): MessageReceipt<out Contact>
/**
* 上传一个图片以备发送.
@ -88,4 +94,4 @@ interface Contact : CoroutineScope {
suspend inline fun Contact.sendMessage(message: Message) = sendMessage(message.toChain())
suspend inline fun Contact.sendMessage(plain: String) = sendMessage(plain.singleChain())
suspend inline fun Contact.sendMessage(plain: String) = sendMessage(plain.toMessage())

View File

@ -40,6 +40,15 @@ class ContactList<C : Contact>(@MiraiInternalAPI val delegate: LockFreeLinkedLis
fun containsAll(elements: Collection<C>): Boolean = elements.all { contains(it) }
fun isEmpty(): Boolean = delegate.isEmpty()
inline fun forEach(block: (C) -> Unit) = delegate.forEach(block)
fun first(): C {
forEach { return it }
throw NoSuchElementException()
}
fun firstOrNull(): C? {
forEach { return it }
return null
}
override fun toString(): String = delegate.joinToString(separator = ", ", prefix = "ContactList(", postfix = ")")
}

View File

@ -11,11 +11,21 @@
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
/**
@ -83,10 +93,18 @@ interface Group : Contact, CoroutineScope {
override val id: Long
/**
* 群主
* 群主.
*
* @return 若机器人是群主, 返回 [botAsMember]. 否则返回相应的成员
*/
val owner: Member
/**
* [Bot] 在群内的 [Member] 实例
*/
@MiraiExperimentalAPI
val botAsMember: Member
/**
* 机器人被禁言还剩余多少秒
*
@ -133,6 +151,17 @@ interface Group : Contact, CoroutineScope {
@MiraiExperimentalAPI("还未支持")
suspend fun quit(): Boolean
/**
* 撤回这条消息.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Group.recall (扩展函数) 接受参数 [MessageChain]
*/
suspend fun recall(source: MessageSource)
/**
* 构造一个 [Member].
* 非特殊情况请不要使用这个函数. 优先使用 [get].
@ -142,6 +171,19 @@ interface Group : Contact, CoroutineScope {
@JvmName("newMember")
fun Member(memberInfo: MemberInfo): Member
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
override suspend fun sendMessage(message: MessageChain): MessageReceipt<Group>
companion object {
/**
@ -184,7 +226,52 @@ interface Group : Contact, CoroutineScope {
fun toFullString(): String = "Group(id=${this.id}, name=$name, owner=${owner.id}, members=${members.idContentString})"
}
/**
* 撤回这条消息.
*
* [Bot] 撤回自己的消息不需要权限.
* [Bot] 撤回群员的消息需要管理员权限.
*
* @throws PermissionDeniedException [Bot] 无权限操作时
* @see Group.recall
*/
suspend inline fun Group.recall(message: MessageChain) = this.recall(message[MessageSource])
/**
* 在一段时间后撤回这条消息.
*
* @param millis 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
fun Group.recallIn(
message: MessageSource,
millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(millis)
recall(message)
}
/**
* 在一段时间后撤回这条消息.
*
* @param millis 延迟的时间, 单位为毫秒
* @param coroutineContext 额外的 [CoroutineContext]
* @see recall
*/
fun Group.recallIn(
message: MessageChain,
millis: Long,
coroutineContext: CoroutineContext = EmptyCoroutineContext
): Job = this.launch(coroutineContext + CoroutineName("MessageRecall")) {
kotlinx.coroutines.delay(millis)
recall(message)
}
/**
* 返回机器人是否正在被禁言
*
* @see Group.botMuteRemaining 剩余禁言时间
*/
val Group.isBotMuted: Boolean get() = this.botMuteRemaining != 0

View File

@ -72,6 +72,9 @@ interface Member : QQ, Contact {
/**
* 禁言.
*
* QQ 中最小操作和显示的时间都是一分钟.
* 机器人可以实现精确到秒, 会被客户端显示为 1 分钟但不影响实际禁言时间.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.

View File

@ -16,6 +16,11 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.data.Profile
import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiExperimentalAPI
/**
@ -68,4 +73,17 @@ interface QQ : Contact, CoroutineScope {
*/
@MiraiExperimentalAPI("还未支持")
suspend fun queryRemark(): FriendNameRemark
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ>
}

View File

@ -62,4 +62,15 @@ interface GroupInfo {
* 机器人被禁言还剩时间, .
*/
val botMuteRemaining: Int
/*
/**
* 机器人的头衔
*/
val botSpecialTitle: String
/**
* 机器人的昵称
*/
val botNameCard: String*/
}

View File

@ -0,0 +1,112 @@
/*
* 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.event
import kotlinx.coroutines.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* 挂起当前协程, 监听这个事件, 并尝试从这个事件中获取一个值.
*
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值
*
* @see subscribingGetAsync 本函数的异步版本
*/
@MiraiExperimentalAPI
suspend inline fun <reified E : Event, R : Any> subscribingGet(
timeoutMillis: Long = -1,
noinline filter: E.(E) -> R? // 不要 crossinline: crossinline 后 stacktrace 会不正常
): R {
require(timeoutMillis == -1L || timeoutMillis > 0) { "timeoutMillis must be -1 or > 0" }
return subscribingGetOrNull(timeoutMillis, filter) ?: error("timeout subscribingGet")
}
/**
* 挂起当前协程, 监听这个事件, 并尝试从这个事件中获取一个值.
*
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值
*
* @see subscribingGetAsync 本函数的异步版本
*/
@MiraiExperimentalAPI
suspend inline fun <reified E : Event, R : Any> subscribingGetOrNull(
timeoutMillis: Long = -1,
noinline filter: E.(E) -> R? // 不要 crossinline: crossinline 后 stacktrace 会不正常
): R? {
require(timeoutMillis == -1L || timeoutMillis > 0) { "timeoutMillis must be -1 or > 0" }
var result: R? = null
var resultThrowable: Throwable? = null
if (timeoutMillis == -1L) {
@Suppress("DuplicatedCode") // for better performance
coroutineScope {
var listener: Listener<E>? = null
listener = this.subscribe {
val value = try {
filter.invoke(this, it)
} catch (e: Exception) {
resultThrowable = e
return@subscribe ListeningStatus.STOPPED.also { listener!!.complete() }
}
if (value != null) {
result = value
return@subscribe ListeningStatus.STOPPED.also { listener!!.complete() }
} else return@subscribe ListeningStatus.LISTENING
}
}
} else {
withTimeoutOrNull(timeoutMillis) {
var listener: Listener<E>? = null
@Suppress("DuplicatedCode") // for better performance
listener = this.subscribe {
val value = try {
filter.invoke(this, it)
} catch (e: Exception) {
resultThrowable = e
return@subscribe ListeningStatus.STOPPED.also { listener!!.complete() }
}
if (value != null) {
result = value
return@subscribe ListeningStatus.STOPPED.also { listener!!.complete() }
} else return@subscribe ListeningStatus.LISTENING
}
}
}
resultThrowable?.let { throw it }
return result
}
/**
* 异步监听这个事件, 并尝试从这个事件中获取一个值.
*
* [filter] 抛出了一个异常, [Deferred.await] 会抛出这个异常或.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* @param coroutineContext 额外的 [CoroutineContext]
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值
*/
@MiraiExperimentalAPI
inline fun <reified E : Event, R : Any> CoroutineScope.subscribingGetAsync(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
timeoutMillis: Long = -1,
noinline filter: E.(E) -> R? // 不要 crossinline: crossinline 后 stacktrace 会不正常
): Deferred<R> = this.async(coroutineContext) {
subscribingGet(timeoutMillis, filter)
}

View File

@ -101,6 +101,9 @@ interface Listener<in E : Event> : CompletableJob {
*
* @param coroutineContext 给事件监听协程的额外的 [CoroutineContext]
*
* @see subscribingGet 监听一个事件, 并尝试从这个事件中获取一个值.
* @see subscribingGetAsync 异步监听一个事件, 并尝试从这个事件中获取一个值.
*
* @see subscribeAlways 一直监听
* @see subscribeOnce 只监听一次
*

View File

@ -13,12 +13,16 @@ 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.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
class FriendMessage(
bot: Bot,
override val sender: QQ,
sender: QQ,
override val message: MessageChain
) : MessagePacket<QQ, QQ>(bot), BroadcastControllable {
) : MessagePacket<QQ, QQ>(), BroadcastControllable {
override val sender: QQ by sender.unsafeWeakRef()
override val bot: Bot get() = sender.bot
override val subject: QQ get() = sender
override fun toString(): String = "FriendMessage(sender=${sender.id}, message=$message)"

View File

@ -9,21 +9,19 @@
package net.mamoe.mirai.message
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
import kotlin.jvm.JvmName
@Suppress("unused", "NOTHING_TO_INLINE")
class GroupMessage(
bot: Bot,
group: Group,
val senderName: String,
/**
* 发送方权限.
@ -31,9 +29,10 @@ class GroupMessage(
val permission: MemberPermission,
sender: Member,
override val message: MessageChain
) : MessagePacket<Member, Group>(bot), Event {
val group: Group by group.unsafeWeakRef()
) : MessagePacket<Member, Group>(), Event {
override val sender: Member by sender.unsafeWeakRef()
val group: Group get() = sender.group
override val bot: Bot get() = sender.bot
override val subject: Group get() = group
@ -45,20 +44,25 @@ class GroupMessage(
* 对于好友消息事件, 这个方法将会给好友 ([subject]) 发送消息
* 对于群消息事件, 这个方法将会给群 ([subject]) 发送消息
*/
suspend inline fun quoteReply(message: MessageChain) = reply(this.message.quote() + message)
suspend inline fun quoteReply(message: MessageChain): MessageReceipt<Group> = reply(this.message.quote() + message)
suspend inline fun quoteReply(message: Message) = reply(this.message.quote() + message)
suspend inline fun quoteReply(plain: String) = reply(this.message.quote() + plain)
suspend inline fun quoteReply(message: Message): MessageReceipt<Group> = reply(this.message.quote() + message)
suspend inline fun quoteReply(plain: String): MessageReceipt<Group> = reply(this.message.quote() + plain)
@JvmName("reply2")
suspend inline fun String.quoteReply() = quoteReply(this)
suspend inline fun String.quoteReply(): MessageReceipt<Group> = quoteReply(this)
@JvmName("reply2")
suspend inline fun Message.quoteReply() = quoteReply(this)
suspend inline fun Message.quoteReply(): MessageReceipt<Group> = quoteReply(this)
@JvmName("reply2")
suspend inline fun MessageChain.quoteReply() = quoteReply(this)
suspend inline fun MessageChain.quoteReply(): MessageReceipt<Group> = quoteReply(this)
suspend inline fun MessageChain.recall() = group.recall(this)
suspend inline fun MessageSource.recall() = group.recall(this)
inline fun MessageSource.recallIn(delay: Long): Job = group.recallIn(this, delay)
inline fun MessageChain.recallIn(delay: Long): Job = group.recallIn(this, delay)
override fun toString(): String =
"GroupMessage(group=${group.id}, senderName=$senderName, sender=${sender.id}, permission=${permission.name}, message=$message)"

View File

@ -21,6 +21,8 @@ import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.Packet
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.event.subscribingGet
import net.mamoe.mirai.event.subscribingGetAsync
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.*
import kotlin.jvm.JvmName
@ -30,18 +32,19 @@ import kotlin.jvm.JvmName
* 请查看各平台的 `actual` 实现的说明.
*/
@UseExperimental(MiraiInternalAPI::class)
expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>(bot: Bot) : MessagePacketBase<TSender, TSubject>
expect abstract class MessagePacket<TSender : QQ, TSubject : Contact>() : MessagePacketBase<TSender, TSubject>
/**
* 仅内部使用, 请使用 [MessagePacket]
*/ // Tips: 在 IntelliJ 中 (左侧边栏) 打开 `Structure`, 可查看类结构
@Suppress("NOTHING_TO_INLINE")
@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST")
@MiraiInternalAPI
abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) : Packet, BotEvent {
abstract class MessagePacketBase<TSender : QQ, TSubject : Contact> : Packet, BotEvent {
/**
* 接受到这条消息的
*/
override val bot: Bot by _bot.unsafeWeakRef()
@WeakRefProperty
abstract override val bot: Bot
/**
* 消息事件主体.
@ -51,6 +54,7 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
*
* 在回复消息时, 可通过 [subject] 作为回复对象
*/
@WeakRefProperty
abstract val subject: TSubject
/**
@ -58,6 +62,7 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
*
* 在好友消息时为 [QQ] 的实例, 在群消息时为 [Member] 的实例
*/
@WeakRefProperty
abstract val sender: TSender
/**
@ -73,20 +78,19 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
* 对于好友消息事件, 这个方法将会给好友 ([subject]) 发送消息
* 对于群消息事件, 这个方法将会给群 ([subject]) 发送消息
*/
suspend inline fun reply(message: MessageChain) = subject.sendMessage(message)
suspend inline fun reply(message: MessageChain): MessageReceipt<TSubject> = subject.sendMessage(message) as MessageReceipt<TSubject>
suspend inline fun reply(message: Message) = subject.sendMessage(message.toChain())
suspend inline fun reply(plain: String) = subject.sendMessage(plain.singleChain())
suspend inline fun reply(message: Message): MessageReceipt<TSubject> = subject.sendMessage(message.toChain()) as MessageReceipt<TSubject>
suspend inline fun reply(plain: String): MessageReceipt<TSubject> = subject.sendMessage(plain.toMessage().toChain()) as MessageReceipt<TSubject>
@JvmName("reply1")
suspend inline fun String.reply() = reply(this)
suspend inline fun String.reply(): MessageReceipt<TSubject> = reply(this)
@JvmName("reply1")
suspend inline fun Message.reply() = reply(this)
suspend inline fun Message.reply(): MessageReceipt<TSubject> = reply(this)
@JvmName("reply1")
suspend inline fun MessageChain.reply() = reply(this)
suspend inline fun MessageChain.reply(): MessageReceipt<TSubject> = reply(this)
// endregion
// region
@ -110,12 +114,16 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
suspend inline fun String.send() = this.toMessage().sendTo(subject)
// endregion
operator fun <M : Message> get(at: Message.Key<M>): M {
return this.message[at]
}
/**
* 创建 @ 这个账号的消息. 当且仅当消息为群消息时可用. 否则将会抛出 [IllegalArgumentException]
*/
inline fun QQ.at(): At = At(this as? Member ?: error("`QQ.at` can only be used in GroupMessage"))
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")
fun At.member(): Member = (this@MessagePacketBase as? GroupMessage)?.group?.get(this.target) ?: error("`At.member` can only be used in GroupMessage")
// endregion
@ -135,16 +143,47 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact>(_bot: Bot) :
*/
suspend inline fun Image.download(): ByteReadPacket = bot.run { download() }
// endregion
}
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("bot.getFriend(this.target)"))
fun At.qq(): QQ = bot.getFriend(this.target)
/**
* 判断两个 [MessagePacket] [MessagePacket.sender] [MessagePacket.subject] 是否相同
*/
fun MessagePacket<*, *>.isContextIdenticalWith(another: MessagePacket<*, *>): Boolean {
return this.sender == another.sender && this.subject == another.subject
}
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("bot.getFriend(this.toLong())"))
fun Int.qq(): QQ = bot.getFriend(this.coerceAtLeastOrFail(0).toLong())
/**
* 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [P] 相同且通过 [筛选][filter] [MessagePacket]
*
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [subscribingGet] 会返回这个值
*
* @see subscribingGetAsync 本函数的异步版本
*/
suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessage(
timeoutMillis: Long = -1,
crossinline filter: P.(P) -> Boolean
): MessageChain {
return subscribingGet<P, P>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessage) }?.takeIf { filter(it, it) }
}.message
}
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("bot.getFriend(this)"))
fun Long.qq(): QQ = bot.getFriend(this.coerceAtLeastOrFail(0))
@Deprecated(message = "这个函数有歧义, 将在不久后删除", replaceWith = ReplaceWith("bot.getGroup(this)"))
fun Long.group(): Group = bot.getGroup(this)
/**
* 挂起当前协程, 等待下一条 [MessagePacket.sender] [MessagePacket.subject] [P] 相同的 [MessagePacket]
*
* [filter] 抛出了一个异常, 本函数会立即抛出这个异常.
*
* @param timeoutMillis 超时. 单位为毫秒. `-1` 为不限制
*
* @see subscribingGetAsync 本函数的异步版本
*/
suspend inline fun <reified P : MessagePacket<*, *>> P.nextMessage(
timeoutMillis: Long = -1
): MessageChain {
return subscribingGet<P, P>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessage) }
}.message
}

View File

@ -0,0 +1,126 @@
/*
* 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.message
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
/**
* 发送消息后得到的回执. 可用于撤回.
*
* 此对象持有 [Contact] 的弱引用, [Bot] 离线后将会释放引用, 届时 [target] 将无法访问.
*
* @see Group.sendMessage 发送群消息, 返回回执此对象
* @see QQ.sendMessage 发送群消息, 返回回执此对象
*/
open class MessageReceipt<C : Contact>(
val originalMessage: MessageChain,
private val source: MessageSource,
target: C
) {
init {
require(target is Group || target is QQ) { "target must be either Group or QQ" }
}
/**
* 发送目标, [Group] [QQ]
*/
val target: C by target.unsafeWeakRef()
private val _isRecalled = atomic(false)
/**
* 撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @see Group.recall
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
@UseExperimental(MiraiExperimentalAPI::class)
suspend fun recall() {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
when (val contact = target) {
is Group -> {
contact.recall(source)
}
is QQ -> {
TODO()
}
else -> error("Unknown contact type")
}
} else error("message is already or planned to be recalled")
}
/**
* 撤回这条消息. [recall] [recallIn] 只能被调用一次.
*
* @param millis 延迟时间, 单位为毫秒
*
* @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
*/
@UseExperimental(MiraiExperimentalAPI::class)
fun recallIn(millis: Long): Job {
@Suppress("BooleanLiteralArgument")
if (_isRecalled.compareAndSet(false, true)) {
when (val contact = target) {
is Group -> {
return contact.recallIn(source, millis)
}
is QQ -> {
TODO()
}
else -> error("Unknown contact type")
}
} else error("message is already or planned to be recalled")
}
/**
* 引用这条消息. 仅群消息能被引用
*
* @see MessageChain.quote 引用一条消息
*
* @throws IllegalStateException 当此消息不是群消息时
*/
@MiraiExperimentalAPI("unstable")
open fun quote(): MessageChain {
val target = target
check(target is Group) { "quote is only available for GroupMessage" }
return this.source.quote(target.botAsMember)
}
/**
* 引用这条消息并回复. 仅群消息能被引用
*
* @see MessageChain.quote 引用一条消息
*
* @throws IllegalStateException 当此消息不是群消息时
*/
@MiraiExperimentalAPI("unstable")
suspend fun quoteReply(message: MessageChain) {
target.sendMessage(this.quote() + message)
}
}
@MiraiExperimentalAPI("unstable")
suspend inline fun MessageReceipt<out Contact>.quoteReply(message: Message) {
return this.quoteReply(message.toChain())
}
@MiraiExperimentalAPI("unstable")
suspend inline fun MessageReceipt<out Contact>.quoteReply(message: String) {
return this.quoteReply(message.toMessage().toChain())
}

View File

@ -34,10 +34,6 @@ class At @MiraiInternalAPI constructor(val target: Long, val display: String) :
companion object Key : Message.Key<At>
override fun eq(other: Message): Boolean {
return other is At && other.target == this.target
}
// 自动为消息补充 " "
override fun followedBy(tail: Message): MessageChain {

View File

@ -16,7 +16,9 @@ import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
/**
* "@全体成员"
* "@全体成员".
*
* 非会员每天只能发送 10 [AtAll]. 超出部分会被以普通文字看待.
*
* @see At at 单个群成员
*/
@ -26,7 +28,7 @@ object AtAll : Message, Message.Key<AtAll> {
// 自动为消息补充 " "
override fun followedBy(tail: Message): MessageChain {
if(tail is PlainText && tail.stringValue.startsWith(' ')){
if (tail is PlainText && tail.stringValue.startsWith(' ')) {
return super.followedBy(tail)
}
return super.followedBy(PlainText(" ")) + tail

View File

@ -167,8 +167,4 @@ class Face(val id: Int) : Message {
const val shouqiang: Int = 169
const val qingwa: Int = 170
}
override fun eq(other: Message): Boolean {
return other is Face && other.id == this.id
}
}

View File

@ -49,11 +49,6 @@ sealed class Image : Message {
final override fun toString(): String {
return "[mirai:$imageId]"
}
final override fun eq(other: Message): Boolean {
return if (other is Image) return other.imageId == this.imageId
else this.toString() == other.toString()
}
}
abstract class CustomFace : Image() {

View File

@ -13,9 +13,6 @@
package net.mamoe.mirai.message.data
import net.mamoe.mirai.message.data.NullMessageChain.toString
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.js.JsName
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
@ -60,12 +57,15 @@ interface MessageChain : Message, MutableList<Message> {
* @param key 由各个类型消息的伴生对象持有. [PlainText.Key]
*/
operator fun <M : Message> get(key: Message.Key<M>): M = first(key)
}
override fun eq(other: Message): Boolean {
if (other is MessageChain && other.size != this.size)
return false
return this.toString() == other.toString()
}
/**
* 先删除同类型的消息, 再添加 [message]
*/
fun <T : Message> MessageChain.addOrRemove(message: T) {
val clazz = message::class
this.removeAll { clazz.isInstance(it) }
this.add(message)
}
/**
@ -132,6 +132,16 @@ fun MessageChain(vararg messages: Message): MessageChain =
if (messages.isEmpty()) EmptyMessageChain()
else MessageChainImpl(messages.toMutableList())
/**
* 构造 [MessageChain] 的快速途径 ( [Array] 创建)
* 若仅提供一个参数, 请考虑使用 [Message.toChain] 以优化性能
*/
@JvmName("newChain")
@JsName("newChain")
@Suppress("FunctionName")
fun MessageChain(message: Message): MessageChain =
MessageChainImpl(mutableListOf(message))
/**
* 构造 [MessageChain]
*/
@ -141,30 +151,6 @@ fun MessageChain(vararg messages: Message): MessageChain =
fun MessageChain(messages: Iterable<Message>): MessageChain =
MessageChainImpl(messages.toMutableList())
/**
* 构造单元素的不可修改的 [MessageChain]. 内部类实现为 [SingleMessageChain]
*
* 参数 [delegate] 不能为 [MessageChain] 的实例, 否则将会抛出异常.
* 使用 [Message.toChain] 将帮助提前处理这个问题.
*
* @param delegate 所构造的单元素 [MessageChain] 代表的 [Message]
* @throws IllegalArgumentException [delegate] [MessageChain] 的实例时
*
* @see Message.toChain receiver 模式
*/
@JvmName("newSingleMessageChain")
@JsName("newChain")
@MiraiExperimentalAPI
@UseExperimental(ExperimentalContracts::class)
@Suppress("FunctionName")
fun SingleMessageChain(delegate: Message): MessageChain {
contract {
returns() implies (delegate !is MessageChain)
}
require(delegate !is MessageChain) { "delegate for SingleMessageChain should not be any instance of MessageChain" }
return SingleMessageChainImpl(delegate)
}
/**
* 得到包含 [this] [MessageChain].
@ -217,7 +203,7 @@ fun <M : Message> MessageChain.firstOrNull(key: Message.Key<M>): M? = when (key)
* @throws [NoSuchElementException] 如果找不到该类型的实例
*/
@Suppress("UNCHECKED_CAST")
fun <M : Message> MessageChain.first(key: Message.Key<M>): M = firstOrNull(key) ?: error("unknown key: $key")
fun <M : Message> MessageChain.first(key: Message.Key<M>): M = firstOrNull(key) ?: throw NoSuchElementException("no such element: $key")
/**
* 获取第一个 [M] 类型的 [Message] 实例
@ -387,97 +373,3 @@ internal inline class MessageChainImpl constructor(
// endregion
}
/**
* 单个成员的不可修改的 [MessageChain].
*
* 在连接时将会把它当做一个普通 [Message] 看待, 但它不能被 [plusAssign]
*/
@PublishedApi
internal inline class SingleMessageChainImpl(
private val delegate: Message
) : Message, MutableList<Message>,
MessageChain {
// region Message override
override operator fun contains(sub: String): Boolean = delegate.contains(sub)
override fun followedBy(tail: Message): MessageChain {
require(tail !is SingleOnly) { "SingleOnly Message cannot follow another message" }
return if (tail is MessageChain) tail.apply { followedBy(delegate) }
else MessageChain(delegate, tail)
}
override fun plusAssign(message: Message) =
throw UnsupportedOperationException("SingleMessageChainImpl cannot be plusAssigned")
override fun toString(): String = delegate.toString()
// endregion
// region MutableList override
override fun containsAll(elements: Collection<Message>): Boolean = elements.all { it == delegate }
override operator fun get(index: Int): Message = if (index == 0) delegate else throw NoSuchElementException()
override fun indexOf(element: Message): Int = if (delegate == element) 0 else -1
override fun isEmpty(): Boolean = false
override fun lastIndexOf(element: Message): Int = if (delegate == element) 0 else -1
override fun add(element: Message): Boolean = throw UnsupportedOperationException()
override fun add(index: Int, element: Message) = throw UnsupportedOperationException()
override fun addAll(index: Int, elements: Collection<Message>): Boolean = throw UnsupportedOperationException()
override fun addAll(elements: Collection<Message>): Boolean = throw UnsupportedOperationException()
override fun clear() = throw UnsupportedOperationException()
override fun listIterator(): MutableListIterator<Message> = object : MutableListIterator<Message> {
private var hasNext = true
override fun hasPrevious(): Boolean = !hasNext
override fun nextIndex(): Int = if (hasNext) 0 else -1
override fun previous(): Message =
if (hasPrevious()) {
hasNext = true
delegate
} else throw NoSuchElementException()
override fun previousIndex(): Int = if (!hasNext) 0 else -1
override fun add(element: Message) = throw UnsupportedOperationException()
override fun hasNext(): Boolean = hasNext
override fun next(): Message =
if (hasNext) {
hasNext = false
delegate
} else throw NoSuchElementException()
override fun remove() = throw UnsupportedOperationException()
override fun set(element: Message) = throw UnsupportedOperationException()
}
override fun listIterator(index: Int): MutableListIterator<Message> =
if (index == 0) listIterator() else throw UnsupportedOperationException()
override fun remove(element: Message): Boolean = throw UnsupportedOperationException()
override fun removeAll(elements: Collection<Message>): Boolean = throw UnsupportedOperationException()
override fun removeAt(index: Int): Message = throw UnsupportedOperationException()
override fun retainAll(elements: Collection<Message>): Boolean = throw UnsupportedOperationException()
override fun set(index: Int, element: Message): Message = throw UnsupportedOperationException()
override fun subList(fromIndex: Int, toIndex: Int): MutableList<Message> {
return if (fromIndex == 0) when (toIndex) {
1 -> mutableListOf<Message>(this)
0 -> mutableListOf()
else -> throw UnsupportedOperationException()
}
else throw UnsupportedOperationException()
}
override fun iterator(): MutableIterator<Message> = object : MutableIterator<Message> {
private var hasNext = true
override fun hasNext(): Boolean = hasNext
override fun next(): Message =
if (hasNext) {
hasNext = false
delegate
} else throw NoSuchElementException()
override fun remove() = throw UnsupportedOperationException()
}
override operator fun contains(element: Message): Boolean = element == delegate
override val size: Int get() = 1
// endregion
}

View File

@ -26,10 +26,22 @@ import kotlin.jvm.JvmName
interface MessageSource : Message {
companion object Key : Message.Key<MessageSource>
/**
* 序列号. 若是机器人发出去的消息, 请先 [确保 sequenceId 可用][ensureSequenceIdAvailable]
*/
val sequenceId: Int
/**
* 等待 [sequenceId] 获取, 确保其可用.
*
* 若原消息发送失败, 这个方法会等待最多 3 秒随后抛出 [IllegalStateException]
*/
suspend fun ensureSequenceIdAvailable()
/**
* 实际上是个随机数, 但服务器确实是用它当做 uid
*/
val messageUid: Long
val messageUid: Int
/**
* 发送时间, 单位为秒
@ -42,17 +54,22 @@ interface MessageSource : Message {
val senderId: Long
/**
* 群号码
* 群号码, 0 时则来自好友消息
*/
val groupId: Long
/**
* 原消息内容
*/
val sourceMessage: MessageChain
/**
* 固定返回空字符串 ("")
*/
override fun toString(): String
}
}
/**
* 消息唯一标识符. 实际上是个随机数, 但服务器确实是用它当做 uid
*/
val MessageChain.messageUid get() = this[MessageSource].messageUid
/**
* 消息序列号, 可能来自服务器也可以发送时赋值, 不唯一.
*/
val MessageChain.sequenceId get() = this[MessageSource].sequenceId

View File

@ -25,28 +25,10 @@ inline class PlainText(val stringValue: String) : Message {
override fun toString(): String = stringValue
companion object Key : Message.Key<PlainText>
override fun eq(other: Message): Boolean {
if (other is MessageChain) {
return other eq this.toString()
}
return other is PlainText && other.stringValue == this.stringValue
}
}
/**
* 构造 [PlainText]
*/
@Suppress("NOTHING_TO_INLINE")
inline fun String.toMessage(): PlainText = PlainText(this)
/**
* 得到包含作为 [PlainText] [this] [MessageChain].
*
* @return 唯一成员且不可修改的 [SingleMessageChainImpl]
*
* @see SingleMessageChain
* @see SingleMessageChainImpl
*/
@Suppress("NOTHING_TO_INLINE")
inline fun String.singleChain(): MessageChain = SingleMessageChainImpl(this.toMessage())
inline fun String.toMessage(): PlainText = PlainText(this)

View File

@ -27,10 +27,6 @@ inline class XMLMessage(val stringValue: String) : Message,
SingleOnly {
override fun followedBy(tail: Message): Nothing = error("XMLMessage Message cannot be followed")
override fun toString(): String = stringValue
override fun eq(other: Message): Boolean {
return other is XMLMessage && other.stringValue == this.stringValue
}
}
/**

View File

@ -51,9 +51,39 @@ class ExternalImage(
filename: String
): ExternalImage = ExternalImage(width, height, md5, format, data, data.remaining, filename)
fun generateUUID(md5: ByteArray): String{
fun generateUUID(md5: ByteArray): String {
return "${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}"
}
fun generateImageId(md5: ByteArray, imageType: Int): String {
return """{${generateUUID(md5)}}.${determineFormat(imageType)}"""
}
fun determineImageType(format: String): Int {
return when (format) {
"jpg" -> 1000
"png" -> 1001
"webp" -> 1002
"bmp" -> 1005
"gig" -> 2000
"apng" -> 2001
"sharpp" -> 1004
else -> 1000 // unsupported, just make it jpg
}
}
fun determineFormat(imageType: Int): String {
return when (imageType) {
1000 -> "jpg"
1001 -> "png"
1002 -> "webp"
1005 -> "bmp"
2000 -> "gig"
2001 -> "apng"
1004 -> "sharpp"
else -> "jpg" // unsupported, just make it jpg
}
}
}
val format: String =
@ -73,16 +103,7 @@ class ExternalImage(
* SHARPP: 1004
*/
val imageType: Int
get() = when (format) {
"jpg" -> 1000
"png" -> 1001
"webp" -> 1002
"bmp" -> 1005
"gig" -> 2000
"apng" -> 2001
"sharpp" -> 1004
else -> 1000 // unsupported, just make it jpg
}
get() = determineImageType(format)
override fun toString(): String = "[ExternalImage(${width}x$height $format)]"

View File

@ -68,6 +68,10 @@ fun <E> LockFreeLinkedList<E>.asSequence(): Sequence<E> {
}
}
operator fun <E> LockFreeLinkedList<E>.iterator(): Iterator<E> {
return asSequence().iterator()
}
/**
* 构建链表结构然后转为 [LockFreeLinkedList]
*/

View File

@ -13,7 +13,7 @@ import kotlinx.io.pool.DefaultPool
import kotlinx.io.pool.ObjectPool
internal const val DEFAULT_BYTE_ARRAY_POOL_SIZE = 256
internal const val DEFAULT_BYTE_ARRAY_SIZE = 81920
internal const val DEFAULT_BYTE_ARRAY_SIZE = 81920 / 2
val ByteArrayPool: ObjectPool<ByteArray> = ByteArrayPoolImpl

View File

@ -16,7 +16,6 @@ import kotlinx.coroutines.withContext
import kotlinx.io.core.Input
import kotlinx.io.core.use
import kotlinx.io.streams.inputStream
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.message.data.Image
@ -35,7 +34,7 @@ import javax.imageio.ImageIO
* JVM 平台相关扩展
*/
@UseExperimental(MiraiInternalAPI::class)
actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual constructor(bot: Bot) : MessagePacketBase<TSender, TSubject>(bot) {
actual abstract class MessagePacket<TSender : QQ, TSubject : Contact> actual constructor() : MessagePacketBase<TSender, TSubject>() {
// region 上传图片
suspend inline fun uploadImage(image: BufferedImage): Image = subject.uploadImage(image)

View File

@ -16,17 +16,14 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.alsoLogin
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.contact.isOperator
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.event.*
import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.data.AtAll
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.firstOrNull
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.nextMessage
import net.mamoe.mirai.message.sendAsImageTo
import net.mamoe.mirai.qqandroid.Bot
import net.mamoe.mirai.qqandroid.QQAndroid
import net.mamoe.mirai.utils.FileBasedDeviceInfo
import net.mamoe.mirai.utils.MiraiInternalAPI
import java.io.File
@ -49,7 +46,7 @@ private fun readTestAccount(): BotAccount? {
@Suppress("UNUSED_VARIABLE")
suspend fun main() {
val bot = QQAndroid.Bot( // JVM 下也可以不写 `QQAndroid.` 引用顶层函数
val bot = Bot( // JVM 下也可以不写 `QQAndroid.` 引用顶层函数
123456789,
"123456"
) {
@ -207,6 +204,25 @@ fun Bot.messageDSL() {
// sender: QQ
// it: String (来自 MessageChain.toString)
// group: Group
case("recall") {
reply("😎").recallIn(3000) // 3 秒后自动撤回这条消息
}
case("禁言") {
// 挂起当前协程, 等待下一条满足条件的消息.
// 发送 "禁言" 后需要再发送一条消息 at 一个人.
val value: At = nextMessage { message.any(At) }[At]
value.member().mute(10)
}
startsWith("群名=") {
if (!sender.isOperator()) {
sender.mute(5)
return@startsWith
}
group.name = it
}
}
}

View File

@ -9,6 +9,7 @@ import net.mamoe.mirai.event.events.EventCancelledException;
import net.mamoe.mirai.event.events.ImageUploadEvent;
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent;
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent;
import net.mamoe.mirai.message.MessageReceipt;
import net.mamoe.mirai.message.data.Image;
import net.mamoe.mirai.message.data.Message;
import net.mamoe.mirai.message.data.MessageChain;
@ -42,8 +43,7 @@ public interface BlockingContact {
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*/
// kotlin bug
void sendMessage(@NotNull MessageChain messages) throws EventCancelledException, IllegalStateException;
MessageReceipt<? extends Contact> sendMessage(@NotNull MessageChain messages) throws EventCancelledException, IllegalStateException;
/**
* 向这个对象发送消息.
@ -53,7 +53,7 @@ public interface BlockingContact {
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*/
void sendMessage(@NotNull String message) throws EventCancelledException, IllegalStateException;
MessageReceipt<? extends Contact> sendMessage(@NotNull String message) throws EventCancelledException, IllegalStateException;
/**
* 向这个对象发送消息.
@ -63,7 +63,7 @@ public interface BlockingContact {
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*/
void sendMessage(@NotNull Message message) throws EventCancelledException, IllegalStateException;
MessageReceipt<? extends Contact> sendMessage(@NotNull Message message) throws EventCancelledException, IllegalStateException;
/**
* 上传一个图片以备发送.

View File

@ -3,6 +3,9 @@ package net.mamoe.mirai.japt;
import net.mamoe.mirai.contact.*;
import net.mamoe.mirai.data.MemberInfo;
import net.mamoe.mirai.event.events.*;
import net.mamoe.mirai.message.MessageReceipt;
import net.mamoe.mirai.message.data.Message;
import net.mamoe.mirai.message.data.MessageChain;
import net.mamoe.mirai.utils.MiraiExperimentalAPI;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -152,6 +155,36 @@ public interface BlockingGroup extends BlockingContact {
@Nullable
BlockingMember getMemberOrNull(long id);
/**
* 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see MessageSendEvent.FriendMessageSendEvent 发送好友信息事件, cancellable
* @see MessageSendEvent.GroupMessageSendEvent 发送群消息事件. cancellable
*/
MessageReceipt<Group> sendMessage(@NotNull MessageChain messages) throws EventCancelledException, IllegalStateException;
/**
* 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see MessageSendEvent.FriendMessageSendEvent 发送好友信息事件, cancellable
* @see MessageSendEvent.GroupMessageSendEvent 发送群消息事件. cancellable
*/
MessageReceipt<Group> sendMessage(@NotNull String message) throws EventCancelledException, IllegalStateException;
/**
* 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see MessageSendEvent.FriendMessageSendEvent 发送好友信息事件, cancellable
* @see MessageSendEvent.GroupMessageSendEvent 发送群消息事件. cancellable
*/
MessageReceipt<Group> sendMessage(@NotNull Message message) throws EventCancelledException, IllegalStateException;
/**
* 检查此 id 的群成员是否存在
*/

View File

@ -1,8 +1,14 @@
package net.mamoe.mirai.japt;
import net.mamoe.mirai.contact.QQ;
import net.mamoe.mirai.data.FriendNameRemark;
import net.mamoe.mirai.data.PreviousNameList;
import net.mamoe.mirai.data.Profile;
import net.mamoe.mirai.event.events.EventCancelledException;
import net.mamoe.mirai.event.events.MessageSendEvent;
import net.mamoe.mirai.message.MessageReceipt;
import net.mamoe.mirai.message.data.Message;
import net.mamoe.mirai.message.data.MessageChain;
import net.mamoe.mirai.utils.MiraiExperimentalAPI;
import org.jetbrains.annotations.NotNull;
@ -47,4 +53,35 @@ public interface BlockingQQ extends BlockingContact {
@MiraiExperimentalAPI(message = "还未支持")
@NotNull
FriendNameRemark queryRemark();
/**
* 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see MessageSendEvent.FriendMessageSendEvent 发送好友信息事件, cancellable
* @see MessageSendEvent.GroupMessageSendEvent 发送群消息事件. cancellable
*/
MessageReceipt<QQ> sendMessage(@NotNull MessageChain messages) throws EventCancelledException, IllegalStateException;
/**
* 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see MessageSendEvent.FriendMessageSendEvent 发送好友信息事件, cancellable
* @see MessageSendEvent.GroupMessageSendEvent 发送群消息事件. cancellable
*/
MessageReceipt<QQ> sendMessage(@NotNull String message) throws EventCancelledException, IllegalStateException;
/**
* 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see MessageSendEvent.FriendMessageSendEvent 发送好友信息事件, cancellable
* @see MessageSendEvent.GroupMessageSendEvent 发送群消息事件. cancellable
*/
MessageReceipt<QQ> sendMessage(@NotNull Message message) throws EventCancelledException, IllegalStateException;
}

View File

@ -24,6 +24,7 @@ import net.mamoe.mirai.japt.BlockingBot
import net.mamoe.mirai.japt.BlockingGroup
import net.mamoe.mirai.japt.BlockingMember
import net.mamoe.mirai.japt.BlockingQQ
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiExperimentalAPI
@ -35,9 +36,9 @@ internal class BlockingQQImpl(private val delegate: QQ) : BlockingQQ {
override fun getId(): Long = delegate.id
override fun getNick(): String = delegate.nick
override fun sendMessage(messages: MessageChain) = runBlocking { delegate.sendMessage(messages) }
override fun sendMessage(message: String) = runBlocking { delegate.sendMessage(message.toMessage().toChain()) }
override fun sendMessage(message: Message) = runBlocking { delegate.sendMessage(message.toChain()) }
override fun sendMessage(messages: MessageChain): MessageReceipt<QQ> = runBlocking { delegate.sendMessage(messages) }
override fun sendMessage(message: String): MessageReceipt<QQ> = runBlocking { delegate.sendMessage(message.toMessage().toChain()) }
override fun sendMessage(message: Message): MessageReceipt<QQ> = runBlocking { delegate.sendMessage(message.toChain()) }
override fun uploadImage(image: ExternalImage): Image = runBlocking { delegate.uploadImage(image) }
@MiraiExperimentalAPI
@ -51,9 +52,9 @@ internal class BlockingQQImpl(private val delegate: QQ) : BlockingQQ {
}
internal class BlockingGroupImpl(private val delegate: Group) : BlockingGroup {
override fun sendMessage(messages: MessageChain) = runBlocking { delegate.sendMessage(messages) }
override fun sendMessage(message: String) = runBlocking { delegate.sendMessage(message.toMessage().toChain()) }
override fun sendMessage(message: Message) = runBlocking { delegate.sendMessage(message.toChain()) }
override fun sendMessage(messages: MessageChain): MessageReceipt<Group> = runBlocking { delegate.sendMessage(messages) }
override fun sendMessage(message: String): MessageReceipt<Group> = runBlocking { delegate.sendMessage(message.toMessage().toChain()) }
override fun sendMessage(message: Message): MessageReceipt<Group> = runBlocking { delegate.sendMessage(message.toChain()) }
override fun getOwner(): BlockingMember = delegate.owner.blocking()
@MiraiExperimentalAPI
override fun newMember(memberInfo: MemberInfo): Member = delegate.Member(memberInfo)