1+ /*
2+ * Copyright 2020-2021 KonnyakuCamp.
3+ *
4+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
5+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
6+ *
7+ * https://github.com/KonnyakuCamp/SuperCourseTimetableBot/blob/main/LICENSE
8+ */
9+ @file:Suppress(" unused" , " unchecked" )
10+ package stageguard.sctimetable.utils
11+
12+ import kotlinx.coroutines.*
13+ import net.mamoe.mirai.Bot
14+ import net.mamoe.mirai.console.util.cast
15+ import net.mamoe.mirai.contact.Contact
16+ import net.mamoe.mirai.event.events.MessageEvent
17+ import net.mamoe.mirai.message.data.Message
18+ import net.mamoe.mirai.message.data.PlainText
19+ import net.mamoe.mirai.message.data.firstIsInstanceOrNull
20+ import net.mamoe.mirai.message.nextMessage
21+ import kotlin.contracts.ExperimentalContracts
22+ import kotlin.contracts.contract
23+
24+ /* *
25+ * 交互式对话对象。
26+ *
27+ * 使用 [interactiveConversation] 创建一个交互式对话
28+ *
29+ * 限定 [judge], [receive], [select] 和 [collect] 在这个对象中使用。
30+ *
31+ * @see interactiveConversation
32+ * @see judge
33+ * @see receive
34+ * @see select
35+ * @see collect
36+ */
37+ class InteractiveConversationBuilder (
38+ private val eventContext : MessageEvent ,
39+ private val coroutineScope : CoroutineScope ,
40+ private val timeoutLimitation : Long ,
41+ private val tryCountLimitation : Int
42+ ) {
43+ private val target: Contact = eventContext.subject
44+ private val bot: Bot = eventContext.bot
45+ val capturedList: MutableMap <String , Any > = mutableMapOf ()
46+
47+ /* *
48+ * 发送一条文本消息
49+ */
50+ suspend fun send (msg : String ) { if (bot.isOnline) target.sendMessage(msg) }
51+ /* *
52+ * 发送一条消息
53+ */
54+ suspend fun send (msg : Message ) { if (bot.isOnline) target.sendMessage(msg) }
55+
56+ /* *
57+ * 将任何值存入捕获列表。
58+ *
59+ * 使用:
60+ * ```
61+ * interactiveConversation {
62+ * val foo: Foo = xxx
63+ * val bar: Bar = xxx
64+ * collect("foo", foo)
65+ * collect("bar", bar)
66+ * }.finish {
67+ * val foo = it["foo"].cast<Foo>()
68+ * val bar: Bar = it["bar"].cast()
69+ * }
70+ * ```
71+ * @param key 键名称
72+ */
73+ fun <T : Any > collect (key : String , any : T ) {
74+ capturedList[key] = any
75+ }
76+
77+ /* *
78+ * 同 [receive] 阻塞对话过程来监听下一条消息,但不存入捕获列表。
79+ *
80+ * 通常用于判断消息。
81+ *
82+ * @return 返回监听到的消息。
83+ * @see receive
84+ */
85+ suspend fun judge (
86+ tryLimit : Int = tryCountLimitation,
87+ timeoutLimit : Long = timeoutLimitation,
88+ checkBlock : ((String ) -> Boolean )? = null
89+ ) : String = receive(tryLimit, timeoutLimit, null , checkBlock)
90+
91+ /* *
92+ * 阻塞对话过程来监听下一条消息,并存储到捕获列表中。
93+ *
94+ * [checkBlock] 不为 `null`时会对消息进行判断,判断为 `false` 或出现错误时会反复询问,直到符合要求。
95+ *
96+ * 使用:
97+ * ```
98+ * interactiveConversation {
99+ * //不进行判断,直接存储
100+ * receive(key = "foo")
101+ * //正则匹配
102+ * receive(key = "bar") {
103+ * Pattern.compile("...").matcher(it).find()
104+ * }
105+ * ///转换为 Int 进行判断。
106+ * receive(key = "baz") { it.toInt() > 0 }
107+ * }
108+ * ```
109+ *
110+ * @param tryLimit 设置尝试最大次数。会覆盖在 [interactiveConversation] 中设置的 `eachTryLimit`。
111+ * @param timeoutLimit 设置每次监听的最大时长限制,会覆盖在 [interactiveConversation] 中设置的 `eachTimeLimit`。
112+ * @param key 键名称
113+ *
114+ * @return 返回监听到的消息。
115+ *
116+ * @see [judge]
117+ * @see [select]
118+ */
119+ suspend fun receive (
120+ tryLimit : Int = tryCountLimitation,
121+ timeoutLimit : Long = timeoutLimitation,
122+ key : String? = null,
123+ checkBlock : ((String ) -> Boolean )? = null,
124+ ): String {
125+ if (checkBlock == null ) {
126+ (eventContext.nextMessage(timeoutLimit).firstIsInstanceOrNull<PlainText >()?.content ? : " " ).also {
127+ if (key != null ) capturedList[key] = it
128+ return it
129+ }
130+ } else {
131+ if (tryLimit == - 1 ) {
132+ while (true ) {
133+ val plainText = eventContext.nextMessage(timeoutLimit).firstIsInstanceOrNull<PlainText >()?.content ? : " "
134+ kotlin.runCatching {
135+ if (checkBlock(plainText)) {
136+ if (key != null ) capturedList[key] = plainText
137+ return plainText
138+ }
139+ }
140+ send(" 输入不符合要求,请重新输入!" )
141+ }
142+ } else {
143+ repeat(tryLimit) {
144+ val plainText = eventContext.nextMessage(timeoutLimit).firstIsInstanceOrNull<PlainText >()?.content ? : " "
145+ kotlin.runCatching {
146+ if (checkBlock(plainText)) {
147+ if (key != null ) capturedList[key] = plainText
148+ return plainText
149+ }
150+ }
151+ if (it != tryLimit - 1 ) send(" 输入不符合要求,请重新输入!" )
152+ }
153+ // 会话直接结束
154+ throw QuitConversationExceptions .IllegalInputException ()
155+ }
156+ }
157+ }
158+
159+ /* *
160+ * 阻塞对话过程来监听下一条消息,并进行类似 `when` 一样的选择判断
161+ *
162+ * 可以经过初步筛选后再判断,筛选实际上是委托给 [receive] 进行的。
163+ *
164+ * 使用:
165+ * ```
166+ * interactiveConversation {
167+ * //无筛选条件监听
168+ * select {
169+ * //当消息为 foo 时执行
170+ * "foo" { }
171+ * //当消息为 bar 时执行
172+ * "bar" { }
173+ * //当上面任何都不匹配时执行
174+ * default { }
175+ * }
176+ * //初步筛选后监听
177+ * select({ it == "foo" || it == "bar" }) {
178+ * //当消息为 foo 时执行
179+ * "foo" { }
180+ * //当消息为 bar 时执行
181+ * "bar" { }
182+ * //不必需要 default,因为经过初步筛选只有上述两种情况
183+ * }
184+ * }
185+ * ```
186+ * @param key 当不为 `null` 时将监听到的消息存储进捕获列表。
187+ * @see receive
188+ */
189+ suspend fun select (
190+ checkBlock : ((String ) -> Boolean )? = null,
191+ tryLimit : Int = tryCountLimitation,
192+ timeoutLimit : Long = timeoutLimitation,
193+ key : String? = null,
194+ runBlock : suspend SelectionLambdaExpression .(String ) -> Unit
195+ ) = SelectionLambdaExpression (receive(tryLimit, timeoutLimit, key, checkBlock))(runBlock)
196+
197+ class SelectionLambdaExpression (private val content : String ) {
198+ private var matches = false
199+
200+ /* *
201+ * 在 [select] 中判断消息为这个字符串时执行
202+ */
203+ suspend operator fun String.invoke (caseLambda : suspend () -> Unit ) {
204+ if (content == this ) {
205+ matches = true
206+ caseLambda()
207+ }
208+ }
209+
210+ /* *
211+ * 当任何字符都不匹配时执行。
212+ *
213+ * 只有放到最底部才有效果。
214+ */
215+ suspend fun default (caseLambda : suspend () -> Unit ) {
216+ if (! matches) caseLambda()
217+ }
218+ // For execute
219+ suspend operator fun invoke (runBlock : suspend SelectionLambdaExpression .(String ) -> Unit ) = runBlock(content)
220+ }
221+
222+ /* *
223+ * 提前结束会话。
224+ * 并在 [exception] 中抛出 [QuitConversationExceptions.AdvancedQuitException]
225+ */
226+ fun finish () { throw QuitConversationExceptions .AdvancedQuitException () }
227+ }
228+
229+ sealed class QuitConversationExceptions : Exception () {
230+ /* *
231+ * 提前结束会话时抛出
232+ * @see InteractiveConversationBuilder.finish
233+ */
234+ class AdvancedQuitException : QuitConversationExceptions ()
235+
236+ /* *
237+ * 超过了尝试次数后未捕获到合适的消息时抛出
238+ */
239+ class IllegalInputException : QuitConversationExceptions ()
240+
241+ /* *
242+ * 超过了时间未捕获到适合的消息时抛出
243+ */
244+ class TimeoutException : QuitConversationExceptions ()
245+ }
246+
247+ /* *
248+ * 交互式对话的创建器
249+ *
250+ * 适用于任何继承 [MessageEvent] 的消息事件
251+ *
252+ * 使用:
253+ * ```
254+ * interactiveConversation { } //创建
255+ * .finish { } //正常结束
256+ * .exception { } //异常结束
257+ *
258+ * ```
259+ *
260+ * @param scope 协程作用域,默认为全局作用域 [GlobalScope]。
261+ * 建议制定为*插件作用域*或自定义协程作用域。
262+ * @param eachTimeLimit 每一次捕获消息的最大等待时间,默认为 `-1L` 即无限等待。
263+ * 若超过了这个时间未捕获到适合的消息,则在 [exception] 中抛出 [QuitConversationExceptions.IllegalInputException]。
264+ * @param eachTryLimit 每一次捕获消息时允许尝试的最大次数。
265+ * 若超过了这个次数未捕获到适合的消息,则在 [exception] 中抛出 [QuitConversationExceptions.TimeoutException]。
266+ * @see InteractiveConversationBuilder.receive
267+ * @see InteractiveConversationBuilder.judge
268+ * @see InteractiveConversationBuilder.select
269+ * @see InteractiveConversationBuilder.collect
270+ * @see InteractiveConversationBuilder.finish
271+ * @see finish
272+ * @see exception
273+ */
274+ inline fun <T : MessageEvent > T.interactiveConversation (
275+ scope : CoroutineScope = GlobalScope ,
276+ eachTimeLimit : Long = -1L,
277+ eachTryLimit : Int = -1,
278+ crossinline block : suspend InteractiveConversationBuilder .() -> Unit
279+ ): Pair <CoroutineScope , Deferred <Either <InteractiveConversationBuilder , QuitConversationExceptions >>> = scope to scope.async (
280+ scope.coroutineContext
281+ ) {
282+ try {
283+ InteractiveConversationBuilder (
284+ eventContext = this @interactiveConversation,
285+ coroutineScope = scope,
286+ tryCountLimitation = eachTryLimit,
287+ timeoutLimitation = eachTimeLimit
288+ ).also { block(it) }.let { Either .Left (it) }
289+ } catch (ex: Exception ) {
290+ when (ex) {
291+ is TimeoutCancellationException -> Either .Right (QuitConversationExceptions .TimeoutException ())
292+ else -> Either .Right (ex as QuitConversationExceptions )
293+ }
294+ }
295+ }
296+
297+ /* *
298+ * 会话非正常退出时调用这个函数
299+ * 处理方式:
300+ * ```
301+ * .exception { when(it) {
302+ * is QuitConversationExceptions.TimeoutException -> {} //超时结束
303+ * is QuitConversationExceptions.AdvancedQuitException -> {} //提前结束会话
304+ * is QuitConversationExceptions.IllegalInputException -> {} //输入有误次数过多
305+ * } }
306+ * ```
307+ * 不必为 `when` 添加 `else` 分支,因为 [QuitConversationExceptions] 是一个 `sealed class`
308+ */
309+ fun Pair <CoroutineScope , Deferred <Either <InteractiveConversationBuilder , QuitConversationExceptions >>>.exception (
310+ failed : suspend (QuitConversationExceptions ) -> Unit = { }
311+ ) : Pair <CoroutineScope , Deferred <Either <InteractiveConversationBuilder , QuitConversationExceptions >>> {
312+ first.launch(first.coroutineContext) {
313+ when (val icBuilder = this @exception.second.await()) { is Either .Right -> failed(icBuilder.value) }
314+ }
315+ return this
316+ }
317+
318+ /* *
319+ * 会话正常退出时调用这个函数。
320+ *
321+ * 在 [interactiveConversation] 中通过 [InteractiveConversationBuilder.receive], [InteractiveConversationBuilder.collect] 和 [InteractiveConversationBuilder.select] (指定 `key`) 的接收最终都会在这里捕获。
322+ *
323+ * 捕获结果是一个 [Map],`Key` 为 `String` 类型, `Value` 为 `Any` 类型。
324+ *
325+ * 使用方式:
326+ * ```
327+ * .finish {
328+ * //通过 `checkBlock` 筛选你希望捕获一个 Number。
329+ * val number = it["key1"].cast<String>().toInt()
330+ * //通过 `checkBlock` 筛选你希望捕获一个 String
331+ * val str: String = it.cast()
332+ * }
333+ * ```
334+ * 使用 [cast] 做类型转换,即把 `Any` 类型转换为你需要的类型。
335+ *
336+ * 除了使用 [InteractiveConversationBuilder.collect] 捕获的,其他捕获存储于 `Map` 中的类型实际都为 `String` (因为消息都为文本消息)。
337+ *
338+ * 所以如果想捕获一个 `Number`,你可以这样:
339+ * ```
340+ * interactiveConversation {
341+ * receive(key = "number1") { it.toInt() }
342+ * collect(key = "number2", judge { it.toInt() }.toInt())
343+ * }.finish {
344+ * val number1 = it["number1"].cast<String>().toInt()
345+ * val number2 = it["number2"].cast<Int>()
346+ * }
347+ * ```
348+ * 注意:[cast] 会忽略 `Map` 中的 `null` 检查!
349+ *
350+ */
351+ fun Pair <CoroutineScope , Deferred <Either <InteractiveConversationBuilder , QuitConversationExceptions >>>.finish (
352+ success : suspend (Map <String , Any >) -> Unit = { }
353+ ) : Pair <CoroutineScope , Deferred <Either <InteractiveConversationBuilder , QuitConversationExceptions >>> {
354+ first.launch(first.coroutineContext) {
355+ when (val icBuilder = this @finish.second.await()) { is Either .Left -> success(icBuilder.value.capturedList.toMap()) }
356+ }
357+ return this
358+ }
359+
360+ /* *
361+ * Perform this as T.
362+ *
363+ * `a.cast<B>()` 相当于 `a as B`
364+ * @see cast
365+ */
366+ @ExperimentalContracts
367+ inline fun <reified T : Any > Any?.cast (): T {
368+ contract { returns() implies (this @cast is T ) }
369+ return this as T
370+ }
0 commit comments