Merge remote-tracking branch 'origin/master'

This commit is contained in:
ryoii 2020-04-09 16:20:09 +08:00
commit 98542546bc
21 changed files with 231 additions and 652 deletions

View File

@ -51,6 +51,7 @@ Mirai 源码完全开放, 您可以参考 Mirai 的协议实现来开发其他
- (社区)`JavaScript`(`Node.js`): [node-mirai](https://github.com/RedBeanN/node-mirai) mirai 的 Node.js SDK - (社区)`JavaScript`(`Node.js`): [node-mirai](https://github.com/RedBeanN/node-mirai) mirai 的 Node.js SDK
- (社区)`Go`: [gomirai](https://github.com/Logiase/gomirai) 基于 mirai-api-http 的 GoLang SDK - (社区)`Go`: [gomirai](https://github.com/Logiase/gomirai) 基于 mirai-api-http 的 GoLang SDK
- (社区)`Mozilla Rhino`: [mirai-rhinojs-sdk](https://github.com/StageGuard/mirai-rhinojs-sdk) - (社区)`Mozilla Rhino`: [mirai-rhinojs-sdk](https://github.com/StageGuard/mirai-rhinojs-sdk)
- (社区)`Lua`: [lua-mirai](https://github.com/only52607/lua-mirai)
- (官方)其他任意语言:使用由 [mirai-api-http](https://github.com/mamoe/mirai-api-http) 提供的 http 接口进行接入 - (官方)其他任意语言:使用由 [mirai-api-http](https://github.com/mamoe/mirai-api-http) 提供的 http 接口进行接入
#### 使用 mirai 为第三方依赖库引入项目 #### 使用 mirai 为第三方依赖库引入项目

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("INAPPLICABLE_JVM_NAME") @file:Suppress("INAPPLICABLE_JVM_NAME", "DEPRECATION_ERROR")
@file:OptIn(MiraiInternalAPI::class, LowLevelAPI::class) @file:OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
package net.mamoe.mirai.qqandroid.contact package net.mamoe.mirai.qqandroid.contact
@ -93,7 +93,7 @@ internal class GroupImpl(
@OptIn(MiraiExperimentalAPI::class) @OptIn(MiraiExperimentalAPI::class)
override lateinit var botPermission: MemberPermission override lateinit var botPermission: MemberPermission
var _botMuteTimestamp: Int = groupInfo.botMuteRemaining var _botMuteTimestamp: Int = groupInfo.botMuteTimestamp
override val botMuteRemaining: Int = override val botMuteRemaining: Int =
if (_botMuteTimestamp == 0 || _botMuteTimestamp == 0xFFFFFFFF.toInt()) { if (_botMuteTimestamp == 0 || _botMuteTimestamp == 0xFFFFFFFF.toInt()) {

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:Suppress("EXPERIMENTAL_API_USAGE") @file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION_ERROR")
package net.mamoe.mirai.qqandroid.contact package net.mamoe.mirai.qqandroid.contact
@ -196,9 +196,13 @@ internal class MemberImpl constructor(
net.mamoe.mirai.event.events.MemberUnmuteEvent(this@MemberImpl, null).broadcast() net.mamoe.mirai.event.events.MemberUnmuteEvent(this@MemberImpl, null).broadcast()
} }
@OptIn(MiraiInternalAPI::class)
@JvmSynthetic @JvmSynthetic
override suspend fun kick(message: String) { override suspend fun kick(message: String) {
checkBotPermissionHigherThanThis() checkBotPermissionHigherThanThis()
check(group.members.getOrNull(this.id) != null) {
"Member ${this.id} had already been kicked from group ${group.id}"
}
bot.network.run { bot.network.run {
val response: TroopManagement.Kick.Response = TroopManagement.Kick( val response: TroopManagement.Kick.Response = TroopManagement.Kick(
client = bot.client, client = bot.client,
@ -206,8 +210,9 @@ internal class MemberImpl constructor(
message = message message = message
).sendAndExpect() ).sendAndExpect()
check(response.success) { "kick failed: $message" } check(response.success) { "kick failed: ${response.ret}" }
group.members.delegate.removeIf { it.id == this@MemberImpl.id }
MemberLeaveEvent.Kick(this@MemberImpl, null).broadcast() MemberLeaveEvent.Kick(this@MemberImpl, null).broadcast()
} }
} }

View File

@ -8,7 +8,7 @@
*/ */
@file:OptIn(MiraiInternalAPI::class, LowLevelAPI::class) @file:OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
@file:Suppress("EXPERIMENTAL_API_USAGE") @file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION_ERROR")
package net.mamoe.mirai.qqandroid.contact package net.mamoe.mirai.qqandroid.contact

View File

@ -28,6 +28,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
@OptIn(MiraiInternalAPI::class)
internal suspend fun QQ.sendMessageImpl(message: Message): MessageReceipt<QQ> { internal suspend fun QQ.sendMessageImpl(message: Message): MessageReceipt<QQ> {
val event = MessageSendEvent.FriendMessageSendEvent(this, message.asMessageChain()).broadcast() val event = MessageSendEvent.FriendMessageSendEvent(this, message.asMessageChain()).broadcast()
if (event.isCancelled) { if (event.isCancelled) {

View File

@ -255,6 +255,10 @@ internal class OfflineMessageSourceImplBySourceMsg( // from others' quotation
override val bot: Bot, override val bot: Bot,
groupIdOrZero: Long groupIdOrZero: Long
) : OfflineMessageSource(), MessageSourceImpl { ) : OfflineMessageSource(), MessageSourceImpl {
init {
println(delegate._miraiContentToString())
}
override val kind: Kind get() = if (delegate.srcMsg == null) Kind.GROUP else Kind.FRIEND override val kind: Kind get() = if (delegate.srcMsg == null) Kind.GROUP else Kind.FRIEND
private val isRecalled: AtomicBoolean = atomic(false) private val isRecalled: AtomicBoolean = atomic(false)
@ -276,7 +280,7 @@ internal class OfflineMessageSourceImplBySourceMsg( // from others' quotation
override val id: Int override val id: Int
get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids?.toInt() get() = delegate.pbReserve.loadAs(SourceMsg.ResvAttr.serializer()).origUids?.toInt()
?: error("在读取 OfflineMessageSourceImplBySourceMsg.id 时找不到 origUids, delegate=${delegate._miraiContentToString()}") ?: 0
// override val sourceMessage: MessageChain get() = delegate.toMessageChain() // override val sourceMessage: MessageChain get() = delegate.toMessageChain()
override val fromId: Long get() = delegate.senderUin override val fromId: Long get() = delegate.senderUin

View File

@ -30,6 +30,7 @@ import net.mamoe.mirai.qqandroid.contact.GroupImpl
import net.mamoe.mirai.qqandroid.contact.QQImpl import net.mamoe.mirai.qqandroid.contact.QQImpl
import net.mamoe.mirai.qqandroid.contact.singleLine import net.mamoe.mirai.qqandroid.contact.singleLine
import net.mamoe.mirai.qqandroid.event.PacketReceivedEvent import net.mamoe.mirai.qqandroid.event.PacketReceivedEvent
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopNum
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc
import net.mamoe.mirai.qqandroid.network.protocol.packet.* import net.mamoe.mirai.qqandroid.network.protocol.packet.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl
@ -189,8 +190,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
* Don't use concurrently * Don't use concurrently
*/ */
suspend fun reloadFriendList() { suspend fun reloadFriendList() {
// 不要用 fun, 不要 join declaration, 不要用 val, 编译失败警告 logger.info { "开始加载好友信息" }
logger.info("开始加载好友信息")
var currentFriendCount = 0 var currentFriendCount = 0
var totalFriendCount: Short var totalFriendCount: Short
while (true) { while (true) {
@ -221,6 +221,58 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
logger.info { "好友列表加载完成, 共 ${currentFriendCount}" } logger.info { "好友列表加载完成, 共 ${currentFriendCount}" }
} }
suspend fun StTroopNum.reloadGroup() {
retryCatching(3) {
bot.groups.delegate.addLast(
@Suppress("DuplicatedCode")
GroupImpl(
bot = bot,
coroutineContext = bot.coroutineContext,
id = groupCode,
groupInfo = bot._lowLevelQueryGroupInfo(groupCode).apply {
this as GroupInfoImpl
if (this.delegate.groupName == null) {
this.delegate.groupName = groupName
}
if (this.delegate.groupMemo == null) {
this.delegate.groupMemo = groupMemo
}
if (this.delegate.groupUin == null) {
this.delegate.groupUin = groupUin
}
this.delegate.groupCode = this@reloadGroup.groupCode
},
members = bot._lowLevelQueryGroupMemberList(
groupUin,
groupCode,
dwGroupOwnerUin
)
)
)
}.getOrThrow()
}
suspend fun reloadGroupList() {
logger.info { "开始加载群组列表与群成员列表" }
val troopListData = FriendList.GetTroopListSimplify(bot.client)
.sendAndExpect<FriendList.GetTroopListSimplify.Response>(retry = 3)
troopListData.groups.chunked(50).forEach { chunk ->
coroutineScope {
chunk.forEach {
launch {
retryCatching(3) { it.reloadGroup() }.getOrThrow()
}
}
}
}
logger.info { "群组列表与群成员加载完成, 共 ${troopListData.groups.size}" }
}
@OptIn(MiraiExperimentalAPI::class, ExperimentalTime::class) @OptIn(MiraiExperimentalAPI::class, ExperimentalTime::class)
override suspend fun init(): Unit = coroutineScope { override suspend fun init(): Unit = coroutineScope {
check(bot.isActive) { "bot is dead therefore network can't init" } check(bot.isActive) { "bot is dead therefore network can't init" }
@ -236,92 +288,16 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
_pendingEnabled.value = true _pendingEnabled.value = true
} }
supervisorScope { coroutineScope {
this.launch { reloadFriendList() } launch { reloadFriendList() }
launch { reloadGroupList() }
launch {
try {
logger.info("开始加载群组列表与群成员列表")
val troopListData = FriendList.GetTroopListSimplify(bot.client)
.sendAndExpect<FriendList.GetTroopListSimplify.Response>(retry = 3)
troopListData.groups.chunked(50).forEach { chunk ->
supervisorScope {
chunk.forEach { troopNum ->
// 别用 fun, 别 val, 编译失败警告
lateinit var loadGroup: suspend () -> Unit
loadGroup = suspend {
retryCatching(3) {
bot.groups.delegate.addLast(
@Suppress("DuplicatedCode")
(GroupImpl(
bot = bot,
coroutineContext = bot.coroutineContext,
id = troopNum.groupCode,
groupInfo = bot._lowLevelQueryGroupInfo(troopNum.groupCode).apply {
this as GroupInfoImpl
if (this.delegate.groupName == null) {
this.delegate.groupName = troopNum.groupName
} }
if (this.delegate.groupMemo == null) {
this.delegate.groupMemo = troopNum.groupMemo
}
if (this.delegate.groupUin == null) {
this.delegate.groupUin = troopNum.groupUin
}
this.delegate.groupCode = troopNum.groupCode
},
members = bot._lowLevelQueryGroupMemberList(
troopNum.groupUin,
troopNum.groupCode,
troopNum.dwGroupOwnerUin
)
))
)
}.exceptionOrNull()?.let {
logger.error { "${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试" }
logger.error(it)
this@QQAndroidBotNetworkHandler.launch {
delay(10_000)
loadGroup()
}
}
Unit // 别删, 编译失败警告
}
launch {
loadGroup()
}
}
}
}
logger.info { "群组列表与群成员加载完成, 共 ${troopListData.groups.size}" }
} catch (e: Exception) {
logger.error { "加载组信息失败|一般这是由于加载过于频繁导致/将以热加载方式加载群列表" }
logger.error(e)
}
}
}
runCatching {
withTimeoutOrNull(30000) { withTimeoutOrNull(30000) {
lateinit var listener: Listener<PacketReceivedEvent> launch { subscribingGet<MessageSvc.PbGetMsg.GetMsgSuccess, Unit> { Unit } }
listener = this.subscribeAlways {
if (it.packet is MessageSvc.PbGetMsg.GetMsgSuccess) {
listener.complete()
}
}
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendAndExpect<Packet>() MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendAndExpect<Packet>()
} ?: error("timeout syncing friend message history") } ?: error("timeout syncing friend message history")
}.exceptionOrNull()?.let {
logger.error("exception while loading syncing friend message history: ${it.message}")
logger.error(it)
}
bot.firstLoginSucceed = true bot.firstLoginSucceed = true
_pendingEnabled.value = false _pendingEnabled.value = false
@ -568,19 +544,15 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
check(bot.isActive) { "bot is dead therefore can't send any packet" } check(bot.isActive) { "bot is dead therefore can't send any packet" }
check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" } check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" }
logger.verbose("Send: ${this.commandName}") logger.verbose("Send: ${this.commandName}")
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
PacketLogger.debug { "Channel sending: $commandName" }
channel.send(delegate) channel.send(delegate)
PacketLogger.debug { "Channel send done: $commandName" }
}
} }
class TimeoutException(override val message: String?) : Exception() class TimeoutException(override val message: String?) : Exception()
/** /**
* 发送一个包, 挂起直到接收到指定的返回包或超时(3000ms) * 发送一个包, 挂起协程直到接收到指定的返回包或超时
*/ */
suspend fun <E : Packet> OutgoingPacket.sendAndExpect(timeoutMillis: Long = 3000, retry: Int = 2): E { suspend fun <E : Packet> OutgoingPacket.sendAndExpect(timeoutMillis: Long = 5000, retry: Int = 2): E {
require(timeoutMillis > 100) { "timeoutMillis must > 100" } require(timeoutMillis > 100) { "timeoutMillis must > 100" }
require(retry >= 0) { "retry must >= 0" } require(retry >= 0) { "retry must >= 0" }
@ -588,19 +560,13 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" } check(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" }
suspend fun doSendAndReceive(handler: PacketListener, data: Any, length: Int): E { suspend fun doSendAndReceive(handler: PacketListener, data: Any, length: Int): E {
withTimeoutOrNull(3000) {
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
PacketLogger.debug { "Channel sending: $commandName" }
when (data) { when (data) {
is ByteArray -> channel.send(data, 0, length) is ByteArray -> channel.send(data, 0, length)
is ByteReadPacket -> channel.send(data) is ByteReadPacket -> channel.send(data)
else -> error("Internal error: unexpected data type: ${data::class.simpleName}") else -> error("Internal error: unexpected data type: ${data::class.simpleName}")
} }
PacketLogger.debug { "Channel send done: $commandName" }
}
} ?: throw TimeoutException("timeout sending packet $commandName")
logger.verbose("Send done: $commandName") logger.verbose { "Send done: $commandName" }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return withTimeoutOrNull(timeoutMillis) { return withTimeoutOrNull(timeoutMillis) {

View File

@ -44,7 +44,7 @@ internal class GroupInfoImpl(
override val autoApprove get() = delegate.groupFlagext3?.and(0x00100000) == 0 override val autoApprove get() = delegate.groupFlagext3?.and(0x00100000) == 0
override val confessTalk get() = delegate.groupFlagext3?.and(0x00002000) == 0 override val confessTalk get() = delegate.groupFlagext3?.and(0x00002000) == 0
override val muteAll: Boolean get() = delegate.shutupTimestamp != 0 override val muteAll: Boolean get() = delegate.shutupTimestamp != 0
override val botMuteRemaining: Int get() = delegate.shutupTimestampMe ?: 0 override val botMuteTimestamp: Int get() = delegate.shutupTimestampMe ?: 0
} }
internal class TroopManagement { internal class TroopManagement {
@ -146,14 +146,17 @@ internal class TroopManagement {
internal object Kick : OutgoingPacketFactory<Kick.Response>("OidbSvc.0x8a0_0") { internal object Kick : OutgoingPacketFactory<Kick.Response>("OidbSvc.0x8a0_0") {
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val ret = this.readBytes()
.loadAs(OidbSso.OIDBSSOPkg.serializer()).bodybuffer.loadAs(Oidb0x8a0.RspBody.serializer()).msgKickResult!![0].optUint32Result
return Response( return Response(
this.readBytes() ret == 0,
.loadAs(OidbSso.OIDBSSOPkg.serializer()).bodybuffer.loadAs(Oidb0x8a0.RspBody.serializer()).msgKickResult!![0].optUint32Result == 1 ret
) )
} }
class Response( class Response(
val success: Boolean val success: Boolean,
val ret: Int
) : Packet { ) : Packet {
override fun toString(): String = "TroopManagement.Kick.Response($success)" override fun toString(): String = "TroopManagement.Kick.Response($success)"
} }

View File

@ -23,6 +23,7 @@ import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.events.BotJoinGroupEvent import net.mamoe.mirai.event.events.BotJoinGroupEvent
import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.MemberJoinEvent import net.mamoe.mirai.event.events.MemberJoinEvent
@ -116,7 +117,8 @@ internal class MessageSvc {
} }
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
open class GetMsgSuccess(delegate: List<Packet>) : Response(MsgSvc.SyncFlag.STOP, delegate) { open class GetMsgSuccess(delegate: List<Packet>) : Response(MsgSvc.SyncFlag.STOP, delegate), Event,
Packet.NoLog {
override fun toString(): String = "MessageSvc.PbGetMsg.GetMsgSuccess(messages=<Iterable>))" override fun toString(): String = "MessageSvc.PbGetMsg.GetMsgSuccess(messages=<Iterable>))"
} }

View File

@ -1,247 +0,0 @@
@file:Suppress("unused")
package net.mamoe.mirai
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.io.ByteReadChannel
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.event.events.MemberJoinRequestEvent
import net.mamoe.mirai.event.events.NewFriendRequestEvent
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")
@OptIn(
MiraiInternalAPI::class, LowLevelAPI::class, MiraiExperimentalAPI::class, JavaFriendlyAPI::class
)
actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI() {
actual companion object {
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
actual val instances: List<WeakRef<Bot>>
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
@PlannedRemoval("1.0.0")
@Deprecated("use id instead", replaceWith = ReplaceWith("id"))
actual abstract val uin: Long
/**
* QQ 号码. 实际类型为 uint
*/
@SinceMirai("0.32.0")
actual abstract val id: Long
/**
* 昵称
*/
actual abstract val nick: String
/**
* 日志记录器
*/
actual abstract val logger: MiraiLogger
// region contacts
actual abstract val selfQQ: QQ
/**
* 机器人的好友列表. 它将与服务器同步更新
*/
actual abstract val friends: ContactList<QQ>
/**
* 获取一个好友对象. 若没有这个好友, 则会抛出异常 [NoSuchElementException]
*/
actual fun getFriend(id: Long): QQ {
if (id == this.id) return selfQQ
return friends.delegate.getOrNull(id)
?: throw NoSuchElementException("No such friend $id for bot ${this.id}")
}
/**
* 机器人加入的群列表.
*/
actual abstract val groups: ContactList<Group>
/**
* 获取一个机器人加入的群.
*
* @throws NoSuchElementException 当不存在这个群时
*/
actual fun getGroup(id: Long): Group {
return groups.delegate.getOrNull(id)
?: throw NoSuchElementException("No such group $id for bot ${this.id}")
}
// endregion
// region network
/**
* 网络模块
*/
actual abstract val network: BotNetworkHandler
/**
* 挂起协程直到 [Bot] 下线.
*/
@JvmSynthetic
actual suspend inline fun join() = network.join()
/**
* 登录, 或重新登录.
* 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表.
*
* 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况.
*
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin]
*
* @throws LoginFailedException
*/
@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
*/
@JvmSynthetic
actual abstract suspend fun recall(source: MessageSource)
/**
* 获取图片下载链接
*/
@JvmSynthetic
actual abstract suspend fun queryImageUrl(image: Image): String
/**
* 获取图片下载链接并开始下载.
*
* @see ByteReadChannel.copyAndClose
* @see ByteReadChannel.copyTo
*/
@JvmSynthetic
actual abstract suspend fun openChannel(image: Image): ByteReadChannel
/**
* 添加一个好友
*
* @param message 若需要验证请求时的验证消息.
* @param remark 好友备注
*/
@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?)
@OptIn(LowLevelAPI::class, MiraiExperimentalAPI::class)
actual final override fun toString(): String = "Bot($id)"
/**
* 通过好友验证
*
* @param event 好友验证的事件对象
*/
@JvmSynthetic
actual abstract suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent)
/**
* 拒绝好友验证
*
* @param event 好友验证的事件对象
* @param blackList 拒绝后是否拉入黑名单
*/
@JvmSynthetic
actual abstract suspend fun rejectNewFriendRequest(event: NewFriendRequestEvent, blackList: Boolean)
/**
* 通过加群验证需管理员权限
*
* @param event 加群验证的事件对象
*/
@JvmSynthetic
actual abstract suspend fun acceptMemberJoinRequest(event: MemberJoinRequestEvent)
/**
* 拒绝加群验证需管理员权限
*
* @param event 加群验证的事件对象
* @param blackList 拒绝后是否拉入黑名单
*/
@JvmSynthetic
actual abstract suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean)
/**
* 忽略加群验证需管理员权限
*
* @param event 加群验证的事件对象
* @param blackList 忽略后是否拉入黑名单
*/
@JvmSynthetic
actual abstract suspend fun ignoreMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean)
}

View File

@ -48,24 +48,25 @@ suspend inline fun <B : Bot> B.alsoLogin(): B = also { login() }
*/ */
@Suppress("INAPPLICABLE_JVM_NAME") @Suppress("INAPPLICABLE_JVM_NAME")
@OptIn(MiraiInternalAPI::class, LowLevelAPI::class) @OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor { abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI() {
companion object { companion object {
/** /**
* 复制一份此时的 [Bot] 实例列表. * 复制一份此时的 [Bot] 实例列表.
*/ */
@JvmStatic @JvmStatic
val instances: List<WeakRef<Bot>> val instances: List<WeakRef<Bot>>
get() = BotImpl.instances.toList()
/** /**
* 遍历每一个 [Bot] 实例 * 遍历每一个 [Bot] 实例
*/ */
inline fun forEachInstance(block: (Bot) -> Unit) inline fun forEachInstance(block: (Bot) -> Unit) = BotImpl.forEachInstance(block)
/** /**
* 获取一个 [Bot] 实例, 找不到则 [NoSuchElementException] * 获取一个 [Bot] 实例, 找不到则 [NoSuchElementException]
*/ */
@JvmStatic @JvmStatic
fun getInstance(qq: Long): Bot fun getInstance(qq: Long): Bot = BotImpl.getInstance(qq = qq)
} }
/** /**
@ -105,52 +106,39 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
abstract val selfQQ: QQ abstract val selfQQ: QQ
/** /**
* 机器人的好友列表. 它将与服务器同步更新 * 机器人的好友列表. 与服务器同步更新
*/ */
abstract val friends: ContactList<QQ> abstract val friends: ContactList<QQ>
/** /**
* 获取一个好友对象. 若没有这个好友, 则会抛出异常 [NoSuchElementException] * 获取一个好友对象.
* @throws [NoSuchElementException] 当不存在这个好友时抛出
*/ */
fun getFriend(id: Long): QQ fun getFriend(id: Long): QQ = friends.firstOrNull { it.id == id } ?: throw NoSuchElementException("group $id")
/** /**
* 机器人加入的群列表. * 机器人加入的群列表. 与服务器同步更新
*/ */
abstract val groups: ContactList<Group> abstract val groups: ContactList<Group>
/** /**
* 获取一个机器人加入的群. * 获取一个机器人加入的群.
* * @throws NoSuchElementException 当不存在这个群时抛出
* @throws NoSuchElementException 当不存在这个群时
*/ */
fun getGroup(id: Long): Group fun getGroup(id: Long): Group = groups.firstOrNull { it.id == id } ?: throw NoSuchElementException("group $id")
// endregion // endregion
// region network // region network
/**
* 网络模块
*/
abstract val network: BotNetworkHandler
/**
* 挂起协程直到 [Bot] 下线.
*/
@JvmSynthetic
suspend inline fun join()
/** /**
* 登录, 或重新登录. * 登录, 或重新登录.
* 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表. * 这个函数总是关闭一切现有网路任务 (但不会关闭其他任务), 然后重新登录并重新缓存好友列表和群列表.
* *
* 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况. * 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况.
* *
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin] * @throws LoginFailedException 正常登录失败时抛出
* * @see alsoLogin `.apply { login() }` 捷径
* @throws LoginFailedException
* @see alsoLogin
*/ */
@JvmSynthetic @JvmSynthetic
abstract suspend fun login() abstract suspend fun login()
@ -264,9 +252,26 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
abstract fun close(cause: Throwable? = null) abstract fun close(cause: Throwable? = null)
@OptIn(LowLevelAPI::class, MiraiExperimentalAPI::class) @OptIn(LowLevelAPI::class, MiraiExperimentalAPI::class)
final override fun toString(): String final override fun toString(): String = "Bot($id)"
/**
* 网络模块.
* 此为内部 API: 它可能在任意时刻被改动.
*/
@MiraiInternalAPI
abstract val network: BotNetworkHandler
@PlannedRemoval("1.0.0")
@Deprecated("for binary compatibility until 1.0.0", level = DeprecationLevel.HIDDEN)
suspend inline fun Bot.join() = this.coroutineContext[Job]!!.join()
} }
/**
* 挂起协程直到 [Bot] 下线.
*/
@JvmSynthetic
suspend inline fun Bot.join() = this.coroutineContext[Job]!!.join()
/** /**
* 撤回这条消息. * 撤回这条消息.
* *

View File

@ -16,14 +16,17 @@ import kotlin.jvm.JvmName
/** /**
* 只读联系人列表, lock-free 实现 * 只读联系人列表, 无锁链表实现
* *
* @see ContactList.asSequence * @see ContactList.asSequence
*/ */
@OptIn(MiraiInternalAPI::class) @OptIn(MiraiInternalAPI::class)
@Suppress("unused") @Suppress("unused")
class ContactList<C : Contact>(@MiraiInternalAPI val delegate: LockFreeLinkedList<C>) : Iterable<C> { class ContactList<C : Contact>(@MiraiInternalAPI("Implementation may change in future release") val delegate: LockFreeLinkedList<C>) :
operator fun get(id: Long): C = delegate.asSequence().first { it.id == id } Iterable<C> {
operator fun get(id: Long): C =
delegate.asSequence().firstOrNull { it.id == id } ?: throw NoSuchElementException("Contact id $id")
fun getOrNull(id: Long): C? = delegate.getOrNull(id) fun getOrNull(id: Long): C? = delegate.getOrNull(id)
val size: Int get() = delegate.size val size: Int get() = delegate.size

View File

@ -63,7 +63,7 @@ interface GroupInfo {
/** /**
* 机器人被禁言还剩时间, . * 机器人被禁言还剩时间, .
*/ */
val botMuteRemaining: Int val botMuteTimestamp: Int
/* /*
/** /**

View File

@ -356,7 +356,7 @@ data class GroupAllowMemberInviteEvent(
data class MemberJoinEvent(override val member: Member) : GroupMemberEvent, BotPassiveEvent, Packet data class MemberJoinEvent(override val member: Member) : GroupMemberEvent, BotPassiveEvent, Packet
/** /**
* 成员离开群的事件 * 成员离开群的事件. 在事件广播前成员就已经从 [Group.members] 中删除
*/ */
sealed class MemberLeaveEvent : GroupMemberEvent { sealed class MemberLeaveEvent : GroupMemberEvent {
/** /**
@ -365,7 +365,7 @@ sealed class MemberLeaveEvent : GroupMemberEvent {
data class Kick( data class Kick(
override val member: Member, override val member: Member,
/** /**
* 操作人. null 则是机器人操作 * 操作人. null 则是机器人操作.
*/ */
override val operator: Member? override val operator: Member?
) : MemberLeaveEvent(), Packet, GroupOperableEvent { ) : MemberLeaveEvent(), Packet, GroupOperableEvent {

View File

@ -23,6 +23,7 @@ import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.ContactMessage import net.mamoe.mirai.message.ContactMessage
import net.mamoe.mirai.message.FriendMessage import net.mamoe.mirai.message.FriendMessage
import net.mamoe.mirai.message.GroupMessage import net.mamoe.mirai.message.GroupMessage
import net.mamoe.mirai.message.TempMessage
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.firstIsInstance import net.mamoe.mirai.message.data.firstIsInstance
import net.mamoe.mirai.utils.SinceMirai import net.mamoe.mirai.utils.SinceMirai
@ -115,6 +116,31 @@ fun <R> CoroutineScope.subscribeFriendMessages(
}.run(listeners) }.run(listeners)
} }
typealias TempMessageSubscribersBuilder = MessageSubscribersBuilder<TempMessage, Listener<TempMessage>, Unit, Unit>
/**
* 订阅来自所有 [Bot] 的所有临时会话消息事件
*
* @see CoroutineScope.incoming 打开一个指定事件的接收通道
*/
@OptIn(ExperimentalContracts::class)
fun <R> CoroutineScope.subscribeTempMessages(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
concurrencyKind: Listener.ConcurrencyKind = Listener.ConcurrencyKind.CONCURRENT,
listeners: TempMessageSubscribersBuilder.() -> R
): R {
contract {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
}
return TempMessageSubscribersBuilder(Unit) { filter, listener ->
subscribeAlways(coroutineContext, concurrencyKind) {
val toString = this.message.contentToString()
if (filter(this, toString))
listener(this, toString)
}
}.run(listeners)
}
/** /**
* 订阅来自这个 [Bot] 的所有联系人的消息事件. 联系人可以是任意群或任意好友或临时会话. * 订阅来自这个 [Bot] 的所有联系人的消息事件. 联系人可以是任意群或任意好友或临时会话.
* *
@ -186,6 +212,31 @@ fun <R> Bot.subscribeFriendMessages(
}.run(listeners) }.run(listeners)
} }
/**
* 订阅来自这个 [Bot] 的所有临时会话消息事件.
*
* @see CoroutineScope.incoming 打开一个指定事件的接收通道
*/
@SinceMirai("0.35.0")
@OptIn(ExperimentalContracts::class)
fun <R> Bot.subscribeTempMessages(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
concurrencyKind: Listener.ConcurrencyKind = Listener.ConcurrencyKind.CONCURRENT,
listeners: TempMessageSubscribersBuilder.() -> R
): R {
contract {
callsInPlace(listeners, InvocationKind.EXACTLY_ONCE)
}
return TempMessageSubscribersBuilder(Unit) { filter, listener ->
this.subscribeAlways(coroutineContext, concurrencyKind) {
val toString = this.message.contentToString()
if (filter(this, toString))
listener(this, toString)
}
}.run(listeners)
}
/** /**
* 打开一个指定事件的接收通道 * 打开一个指定事件的接收通道
* *
@ -582,6 +633,10 @@ open class MessageSubscribersBuilder<M : ContactMessage, out Ret, R : RR, RR>(
@MessageDsl @MessageDsl
fun sentByFriend(): ListeningFilter = newListeningFilter { this is FriendMessage } fun sentByFriend(): ListeningFilter = newListeningFilter { this is FriendMessage }
/** 如果是好友发来的消息 */
@MessageDsl
fun sentByTemp(): ListeningFilter = newListeningFilter { this is TempMessage }
/** 如果是管理员或群主发的消息 */ /** 如果是管理员或群主发的消息 */
@MessageDsl @MessageDsl
fun sentByOperator(): ListeningFilter = fun sentByOperator(): ListeningFilter =

View File

@ -20,6 +20,7 @@ import net.mamoe.mirai.utils.WeakRef
/** /**
* 标示这个 API 是低级的 API. * 标示这个 API 是低级的 API.
* *
* 低级的 API 可能在任意时刻被改动.
* 使用低级的 API 无法带来任何安全和便捷保障. * 使用低级的 API 无法带来任何安全和便捷保障.
* 仅在某些使用结构化 API 可能影响性能的情况下使用这些低级 API. * 仅在某些使用结构化 API 可能影响性能的情况下使用这些低级 API.
*/ */

View File

@ -13,15 +13,24 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.QQ import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.event.BroadcastControllable import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.OnlineMessageSource import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.source import net.mamoe.mirai.message.data.source
import net.mamoe.mirai.utils.getValue import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef import net.mamoe.mirai.utils.unsafeWeakRef
/**
* 好友消息
*/
class FriendMessage( class FriendMessage(
sender: QQ, sender: QQ,
override val message: MessageChain override val message: MessageChain
) : ContactMessage(), BroadcastControllable { ) : ContactMessage(), BroadcastControllable {
init {
val source = message.getOrNull(MessageSource) ?: error("Cannot find MessageSource from message")
check(source is OnlineMessageSource.Incoming.FromFriend) { "source provided to a FriendMessage must be an instance of OnlineMessageSource.Incoming.FromFriend" }
}
override val sender: QQ by sender.unsafeWeakRef() override val sender: QQ by sender.unsafeWeakRef()
override val bot: Bot get() = sender.bot override val bot: Bot get() = sender.bot
override val subject: QQ get() = sender override val subject: QQ get() = sender

View File

@ -15,6 +15,7 @@ import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.MemberPermission import net.mamoe.mirai.contact.MemberPermission
import net.mamoe.mirai.event.Event import net.mamoe.mirai.event.Event
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.OnlineMessageSource import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.source import net.mamoe.mirai.message.data.source
import net.mamoe.mirai.utils.getValue import net.mamoe.mirai.utils.getValue
@ -30,6 +31,11 @@ class GroupMessage(
sender: Member, sender: Member,
override val message: MessageChain override val message: MessageChain
) : ContactMessage(), Event { ) : ContactMessage(), Event {
init {
val source = message.getOrNull(MessageSource) ?: error("Cannot find MessageSource from message")
check(source is OnlineMessageSource.Incoming.FromGroup) { "source provided to a GroupMessage must be an instance of OnlineMessageSource.Incoming.FromGroup" }
}
override val sender: Member by sender.unsafeWeakRef() override val sender: Member by sender.unsafeWeakRef()
val group: Group get() = sender.group val group: Group get() = sender.group
override val bot: Bot get() = sender.bot override val bot: Bot get() = sender.bot

View File

@ -4,19 +4,31 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.event.BroadcastControllable import net.mamoe.mirai.event.BroadcastControllable
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.OnlineMessageSource import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.source import net.mamoe.mirai.message.data.source
import net.mamoe.mirai.utils.SinceMirai
import net.mamoe.mirai.utils.getValue import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef import net.mamoe.mirai.utils.unsafeWeakRef
/**
* 临时会话消息
*/
@SinceMirai("0.35.0")
class TempMessage( class TempMessage(
sender: Member, sender: Member,
override val message: MessageChain override val message: MessageChain
) : ContactMessage(), BroadcastControllable { ) : ContactMessage(), BroadcastControllable {
init {
val source = message.getOrNull(MessageSource) ?: error("Cannot find MessageSource from message")
check(source is OnlineMessageSource.Incoming.FromTemp) { "source provided to a TempMessage must be an instance of OnlineMessageSource.Incoming.FromTemp" }
}
override val sender: Member by sender.unsafeWeakRef() override val sender: Member by sender.unsafeWeakRef()
override val bot: Bot get() = sender.bot override val bot: Bot get() = sender.bot
override val subject: Member get() = sender override val subject: Member get() = sender
override val source: OnlineMessageSource.Incoming.FromTemp get() = message.source as OnlineMessageSource.Incoming.FromTemp override val source: OnlineMessageSource.Incoming.FromTemp get() = message.source as OnlineMessageSource.Incoming.FromTemp
override fun toString(): String = "TempMessage(sender=${sender.id} from group(${sender.group.id}), message=$message)" override fun toString(): String =
"TempMessage(sender=${sender.id} from group(${sender.group.id}), message=$message)"
} }

View File

@ -56,6 +56,8 @@ sealed class MessageSource : Message, MessageMetadata, ConstrainSingle<OnlineMes
/** /**
* 消息 id. * 消息 id.
* [OnlineMessageSource] 时为随机数.
* [OfflineMessageSource] 时可能为 0, 取决于服务器是否提供这个值.
*/ */
abstract val id: Int // random abstract val id: Int // random
@ -235,7 +237,7 @@ inline fun MessageSource.isAboutGroup(): Boolean {
} }
inline fun MessageSource.isAboutTemp(): Boolean { inline fun MessageSource.isAboutTemp(): Boolean {
return when(this) { return when (this) {
is OnlineMessageSource -> subject is Member is OnlineMessageSource -> subject is Member
is OfflineMessageSource -> kind == OfflineMessageSource.Kind.TEMP is OfflineMessageSource -> kind == OfflineMessageSource.Kind.TEMP
} }
@ -302,6 +304,12 @@ abstract class OfflineMessageSource : MessageSource() {
*/ */
abstract val kind: Kind abstract val kind: Kind
/**
* 消息 id.
* 服务器不一定提供 id. 因此此值可能为 0
*/
abstract override val id: Int
// final override fun toString(): String = "OfflineMessageSource(sender=$senderId, target=$targetId)" // final override fun toString(): String = "OfflineMessageSource(sender=$senderId, target=$targetId)"
} }

View File

@ -1,255 +0,0 @@
@file:Suppress("unused")
package net.mamoe.mirai
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.io.ByteReadChannel
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.event.events.NewFriendRequestEvent
import net.mamoe.mirai.event.events.MemberJoinRequestEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
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")
@OptIn(
MiraiInternalAPI::class, LowLevelAPI::class, MiraiExperimentalAPI::class, JavaFriendlyAPI::class
)
actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI() {
actual companion object {
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
actual val instances: List<WeakRef<Bot>>
get() = BotImpl.instances.toList()
/**
* 遍历每一个 [Bot] 实例
*/
@JvmName("forEachInstanceKotlin")
@JvmSynthetic
actual inline fun forEachInstance(block: (Bot) -> Unit) = BotImpl.forEachInstance(block)
/**
* 遍历每一个 [Bot] 实例
*/
@JavaFriendlyAPI
@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 的默认实现为 [net.mamoe.mirai.utils.Context]
* Android 实现为 `android.content.Context`
*/
actual abstract val context: Context
@PlannedRemoval("1.0.0")
@Deprecated("use id instead", replaceWith = ReplaceWith("id"))
actual abstract val uin: Long
/**
* QQ 号码. 实际类型为 uint
*/
@SinceMirai("0.32.0")
actual abstract val id: Long
/**
* 昵称
*/
actual abstract val nick: String
/**
* 日志记录器
*/
actual abstract val logger: MiraiLogger
// region contacts
actual abstract val selfQQ: QQ
/**
* 机器人的好友列表. 它将与服务器同步更新
*/
actual abstract val friends: ContactList<QQ>
/**
* 获取一个好友对象. 若没有这个好友, 则会抛出异常 [NoSuchElementException]
*/
actual fun getFriend(id: Long): QQ {
if (id == this.id) return selfQQ
return friends.delegate.getOrNull(id)
?: throw NoSuchElementException("No such friend $id for bot ${this.id}")
}
/**
* 机器人加入的群列表.
*/
actual abstract val groups: ContactList<Group>
/**
* 获取一个机器人加入的群.
*
* @throws NoSuchElementException 当不存在这个群时
*/
actual fun getGroup(id: Long): Group {
return groups.delegate.getOrNull(id)
?: throw NoSuchElementException("No such group $id for bot ${this.id}")
}
// endregion
// region network
/**
* 网络模块
*/
actual abstract val network: BotNetworkHandler
/**
* 挂起协程直到 [Bot] 下线.
*/
@JvmSynthetic
actual suspend inline fun join() = network.join()
/**
* 登录, 或重新登录.
* 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表.
*
* 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况.
*
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin]
*
* @throws LoginFailedException
*/
@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
*/
@JvmSynthetic
actual abstract suspend fun recall(source: MessageSource)
/**
* 获取图片下载链接
*/
@JvmSynthetic
actual abstract suspend fun queryImageUrl(image: Image): String
/**
* 获取图片下载链接并开始下载.
*
* @see ByteReadChannel.copyAndClose
* @see ByteReadChannel.copyTo
*/
@JvmSynthetic
actual abstract suspend fun openChannel(image: Image): ByteReadChannel
/**
* 添加一个好友
*
* @param message 若需要验证请求时的验证消息.
* @param remark 好友备注
*/
@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?)
@OptIn(LowLevelAPI::class, MiraiExperimentalAPI::class)
actual final override fun toString(): String = "Bot($id)"
/**
* 通过好友验证
*
* @param event 好友验证的事件对象
*/
@JvmSynthetic
actual abstract suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent)
/**
* 拒绝好友验证
*
* @param event 好友验证的事件对象
* @param blackList 拒绝后是否拉入黑名单
*/
@JvmSynthetic
actual abstract suspend fun rejectNewFriendRequest(event: NewFriendRequestEvent, blackList: Boolean)
/**
* 通过加群验证需管理员权限
*
* @param event 加群验证的事件对象
*/
@JvmSynthetic
actual abstract suspend fun acceptMemberJoinRequest(event: MemberJoinRequestEvent)
/**
* 拒绝加群验证需管理员权限
*
* @param event 加群验证的事件对象
* @param blackList 拒绝后是否拉入黑名单
*/
@JvmSynthetic
actual abstract suspend fun rejectMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean)
/**
* 忽略加群验证需管理员权限
*
* @param event 加群验证的事件对象
* @param blackList 忽略后是否拉入黑名单
*/
@JvmSynthetic
actual abstract suspend fun ignoreMemberJoinRequest(event: MemberJoinRequestEvent, blackList: Boolean)
}