Merge remote-tracking branch 'origin/master'

This commit is contained in:
jiahua.liu 2020-02-15 19:55:06 +08:00
commit e27c9bd459
43 changed files with 756 additions and 140 deletions

View File

@ -2,6 +2,14 @@
开发版本. 频繁更新, 不保证高稳定性 开发版本. 频繁更新, 不保证高稳定性
## `0.15.1` Unreleased
### mirai-core
- 统一异常处理: 所有群成员相关操作无权限时均抛出异常而不返回 `false`.
### mirai-core-qqandroid
- 初始化未完成时缓存接收的所有事件包 (#46)
## `0.15.0` 2020/2/14 ## `0.15.0` 2020/2/14
### mirai-core ### mirai-core

View File

@ -2,6 +2,7 @@
kotlin.code.style=official kotlin.code.style=official
# config # config
mirai_version=0.15.0 mirai_version=0.15.0
mirai_japt_version=1.0.0
kotlin.incremental.multiplatform=true kotlin.incremental.multiplatform=true
kotlin.parallel.tasks.in.project=true kotlin.parallel.tasks.in.project=true
# kotlin # kotlin

View File

@ -27,11 +27,6 @@ object NotVerifiedSessionException : IllegalAccessException("Session未激活")
*/ */
object NoSuchBotException: IllegalAccessException("指定Bot不存在") object NoSuchBotException: IllegalAccessException("指定Bot不存在")
/**
* 指定Bot不存在
*/
object PermissionDeniedException: IllegalAccessException("无操作限权")
/** /**
* 错误参数 * 错误参数
*/ */

View File

@ -35,6 +35,7 @@ import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO import net.mamoe.mirai.api.http.data.common.VerifyDTO
import net.mamoe.mirai.api.http.util.jsonParseOrNull import net.mamoe.mirai.api.http.util.jsonParseOrNull
import net.mamoe.mirai.api.http.util.toJson import net.mamoe.mirai.api.http.util.toJson
import net.mamoe.mirai.contact.PermissionDeniedException
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.helpers.NOPLogger import org.slf4j.helpers.NOPLogger
import org.slf4j.helpers.NOPLoggerFactory import org.slf4j.helpers.NOPLoggerFactory

View File

@ -4,7 +4,6 @@ import io.ktor.application.Application
import io.ktor.application.call import io.ktor.application.call
import io.ktor.routing.routing import io.ktor.routing.routing
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.mamoe.mirai.api.http.data.PermissionDeniedException
import net.mamoe.mirai.api.http.data.StateCode import net.mamoe.mirai.api.http.data.StateCode
import net.mamoe.mirai.api.http.data.common.DTO import net.mamoe.mirai.api.http.data.common.DTO
import net.mamoe.mirai.api.http.data.common.VerifyDTO import net.mamoe.mirai.api.http.data.common.VerifyDTO
@ -19,37 +18,31 @@ fun Application.groupManageModule() {
* 禁言需要相关权限 * 禁言需要相关权限
*/ */
miraiVerify<MuteDTO>("/muteAll") { miraiVerify<MuteDTO>("/muteAll") {
it.session.bot.getGroup(it.target).muteAll = true it.session.bot.getGroup(it.target).isMuteAll = true
call.respondStateCode(StateCode.Success) call.respondStateCode(StateCode.Success)
} }
miraiVerify<MuteDTO>("/unmuteAll") { miraiVerify<MuteDTO>("/unmuteAll") {
it.session.bot.getGroup(it.target).muteAll = false it.session.bot.getGroup(it.target).isMuteAll = false
call.respondStateCode(StateCode.Success) call.respondStateCode(StateCode.Success)
} }
miraiVerify<MuteDTO>("/mute") { miraiVerify<MuteDTO>("/mute") {
when (it.session.bot.getGroup(it.target)[it.memberId].mute(it.time)) { it.session.bot.getGroup(it.target)[it.memberId].mute(it.time)
true -> call.respondStateCode(StateCode.Success) call.respondStateCode(StateCode.Success)
else -> throw PermissionDeniedException
}
} }
miraiVerify<MuteDTO>("/unmute") { miraiVerify<MuteDTO>("/unmute") {
when (it.session.bot.getGroup(it.target).members[it.memberId].unmute()) { it.session.bot.getGroup(it.target).members[it.memberId].unmute()
true -> call.respondStateCode(StateCode.Success) call.respondStateCode(StateCode.Success)
else -> throw PermissionDeniedException
}
} }
/** /**
* 移出群聊需要相关权限 * 移出群聊需要相关权限
*/ */
miraiVerify<KickDTO>("/kick") { miraiVerify<KickDTO>("/kick") {
when (it.session.bot.getGroup(it.target)[it.memberId].kick(it.msg)) { it.session.bot.getGroup(it.target)[it.memberId].kick(it.msg)
true -> call.respondStateCode(StateCode.Success) call.respondStateCode(StateCode.Success)
else -> throw PermissionDeniedException
}
} }
/** /**
@ -65,8 +58,8 @@ fun Application.groupManageModule() {
with(dto.config) { with(dto.config) {
name?.let { group.name = it } name?.let { group.name = it }
announcement?.let { group.entranceAnnouncement = it } announcement?.let { group.entranceAnnouncement = it }
confessTalk?.let { group.confessTalk = it } confessTalk?.let { group.isConfessTalkEnabled = it }
allowMemberInvite?.let { group.allowMemberInvite = it } allowMemberInvite?.let { group.isAllowMemberInvite = it }
// TODO: 待core接口实现设置可改 // TODO: 待core接口实现设置可改
// autoApprove?.let { group.autoApprove = it } // autoApprove?.let { group.autoApprove = it }
// anonymousChat?.let { group.anonymousChat = it } // anonymousChat?.let { group.anonymousChat = it }
@ -128,8 +121,8 @@ private data class GroupDetailDTO(
val anonymousChat: Boolean? = null val anonymousChat: Boolean? = null
) : DTO { ) : DTO {
constructor(group: Group) : this( constructor(group: Group) : this(
group.name, group.entranceAnnouncement, group.confessTalk, group.allowMemberInvite, group.name, group.entranceAnnouncement, group.isConfessTalkEnabled, group.isAllowMemberInvite,
group.autoApprove, group.anonymousChat group.isAutoApproveEnabled, group.isAnonymousChatEnabled
) )
} }

View File

@ -227,9 +227,9 @@ internal class MemberImpl(
override val bot: QQAndroidBot get() = qq.bot override val bot: QQAndroidBot get() = qq.bot
override suspend fun mute(durationSeconds: Int): Boolean { override suspend fun mute(durationSeconds: Int) {
if (group.botPermission != MemberPermission.OWNER && (!group.botPermission.isOperator() || this.isOperator())) { if (group.botPermission != MemberPermission.OWNER && (!group.botPermission.isOperator() || this.isOperator())) {
return false throw PermissionDeniedException()
} }
bot.network.run { bot.network.run {
@ -243,12 +243,11 @@ internal class MemberImpl(
@Suppress("RemoveRedundantQualifierName") // or unresolved reference @Suppress("RemoveRedundantQualifierName") // or unresolved reference
net.mamoe.mirai.event.events.MemberMuteEvent(this@MemberImpl, durationSeconds, null).broadcast() net.mamoe.mirai.event.events.MemberMuteEvent(this@MemberImpl, durationSeconds, null).broadcast()
return true
} }
override suspend fun unmute(): Boolean { override suspend fun unmute() {
if (group.botPermission != MemberPermission.OWNER && (!group.botPermission.isOperator() || this.isOperator())) { if (group.botPermission != MemberPermission.OWNER && (!group.botPermission.isOperator() || this.isOperator())) {
return false throw PermissionDeniedException()
} }
bot.network.run { bot.network.run {
@ -262,16 +261,15 @@ internal class MemberImpl(
@Suppress("RemoveRedundantQualifierName") // or unresolved reference @Suppress("RemoveRedundantQualifierName") // or unresolved reference
net.mamoe.mirai.event.events.MemberUnmuteEvent(this@MemberImpl, null).broadcast() net.mamoe.mirai.event.events.MemberUnmuteEvent(this@MemberImpl, null).broadcast()
return true
} }
override suspend fun kick(message: String): Boolean { override suspend fun kick(message: String) {
if (group.botPermission != MemberPermission.OWNER && (!group.botPermission.isOperator() || this.isOperator())) { if (group.botPermission != MemberPermission.OWNER && (!group.botPermission.isOperator() || this.isOperator())) {
return false throw PermissionDeniedException()
} }
bot.network.run { bot.network.run {
return TroopManagement.Kick( TroopManagement.Kick(
client = bot.client, client = bot.client,
member = this@MemberImpl, member = this@MemberImpl,
message = message message = message
@ -394,7 +392,7 @@ internal class GroupImpl(
} }
override var allowMemberInvite: Boolean override var isAllowMemberInvite: Boolean
get() = _allowMemberInvite get() = _allowMemberInvite
set(newValue) { set(newValue) {
this.checkBotPermissionOperator() this.checkBotPermissionOperator()
@ -414,19 +412,19 @@ internal class GroupImpl(
} }
} }
override var autoApprove: Boolean override var isAutoApproveEnabled: Boolean
get() = _autoApprove get() = _autoApprove
set(newValue) { set(newValue) {
TODO() TODO()
} }
override var anonymousChat: Boolean override var isAnonymousChatEnabled: Boolean
get() = _anonymousChat get() = _anonymousChat
set(newValue) { set(newValue) {
TODO() TODO()
} }
override var confessTalk: Boolean override var isConfessTalkEnabled: Boolean
get() = _confessTalk get() = _confessTalk
set(newValue) { set(newValue) {
this.checkBotPermissionOperator() this.checkBotPermissionOperator()
@ -447,7 +445,7 @@ internal class GroupImpl(
} }
override var muteAll: Boolean override var isMuteAll: Boolean
get() = _muteAll get() = _muteAll
set(newValue) { set(newValue) {
this.checkBotPermissionOperator() this.checkBotPermissionOperator()

View File

@ -180,14 +180,14 @@ internal class OnlinePush {
return if (target == 0L) { return if (target == 0L) {
if (time == 0) { if (time == 0) {
GroupMuteAllEvent( GroupMuteAllEvent(
origin = group.muteAll.also { group._muteAll = false }, origin = group.isMuteAll.also { group._muteAll = false },
new = false, new = false,
operator = operator, operator = operator,
group = group group = group
) )
} else { } else {
GroupMuteAllEvent( GroupMuteAllEvent(
origin = group.muteAll.also { group._muteAll = true }, origin = group.isMuteAll.also { group._muteAll = true },
new = true, new = true,
operator = operator, operator = operator,
group = group group = group
@ -213,7 +213,7 @@ internal class OnlinePush {
val operator = group[this.readUInt().toLong()] val operator = group[this.readUInt().toLong()]
val switch = this.readInt() == 0 val switch = this.readInt() == 0
return GroupAllowAnonymousChatEvent( return GroupAllowAnonymousChatEvent(
origin = group.anonymousChat.also { group._anonymousChat = switch }, origin = group.isAnonymousChatEnabled.also { group._anonymousChat = switch },
new = switch, new = switch,
operator = operator, operator = operator,
group = group group = group
@ -236,7 +236,7 @@ internal class OnlinePush {
when (message) { when (message) {
"管理员已关闭群聊坦白说" -> { "管理员已关闭群聊坦白说" -> {
return GroupAllowConfessTalkEvent( return GroupAllowConfessTalkEvent(
origin = group.confessTalk.also { group._confessTalk = false }, origin = group.isConfessTalkEnabled.also { group._confessTalk = false },
new = false, new = false,
group = group, group = group,
isByBot = false isByBot = false
@ -244,7 +244,7 @@ internal class OnlinePush {
} }
"管理员已开启群聊坦白说" -> { "管理员已开启群聊坦白说" -> {
return GroupAllowConfessTalkEvent( return GroupAllowConfessTalkEvent(
origin = group.confessTalk.also { group._confessTalk = true }, origin = group.isConfessTalkEnabled.also { group._confessTalk = true },
new = true, new = true,
group = group, group = group,
isByBot = false isByBot = false

View File

@ -215,7 +215,7 @@ fun ByteReadPacket.readIoBuffer(
* 解析 SSO 层包装 * 解析 SSO 层包装
*/ */
@UseExperimental(ExperimentalUnsignedTypes::class) @UseExperimental(ExperimentalUnsignedTypes::class)
private fun parseSsoFrame(flag3: Int, input: ByteReadPacket): KnownPacketFactories.IncomingPacket { private fun parseSsoFrame(flag3: Int, input: ByteReadPacket): KnownPacketFactories.IncomingPacket<*> {
val commandName: String val commandName: String
val ssoSequenceId: Int val ssoSequenceId: Int
@ -257,7 +257,7 @@ private fun parseSsoFrame(flag3: Int, input: ByteReadPacket): KnownPacketFactori
* 解析 Uni 层包装 * 解析 Uni 层包装
*/ */
@UseExperimental(ExperimentalUnsignedTypes::class) @UseExperimental(ExperimentalUnsignedTypes::class)
private fun parseUniFrame(input: ByteReadPacket): KnownPacketFactories.IncomingPacket { private fun parseUniFrame(input: ByteReadPacket): KnownPacketFactories.IncomingPacket<*> {
// 00 00 00 30 00 01 2F 7C 00 00 00 00 00 00 00 04 00 00 00 14 67 78 68 72 65 70 6F 72 74 2E 72 65 70 6F 72 74 00 00 00 08 66 82 D3 0B 00 00 00 00 // 00 00 00 30 00 01 2F 7C 00 00 00 00 00 00 00 04 00 00 00 14 67 78 68 72 65 70 6F 72 74 2E 72 65 70 6F 72 74 00 00 00 08 66 82 D3 0B 00 00 00 00
// 00 00 00 06 08 00 // 00 00 00 06 08 00

View File

@ -0,0 +1,9 @@
package net.mamoe.mirai.contact
/**
* 权限不足
*/
actual class PermissionDeniedException : IllegalStateException {
actual constructor() : super("Permission denied")
actual constructor(message: String?) : super(message)
}

View File

@ -0,0 +1,9 @@
package net.mamoe.mirai.event.events
@Suppress("unused")
actual class EventCancelledException : RuntimeException {
actual constructor() : super()
actual constructor(message: String?) : super(message)
actual constructor(message: String?, cause: Throwable?) : super(message, cause)
actual constructor(cause: Throwable?) : super(cause)
}

View File

@ -27,6 +27,7 @@ import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.transferTo import net.mamoe.mirai.utils.io.transferTo
import kotlin.jvm.JvmStatic
/** /**
* 机器人对象. 一个机器人实例登录一个 QQ 账号. * 机器人对象. 一个机器人实例登录一个 QQ 账号.
@ -42,6 +43,7 @@ abstract class Bot : CoroutineScope {
/** /**
* 复制一份此时的 [Bot] 实例列表. * 复制一份此时的 [Bot] 实例列表.
*/ */
@JvmStatic
val instances: List<WeakRef<Bot>> get() = BotImpl.instances.toList() val instances: List<WeakRef<Bot>> get() = BotImpl.instances.toList()
/** /**
@ -52,6 +54,7 @@ abstract class Bot : CoroutineScope {
/** /**
* 获取一个 [Bot] 实例, 找不到则 [NoSuchElementException] * 获取一个 [Bot] 实例, 找不到则 [NoSuchElementException]
*/ */
@JvmStatic
fun instanceWhose(qq: Long): Bot = BotImpl.instanceWhose(qq = qq) fun instanceWhose(qq: Long): Bot = BotImpl.instanceWhose(qq = qq)
} }

View File

@ -26,6 +26,24 @@ data class BotAccount(
val passwordMd5: ByteArray // md5 val passwordMd5: ByteArray // md5
) { ) {
constructor(id: Long, passwordPlainText: String) : this(id, md5(passwordPlainText.toByteArray())) constructor(id: Long, passwordPlainText: String) : this(id, md5(passwordPlainText.toByteArray()))
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as BotAccount
if (id != other.id) return false
if (!passwordMd5.contentEquals(other.passwordMd5)) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + passwordMd5.contentHashCode()
return result
}
} }
/** /**

View File

@ -38,8 +38,8 @@ interface Contact : CoroutineScope {
/** /**
* 可以是 QQ 号码或者群号码. * 可以是 QQ 号码或者群号码.
* *
* 对于 QQ, `uin` `id` 是相同的意思. * 对于 [QQ], `uin` `id` 是相同的意思.
* 对于 Group, `groupCode` `id` 是相同的意思. * 对于 [Group], `groupCode` `id` 是相同的意思.
*/ */
val id: Long val id: Long

View File

@ -46,8 +46,9 @@ interface Group : Contact, CoroutineScope {
* 当前仅能修改状态. * 当前仅能修改状态.
* *
* @see GroupMuteAllEvent * @see GroupMuteAllEvent
*/// TODO: 2020/2/5 实现 muteAll 的查询 * @throws PermissionDeniedException 无权限修改时将会抛出异常
var muteAll: Boolean */
var isMuteAll: Boolean
/** /**
* 坦白说状态. `true` 为允许. * 坦白说状态. `true` 为允许.
* *
@ -56,7 +57,7 @@ interface Group : Contact, CoroutineScope {
* @see GroupAllowConfessTalkEvent * @see GroupAllowConfessTalkEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常 * @throws PermissionDeniedException 无权限修改时将会抛出异常
*/ */
var confessTalk: Boolean var isConfessTalkEnabled: Boolean
/** /**
* 允许群员邀请好友入群的状态. `true` 为允许 * 允许群员邀请好友入群的状态. `true` 为允许
* *
@ -65,15 +66,15 @@ interface Group : Contact, CoroutineScope {
* @see GroupAllowMemberInviteEvent * @see GroupAllowMemberInviteEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常 * @throws PermissionDeniedException 无权限修改时将会抛出异常
*/ */
var allowMemberInvite: Boolean var isAllowMemberInvite: Boolean
/** /**
* 自动加群审批 * 自动加群审批
*/ */
val autoApprove: Boolean val isAutoApproveEnabled: Boolean
/** /**
* 匿名聊天 * 匿名聊天
*/ */
val anonymousChat: Boolean val isAnonymousChatEnabled: Boolean
/** /**
* 同为 groupCode, 用户看到的群号码. * 同为 groupCode, 用户看到的群号码.
@ -143,7 +144,7 @@ interface Group : Contact, CoroutineScope {
/** /**
* by @kar98k * by @kar98k
*/ */ // don't @JvmStatic: JDK 1.8 required
fun calculateGroupUinByGroupCode(groupCode: Long): Long { fun calculateGroupUinByGroupCode(groupCode: Long): Long {
var left: Long = groupCode / 1000000L var left: Long = groupCode / 1000000L

View File

@ -35,20 +35,22 @@ interface Member : QQ, Contact {
/** /**
* 群名片. 可能为空. 修改时将会触发事件 * 群名片. 可能为空. 修改时将会触发事件
* *
* 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException] * 在修改时将会异步上传至服务器.
* *
* @see [groupCardOrNick] 获取非空群名片或昵称 * @see [groupCardOrNick] 获取非空群名片或昵称
* *
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件 * @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件
* @throws PermissionDeniedException 无权限修改时
*/ */
var nameCard: String var nameCard: String
/** /**
* 群头衔 * 群头衔
* *
* 在修改时将会异步上传至服务器. 无权限修改时将会抛出异常 [PermissionDeniedException] * 在修改时将会异步上传至服务器.
* *
* @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件 * @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件
* @throws PermissionDeniedException 无权限修改时
*/ */
var specialTitle: String var specialTitle: String
@ -63,22 +65,25 @@ interface Member : QQ, Contact {
* @see Int.daysToSeconds * @see Int.daysToSeconds
* *
* @see MemberMuteEvent 成员被禁言事件 * @see MemberMuteEvent 成员被禁言事件
* @throws PermissionDeniedException 无权限修改时
*/ */
suspend fun mute(durationSeconds: Int): Boolean suspend fun mute(durationSeconds: Int)
/** /**
* 解除禁言. 机器人无权限时返回 `false`. * 解除禁言.
* *
* @see MemberUnmuteEvent 成员被取消禁言事件. * @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/ */
suspend fun unmute(): Boolean suspend fun unmute()
/** /**
* 踢出该成员. 机器人无权限时返回 `false`. * 踢出该成员.
* *
* @see MemberLeaveEvent.Kick 成员被踢出事件. * @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/ */
suspend fun kick(message: String = ""): Boolean suspend fun kick(message: String = "")
/** /**
* 当且仅当 `[other] is [Member] && [other].id == this.id && [other].group == this.group` 时为 true * 当且仅当 `[other] is [Member] && [other].id == this.id && [other].group == this.group` 时为 true
@ -94,10 +99,10 @@ interface Member : QQ, Contact {
val Member.groupCardOrNick: String get() = this.nameCard.takeIf { it.isNotEmpty() } ?: this.nick val Member.groupCardOrNick: String get() = this.nameCard.takeIf { it.isNotEmpty() } ?: this.nick
@ExperimentalTime @ExperimentalTime
suspend inline fun Member.mute(duration: Duration): Boolean { suspend inline fun Member.mute(duration: Duration) {
require(duration.inDays <= 30) { "duration must be at most 1 month" } require(duration.inDays <= 30) { "duration must be at most 1 month" }
require(duration.inSeconds > 0) { "duration must be greater than 0 second" } require(duration.inSeconds > 0) { "duration must be greater than 0 second" }
return this.mute(duration.inSeconds.toInt()) this.mute(duration.inSeconds.toInt())
} }
suspend inline fun Member.mute(durationSeconds: Long) = this.mute(durationSeconds.toInt()) suspend inline fun Member.mute(durationSeconds: Long) = this.mute(durationSeconds.toInt())

View File

@ -72,9 +72,9 @@ inline fun Member.isOperator(): Boolean = this.permission.isOperator()
/** /**
* 权限不足 * 权限不足
*/ */
class PermissionDeniedException : IllegalStateException { expect class PermissionDeniedException : IllegalStateException {
constructor() : super("Permission denied") constructor()
constructor(message: String?) : super(message) constructor(message: String?)
} }
@UseExperimental(MiraiExperimentalAPI::class) @UseExperimental(MiraiExperimentalAPI::class)

View File

@ -438,14 +438,14 @@ class MessageSubscribersBuilder<T : MessagePacket<*, *>>(
*/ */
@MessageDsl @MessageDsl
inline fun <reified M : Message> has(): ListeningFilter = inline fun <reified M : Message> has(): ListeningFilter =
content { message.any { it::class == M::class } } content { message.any { it is M } }
/** /**
* 如果消息内容包含 [M] 类型的 [Message], 就执行 [onEvent] * 如果消息内容包含 [M] 类型的 [Message], 就执行 [onEvent]
*/ */
@MessageDsl @MessageDsl
inline fun <reified M : Message> has(crossinline onEvent: MessageListener<T>): Listener<T> = inline fun <reified M : Message> has(crossinline onEvent: MessageListener<T>): Listener<T> =
content({ message.any { it::class == M::class } }, onEvent) content({ message.any { it is M } }, onEvent)
/** /**
* 如果 [filter] 返回 `true` * 如果 [filter] 返回 `true`

View File

@ -22,11 +22,11 @@ import net.mamoe.mirai.utils.MiraiExperimentalAPI
@Suppress("unused") @Suppress("unused")
class EventCancelledException : RuntimeException { expect class EventCancelledException : RuntimeException {
constructor() : super() constructor()
constructor(message: String?) : super(message) constructor(message: String?)
constructor(message: String?, cause: Throwable?) : super(message, cause) constructor(message: String?, cause: Throwable?)
constructor(cause: Throwable?) : super(cause) constructor(cause: Throwable?)
} }
// note: 若你使用 IntelliJ IDEA, 按 alt + 7 可打开结构 // note: 若你使用 IntelliJ IDEA, 按 alt + 7 可打开结构

View File

@ -13,6 +13,7 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member 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.message.data.At import net.mamoe.mirai.message.data.At
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
@ -31,7 +32,7 @@ class GroupMessage(
val permission: MemberPermission, val permission: MemberPermission,
sender: Member, sender: Member,
override val message: MessageChain override val message: MessageChain
) : MessagePacket<Member, Group>(bot) { ) : MessagePacket<Member, Group>(bot), Event {
val group: Group by group.unsafeWeakRef() val group: Group by group.unsafeWeakRef()
override val sender: Member by sender.unsafeWeakRef() override val sender: Member by sender.unsafeWeakRef()

View File

@ -12,6 +12,7 @@
package net.mamoe.mirai.message.data package net.mamoe.mirai.message.data
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.groupCardOrNick
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
@ -22,7 +23,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
*/ */
class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message { class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message {
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
constructor(member: Member) : this(member.id, "@${member.nick}") constructor(member: Member) : this(member.id, "@${member.groupCardOrNick}")
override fun toString(): String = display override fun toString(): String = display

View File

@ -44,6 +44,7 @@ internal val factory: BotFactory = run {
/** /**
* 加载现有协议的 [BotFactory], 并使用指定的 [配置][configuration] 构造 [Bot] 实例 * 加载现有协议的 [BotFactory], 并使用指定的 [配置][configuration] 构造 [Bot] 实例
*/ */
@JvmOverloads
fun Bot(context: Context, qq: Long, password: String, configuration: BotConfiguration = BotConfiguration.Default): Bot = fun Bot(context: Context, qq: Long, password: String, configuration: BotConfiguration = BotConfiguration.Default): Bot =
factory.Bot(context, qq, password, configuration) factory.Bot(context, qq, password, configuration)
@ -57,6 +58,7 @@ inline fun Bot(context: Context, qq: Long, password: String, configuration: (Bot
/** /**
* 加载现有协议的 [BotFactory], 并使用指定的 [配置][configuration] 构造 [Bot] 实例 * 加载现有协议的 [BotFactory], 并使用指定的 [配置][configuration] 构造 [Bot] 实例
*/ */
@JvmOverloads
fun Bot(qq: Long, password: String, configuration: BotConfiguration = BotConfiguration.Default): Bot = fun Bot(qq: Long, password: String, configuration: BotConfiguration = BotConfiguration.Default): Bot =
factory.Bot(ContextImpl(), qq, password, configuration) factory.Bot(ContextImpl(), qq, password, configuration)

View File

@ -0,0 +1,9 @@
package net.mamoe.mirai.contact
/**
* 权限不足
*/ // 不要删除多平台结构
actual class PermissionDeniedException : IllegalStateException {
actual constructor() : super("Permission denied")
actual constructor(message: String?) : super(message)
}

View File

@ -0,0 +1,12 @@
package net.mamoe.mirai.event.events
// 不要删除跨平台结构.
// 否则在 Java 中这个 class 不会被认为是 java.lang.RuntimeException (Kotlin bug)
@Suppress("unused")
actual class EventCancelledException : RuntimeException {
actual constructor() : super()
actual constructor(message: String?) : super(message)
actual constructor(message: String?, cause: Throwable?) : super(message, cause)
actual constructor(cause: Throwable?) : super(cause)
}

View File

@ -62,7 +62,7 @@ internal class DefaultLoginSolver : LoginSolver() {
} }
} }
bot.logger.info("请输入 4 位字母验证码. 若要更换验证码, 请直接回车") bot.logger.info("请输入 4 位字母验证码. 若要更换验证码, 请直接回车")
return readLine()?.takeUnless { it.isEmpty() || it.length != 4 }.also { return readLine()!!.takeUnless { it.isEmpty() || it.length != 4 }.also {
bot.logger.info("正在提交[$it]中...") bot.logger.info("正在提交[$it]中...")
} }
} }

View File

@ -0,0 +1,18 @@
apply plugin: "java"
apply plugin: "kotlin"
dependencies {
implementation files("../../mirai-core/build/classes/kotlin/jvm/main") // IDE bug
implementation files("../../mirai-core-qqandroid/build/classes/kotlin/jvm/main") // IDE bug
implementation project(":mirai-core-qqandroid")
implementation project(":mirai-japt")
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'

View File

@ -0,0 +1,28 @@
package demo;
import net.mamoe.mirai.japt.BlockingBot;
import net.mamoe.mirai.japt.BlockingContacts;
import net.mamoe.mirai.japt.BlockingQQ;
import net.mamoe.mirai.japt.Events;
import net.mamoe.mirai.message.GroupMessage;
class BlockingTest {
public static void main(String[] args) throws InterruptedException {
BlockingBot bot = BlockingBot.newInstance(123456, "");
bot.login();
bot.getFriendList().forEach(friend -> {
System.out.println(friend.getNick());
});
Events.subscribeAlways(GroupMessage.class, (GroupMessage message) -> {
final BlockingQQ sender = BlockingContacts.createBlocking(message.getSender());
sender.sendMessage("Hello");
});
Thread.sleep(999999999);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -3,7 +3,51 @@
Mirai Java Apt Mirai Java Apt
提供一些阻塞/异步/RxJava API 来让 Java 调用 Mirai 的挂起函数 API 更容易 提供阻塞API 来让 Java 调用 Mirai 的 API 更容易
提供 Utils 类来让 Java 调用 Mirai 的内联方法更容易
该模块暂未完成. ## Requirements
- JDK 1.8+
## 开始
```java
class Test{
public static void main(String[] args){
BlockingBot bot = BlockingBot.newInstance(123456, "");
bot.login();
bot.getFriendList().forEach(friend -> {
System.out.println(friend.getNick());
});
Events.subscribeAlways(GroupMessage.class, (GroupMessage message) -> {
final BlockingQQ sender = BlockingContacts.createBlocking(message.getSender());
sender.sendMessage("Hello");
});
Thread.sleep(999999999);
}
}
```
## 便捷开发
在 IntelliJ IDEA 或 Android Studio 中找到设置 `Editor -> General -> Postfix Completion`, 添加一个设置到 `Java` 分类中:
![](.README_images/ce3034e3.png)
Applicable expression types:
```
net.mamoe.mirai.contact.Contact
```
转换后表达式:
```
net.mamoe.mirai.japt.BlockingContacts.createBlocking($EXPR$)
```
效果:
![4SY8BC@J4ZKQM7OZ_~BC1I_1](.README_images/4SY8BC%40J4ZKQM%5D7OZ_~BC1I_1.png)
![722WEHTTXD6XFFH43](.README_images/722W%28E%24HTTX%7BD6XFFH%5D%5D%2443.png)

View File

@ -1,8 +1,12 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
java java
`maven-publish`
id("com.jfrog.bintray") version "1.8.4-jetbrains-3" // DO NOT CHANGE THIS VERSION UNLESS YOU WANT TO WASTE YOUR TIME
} }
apply(from = rootProject.file("gradle/publish.gradle"))
val kotlinVersion: String by rootProject.ext val kotlinVersion: String by rootProject.ext
val atomicFuVersion: String by rootProject.ext val atomicFuVersion: String by rootProject.ext
val coroutinesVersion: String by rootProject.ext val coroutinesVersion: String by rootProject.ext
@ -13,6 +17,12 @@ val serializationVersion: String by rootProject.ext
val klockVersion: String by rootProject.ext val klockVersion: String by rootProject.ext
val ktorVersion: String by rootProject.ext val ktorVersion: String by rootProject.ext
description = "Java helper for Mirai"
@Suppress("PropertyName")
val mirai_japt_version: String by rootProject.ext
version = mirai_japt_version
kotlin { kotlin {
sourceSets { sourceSets {
all { all {
@ -38,6 +48,8 @@ dependencies {
api(kotlinx("io", kotlinXIoVersion)) api(kotlinx("io", kotlinXIoVersion))
api(kotlinx("coroutines-io", coroutinesIoVersion)) api(kotlinx("coroutines-io", coroutinesIoVersion))
api(kotlinx("coroutines-core", coroutinesVersion)) api(kotlinx("coroutines-core", coroutinesVersion))
api(kotlin("stdlib-jdk7", kotlinVersion))
api(kotlin("stdlib-jdk8", kotlinVersion))
} }
tasks.withType<JavaCompile>() { tasks.withType<JavaCompile>() {

View File

@ -1,21 +1,57 @@
package net.mamoe.mirai.japt; package net.mamoe.mirai.japt;
import kotlinx.io.core.ByteReadPacket; import kotlinx.io.core.ByteReadPacket;
import net.mamoe.mirai.Bot;
import net.mamoe.mirai.BotAccount; import net.mamoe.mirai.BotAccount;
import net.mamoe.mirai.BotFactoryJvmKt;
import net.mamoe.mirai.contact.QQ;
import net.mamoe.mirai.data.AddFriendResult; import net.mamoe.mirai.data.AddFriendResult;
import net.mamoe.mirai.data.GroupInfo;
import net.mamoe.mirai.data.MemberInfo;
import net.mamoe.mirai.message.data.Image; import net.mamoe.mirai.message.data.Image;
import net.mamoe.mirai.network.BotNetworkHandler; import net.mamoe.mirai.network.BotNetworkHandler;
import net.mamoe.mirai.utils.BotConfiguration;
import net.mamoe.mirai.utils.MiraiExperimentalAPI;
import net.mamoe.mirai.utils.MiraiInternalAPI; import net.mamoe.mirai.utils.MiraiInternalAPI;
import net.mamoe.mirai.utils.MiraiLogger; import net.mamoe.mirai.utils.MiraiLogger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.OutputStream;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Stream;
/**
* {@link Bot} 的阻塞式包装
*
* @see Bot
*/
@SuppressWarnings("unused") @SuppressWarnings("unused")
public interface BlockingBot { public interface BlockingBot {
/**
* 使用默认配置创建一个机器人实例
*
* @param id qq
* @param password 密码
* @return 机器人实例
*/
static BlockingBot newInstance(long id, String password) {
return BlockingContacts.createBlocking(BotFactoryJvmKt.Bot(id, password));
}
// TODO: 2020/2/3 需要更新 /**
* 使用特定配置创建一个机器人实例
*
* @param id qq
* @param password 密码
* @return 机器人实例
*/
static BlockingBot newInstance(long id, String password, BotConfiguration configuration) {
return BlockingContacts.createBlocking(BotFactoryJvmKt.Bot(id, password, configuration));
}
// 要获取 Bot 实例列表, 请前往 BotKt
/** /**
* 账号信息 * 账号信息
@ -29,6 +65,13 @@ public interface BlockingBot {
*/ */
long getUin(); long getUin();
/**
* 获取昵称
*/
@NotNull
@MiraiExperimentalAPI(message = "还未支持")
String getNick();
/** /**
* 日志记录器 * 日志记录器
*/ */
@ -37,27 +80,33 @@ public interface BlockingBot {
// region contacts // region contacts
/**
* 获取自身 QQ 实例
*/
@NotNull
QQ getSelfQQ();
/** /**
* 与这个机器人相关的 QQ 列表. 机器人与 QQ 不一定是好友 * 与这个机器人相关的 QQ 列表. 机器人与 QQ 不一定是好友
*/ */
@NotNull @NotNull
List<BlockingQQ> getQQs(); List<BlockingQQ> getFriendList();
/** /**
* 获取缓存的 QQ 对象. 若没有对应的缓存, 则会线程安全地创建一个. * 获取缓存的 QQ 对象. 若没有对应的缓存, 则会线程安全地创建一个.
*/ */
@NotNull @NotNull
BlockingQQ getQQ(long id); BlockingQQ getFriend(long id);
/** /**
* 与这个机器人相关的群列表. 机器人不一定是群成员. * 与这个机器人相关的群列表. 机器人不一定是群成员.
*/ */
@NotNull @NotNull
List<BlockingGroup> getGroups(); List<BlockingGroup> getGroupList();
/** /**
* 获取缓存的群对象. 若没有对应的缓存, 则会线程安全地创建一个. * 获取缓存的群对象. 若没有对应的缓存, 则会线程安全地创建一个.
* {@code id} 无效, 将会抛出 {@link java.util.NoSuchElementException} * {@code id} 无效, 将会抛出 {@link NoSuchElementException}
*/ */
@NotNull @NotNull
BlockingGroup getGroup(long id); BlockingGroup getGroup(long id);
@ -75,20 +124,48 @@ public interface BlockingBot {
/** /**
* 登录. * 登录.
* <p>
* 最终调用 [net.mamoe.mirai.network.BotNetworkHandler.login]
*/ */
void login(); void login();
/**
* 查询群列表. 返回值前 32 bits uin, 32 bits groupCode
*/
@NotNull
Stream<Long> queryGroupList();
/**
* 查询群资料. 获得的仅为当前时刻的资料.
* 请优先使用 {@link #getGroup(long)} 然后查看群资料.
*/
@NotNull
GroupInfo queryGroupInfo(long groupCode);
/**
* 查询群成员列表.
* 请优先使用 {@link #getGroup(long)} , {@link BlockingGroup#getMembers()} 查看群成员.
* <p>
* 这个函数很慢. 请不要频繁使用.
*/
@NotNull
Stream<MemberInfo> queryGroupMemberList(long groupUin, long groupCode, long ownerId);
// endregion // endregion
// region actions // region actions
@NotNull
byte[] downloadAsByteArray(@NotNull Image image); byte[] downloadAsByteArray(@NotNull Image image);
@NotNull @NotNull
ByteReadPacket download(@NotNull Image image); ByteReadPacket download(@NotNull Image image);
/**
* 下载图片到 {@code outputStream}.
* <p>
* 不会自动关闭 {@code outputStream}
*/
void download(@NotNull Image image, @NotNull OutputStream outputStream);
/** /**
* 添加一个好友 * 添加一个好友
* *

View File

@ -1,34 +1,87 @@
package net.mamoe.mirai.japt; package net.mamoe.mirai.japt;
import net.mamoe.mirai.Bot;
import net.mamoe.mirai.contact.Contact;
import net.mamoe.mirai.contact.Member;
import net.mamoe.mirai.contact.QQ;
import net.mamoe.mirai.event.events.BeforeImageUploadEvent;
import net.mamoe.mirai.event.events.EventCancelledException;
import net.mamoe.mirai.event.events.ImageUploadEvent;
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent;
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent;
import net.mamoe.mirai.message.data.Image;
import net.mamoe.mirai.message.data.Message; import net.mamoe.mirai.message.data.Message;
import net.mamoe.mirai.message.data.MessageChain; import net.mamoe.mirai.message.data.MessageChain;
import net.mamoe.mirai.utils.ExternalImage;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/**
* {@link Contact} 的阻塞式包装.
*/
@SuppressWarnings("unused") @SuppressWarnings("unused")
public interface BlockingContact { public interface BlockingContact {
/** /**
* 这个联系人所属 [Bot] * 这个联系人所属 {@link Bot}
*/ */
@NotNull @NotNull
BlockingBot getBot(); BlockingBot getBot();
/** /**
* 可以是 QQ 号码或者群号码 [GroupId]. * 可以是 QQ 号码或者群号码.
* <p>
* 对于 QQ, {@code uin} {@code id} 是相同的意思.
* 对于 Group, {@code groupCode} {@code id} 是相同的意思.
*/ */
long getId(); long getId();
/** /**
* 向这个对象发送消息. * 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*/ */
void sendMessage(@NotNull MessageChain messages); // kotlin bug
void sendMessage(@NotNull MessageChain messages) throws EventCancelledException, IllegalStateException;
/** /**
* 向这个对象发送消息. * 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*/ */
void sendMessage(@NotNull String message); void sendMessage(@NotNull String message) throws EventCancelledException, IllegalStateException;
/** /**
* 向这个对象发送消息. * 向这个对象发送消息.
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*/ */
void sendMessage(@NotNull Message message); void sendMessage(@NotNull Message message) throws EventCancelledException, IllegalStateException;
/**
* 上传一个图片以备发送.
* 群图片与好友图片在服务器上是通用的, mirai 目前不通用.
*
* @throws EventCancelledException 当发送消息事件被取消
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
*/
Image uploadImage(@NotNull ExternalImage image) throws EventCancelledException;
/**
* 判断 {@code this} {@code other} 是否是相同的类型, 并且 {@link Contact#getId()} 相同.
* <p>
* :
* {@link Contact#getId()} 相同的 {@link Member} {@link QQ}, 他们并不 equals.
* 因为, {@link Member} 含义为群员, 必属于一个群.
* {@link QQ} 含义为一个独立的人, 可以是好友, 也可以是陌生人.
*/
boolean equals(Object other);
} }

View File

@ -8,24 +8,31 @@ import net.mamoe.mirai.japt.internal.BlockingBotImpl;
import net.mamoe.mirai.japt.internal.BlockingGroupImpl; import net.mamoe.mirai.japt.internal.BlockingGroupImpl;
import net.mamoe.mirai.japt.internal.BlockingMemberImpl; import net.mamoe.mirai.japt.internal.BlockingMemberImpl;
import net.mamoe.mirai.japt.internal.BlockingQQImpl; import net.mamoe.mirai.japt.internal.BlockingQQImpl;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/** /**
* 构造阻塞式的联系人. * 构造阻塞式的联系人.
*/ */
public final class BlockingContacts { public final class BlockingContacts {
public static BlockingQQ createBlocking(QQ qq) { @NotNull
return new BlockingQQImpl(qq); public static BlockingQQ createBlocking(@NotNull QQ qq) {
return new BlockingQQImpl(Objects.requireNonNull(qq));
} }
public static BlockingGroup createBlocking(Group group) { @NotNull
return new BlockingGroupImpl(group); public static BlockingGroup createBlocking(@NotNull Group group) {
return new BlockingGroupImpl(Objects.requireNonNull(group));
} }
public static BlockingMember createBlocking(Member member) { @NotNull
return new BlockingMemberImpl(member); public static BlockingMember createBlocking(@NotNull Member member) {
return new BlockingMemberImpl(Objects.requireNonNull(member));
} }
public static BlockingBot createBlocking(Bot bot) { @NotNull
return new BlockingBotImpl(bot); public static BlockingBot createBlocking(@NotNull Bot bot) {
return new BlockingBotImpl(Objects.requireNonNull(bot));
} }
} }

View File

@ -1,13 +1,106 @@
package net.mamoe.mirai.japt; package net.mamoe.mirai.japt;
import net.mamoe.mirai.contact.Group; import net.mamoe.mirai.contact.*;
import net.mamoe.mirai.data.MemberInfo;
import net.mamoe.mirai.event.events.*;
import net.mamoe.mirai.utils.MiraiExperimentalAPI;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public interface BlockingGroup extends BlockingContact { public interface BlockingGroup extends BlockingContact {
/**
* 群名称.
*/
@NotNull
String getName();
/**
* 修改群名称
* 频繁修改可能会被服务器拒绝.
*
* @throws PermissionDeniedException 无权限修改时将会抛出异常
* @see MemberPermissionChangeEvent
*/
void setName(@NotNull String name) throws PermissionDeniedException;
/**
* 入群公告, 没有时为空字符串. (同步事件更新)
*/
@NotNull
String getEntranceAnnouncement();
/**
* 修改入群公告.
*
* @throws PermissionDeniedException 无权限修改时将会抛出异常
* @see GroupEntranceAnnouncementChangeEvent
*/
void setEntranceAnnouncement(@NotNull String announcement) throws PermissionDeniedException;
/**
* 获取全员禁言状态
*
* @return 全员禁言状态. true 为开启
*/
boolean isMuteAll();
/**
* 设置全体禁言
*
* @see GroupMuteAllEvent
*/
void setMuteAll(boolean enabled) throws PermissionDeniedException;
/**
* 获取坦白说状态
*
* @return 坦白说状态, true 为允许
*/
boolean isConfessTalkEnabled();
/**
* 设置坦白说状态
*
* @throws PermissionDeniedException 无权限修改时将会抛出异常
* @see GroupAllowConfessTalkEvent
*/
void setConfessTalk(boolean enabled) throws PermissionDeniedException;
/**
* 获取允许群员邀请好友入群的状态.
*
* @return 允许群员邀请好友入群的状态. `true` 为允许
*/
boolean isAllowMemberInvite();
/**
* 设置允许群员邀请好友入群的状态.
*
* @throws PermissionDeniedException 无权限修改时将会抛出异常
* @see GroupAllowMemberInviteEvent
*/
void setAllowMemberInvite(boolean allow) throws PermissionDeniedException;
/**
* 获取自动加群审批的状态
*/
boolean isAutoApproveEnabled();
/**
* 匿名聊天是否开启
*/
boolean isAnonymousChatEnabled();
/**
* 同为 groupCode, 用户看到的群号码.
*/
@Override
long getId();
/** /**
* 群主 (同步事件更新) * 群主 (同步事件更新)
*/ */
@ -15,16 +108,29 @@ public interface BlockingGroup extends BlockingContact {
BlockingMember getOwner(); BlockingMember getOwner();
/** /**
* 群名称 (同步事件更新) * 机器人被禁言还剩余多少秒
*
* @see BotMuteEvent
* @see GroupKt#isBotMuted
*/ */
@NotNull int getBotMuteRemaining();
String getName();
/** /**
* 入群公告, 没有时为空字符串. (同步事件更新) * 检查机器人是否正处于禁言状态
*/
default boolean isBotMuted() {
int time = getBotMuteRemaining();
return time != 0 && time != 0xFFFFFFFF;
}
/**
* 机器人在这个群里的权限
*
* @see BotGroupPermissionChangeEvent
*/ */
@NotNull @NotNull
String getAnnouncement(); @MiraiExperimentalAPI
MemberPermission getBotPermission();
/** /**
* {@link Group} 实例创建的时候查询一次. 并与事件同步事件更新 * {@link Group} 实例创建的时候查询一次. 并与事件同步事件更新
@ -32,7 +138,7 @@ public interface BlockingGroup extends BlockingContact {
* **注意**: 获得的列表仅为这一时刻的成员列表的镜像. 它将不会被更新 * **注意**: 获得的列表仅为这一时刻的成员列表的镜像. 它将不会被更新
*/ */
@NotNull @NotNull
Map<Long, BlockingMember> getMembers(); List<BlockingMember> getMembers();
/** /**
* 获取群成员. 若此 ID 的成员不存在, 则会抛出 {@link NoSuchElementException} * 获取群成员. 若此 ID 的成员不存在, 则会抛出 {@link NoSuchElementException}
@ -40,11 +146,38 @@ public interface BlockingGroup extends BlockingContact {
@NotNull @NotNull
BlockingMember getMember(long id); BlockingMember getMember(long id);
/**
* 获取群成员. 若此 ID 的成员不存在则返回 null
*/
@Nullable
BlockingMember getMemberOrNull(long id);
/**
* 检查此 id 的群成员是否存在
*/
boolean containsMember(long id);
/** /**
* 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败 * 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败
*/ */
boolean quit(); boolean quit();
/**
* 构造一个 [Member].
* 非特殊情况请不要使用这个函数. 优先使用 [get].
*/
@MiraiExperimentalAPI(message = "dangerous")
@NotNull
Member newMember(@NotNull MemberInfo memberInfo);
@NotNull @NotNull
String toFullString(); String toFullString();
static long calculateGroupUinByGroupCode(long groupCode) {
return Group.Companion.calculateGroupUinByGroupCode(groupCode);
}
static long calculateGroupCodeByGroupUin(long groupUin) {
return Group.Companion.calculateGroupCodeByGroupUin(groupUin);
}
} }

View File

@ -1,10 +1,13 @@
package net.mamoe.mirai.japt; package net.mamoe.mirai.japt;
import kotlin.text.StringsKt;
import net.mamoe.mirai.contact.MemberPermission; import net.mamoe.mirai.contact.MemberPermission;
import net.mamoe.mirai.contact.PermissionDeniedException;
import net.mamoe.mirai.event.events.MemberCardChangeEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public interface BlockingMember { public interface BlockingMember extends BlockingQQ {
/** /**
* 所在的群 * 所在的群
*/ */
@ -17,16 +20,72 @@ public interface BlockingMember {
@NotNull @NotNull
MemberPermission getPermission(); MemberPermission getPermission();
/**
* 群名片. 可能为空.
*/
@NotNull
String getNameCard();
/**
* 修改群名片. 将会触发事件
*
* @throws PermissionDeniedException 无权限修改时
* @see #getGroupCardOrNick() 获取非空群名片或昵称
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件
*/
void setNameCard(@NotNull String nameCard) throws PermissionDeniedException;
/**
* 获取群名片或昵称
*/
@NotNull
default String getGroupCardOrNick() {
String nameCard = this.getNameCard();
if (!StringsKt.isBlank(nameCard)) {
return nameCard;
}
return this.getNick();
}
/** /**
* 禁言 * 禁言
* *
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常. * @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 若机器人无权限禁言这个群成员, 返回 `false` * @throws PermissionDeniedException 无权限修改时
*/ */
boolean mute(int durationSeconds); void mute(int durationSeconds);
/**
* 禁言
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @throws PermissionDeniedException 无权限修改时
*/
default void mute(long durationSeconds) {
mute((int) durationSeconds);
}
/** /**
* 解除禁言 * 解除禁言
*
* @throws PermissionDeniedException 无权限修改时
*/ */
boolean unmute(); void unmute();
/**
* 踢出该成员.
*
* @param message 消息
* @throws PermissionDeniedException 无权限修改时
*/
void kick(@NotNull String message);
/**
* 踢出该成员.
*
* @throws PermissionDeniedException 无权限修改时
*/
default void kick() {
kick("");
}
} }

View File

@ -3,13 +3,30 @@ package net.mamoe.mirai.japt;
import net.mamoe.mirai.data.FriendNameRemark; import net.mamoe.mirai.data.FriendNameRemark;
import net.mamoe.mirai.data.PreviousNameList; import net.mamoe.mirai.data.PreviousNameList;
import net.mamoe.mirai.data.Profile; import net.mamoe.mirai.data.Profile;
import net.mamoe.mirai.utils.MiraiExperimentalAPI;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public interface BlockingQQ extends BlockingContact { public interface BlockingQQ extends BlockingContact {
/**
* 获取 QQ 号码
*
* @return QQ 号码
*/
@Override
long getId();
/**
* 获取昵称
*
* @return 昵称
*/
String getNick();
/** /**
* 查询用户资料 * 查询用户资料
*/ */
@MiraiExperimentalAPI(message = "还未支持")
@NotNull @NotNull
Profile queryProfile(); Profile queryProfile();
@ -20,12 +37,14 @@ public interface BlockingQQ extends BlockingContact {
* - 昵称 * - 昵称
* - 共同群内的群名片 * - 共同群内的群名片
*/ */
@MiraiExperimentalAPI(message = "还未支持")
@NotNull @NotNull
PreviousNameList queryPreviousNameList(); PreviousNameList queryPreviousNameList();
/** /**
* 查询机器人账号给这个人设置的备注 * 查询机器人账号给这个人设置的备注
*/ */
@MiraiExperimentalAPI(message = "还未支持")
@NotNull @NotNull
FriendNameRemark queryRemark(); FriendNameRemark queryRemark();
} }

View File

@ -20,18 +20,46 @@ import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
/**
* 事件处理
*/
public final class Events { public final class Events {
/**
* 监听一个事件, {@code onEvent} 返回 {@link ListeningStatus#STOPPED} 时停止监听.
* 机器人离线后不会停止监听.
*
* @param eventClass 事件类
* @param onEvent 事件处理. 返回 {@link ListeningStatus#LISTENING} 时继续监听.
* @param <E> 事件类型
* @return 事件监听器. 可调用 {@link Listener#complete()} {@link Listener#completeExceptionally(Throwable)} 让监听正常停止或异常停止.
*/
@NotNull @NotNull
public static <E extends Event> Listener<E> subscribe(@NotNull Class<E> eventClass, @NotNull Function<E, ListeningStatus> onEvent) { public static <E extends Event> Listener<E> subscribe(@NotNull Class<E> eventClass, @NotNull Function<E, ListeningStatus> onEvent) {
return EventInternalJvmKt._subscribeEventForJaptOnly(eventClass, GlobalScope.INSTANCE, onEvent); return EventInternalJvmKt._subscribeEventForJaptOnly(eventClass, GlobalScope.INSTANCE, onEvent);
} }
/**
* 监听一个事件, 直到手动停止.
* 机器人离线后不会停止监听.
*
* @param eventClass 事件类
* @param onEvent 事件处理. 返回 {@link ListeningStatus#LISTENING} 时继续监听.
* @param <E> 事件类型
* @return 事件监听器. 可调用 {@link Listener#complete()} {@link Listener#completeExceptionally(Throwable)} 让监听正常停止或异常停止.
*/
@NotNull @NotNull
public static <E extends Event> Listener<E> subscribeAlways(@NotNull Class<E> eventClass, @NotNull Consumer<E> onEvent) { public static <E extends Event> Listener<E> subscribeAlways(@NotNull Class<E> eventClass, @NotNull Consumer<E> onEvent) {
return EventInternalJvmKt._subscribeEventForJaptOnly(eventClass, GlobalScope.INSTANCE, onEvent); return EventInternalJvmKt._subscribeEventForJaptOnly(eventClass, GlobalScope.INSTANCE, onEvent);
} }
/**
* 阻塞地广播一个事件.
*
* @param event 事件
* @param <E> 事件类型
* @return {@code event} 本身
*/
@NotNull @NotNull
public static <E extends Event> E broadcast(@NotNull E event) { public static <E extends Event> E broadcast(@NotNull E event) {
return EventsImplKt.broadcast(event); return EventsImplKt.broadcast(event);

View File

@ -14,34 +14,55 @@ import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.readBytes import kotlinx.io.core.readBytes
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotAccount import net.mamoe.mirai.BotAccount
import net.mamoe.mirai.contact.QQ
import net.mamoe.mirai.data.AddFriendResult import net.mamoe.mirai.data.AddFriendResult
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.japt.BlockingBot import net.mamoe.mirai.japt.BlockingBot
import net.mamoe.mirai.japt.BlockingGroup import net.mamoe.mirai.japt.BlockingGroup
import net.mamoe.mirai.japt.BlockingQQ import net.mamoe.mirai.japt.BlockingQQ
import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.toList import net.mamoe.mirai.utils.toList
import java.io.OutputStream
import java.util.stream.Stream
import kotlin.streams.asStream
internal class BlockingBotImpl(private val bot: Bot) : BlockingBot { internal class BlockingBotImpl(private val bot: Bot) : BlockingBot {
@MiraiInternalAPI @MiraiInternalAPI
override fun getAccount(): BotAccount = bot.account override fun getAccount(): BotAccount = bot.account
override fun getUin(): Long = bot.uin override fun getUin(): Long = bot.uin
override fun getLogger(): MiraiLogger = bot.logger @MiraiExperimentalAPI
@UseExperimental(MiraiInternalAPI::class) override fun getNick(): String = bot.nick
override fun getQQs(): List<BlockingQQ> = bot.qqs.delegate.toList().map { it.blocking() }
override fun getLogger(): MiraiLogger = bot.logger
override fun getSelfQQ(): QQ = bot.selfQQ
override fun queryGroupMemberList(groupUin: Long, groupCode: Long, ownerId: Long): Stream<MemberInfo> =
runBlocking { bot.queryGroupMemberList(groupUin, groupCode, ownerId) }.asStream()
override fun getQQ(id: Long): BlockingQQ = bot.getFriend(id).blocking()
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
override fun getGroups(): List<BlockingGroup> = bot.groups.delegate.toList().map { it.blocking() } override fun getFriendList(): List<BlockingQQ> = bot.qqs.delegate.toList().map { it.blocking() }
override fun getFriend(id: Long): BlockingQQ = bot.getFriend(id).blocking()
override fun queryGroupList(): Stream<Long> = runBlocking { bot.queryGroupList() }.asStream()
@UseExperimental(MiraiInternalAPI::class)
override fun getGroupList(): List<BlockingGroup> = bot.groups.delegate.toList().map { it.blocking() }
override fun queryGroupInfo(code: Long): GroupInfo = runBlocking { bot.queryGroupInfo(code) }
override fun getGroup(id: Long): BlockingGroup = runBlocking { bot.getGroup(id).blocking() } override fun getGroup(id: Long): BlockingGroup = runBlocking { bot.getGroup(id).blocking() }
override fun getNetwork(): BotNetworkHandler = bot.network override fun getNetwork(): BotNetworkHandler = bot.network
override fun login() = runBlocking { bot.login() } override fun login() = runBlocking { bot.login() }
override fun downloadAsByteArray(image: Image): ByteArray = bot.run { runBlocking { image.download().readBytes() } } override fun downloadAsByteArray(image: Image): ByteArray = bot.run { runBlocking { image.download().readBytes() } }
override fun download(image: Image): ByteReadPacket = bot.run { runBlocking { image.download() } } override fun download(image: Image): ByteReadPacket = bot.run { runBlocking { image.download() } }
override fun download(image: Image, outputStream: OutputStream) = bot.run { runBlocking { image.downloadTo(outputStream) } }
override fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult = runBlocking { bot.addFriend(id, message, remark) } override fun addFriend(id: Long, message: String?, remark: String?): AddFriendResult = runBlocking { bot.addFriend(id, message, remark) }
override fun approveFriendAddRequest(id: Long, remark: String?) = runBlocking { bot.approveFriendAddRequest(id, remark) } override fun approveFriendAddRequest(id: Long, remark: String?) = runBlocking { bot.approveFriendAddRequest(id, remark) }
override fun dispose(throwable: Throwable?) = bot.close(throwable) override fun dispose(throwable: Throwable?) = bot.close(throwable)

View File

@ -17,28 +17,36 @@ 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.FriendNameRemark import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.data.PreviousNameList import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.data.Profile import net.mamoe.mirai.data.Profile
import net.mamoe.mirai.japt.BlockingBot import net.mamoe.mirai.japt.BlockingBot
import net.mamoe.mirai.japt.BlockingGroup import net.mamoe.mirai.japt.BlockingGroup
import net.mamoe.mirai.japt.BlockingMember import net.mamoe.mirai.japt.BlockingMember
import net.mamoe.mirai.japt.BlockingQQ import net.mamoe.mirai.japt.BlockingQQ
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.message.data.toChain import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.message.data.toMessage
import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.toList import net.mamoe.mirai.utils.toList
internal class BlockingQQImpl(private val delegate: QQ) : BlockingQQ { internal class BlockingQQImpl(private val delegate: QQ) : BlockingQQ {
override fun getBot(): BlockingBot = delegate.bot.blocking() override fun getBot(): BlockingBot = delegate.bot.blocking()
override fun getId(): Long = delegate.id override fun getId(): Long = delegate.id
override fun getNick(): String = delegate.nick
override fun sendMessage(messages: MessageChain) = runBlocking { delegate.sendMessage(messages) } override fun sendMessage(messages: MessageChain) = runBlocking { delegate.sendMessage(messages) }
override fun sendMessage(message: String) = runBlocking { delegate.sendMessage(message.toMessage().toChain()) } override fun sendMessage(message: String) = runBlocking { delegate.sendMessage(message.toMessage().toChain()) }
override fun sendMessage(message: Message) = runBlocking { delegate.sendMessage(message.toChain()) } override fun sendMessage(message: Message) = runBlocking { delegate.sendMessage(message.toChain()) }
override fun uploadImage(image: ExternalImage): Image = runBlocking { delegate.uploadImage(image) }
@MiraiExperimentalAPI
override fun queryProfile(): Profile = runBlocking { delegate.queryProfile() } override fun queryProfile(): Profile = runBlocking { delegate.queryProfile() }
@MiraiExperimentalAPI
override fun queryPreviousNameList(): PreviousNameList = runBlocking { delegate.queryPreviousNameList() } override fun queryPreviousNameList(): PreviousNameList = runBlocking { delegate.queryPreviousNameList() }
@MiraiExperimentalAPI
override fun queryRemark(): FriendNameRemark = runBlocking { delegate.queryRemark() } override fun queryRemark(): FriendNameRemark = runBlocking { delegate.queryRemark() }
} }
@ -47,22 +55,76 @@ internal class BlockingGroupImpl(private val delegate: Group) : BlockingGroup {
override fun sendMessage(message: String) = runBlocking { delegate.sendMessage(message.toMessage().toChain()) } override fun sendMessage(message: String) = runBlocking { delegate.sendMessage(message.toMessage().toChain()) }
override fun sendMessage(message: Message) = runBlocking { delegate.sendMessage(message.toChain()) } override fun sendMessage(message: Message) = runBlocking { delegate.sendMessage(message.toChain()) }
override fun getOwner(): BlockingMember = delegate.owner.blocking() override fun getOwner(): BlockingMember = delegate.owner.blocking()
@MiraiExperimentalAPI
override fun newMember(memberInfo: MemberInfo): Member = delegate.Member(memberInfo)
override fun uploadImage(image: ExternalImage): Image = runBlocking { delegate.uploadImage(image) }
override fun setEntranceAnnouncement(announcement: String) {
delegate.entranceAnnouncement = announcement
}
override fun getName(): String = delegate.name override fun getName(): String = delegate.name
override fun getId(): Long = delegate.id override fun getId(): Long = delegate.id
@MiraiExperimentalAPI
override fun getBotPermission(): MemberPermission = delegate.botPermission
override fun setConfessTalk(enabled: Boolean) {
delegate.isConfessTalkEnabled = enabled
}
override fun isAnonymousChatEnabled(): Boolean = delegate.isAnonymousChatEnabled
override fun isAutoApproveEnabled(): Boolean = delegate.isAutoApproveEnabled
override fun isConfessTalkEnabled(): Boolean = delegate.isConfessTalkEnabled
override fun toFullString(): String = delegate.toFullString() override fun toFullString(): String = delegate.toFullString()
override fun containsMember(id: Long): Boolean = delegate.contains(id)
override fun isAllowMemberInvite(): Boolean = delegate.isAllowMemberInvite
override fun getMember(id: Long): BlockingMember = delegate[id].blocking() override fun getMember(id: Long): BlockingMember = delegate[id].blocking()
override fun getBot(): BlockingBot = delegate.bot.blocking() override fun getBot(): BlockingBot = delegate.bot.blocking()
override fun getAnnouncement(): String = delegate.entranceAnnouncement override fun getBotMuteRemaining(): Int = delegate.botMuteRemaining
override fun isMuteAll(): Boolean = delegate.isMuteAll
override fun setName(name: String) {
delegate.name = name
}
override fun setMuteAll(enabled: Boolean) {
delegate.isMuteAll = enabled
}
override fun getEntranceAnnouncement(): String = delegate.entranceAnnouncement
@UseExperimental(MiraiInternalAPI::class) @UseExperimental(MiraiInternalAPI::class)
override fun getMembers(): Map<Long, BlockingMember> = override fun getMembers(): List<BlockingMember> =
delegate.members.delegate.toList().associateBy { it.id }.mapValues { it.value.blocking() } delegate.members.delegate.toList().map { it.blocking() }
override fun setAllowMemberInvite(allow: Boolean) {
delegate.isAllowMemberInvite = allow
}
override fun getMemberOrNull(id: Long): BlockingMember? {
return delegate.getOrNull(id)?.blocking()
}
override fun quit(): Boolean = runBlocking { delegate.quit() } override fun quit(): Boolean = runBlocking { delegate.quit() }
} }
internal class BlockingMemberImpl(private val delegate: Member) : BlockingMember { internal class BlockingMemberImpl(private val delegate: Member) : BlockingMember, BlockingQQ by (delegate as QQ).blocking() {
override fun getGroup(): BlockingGroup = delegate.group.blocking() override fun getGroup(): BlockingGroup = delegate.group.blocking()
override fun getNameCard(): String = delegate.nameCard
override fun getPermission(): MemberPermission = delegate.permission override fun getPermission(): MemberPermission = delegate.permission
override fun mute(durationSeconds: Int): Boolean = runBlocking { delegate.mute(durationSeconds) } override fun setNameCard(nameCard: String) {
override fun unmute() = runBlocking { delegate.unmute() } delegate.nameCard = nameCard
}
override fun mute(durationSeconds: Int) = runBlocking { delegate.mute(durationSeconds) }
override fun unmute() = runBlocking { delegate.unmute() }
override fun kick(message: String) {
runBlocking { delegate.kick(message) }
}
} }

View File

@ -1,14 +0,0 @@
public class BlockingTest {
public static void main(String[] args) {
//Bot bot = new Bot(new BotAccount(123456, ""), EmptyCoroutineContext.INSTANCE);
//if (bot.getNetwork().login() != LoginResult.) {
// throw IllegalStateException("Login failed")
//}
//bot.getContacts().getGroups();
//var qq = BlockingContacts.createBlocking(bot.getContacts().getQQ(123L))
//println(createBlocking.queryRemark())
}
}

View File

@ -21,6 +21,8 @@ pluginManagement {
rootProject.name = 'mirai' rootProject.name = 'mirai'
include(':mirai-demos')
try { try {
def keyProps = new Properties() def keyProps = new Properties()
def keyFile = file("local.properties") def keyFile = file("local.properties")
@ -46,7 +48,7 @@ include(':mirai-console')
include(':mirai-api-http') include(':mirai-api-http')
include(':mirai-demos:mirai-demo-1') include(':mirai-demos:mirai-demo-1')
include(':mirai-demos:mirai-demo-gentleman') include(':mirai-demos:mirai-demo-gentleman')
include(':mirai-demos') include(':mirai-demos:mirai-demo-java')
include(':mirai-plugins') include(':mirai-plugins')
include(':mirai-plugins:image-sender') include(':mirai-plugins:image-sender')
@ -67,6 +69,7 @@ if (versionPos==-1){
project(':mirai-demos:mirai-demo-1').projectDir = file('mirai-demos/mirai-demo-1') project(':mirai-demos:mirai-demo-1').projectDir = file('mirai-demos/mirai-demo-1')
project(':mirai-demos:mirai-demo-gentleman').projectDir = file('mirai-demos/mirai-demo-gentleman') project(':mirai-demos:mirai-demo-gentleman').projectDir = file('mirai-demos/mirai-demo-gentleman')
project(':mirai-demos:mirai-demo-java').projectDir = file('mirai-demos/mirai-demo-java')
project(':mirai-plugins:image-sender').projectDir = file('mirai-plugins/image-sender') project(':mirai-plugins:image-sender').projectDir = file('mirai-plugins/image-sender')
enableFeaturePreview('GRADLE_METADATA') enableFeaturePreview('GRADLE_METADATA')