-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
Message.kt
415 lines (382 loc) · 16.1 KB
/
Message.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
/*
* Copyright 2019-2023 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/dev/LICENSE
*/
@file:JvmMultifileClass
@file:JvmName("MessageUtils")
package net.mamoe.mirai.message.data
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.fold
import kotlinx.serialization.*
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.code.MiraiCode
import net.mamoe.mirai.message.code.MiraiCode.serializeToMiraiCode
import net.mamoe.mirai.message.data.Message.Companion.toString
import net.mamoe.mirai.message.data.MessageChain.Companion.serializeToJsonString
import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.utils.MiraiInternalApi
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
/**
* 可发送的或从服务器接收的消息.
*
* [Message] 派生为 [SingleMessage] 和 [MessageChain].
*
* [SingleMessage] 分为:
* - [MessageMetadata] 消息元数据, 即消息的属性. 包括: [消息来源][MessageSource], [引用回复][QuoteReply] 等.
* - [MessageContent] 含内容的消息, 包括: [纯文本][PlainText], [@群员][At], [@全体成员][AtAll] 等.
*
* [MessageChain] 是链表形式链接的多个 [SingleMessage] 实例, 类似 [List].
*
* ## [Message] 是不可变的
*
* 所有类型的 [Message] 都是不可变的 (immutable), 它们被构造后其所有属性的值就已经固定了. 因此在多线程环境使用是安全的.
* 因此 [contentToString], [serializeToJsonString], [MiraiCode.serializeToMiraiCode] 的返回都是不变的.
*
* [MessageChain] 的 [contentToString] 会被缓存, 只会在第一次调用时计算.
*
* ## 获得 [Message]
*
* 查看 [Message] 子类. 或在 GitHub 查看 [表格](https://github.com/mamoe/mirai/blob/dev/docs/Messages.md#%E6%B6%88%E6%81%AF%E5%85%83%E7%B4%A0).
*
* ## 使用 [Message]
*
* ### 转换为 [MessageChain]
*
* [MessageChain] 为多个 [SingleMessage] 的集合. [Message] 可能表示 single 也可能表示 chain. 可以通过 [toMessageChain] 将 [Message] 转换为 [MessageChain] 统一处理.
*
* ### 连接两个或多个 [Message]
*
* 在 Kotlin, 使用操作符 [Message.plus]:
* ```
* val text = PlainText("Hello ") + PlainText("world") + "!"
* friend.sendMessage(text) // "Hello world!"
* ```
*
* 在 Java, 使用 [plus]:
* ```
* MessageChain text = new PlainText("Hello ")
* .plus(new PlainText("world"))
* .plus("!");
* friend.sendMessage(text); // "Hello world!"
* ```
*
* 注: 若需要拼接较多 [Message], 推荐使用 [MessageChainBuilder] 加快拼接效率
*
* ### 使用 [MessageChainBuilder] 来构建消息
*
* 查看 [MessageChainBuilder].
*
* ### 发送消息
*
* - [Contact.sendMessage] 接收 [Message] 参数
* - [Message.sendTo] 是发送的扩展
*
* ### 处理消息
*
* 除了直接访问 [Message] 子类的对象外, 有时候可能需要将 [Message] 作为字符串处理,
* 通常可以使用 [contentToString] 方法或 [content] 扩展得到与官方客户端显示格式相同的内容字符串.
*
* #### 文字处理示例
*
* 本示例实现处理以 `#` 开头的消息:
*
* Kotlin:
* ```
* val msg = event.message
* val content = msg.content.trim()
* if (content.startsWith("#")) {
* val name = content.substringAfter("#", "")
* when(name) {
* "mute" -> event.sender.mute(60000) // 发 #mute 就把自己禁言 1 分钟
* }
* }
* ```
*
* Java:
* ```
* MessageChain msg = event.message;
* String content = msg.contentToString();
* if (!content.equals("#") && content.startsWith("#")) {
* String name = content.substring(content.indexOf('#') + 1); // `#` 之后的内容
* switch(name) {
* case "mute": event.sender.mute(60000) // 发 #mute 就把自己禁言 1 分钟
* }
* }
* ```
*
* 若使用 Java 对象的 [toString], 会得到包含更多信息. 因此 [toString] 结果可能会随着 mirai 更新变化. [toString] 不适合用来处理消息. 只适合用来调试输出.
*
* [Message] 还提供了 [Mirai 码][MiraiCode] 和 [JSON][MessageChain.serializeToJsonString] 序列化方式. 可在 [MessageChain] 文档详细了解它们.
*
* ### 发送消息
* - 通过 [Contact] 中的成员函数: [Contact.sendMessage]
* - 通过 [Message] 的扩展函数: [Message.sendTo]
*
* @see MessageChain 消息链(即 `List<Message>`)
* @see buildMessageChain 构造一个 [MessageChain]
*
* @see Contact.sendMessage 发送消息
*
* @suppress **注意:** [Message] 类型大多有隐藏的协议实现, 不能被第三方应用继承.
*/
public interface Message {
/**
* 得到包含 mirai 消息元素代码的, 易读的字符串. 如 `At(member) + "test"` 将转为 `"[mirai:at:qqId]test"`.
*
* 在使用消息相关 DSL 和扩展时, 一些内容比较的实现均使用的是 [contentToString] 而不是 [toString].
*
* **注意: ** 即使 [toString] 输出的格式看起来像 [MiraiCode] 的表示, 但它们实际上是不同的.
* [toString] 会返回更随意的和更适合开发者阅读的信息, 通常不能被 [MiraiCode] 解析.
*
* 各个消息类型的转换示例:
* - [PlainText] : `"Hello"`
* - [Image] : `"[mirai:image:{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.mirai]"`
* - [PokeMessage] : `"[mirai:poke:1,-1]"`
* - [MessageChain] : 无间隔地连接所有元素 (`joinToString("")`)
* - ...
*
* @see contentToString 转为最接近官方格式的字符串
*/
public override fun toString(): String
/**
* 转为接近官方格式的字符串, 即 "内容". 如 `At(member) + "test"` 将转为 `"@QQ test"`.
*
* (对于 [At],应使用 [At.getDisplay] 将其转为最接近官方格式的字符串 `"@群名片"`)
*
* 在使用消息相关 DSL 和扩展时, 一些内容比较的实现均使用 [contentToString] 而不是 [toString].
*
* 由于消息元素都是不可变的, [contentToString] 的返回也是不变的.
* [MessageChain] 的 [contentToString] 会被缓存, 只会在第一次调用时计算.
*
* 各个消息类型的转换示例:
* - [PlainText] : `"Hello"`
* - [Image] : `"[图片]"`
* - [PokeMessage] : `"[戳一戳]"`
* - [MessageChain] : 无间隔地连接所有元素 (`joinToString("", transformer=Message::contentToString)`)
* - ...
*
* **注意: ** 即使 [toString] 输出的格式看起来像 [MiraiCode] 的表示, 但它们实际上是不同的.
* [toString] 会返回更随意的和更适合开发者阅读的信息, 通常不能被 [MiraiCode] 解析.
*
* @see toString 得到包含 mirai 消息元素代码的, 易读的字符串
* @see contentEquals
* @see Message.content Kotlin 扩展
*/
public fun contentToString(): String
/**
* 判断内容是否与 [another] 相等即 `this` 与 [another] 的 [contentToString] 相等.
*
* @param ignoreCase 为 `true` 时忽略大小写
* @param strict 为 `true` 时表示执行严格匹配, 即会额外判断每个消息元素的类型, 顺序和属性. 如 [Image] 会判断 [Image.imageId].
* 每个 [Image] 的[内容][contentToString]都是 `"[图片]"`,
* 进行非严格匹配时 [contentEquals] 会返回 `true`.
* 而为进行严格匹配时会额外比较 [Image.imageId], 两张不同的图片的 [contentEquals] 会返回 `false`.
*/
public fun contentEquals(another: Message, ignoreCase: Boolean = false, strict: Boolean = false): Boolean {
return if (strict) this.contentEqualsStrictImpl(another, ignoreCase)
else this.contentToString().equals(another.contentToString(), ignoreCase = ignoreCase)
}
/**
* 判断内容是否与 [another] 相等即 `this` 与 [another] 的 [contentToString] 相等.
*
* 单个消息的顺序和内容不会被检查, 即只要比较两个 [Image], 总是会得到 `true`, 因为 [Image] 的 [contentToString] 都是 `"[图片]"`.
*
*
* 相当于
* ```
* this.contentToString().equals(another.contentToString(), ignoreCase = ignoreCase)
* ```
*
* @param ignoreCase 为 `true` 时忽略大小写
*/
@kotlin.internal.LowPriorityInOverloadResolution
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
public fun contentEquals(another: Message, ignoreCase: Boolean = false): Boolean =
contentEquals(another, ignoreCase, false)
/**
* 判断内容是否与 [another] 相等.
*
*
* 相当于
* ```
* this.contentToString().equals(another, ignoreCase = ignoreCase)
* ```
*
*
* 若本函数返回 `true`, 则表明:
* - `this` 与 [another] 的 [contentToString] 相等
*/
public fun contentEquals(another: String, ignoreCase: Boolean = false): Boolean {
return this.contentToString().equals(another, ignoreCase = ignoreCase)
}
/**
* 将 `this` 和 [tail] 连接.
*
* 连接后可以保证 [ConstrainSingle] 的元素单独存在.
*
* 例:
* ```
* val a = PlainText("Hello ")
* val b = PlainText("world!")
* val c: MessageChain = a + b
* println(c) // "Hello world!"
* ```
*
* 在 Java 使用 [plus]
*
* @see plus `+` 操作符重载
*/
@JvmSynthetic // in Java they should use `plus` instead
public fun followedBy(tail: Message): MessageChain {
var constrainSingleCount = 0
if (this.hasConstrainSingle) constrainSingleCount++
if (tail.hasConstrainSingle) constrainSingleCount++
return if (constrainSingleCount == 0) {
// Future optimize:
// When constrainSingleCount == 1, see if we can connect by CombinedMessage,
// this need some kind of replacement of `hasConstrainSingle` with more information about MessageKeys.
@OptIn(MessageChainConstructor::class, MiraiInternalApi::class)
CombinedMessage(this, tail, false)
} else {
LinearMessageChainImpl.combineCreate(this, tail)
}
}
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
public operator fun plus(another: MessageChain): MessageChain = this + another as Message
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
public operator fun plus(another: Message): MessageChain = this.followedBy(another)
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
public operator fun plus(another: SingleMessage): MessageChain = this.followedBy(another)
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
public operator fun plus(another: String): MessageChain = this.followedBy(PlainText(another))
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
public operator fun plus(another: CharSequence): MessageChain =
this.followedBy(PlainText(another.toString()))
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
public operator fun plus(another: Iterable<Message>): MessageChain =
another.fold(this, Message::plus).toMessageChain()
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
public operator fun plus(another: Array<out Message>): MessageChain =
another.fold(this, Message::plus).toMessageChain()
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
@JvmName("plusIterableString")
@Suppress("INAPPLICABLE_JVM_NAME")
public operator fun plus(another: Iterable<String>): MessageChain =
another.fold(this, Message::plus).toMessageChain()
/**
* 创建一个[消息链][MessageChain], 将 [another] 连接到这个消息的尾部.
* 这不会改变本 [Message], 而是会创建新的 [MessageChain] 实例.
* 返回的 [MessageChain] 实例的第一个元素为本 [Message], 随后为按顺序的 [another] 中的元素.
*/
public operator fun plus(another: Sequence<Message>): MessageChain =
another.fold(this, Message::plus).toMessageChain()
/**
* @suppress 这是内部 API, 不要在任何情况下调用
* @since 2.12
*/
@MiraiInternalApi
public fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R = visitor.visitMessage(this, data)
/**
* @suppress 这是内部 API, 不要在任何情况下调用
* @since 2.12
*/
@MiraiInternalApi
public fun <D> acceptChildren(visitor: MessageVisitor<D, *>, data: D) {
}
public companion object // 用于"注册"扩展
}
/** 将 [another] 按顺序连接到这个消息的尾部. */
@JvmSynthetic
public suspend operator fun Message.plus(another: Flow<Message>): MessageChain =
another.fold(this) { acc, it -> acc + it }.toMessageChain()
/**
* [Message.contentToString] 的捷径
*/
@get:JvmSynthetic
public val Message.content: String
get() = contentToString()
/**
* 将 [this] 发送给指定联系人
*/
@JvmSynthetic
@Suppress("UNCHECKED_CAST")
public suspend inline fun <C : Contact> Message.sendTo(contact: C): MessageReceipt<C> =
contact.sendMessage(this) as MessageReceipt<C>
/**
* 当消息内容为空时返回 `true`.
* @see String.isEmpty
*/
public fun Message.isContentEmpty(): Boolean {
return when (this) {
is MessageChain -> this.all { it.isContentEmpty() }
else -> this.content.isEmpty()
}
}
/**
* 当消息内容为空白时返回 `true`.
* @see String.isBlank
*/
public fun Message.isContentBlank(): Boolean {
return when (this) {
is MessageChain -> this.all { it.isContentBlank() }
else -> this.content.isBlank()
}
}
/**
* 将此消息元素按顺序重复 [count] 次.
*/
public fun Message.repeat(count: Int): MessageChain {
if (this is ConstrainSingle) {
// fast-path
return this.toMessageChain()
}
return buildMessageChain(count) {
repeat(count) l@{
add(this@repeat)
}
}
}
/**
* 将此消息元素按顺序重复 [count] 次.
*/
@JvmSynthetic
public operator fun Message.times(count: Int): MessageChain = this.repeat(count)