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
- (社区)`Go`: [gomirai](https://github.com/Logiase/gomirai) 基于 mirai-api-http 的 GoLang 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 为第三方依赖库引入项目

View File

@ -7,7 +7,7 @@
* 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)
package net.mamoe.mirai.qqandroid.contact
@ -93,7 +93,7 @@ internal class GroupImpl(
@OptIn(MiraiExperimentalAPI::class)
override lateinit var botPermission: MemberPermission
var _botMuteTimestamp: Int = groupInfo.botMuteRemaining
var _botMuteTimestamp: Int = groupInfo.botMuteTimestamp
override val botMuteRemaining: Int =
if (_botMuteTimestamp == 0 || _botMuteTimestamp == 0xFFFFFFFF.toInt()) {

View File

@ -7,7 +7,7 @@
* 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
@ -196,9 +196,13 @@ internal class MemberImpl constructor(
net.mamoe.mirai.event.events.MemberUnmuteEvent(this@MemberImpl, null).broadcast()
}
@OptIn(MiraiInternalAPI::class)
@JvmSynthetic
override suspend fun kick(message: String) {
checkBotPermissionHigherThanThis()
check(group.members.getOrNull(this.id) != null) {
"Member ${this.id} had already been kicked from group ${group.id}"
}
bot.network.run {
val response: TroopManagement.Kick.Response = TroopManagement.Kick(
client = bot.client,
@ -206,8 +210,9 @@ internal class MemberImpl constructor(
message = message
).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()
}
}

View File

@ -8,7 +8,7 @@
*/
@file:OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
@file:Suppress("EXPERIMENTAL_API_USAGE")
@file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION_ERROR")
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.MiraiInternalAPI
@OptIn(MiraiInternalAPI::class)
internal suspend fun QQ.sendMessageImpl(message: Message): MessageReceipt<QQ> {
val event = MessageSendEvent.FriendMessageSendEvent(this, message.asMessageChain()).broadcast()
if (event.isCancelled) {

View File

@ -255,6 +255,10 @@ internal class OfflineMessageSourceImplBySourceMsg( // from others' quotation
override val bot: Bot,
groupIdOrZero: Long
) : OfflineMessageSource(), MessageSourceImpl {
init {
println(delegate._miraiContentToString())
}
override val kind: Kind get() = if (delegate.srcMsg == null) Kind.GROUP else Kind.FRIEND
private val isRecalled: AtomicBoolean = atomic(false)
@ -276,7 +280,7 @@ internal class OfflineMessageSourceImplBySourceMsg( // from others' quotation
override val id: Int
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 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.singleLine
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.packet.*
import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl
@ -189,8 +190,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
* Don't use concurrently
*/
suspend fun reloadFriendList() {
// 不要用 fun, 不要 join declaration, 不要用 val, 编译失败警告
logger.info("开始加载好友信息")
logger.info { "开始加载好友信息" }
var currentFriendCount = 0
var totalFriendCount: Short
while (true) {
@ -221,6 +221,58 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
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)
override suspend fun init(): Unit = coroutineScope {
check(bot.isActive) { "bot is dead therefore network can't init" }
@ -236,92 +288,16 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
_pendingEnabled.value = true
}
supervisorScope {
this.launch { reloadFriendList() }
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)
}
}
coroutineScope {
launch { reloadFriendList() }
launch { reloadGroupList() }
}
runCatching {
withTimeoutOrNull(30000) {
lateinit var listener: Listener<PacketReceivedEvent>
listener = this.subscribeAlways {
if (it.packet is MessageSvc.PbGetMsg.GetMsgSuccess) {
listener.complete()
}
}
withTimeoutOrNull(30000) {
launch { subscribingGet<MessageSvc.PbGetMsg.GetMsgSuccess, Unit> { Unit } }
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendAndExpect<Packet>()
} ?: error("timeout syncing friend message history")
MessageSvc.PbGetMsg(bot.client, MsgSvc.SyncFlag.START, currentTimeSeconds).sendAndExpect<Packet>()
} ?: 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
_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(this@QQAndroidBotNetworkHandler.isActive) { "network is dead therefore can't send any packet" }
logger.verbose("Send: ${this.commandName}")
withContext(this@QQAndroidBotNetworkHandler.coroutineContext + CoroutineName("Packet sender")) {
PacketLogger.debug { "Channel sending: $commandName" }
channel.send(delegate)
PacketLogger.debug { "Channel send done: $commandName" }
}
channel.send(delegate)
}
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(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" }
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) {
is ByteArray -> channel.send(data, 0, length)
is ByteReadPacket -> channel.send(data)
else -> error("Internal error: unexpected data type: ${data::class.simpleName}")
}
PacketLogger.debug { "Channel send done: $commandName" }
}
} ?: throw TimeoutException("timeout sending packet $commandName")
when (data) {
is ByteArray -> channel.send(data, 0, length)
is ByteReadPacket -> channel.send(data)
else -> error("Internal error: unexpected data type: ${data::class.simpleName}")
}
logger.verbose("Send done: $commandName")
logger.verbose { "Send done: $commandName" }
@Suppress("UNCHECKED_CAST")
return withTimeoutOrNull(timeoutMillis) {

View File

@ -44,7 +44,7 @@ internal class GroupInfoImpl(
override val autoApprove get() = delegate.groupFlagext3?.and(0x00100000) == 0
override val confessTalk get() = delegate.groupFlagext3?.and(0x00002000) == 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 {
@ -146,14 +146,17 @@ internal class TroopManagement {
internal object Kick : OutgoingPacketFactory<Kick.Response>("OidbSvc.0x8a0_0") {
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(
this.readBytes()
.loadAs(OidbSso.OIDBSSOPkg.serializer()).bodybuffer.loadAs(Oidb0x8a0.RspBody.serializer()).msgKickResult!![0].optUint32Result == 1
ret == 0,
ret
)
}
class Response(
val success: Boolean
val success: Boolean,
val ret: Int
) : Packet {
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.QQ
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.BotOfflineEvent
import net.mamoe.mirai.event.events.MemberJoinEvent
@ -116,7 +117,8 @@ internal class MessageSvc {
}
@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>))"
}

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")
@OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
abstract class Bot : CoroutineScope, LowLevelBotAPIAccessor, BotJavaFriendlyAPI() {
companion object {
/**
* 复制一份此时的 [Bot] 实例列表.
*/
@JvmStatic
val instances: List<WeakRef<Bot>>
get() = BotImpl.instances.toList()
/**
* 遍历每一个 [Bot] 实例
*/
inline fun forEachInstance(block: (Bot) -> Unit)
inline fun forEachInstance(block: (Bot) -> Unit) = BotImpl.forEachInstance(block)
/**
* 获取一个 [Bot] 实例, 找不到则 [NoSuchElementException]
*/
@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 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>
/**
* 获取一个机器人加入的群.
*
* @throws NoSuchElementException 当不存在这个群时
* @throws NoSuchElementException 当不存在这个群时抛出
*/
fun getGroup(id: Long): Group
fun getGroup(id: Long): Group = groups.firstOrNull { it.id == id } ?: throw NoSuchElementException("group $id")
// endregion
// region network
/**
* 网络模块
*/
abstract val network: BotNetworkHandler
/**
* 挂起协程直到 [Bot] 下线.
*/
@JvmSynthetic
suspend inline fun join()
/**
* 登录, 或重新登录.
* 这个函数总是关闭一切现有网路任务, 然后重新登录并重新缓存好友列表和群列表.
* 这个函数总是关闭一切现有网路任务 (但不会关闭其他任务), 然后重新登录并重新缓存好友列表和群列表.
*
* 一般情况下不需要重新登录. Mirai 能够自动处理掉线情况.
*
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.relogin]
*
* @throws LoginFailedException
* @see alsoLogin
* @throws LoginFailedException 正常登录失败时抛出
* @see alsoLogin `.apply { login() }` 捷径
*/
@JvmSynthetic
abstract suspend fun login()
@ -264,9 +252,26 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
abstract fun close(cause: Throwable? = null)
@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
*/
@OptIn(MiraiInternalAPI::class)
@Suppress("unused")
class ContactList<C : Contact>(@MiraiInternalAPI val delegate: LockFreeLinkedList<C>) : Iterable<C> {
operator fun get(id: Long): C = delegate.asSequence().first { it.id == id }
class ContactList<C : Contact>(@MiraiInternalAPI("Implementation may change in future release") val delegate: LockFreeLinkedList<C>) :
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)
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
/**
* 成员离开群的事件
* 成员离开群的事件. 在事件广播前成员就已经从 [Group.members] 中删除
*/
sealed class MemberLeaveEvent : GroupMemberEvent {
/**
@ -365,7 +365,7 @@ sealed class MemberLeaveEvent : GroupMemberEvent {
data class Kick(
override val member: Member,
/**
* 操作人. null 则是机器人操作
* 操作人. null 则是机器人操作.
*/
override val operator: Member?
) : 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.FriendMessage
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.firstIsInstance
import net.mamoe.mirai.utils.SinceMirai
@ -115,6 +116,31 @@ fun <R> CoroutineScope.subscribeFriendMessages(
}.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] 的所有联系人的消息事件. 联系人可以是任意群或任意好友或临时会话.
*
@ -186,6 +212,31 @@ fun <R> Bot.subscribeFriendMessages(
}.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
fun sentByFriend(): ListeningFilter = newListeningFilter { this is FriendMessage }
/** 如果是好友发来的消息 */
@MessageDsl
fun sentByTemp(): ListeningFilter = newListeningFilter { this is TempMessage }
/** 如果是管理员或群主发的消息 */
@MessageDsl
fun sentByOperator(): ListeningFilter =

View File

@ -20,6 +20,7 @@ import net.mamoe.mirai.utils.WeakRef
/**
* 标示这个 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.event.BroadcastControllable
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.source
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
/**
* 好友消息
*/
class FriendMessage(
sender: QQ,
override val message: MessageChain
) : 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 bot: Bot get() = sender.bot
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.event.Event
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.source
import net.mamoe.mirai.utils.getValue
@ -30,6 +31,11 @@ class GroupMessage(
sender: Member,
override val message: MessageChain
) : 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()
val group: Group get() = sender.group
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.event.BroadcastControllable
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.source
import net.mamoe.mirai.utils.SinceMirai
import net.mamoe.mirai.utils.getValue
import net.mamoe.mirai.utils.unsafeWeakRef
/**
* 临时会话消息
*/
@SinceMirai("0.35.0")
class TempMessage(
sender: Member,
override val message: MessageChain
) : 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 bot: Bot get() = sender.bot
override val subject: Member get() = sender
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.
* [OnlineMessageSource] 时为随机数.
* [OfflineMessageSource] 时可能为 0, 取决于服务器是否提供这个值.
*/
abstract val id: Int // random
@ -235,7 +237,7 @@ inline fun MessageSource.isAboutGroup(): Boolean {
}
inline fun MessageSource.isAboutTemp(): Boolean {
return when(this) {
return when (this) {
is OnlineMessageSource -> subject is Member
is OfflineMessageSource -> kind == OfflineMessageSource.Kind.TEMP
}
@ -302,6 +304,12 @@ abstract class OfflineMessageSource : MessageSource() {
*/
abstract val kind: Kind
/**
* 消息 id.
* 服务器不一定提供 id. 因此此值可能为 0
*/
abstract override val id: Int
// 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)
}