From 2cfd8be0f3c440a07476205f5e855503ad1a6a0e Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 1 Mar 2020 18:09:16 +0800 Subject: [PATCH] Improve Java-friendly APIs, add Java `Future` apis --- .../androidMain/kotlin/net/mamoe/mirai/Bot.kt | 236 +++++++++++++++++ .../kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt | 116 ++++++++- .../commonMain/kotlin/net.mamoe.mirai/Bot.kt | 54 +--- .../kotlin/net.mamoe.mirai/javaHappy.kt | 25 +- .../src/jvmMain/kotlin/net/mamoe/mirai/Bot.kt | 246 ++++++++++++++++++ .../kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt | 116 ++++++++- 6 files changed, 712 insertions(+), 81 deletions(-) create mode 100644 mirai-core/src/androidMain/kotlin/net/mamoe/mirai/Bot.kt create mode 100644 mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Bot.kt diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/Bot.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/Bot.kt new file mode 100644 index 000000000..95a66b35c --- /dev/null +++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/Bot.kt @@ -0,0 +1,236 @@ +@file:Suppress("unused") + +package net.mamoe.mirai + +import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import net.mamoe.mirai.contact.* +import net.mamoe.mirai.data.AddFriendResult +import net.mamoe.mirai.data.FriendInfo +import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageSource +import net.mamoe.mirai.network.BotNetworkHandler +import net.mamoe.mirai.network.LoginFailedException +import net.mamoe.mirai.utils.* + +/** + * 机器人对象. 一个机器人实例登录一个 QQ 账号. + * Mirai 为多账号设计, 可同时维护多个机器人. + * + * 注: Bot 为全协程实现, 没有其他任务时若不使用 [join], 主线程将会退出. + * + * @see Contact 联系人 + * @see kotlinx.coroutines.isActive 判断 [Bot] 是否正常运行中. (在线, 且没有被 [close]) + */ +@Suppress("INAPPLICABLE_JVM_NAME") +@UseExperimental(MiraiInternalAPI::class, LowLevelAPI::class, MiraiExperimentalAPI::class, JavaHappyAPI::class) +actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIAccessor, BotJavaHappyAPI() { + actual companion object { + /** + * 复制一份此时的 [Bot] 实例列表. + */ + @JvmStatic + actual val instances: List> + get() = BotImpl.instances.toList() + + /** + * 遍历每一个 [Bot] 实例 + */ + actual inline fun forEachInstance(block: (Bot) -> Unit) = BotImpl.forEachInstance(block) + + /** + * 获取一个 [Bot] 实例, 找不到则 [NoSuchElementException] + */ + @JvmStatic + actual fun getInstance(qq: Long): Bot = BotImpl.getInstance(qq = qq) + } + + /** + * [Bot] 运行的 [Context]. + * + * 在 JVM 的默认实现为 `class ContextImpl : Context` + * 在 Android 实现为 `android.content.Context` + */ + actual abstract val context: Context + + /** + * 账号信息 + */ + @MiraiInternalAPI + actual abstract val account: BotAccount + + /** + * QQ 号码. 实际类型为 uint + */ + actual abstract val uin: Long + + /** + * 昵称 + */ + @MiraiExperimentalAPI("还未支持") + actual val nick: String + get() = ""// TODO("bot 昵称获取") + + /** + * 日志记录器 + */ + actual abstract val logger: MiraiLogger + + // region contacts + + actual abstract val selfQQ: QQ + + /** + * 机器人的好友列表. 它将与服务器同步更新 + */ + actual abstract val qqs: ContactList + + /** + * 获取一个好友或一个群. + * 在一些情况下这可能会造成歧义. 请考虑后使用. + */ + actual operator fun get(id: Long): Contact { + return this.qqs.getOrNull(id) ?: this.groups.getOrNull(id) ?: throw NoSuchElementException("contact id $id") + } + + /** + * 判断是否有这个 id 的好友或群. + * 在一些情况下这可能会造成歧义. 请考虑后使用. + */ + actual operator fun contains(id: Long): Boolean { + return this.qqs.contains(id) || this.groups.contains(id) + } + + /** + * 获取一个好友对象. 若没有这个好友, 则会抛出异常 [NoSuchElementException] + */ + actual fun getFriend(id: Long): QQ { + if (id == uin) return selfQQ + return qqs.delegate.getOrNull(id) + ?: throw NoSuchElementException("No such friend $id for bot ${this.uin}") + } + + /** + * 构造一个 [QQ] 对象. 它持有对 [Bot] 的弱引用([WeakRef]). + * + * [Bot] 无法管理这个对象, 但这个对象会以 [Bot] 的 [Job] 作为父 Job. + * 因此, 当 [Bot] 被关闭后, 这个对象也会被关闭. + */ + @Suppress("FunctionName") + actual abstract fun QQ(friendInfo: FriendInfo): QQ + + /** + * 机器人加入的群列表. + */ + actual abstract val groups: ContactList + + /** + * 获取一个机器人加入的群. + * + * @throws NoSuchElementException 当不存在这个群时 + */ + actual fun getGroup(id: Long): Group { + return groups.delegate.getOrNull(id) + ?: throw NoSuchElementException("No such group $id for bot ${this.uin}") + } + + // endregion + + // region network + + /** + * 网络模块 + */ + actual abstract val network: BotNetworkHandler + + /** + * 挂起直到 [Bot] 下线. + */ + @JvmName("joinSuspend") + @JvmSynthetic + actual suspend inline fun join() = network.join() + + /** + * 登录, 或重新登录. + * 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表. + * + * 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况. + * + * 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin] + * + * @throws LoginFailedException + */ + @JvmName("loginSuspend") + @JvmSynthetic + actual abstract suspend fun login() + // endregion + + + // region actions + + /** + * 撤回这条消息. 可撤回自己 2 分钟内发出的消息, 和任意时间的群成员的消息. + * + * [Bot] 撤回自己的消息不需要权限. + * [Bot] 撤回群员的消息需要管理员权限. + * + * @param source 消息源. 可从 [MessageReceipt.source] 获得, 或从消息事件中的 [MessageChain] 获得. + * + * @throws PermissionDeniedException 当 [Bot] 无权限操作时 + * + * @see Bot.recall (扩展函数) 接受参数 [MessageChain] + * @see _lowLevelRecallFriendMessage 低级 API + * @see _lowLevelRecallGroupMessage 低级 API + */ + @JvmName("recallSuspend") + @JvmSynthetic + actual abstract suspend fun recall(source: MessageSource) + + /** + * 获取图片下载链接 + */ + @JvmName("queryImageUrlSuspend") + @JvmSynthetic + actual abstract suspend fun queryImageUrl(image: Image): String + + /** + * 获取图片下载链接并开始下载. + * + * @see ByteReadChannel.copyAndClose + * @see ByteReadChannel.copyTo + */ + @JvmName("openChannelSuspend") + @JvmSynthetic + actual abstract suspend fun openChannel(image: Image): ByteReadChannel + + /** + * 添加一个好友 + * + * @param message 若需要验证请求时的验证消息. + * @param remark 好友备注 + */ + @JvmName("addFriendSuspend") + @JvmSynthetic + @MiraiExperimentalAPI("未支持") + actual abstract suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult + + // endregion + + /** + * 关闭这个 [Bot], 立即取消 [Bot] 的 [kotlinx.coroutines.SupervisorJob]. + * 之后 [kotlinx.coroutines.isActive] 将会返回 `false`. + * + * **注意:** 不可重新登录. 必须重新实例化一个 [Bot]. + * + * @param cause 原因. 为 null 时视为正常关闭, 非 null 时视为异常关闭 + * + * @see closeAndJoin 取消并 [Bot.join], 以确保 [Bot] 相关的活动被完全关闭 + */ + actual abstract fun close(cause: Throwable?) + + @UseExperimental(LowLevelAPI::class, MiraiExperimentalAPI::class) + actual final override fun toString(): String = "Bot(${uin})" +} \ No newline at end of file diff --git a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt index 3e7ced322..3040b434a 100644 --- a/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt +++ b/mirai-core/src/androidMain/kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt @@ -1,10 +1,15 @@ package net.mamoe.mirai +import kotlinx.coroutines.* import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiInternalAPI +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException /** * [Bot] 中为了让 Java 使用者调用更方便的 API 列表. @@ -14,42 +19,135 @@ import net.mamoe.mirai.utils.MiraiInternalAPI actual abstract class BotJavaHappyAPI actual constructor() { init { @Suppress("LeakingThis") - check(this is Bot) + assert(this is Bot) } private inline fun runBlocking(crossinline block: suspend Bot.() -> R): R { return kotlinx.coroutines.runBlocking { block(this@BotJavaHappyAPI as Bot) } } + private inline fun future(crossinline block: suspend Bot.() -> R): Future { + return (this as Bot).run { future(block) } + } + @JvmName("login") - actual open fun __loginBlockingForJava__() { + fun __loginBlockingForJava__() { runBlocking { login() } } @JvmName("recall") - actual open fun __recallBlockingForJava__(source: MessageSource) { + fun __recallBlockingForJava__(source: MessageSource) { runBlocking { recall(source) } } + @JvmName("recall") + fun __recallBlockingForJava__(source: MessageChain) { + runBlocking { recall(source) } + } + + @JvmName("recallIn") + fun __recallIn_MemberForJava__(source: MessageSource, millis: Long) { + runBlocking { recallIn(source, millis) } + } + + @JvmName("recallIn") + fun __recallIn_MemberForJava__(source: MessageChain, millis: Long) { + runBlocking { recallIn(source, millis) } + } + @JvmName("queryImageUrl") - actual open fun __queryImageUrlBlockingForJava__(image: Image): String { + fun __queryImageUrlBlockingForJava__(image: Image): String { return runBlocking { queryImageUrl(image) } } @JvmName("join") - actual open fun __joinBlockingForJava__() { + fun __joinBlockingForJava__() { runBlocking { join() } } - @UseExperimental(MiraiExperimentalAPI::class) @JvmOverloads @JvmName("addFriend") - actual open fun __addFriendBlockingForJava__( + fun __addFriendBlockingForJava__( id: Long, - message: String?, - remark: String? + message: String? = null, + remark: String? = null ): AddFriendResult { + @UseExperimental(MiraiExperimentalAPI::class) return runBlocking { addFriend(id, message, remark) } } + + @JvmName("loginAsync") + fun __loginAsyncForJava__(): Future { + return future { login() } + } + + @JvmName("recallAsync") + fun __recallAsyncForJava__(source: MessageSource): Future { + return future { recall(source) } + } + + @JvmName("recallAsync") + fun __recallAsyncForJava__(source: MessageChain): Future { + return future { recall(source) } + } + + @JvmName("queryImageUrlAsync") + fun __queryImageUrlAsyncForJava__(image: Image): Future { + return future { queryImageUrl(image) } + } +} + +private inline fun Bot.future(crossinline block: suspend Bot.() -> R): Future { + return object : Future { + val value: CompletableDeferred = CompletableDeferred() + + init { + launch { + @UseExperimental(ExperimentalCoroutinesApi::class) + value.completeWith(kotlin.runCatching { block() }) + } + } + + override fun isDone(): Boolean { + return value.isCompleted + } + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + override fun get(): R { + if (value.isCompleted) { + @UseExperimental(ExperimentalCoroutinesApi::class) + return value.getCompleted() + } + return runBlocking { value.await() } + } + + override fun get(timeout: Long, unit: TimeUnit): R { + if (value.isCompleted) { + @UseExperimental(ExperimentalCoroutinesApi::class) + return value.getCompleted() + } + return runBlocking { + withTimeoutOrNull(TimeUnit.MILLISECONDS.convert(timeout, unit)) { value.await() } + ?: throw TimeoutException() + } + } + + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + if (value.isCompleted || value.isCancelled) { + return false + } + + return if (mayInterruptIfRunning && value.isActive) { + value.cancel() + true + } else { + false + } + } + + override fun isCancelled(): Boolean { + return value.isCancelled + } + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt index 91ee3edd5..391bfc6b0 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt @@ -29,6 +29,7 @@ import net.mamoe.mirai.utils.* import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.jvm.JvmName +import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic import kotlin.jvm.JvmSynthetic @@ -43,25 +44,24 @@ import kotlin.jvm.JvmSynthetic */ @Suppress("INAPPLICABLE_JVM_NAME") @UseExperimental(MiraiInternalAPI::class, LowLevelAPI::class) -abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaHappyAPI() { +expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor { companion object { /** * 复制一份此时的 [Bot] 实例列表. */ @JvmStatic val instances: List> - get() = BotImpl.instances.toList() /** * 遍历每一个 [Bot] 实例 */ - inline fun forEachInstance(block: (Bot) -> Unit) = BotImpl.forEachInstance(block) + inline fun forEachInstance(block: (Bot) -> Unit) /** * 获取一个 [Bot] 实例, 找不到则 [NoSuchElementException] */ @JvmStatic - fun getInstance(qq: Long): Bot = BotImpl.getInstance(qq = qq) + fun getInstance(qq: Long): Bot } /** @@ -88,7 +88,6 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaHappyAPI() { */ @MiraiExperimentalAPI("还未支持") val nick: String - get() = ""// TODO("bot 昵称获取") /** * 日志记录器 @@ -108,26 +107,18 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaHappyAPI() { * 获取一个好友或一个群. * 在一些情况下这可能会造成歧义. 请考虑后使用. */ - operator fun get(id: Long): Contact { - return this.qqs.getOrNull(id) ?: this.groups.getOrNull(id) ?: throw NoSuchElementException("contact id $id") - } + operator fun get(id: Long): Contact /** * 判断是否有这个 id 的好友或群. * 在一些情况下这可能会造成歧义. 请考虑后使用. */ - operator fun contains(id: Long): Boolean { - return this.qqs.contains(id) || this.groups.contains(id) - } + operator fun contains(id: Long): Boolean /** * 获取一个好友对象. 若没有这个好友, 则会抛出异常 [NoSuchElementException] */ - fun getFriend(id: Long): QQ { - if (id == uin) return selfQQ - return qqs.delegate.getOrNull(id) - ?: throw NoSuchElementException("No such friend $id for bot ${this.uin}") - } + fun getFriend(id: Long): QQ /** * 构造一个 [QQ] 对象. 它持有对 [Bot] 的弱引用([WeakRef]). @@ -147,10 +138,7 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaHappyAPI() { * * @throws NoSuchElementException 当不存在这个群时 */ - fun getGroup(id: Long): Group { - return groups.delegate.getOrNull(id) - ?: throw NoSuchElementException("No such group $id for bot ${this.uin}") - } + fun getGroup(id: Long): Group // endregion @@ -166,7 +154,7 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaHappyAPI() { */ @JvmName("joinSuspend") @JvmSynthetic - suspend inline fun join() = network.join() + suspend inline fun join() /** * 登录, 或重新登录. @@ -244,31 +232,11 @@ abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaHappyAPI() { * * @see closeAndJoin 取消并 [Bot.join], 以确保 [Bot] 相关的活动被完全关闭 */ + @JvmOverloads abstract fun close(cause: Throwable? = null) @UseExperimental(LowLevelAPI::class, MiraiExperimentalAPI::class) - final override fun toString(): String = "Bot(${uin})" - - @JvmName("login") - @JavaHappyAPI - override fun __loginBlockingForJava__() = super.__loginBlockingForJava__() - - @JvmName("recall") - @JavaHappyAPI - override fun __recallBlockingForJava__(source: MessageSource) = super.__recallBlockingForJava__(source) - - @JvmName("queryImageUrl") - @JavaHappyAPI - override fun __queryImageUrlBlockingForJava__(image: Image): String = super.__queryImageUrlBlockingForJava__(image) - - @JvmName("addFriend") - @JavaHappyAPI - override fun __addFriendBlockingForJava__(id: Long, message: String?, remark: String?) = - super.__addFriendBlockingForJava__(id, message, remark) - - @JvmName("join") - @JavaHappyAPI - override fun __joinBlockingForJava__() = super.__joinBlockingForJava__() + final override fun toString(): String } /** diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/javaHappy.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/javaHappy.kt index 627a2a1a7..544af6fc8 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/javaHappy.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/javaHappy.kt @@ -9,12 +9,7 @@ package net.mamoe.mirai -import net.mamoe.mirai.data.AddFriendResult -import net.mamoe.mirai.message.data.Image -import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.utils.MiraiInternalAPI -import kotlin.jvm.JvmName -import kotlin.jvm.JvmOverloads /** * 表明这个 API 是为了让 Java 使用者调用更方便. @@ -26,23 +21,13 @@ annotation class JavaHappyAPI /** * [Bot] 中为了让 Java 使用者调用更方便的 API 列表. - */ // TODO: 2020/3/1 待 https://youtrack.jetbrains.com/issue/KT-36740 修复后添加 Future 相关 API. + */ @MiraiInternalAPI @Suppress("FunctionName", "INAPPLICABLE_JVM_NAME", "unused") expect abstract class BotJavaHappyAPI() { // 不要使用 interface, 会无法添加默认实现 - @JvmName("join") - open fun __joinBlockingForJava__() +} - @JvmName("login") - open fun __loginBlockingForJava__() +// 保留多平台结构, 以避免在 Android 和 JVM 都定义这个类 ---- 这会造成代码重复. +// 待 https://youtrack.jetbrains.com/issue/KT-27801 实现后修改为 hierarchical MPP 架构 - @JvmName("recall") - open fun __recallBlockingForJava__(source: MessageSource) - - @JvmName("queryImageUrl") - open fun __queryImageUrlBlockingForJava__(image: Image): String - - @JvmName("addFriend") - @JvmOverloads - open fun __addFriendBlockingForJava__(id: Long, message: String? = null, remark: String? = null): AddFriendResult -} \ No newline at end of file +// 待 https://youtrack.jetbrains.com/issue/KT-36740 修复后添加 Future 相关 API 到 hierarchical MPP 架构中 \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Bot.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Bot.kt new file mode 100644 index 000000000..38e2f22c2 --- /dev/null +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Bot.kt @@ -0,0 +1,246 @@ +@file:Suppress("unused") + +package net.mamoe.mirai + +import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import net.mamoe.mirai.contact.* +import net.mamoe.mirai.data.AddFriendResult +import net.mamoe.mirai.data.FriendInfo +import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.MessageChain +import net.mamoe.mirai.message.data.MessageSource +import net.mamoe.mirai.network.BotNetworkHandler +import net.mamoe.mirai.network.LoginFailedException +import net.mamoe.mirai.utils.* + +/** + * 机器人对象. 一个机器人实例登录一个 QQ 账号. + * Mirai 为多账号设计, 可同时维护多个机器人. + * + * 注: Bot 为全协程实现, 没有其他任务时若不使用 [join], 主线程将会退出. + * + * @see Contact 联系人 + * @see kotlinx.coroutines.isActive 判断 [Bot] 是否正常运行中. (在线, 且没有被 [close]) + */ +@Suppress("INAPPLICABLE_JVM_NAME") +@UseExperimental(MiraiInternalAPI::class, LowLevelAPI::class, MiraiExperimentalAPI::class, JavaHappyAPI::class) +actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIAccessor, BotJavaHappyAPI() { + actual companion object { + /** + * 复制一份此时的 [Bot] 实例列表. + */ + @JvmStatic + actual val instances: List> + get() = BotImpl.instances.toList() + + /** + * 遍历每一个 [Bot] 实例 + */ + @JvmName("forEachInstanceKotlin") + @JvmSynthetic + actual inline fun forEachInstance(block: (Bot) -> Unit) = BotImpl.forEachInstance(block) + + /** + * 遍历每一个 [Bot] 实例 + */ + @JavaHappyAPI + @JvmName("forEachInstance") + @Suppress("FunctionName") + fun __forEachInstanceForJava__(block: (Bot) -> Unit) = forEachInstance(block) + + /** + * 获取一个 [Bot] 实例, 找不到则 [NoSuchElementException] + */ + @JvmStatic + actual fun getInstance(qq: Long): Bot = BotImpl.getInstance(qq = qq) + } + + /** + * [Bot] 运行的 [Context]. + * + * 在 JVM 的默认实现为 `class ContextImpl : Context` + * 在 Android 实现为 `android.content.Context` + */ + actual abstract val context: Context + + /** + * 账号信息 + */ + @MiraiInternalAPI + actual abstract val account: BotAccount + + /** + * QQ 号码. 实际类型为 uint + */ + actual abstract val uin: Long + + /** + * 昵称 + */ + @MiraiExperimentalAPI("还未支持") + actual val nick: String + get() = ""// TODO("bot 昵称获取") + + /** + * 日志记录器 + */ + actual abstract val logger: MiraiLogger + + // region contacts + + actual abstract val selfQQ: QQ + + /** + * 机器人的好友列表. 它将与服务器同步更新 + */ + actual abstract val qqs: ContactList + + /** + * 获取一个好友或一个群. + * 在一些情况下这可能会造成歧义. 请考虑后使用. + */ + actual operator fun get(id: Long): Contact { + return this.qqs.getOrNull(id) ?: this.groups.getOrNull(id) ?: throw NoSuchElementException("contact id $id") + } + + /** + * 判断是否有这个 id 的好友或群. + * 在一些情况下这可能会造成歧义. 请考虑后使用. + */ + actual operator fun contains(id: Long): Boolean { + return this.qqs.contains(id) || this.groups.contains(id) + } + + /** + * 获取一个好友对象. 若没有这个好友, 则会抛出异常 [NoSuchElementException] + */ + actual fun getFriend(id: Long): QQ { + if (id == uin) return selfQQ + return qqs.delegate.getOrNull(id) + ?: throw NoSuchElementException("No such friend $id for bot ${this.uin}") + } + + /** + * 构造一个 [QQ] 对象. 它持有对 [Bot] 的弱引用([WeakRef]). + * + * [Bot] 无法管理这个对象, 但这个对象会以 [Bot] 的 [Job] 作为父 Job. + * 因此, 当 [Bot] 被关闭后, 这个对象也会被关闭. + */ + @Suppress("FunctionName") + actual abstract fun QQ(friendInfo: FriendInfo): QQ + + /** + * 机器人加入的群列表. + */ + actual abstract val groups: ContactList + + /** + * 获取一个机器人加入的群. + * + * @throws NoSuchElementException 当不存在这个群时 + */ + actual fun getGroup(id: Long): Group { + return groups.delegate.getOrNull(id) + ?: throw NoSuchElementException("No such group $id for bot ${this.uin}") + } + + // endregion + + // region network + + /** + * 网络模块 + */ + actual abstract val network: BotNetworkHandler + + /** + * 挂起直到 [Bot] 下线. + */ + @JvmName("joinSuspend") + @JvmSynthetic + actual suspend inline fun join() = network.join() + + /** + * 登录, 或重新登录. + * 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表. + * + * 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况. + * + * 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin] + * + * @throws LoginFailedException + */ + @JvmName("loginSuspend") + @JvmSynthetic + actual abstract suspend fun login() + // endregion + + + // region actions + + /** + * 撤回这条消息. 可撤回自己 2 分钟内发出的消息, 和任意时间的群成员的消息. + * + * [Bot] 撤回自己的消息不需要权限. + * [Bot] 撤回群员的消息需要管理员权限. + * + * @param source 消息源. 可从 [MessageReceipt.source] 获得, 或从消息事件中的 [MessageChain] 获得. + * + * @throws PermissionDeniedException 当 [Bot] 无权限操作时 + * + * @see Bot.recall (扩展函数) 接受参数 [MessageChain] + * @see _lowLevelRecallFriendMessage 低级 API + * @see _lowLevelRecallGroupMessage 低级 API + */ + @JvmName("recallSuspend") + @JvmSynthetic + actual abstract suspend fun recall(source: MessageSource) + + /** + * 获取图片下载链接 + */ + @JvmName("queryImageUrlSuspend") + @JvmSynthetic + actual abstract suspend fun queryImageUrl(image: Image): String + + /** + * 获取图片下载链接并开始下载. + * + * @see ByteReadChannel.copyAndClose + * @see ByteReadChannel.copyTo + */ + @JvmName("openChannelSuspend") + @JvmSynthetic + actual abstract suspend fun openChannel(image: Image): ByteReadChannel + + /** + * 添加一个好友 + * + * @param message 若需要验证请求时的验证消息. + * @param remark 好友备注 + */ + @JvmName("addFriendSuspend") + @JvmSynthetic + @MiraiExperimentalAPI("未支持") + actual abstract suspend fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult + + // endregion + + /** + * 关闭这个 [Bot], 立即取消 [Bot] 的 [kotlinx.coroutines.SupervisorJob]. + * 之后 [kotlinx.coroutines.isActive] 将会返回 `false`. + * + * **注意:** 不可重新登录. 必须重新实例化一个 [Bot]. + * + * @param cause 原因. 为 null 时视为正常关闭, 非 null 时视为异常关闭 + * + * @see closeAndJoin 取消并 [Bot.join], 以确保 [Bot] 相关的活动被完全关闭 + */ + actual abstract fun close(cause: Throwable?) + + @UseExperimental(LowLevelAPI::class, MiraiExperimentalAPI::class) + actual final override fun toString(): String = "Bot(${uin})" +} \ No newline at end of file diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt index 3e7ced322..3040b434a 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotJavaHappyAPI.kt @@ -1,10 +1,15 @@ package net.mamoe.mirai +import kotlinx.coroutines.* import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiInternalAPI +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException /** * [Bot] 中为了让 Java 使用者调用更方便的 API 列表. @@ -14,42 +19,135 @@ import net.mamoe.mirai.utils.MiraiInternalAPI actual abstract class BotJavaHappyAPI actual constructor() { init { @Suppress("LeakingThis") - check(this is Bot) + assert(this is Bot) } private inline fun runBlocking(crossinline block: suspend Bot.() -> R): R { return kotlinx.coroutines.runBlocking { block(this@BotJavaHappyAPI as Bot) } } + private inline fun future(crossinline block: suspend Bot.() -> R): Future { + return (this as Bot).run { future(block) } + } + @JvmName("login") - actual open fun __loginBlockingForJava__() { + fun __loginBlockingForJava__() { runBlocking { login() } } @JvmName("recall") - actual open fun __recallBlockingForJava__(source: MessageSource) { + fun __recallBlockingForJava__(source: MessageSource) { runBlocking { recall(source) } } + @JvmName("recall") + fun __recallBlockingForJava__(source: MessageChain) { + runBlocking { recall(source) } + } + + @JvmName("recallIn") + fun __recallIn_MemberForJava__(source: MessageSource, millis: Long) { + runBlocking { recallIn(source, millis) } + } + + @JvmName("recallIn") + fun __recallIn_MemberForJava__(source: MessageChain, millis: Long) { + runBlocking { recallIn(source, millis) } + } + @JvmName("queryImageUrl") - actual open fun __queryImageUrlBlockingForJava__(image: Image): String { + fun __queryImageUrlBlockingForJava__(image: Image): String { return runBlocking { queryImageUrl(image) } } @JvmName("join") - actual open fun __joinBlockingForJava__() { + fun __joinBlockingForJava__() { runBlocking { join() } } - @UseExperimental(MiraiExperimentalAPI::class) @JvmOverloads @JvmName("addFriend") - actual open fun __addFriendBlockingForJava__( + fun __addFriendBlockingForJava__( id: Long, - message: String?, - remark: String? + message: String? = null, + remark: String? = null ): AddFriendResult { + @UseExperimental(MiraiExperimentalAPI::class) return runBlocking { addFriend(id, message, remark) } } + + @JvmName("loginAsync") + fun __loginAsyncForJava__(): Future { + return future { login() } + } + + @JvmName("recallAsync") + fun __recallAsyncForJava__(source: MessageSource): Future { + return future { recall(source) } + } + + @JvmName("recallAsync") + fun __recallAsyncForJava__(source: MessageChain): Future { + return future { recall(source) } + } + + @JvmName("queryImageUrlAsync") + fun __queryImageUrlAsyncForJava__(image: Image): Future { + return future { queryImageUrl(image) } + } +} + +private inline fun Bot.future(crossinline block: suspend Bot.() -> R): Future { + return object : Future { + val value: CompletableDeferred = CompletableDeferred() + + init { + launch { + @UseExperimental(ExperimentalCoroutinesApi::class) + value.completeWith(kotlin.runCatching { block() }) + } + } + + override fun isDone(): Boolean { + return value.isCompleted + } + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + override fun get(): R { + if (value.isCompleted) { + @UseExperimental(ExperimentalCoroutinesApi::class) + return value.getCompleted() + } + return runBlocking { value.await() } + } + + override fun get(timeout: Long, unit: TimeUnit): R { + if (value.isCompleted) { + @UseExperimental(ExperimentalCoroutinesApi::class) + return value.getCompleted() + } + return runBlocking { + withTimeoutOrNull(TimeUnit.MILLISECONDS.convert(timeout, unit)) { value.await() } + ?: throw TimeoutException() + } + } + + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + if (value.isCompleted || value.isCancelled) { + return false + } + + return if (mayInterruptIfRunning && value.isActive) { + value.cancel() + true + } else { + false + } + } + + override fun isCancelled(): Boolean { + return value.isCancelled + } + } } \ No newline at end of file