1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-03-26 07:20:09 +08:00

Add Java-friendly APIs for Contacts

This commit is contained in:
Him188 2020-03-01 20:13:52 +08:00
parent 422d84d150
commit b7fa7adf99
25 changed files with 2159 additions and 150 deletions

View File

@ -35,22 +35,6 @@ import net.mamoe.mirai.utils.io.toUHexString
import kotlin.coroutines.CoroutineContext
import net.mamoe.mirai.qqandroid.network.protocol.data.jce.FriendInfo as JceFriendInfo
internal abstract class ContactImpl : Contact {
override fun hashCode(): Int {
var result = bot.hashCode()
result = 31 * result + id.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
@Suppress("DuplicatedCode")
if (this === other) return true
if (other !is Contact) return false
if (this::class != other::class) return false
return this.id == other.id && this.bot == other.bot
}
}
internal inline class FriendInfoImpl(
private val jceFriendInfo: JceFriendInfo
) : FriendInfo {
@ -63,7 +47,7 @@ internal class QQImpl(
override val coroutineContext: CoroutineContext,
override val id: Long,
private val friendInfo: FriendInfo
) : ContactImpl(), QQ {
) : QQ() {
override val bot: QQAndroidBot by bot.unsafeWeakRef()
override val nick: String
get() = friendInfo.nick
@ -159,7 +143,21 @@ internal class QQImpl(
}
} finally {
(image.input as? Closeable)?.close()
(image.input as? io.ktor.utils.io.core.Closeable)?.close()
(image.input as? Closeable)?.close()
}
override fun hashCode(): Int {
var result = bot.hashCode()
result = 31 * result + id.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
@Suppress("DuplicatedCode")
if (this === other) return true
if (other !is Contact) return false
if (this::class != other::class) return false
return this.id == other.id && this.bot == other.bot
}
@MiraiExperimentalAPI
@ -187,10 +185,26 @@ internal class MemberImpl(
group: GroupImpl,
override val coroutineContext: CoroutineContext,
memberInfo: MemberInfo
) : ContactImpl(), Member, QQ by qq {
) : Member() {
override val group: GroupImpl by group.unsafeWeakRef()
val qq: QQImpl by qq.unsafeWeakRef()
// region QQ delegate
override val id: Long = qq.id
override val nick: String = qq.nick
@MiraiExperimentalAPI
override suspend fun queryProfile(): Profile = qq.queryProfile()
@MiraiExperimentalAPI
override suspend fun queryPreviousNameList(): PreviousNameList = qq.queryPreviousNameList()
@MiraiExperimentalAPI
override suspend fun queryRemark(): FriendNameRemark = qq.queryRemark()
override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ> = qq.sendMessage(message)
override suspend fun uploadImage(image: ExternalImage): Image = qq.uploadImage(image)
// endregion
override var permission: MemberPermission = memberInfo.permission
@Suppress("PropertyName")
internal var _nameCard: String = memberInfo.nameCard
@ -307,10 +321,9 @@ internal class MemberImpl(
return result
}
@Suppress("DuplicatedCode")
override fun equals(other: Any?): Boolean { // 不要删除. trust me
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Member) return false
if (other !is Contact) return false
if (this::class != other::class) return false
return this.id == other.id && this.bot == other.bot
}
@ -347,7 +360,7 @@ internal class GroupImpl(
override val id: Long,
groupInfo: GroupInfo,
members: Sequence<MemberInfo>
) : ContactImpl(), Group {
) : Group() {
override val bot: QQAndroidBot by bot.unsafeWeakRef()
val uin: Long = groupInfo.uin
@ -398,11 +411,11 @@ internal class GroupImpl(
}.toLockFreeLinkedList())
internal var _name: String = groupInfo.name
internal var _announcement: String = groupInfo.memo
internal var _allowMemberInvite: Boolean = groupInfo.allowMemberInvite
private var _announcement: String = groupInfo.memo
private var _allowMemberInvite: Boolean = groupInfo.allowMemberInvite
internal var _confessTalk: Boolean = groupInfo.confessTalk
internal var _muteAll: Boolean = groupInfo.muteAll
internal var _autoApprove: Boolean = groupInfo.autoApprove
private var _autoApprove: Boolean = groupInfo.autoApprove
internal var _anonymousChat: Boolean = groupInfo.allowAnonymousChat
override var name: String
@ -666,10 +679,23 @@ internal class GroupImpl(
}
} finally {
(image.input as? Closeable)?.close()
(image.input as? io.ktor.utils.io.core.Closeable)?.close()
}
override fun toString(): String {
return "Group($id)"
}
override fun hashCode(): Int {
var result = bot.hashCode()
result = 31 * result + id.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
@Suppress("DuplicatedCode", "DuplicatedCode")
if (this === other) return true
if (other !is Contact) return false
if (this::class != other::class) return false
return this.id == other.id && this.bot == other.bot
}
}

View File

@ -169,9 +169,9 @@ actual abstract class BotJavaHappyAPI actual constructor() {
}
}
// !!! 不要 crossinline, 会编译失败
// !! 不要 crossinline, 会编译失败
@UseExperimental(ExperimentalCoroutinesApi::class)
private fun <R> Bot.future(block: suspend Bot.() -> R): Future<R> {
internal fun <R, C : CoroutineScope> C.future(block: suspend C.() -> R): Future<R> {
val future = object : Future<R> {
val value: CompletableDeferred<R> = CompletableDeferred()

View File

@ -0,0 +1,96 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaHappyAPI
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.MessageReceipt
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.id
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.OverFileSizeMaxException
import net.mamoe.mirai.utils.WeakRefProperty
/**
* 联系人. 虽然叫做联系人, 但他的子类有 [QQ] [][Group].
*
* @author Him188moe
*/
@UseExperimental(MiraiInternalAPI::class, JavaHappyAPI::class)
actual abstract class Contact : CoroutineScope, ContactJavaHappyAPI() {
/**
* 这个联系人所属 [Bot].
*/
@WeakRefProperty
actual abstract val bot: Bot
/**
* 可以是 QQ 号码或者群号码.
*
* 对于 [QQ], `uin` `id` 是相同的意思.
* 对于 [Group], `groupCode` `id` 是相同的意思.
*
* @see QQ.id
* @see Group.id
*/
actual abstract val id: Long
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. [引用回复][MessageReceipt.quote]仅群聊 [撤回][MessageReceipt.recall] 这条消息.
*/
actual abstract suspend fun sendMessage(message: MessageChain): MessageReceipt<out Contact>
/**
* 上传一个图片以备发送.
*
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
*/
actual abstract suspend fun uploadImage(image: ExternalImage): Image
/**
* 判断 `this` [other] 是否是相同的类型, 并且 [id] 相同.
*
* :
* [id] 相同的 [Member] [QQ], 他们并不 [equals].
* 因为, [Member] 含义为群员, 必属于一个群.
* [QQ] 含义为一个独立的人, 可以是好友, 也可以是陌生人.
*/
actual abstract override fun equals(other: Any?): Boolean
/**
* @return `bot.hashCode() * 31 + id.hashCode()`
*/
actual abstract override fun hashCode(): Int
/**
* @return "QQ($id)" or "Group($id)" or "Member($id)"
*/
actual abstract override fun toString(): String
}

View File

@ -0,0 +1,340 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.contact
import android.graphics.Bitmap
import io.ktor.utils.io.core.Input
import kotlinx.coroutines.Dispatchers
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaHappyAPI
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.future
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.uploadImage
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.OverFileSizeMaxException
import java.io.File
import java.io.InputStream
import java.net.URL
import java.util.concurrent.Future
@MiraiInternalAPI
@JavaHappyAPI
@Suppress("INAPPLICABLE_JVM_NAME", "FunctionName", "unused")
actual abstract class ContactJavaHappyAPI {
private inline fun <R> runBlocking(crossinline block: suspend Contact.() -> R): R {
@Suppress("CAST_NEVER_SUCCEEDS")
return kotlinx.coroutines.runBlocking { block(this@ContactJavaHappyAPI as Contact) }
}
private inline fun <R> future(crossinline block: suspend Contact.() -> R): Future<R> {
@Suppress("CAST_NEVER_SUCCEEDS")
return (this as Contact).run { future { block() } }
}
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. [引用回复][MessageReceipt.quote]仅群聊 [撤回][MessageReceipt.recall] 这条消息.
*/
@Throws(EventCancelledException::class, IllegalStateException::class)
@JvmName("sendMessage")
open fun __sendMessageBlockingForJava__(message: Message) {
runBlocking { sendMessage(message) }
}
@JvmName("sendMessage")
open fun __sendMessageBlockingForJava__(message: String) {
runBlocking { sendMessage(message) }
}
/**
* 上传一个图片以备发送.
*
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: ExternalImage) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: URL) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: InputStream) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: Input) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中将文件作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: File) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中将图片上传, 但不发送. 不会保存临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: Bitmap) {
runBlocking { uploadImage(image) }
}
/**
* 发送消息
* @see Contact.sendMessage
*/
@JvmName("sendMessageAsync")
open fun __sendMessageAsyncForJava__(message: Message): Future<MessageReceipt<Contact>> {
return future { sendMessage(message) }
}
/**
* 发送消息
* @see Contact.sendMessage
*/
@JvmName("sendMessageAsync")
open fun __sendMessageAsyncForJava__(message: String): Future<MessageReceipt<Contact>> {
return future { sendMessage(message) }
}
/**
* 上传一个图片以备发送.
*
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: ExternalImage): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传, 但不发送
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: URL): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: InputStream): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传, 但不发送
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: Input): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中将文件作为图片上传, 但不发送
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: File): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中将图片上传, 但不发送. 不会保存临时文件
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: Bitmap): Future<Image> {
return future { uploadImage(image) }
}
}
@Suppress("INAPPLICABLE_JVM_NAME", "FunctionName", "unused", "unused")
@MiraiInternalAPI
@JavaHappyAPI
actual abstract class MemberJavaHappyAPI : QQ() {
private inline fun <R> runBlocking(crossinline block: suspend Member.() -> R): R {
@Suppress("CAST_NEVER_SUCCEEDS")
return kotlinx.coroutines.runBlocking { block(this@MemberJavaHappyAPI as Member) }
}
private inline fun <R> future(crossinline block: suspend Member.() -> R): Future<R> {
@Suppress("CAST_NEVER_SUCCEEDS")
return (this as Member).run { future { block() } }
}
/**
* 禁言.
*
* QQ 中最小操作和显示的时间都是一分钟.
* 机器人可以实现精确到秒, 会被客户端显示为 1 分钟但不影响实际禁言时间.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 机器人无权限时返回 `false`
*
* @see Int.minutesToSeconds
* @see Int.hoursToSeconds
* @see Int.daysToSeconds
*
* @see MemberMuteEvent 成员被禁言事件
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("mute")
open fun __muteBlockingForJava__(durationSeconds: Int) {
runBlocking { mute(durationSeconds) }
}
/**
* 解除禁言.
*
* 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言.
*
* @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("unmute")
open fun __unmuteBlockingForJava__() {
runBlocking { unmute() }
}
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kick")
open fun __kickBlockingForJava__(message: String) {
runBlocking { kick() }
}
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kick")
open fun __kickBlockingForJava__() = __kickBlockingForJava__("")
/**
* 禁言.
*
* QQ 中最小操作和显示的时间都是一分钟.
* 机器人可以实现精确到秒, 会被客户端显示为 1 分钟但不影响实际禁言时间.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 机器人无权限时返回 `false`
*
* @see Int.minutesToSeconds
* @see Int.hoursToSeconds
* @see Int.daysToSeconds
*
* @see MemberMuteEvent 成员被禁言事件
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("muteAsync")
open fun __muteAsyncForJava__(durationSeconds: Int): Future<Unit> {
return future { mute(durationSeconds) }
}
/**
* 解除禁言.
*
* 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言.
*
* @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("unmuteAsync")
open fun __unmuteAsyncForJava__(): Future<Unit> {
return future { unmute() }
}
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kickAsync")
open fun __kickAsyncForJava__(message: String): Future<Unit> {
return future { kick() }
}
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kickAsync")
open fun __kickAsyncForJava__(): Future<Unit> = __kickAsyncForJava__("")
}

View File

@ -0,0 +1,208 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiExperimentalAPI
/**
* . QQ Android 中叫做 "Troop"
*/
actual abstract class Group : Contact(), CoroutineScope {
/**
* 群名称.
*
* 在修改时将会异步上传至服务器.
* 频繁修改可能会被服务器拒绝.
*
* @see MemberPermissionChangeEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var name: String
/**
* 入群公告, 没有时为空字符串.
*
* 在修改时将会异步上传至服务器.
*
* @see GroupEntranceAnnouncementChangeEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var entranceAnnouncement: String
/**
* 全体禁言状态. `true` 为开启.
*
* 当前仅能修改状态.
*
* @see GroupMuteAllEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var isMuteAll: Boolean
/**
* 坦白说状态. `true` 为允许.
*
* 在修改时将会异步上传至服务器.
*
* @see GroupAllowConfessTalkEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var isConfessTalkEnabled: Boolean
/**
* 允许群员邀请好友入群的状态. `true` 为允许
*
* 在修改时将会异步上传至服务器.
*
* @see GroupAllowMemberInviteEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var isAllowMemberInvite: Boolean
/**
* 自动加群审批
*/
actual abstract val isAutoApproveEnabled: Boolean
/**
* 匿名聊天
*/
actual abstract val isAnonymousChatEnabled: Boolean
/**
* 同为 groupCode, 用户看到的群号码.
*/
actual abstract override val id: Long
/**
* 群主.
*
* @return 若机器人是群主, 返回 [botAsMember]. 否则返回相应的成员
*/
actual abstract val owner: Member
/**
* [Bot] 在群内的 [Member] 实例
*/
@MiraiExperimentalAPI
actual abstract val botAsMember: Member
/**
* 机器人被禁言还剩余多少秒
*
* @see BotMuteEvent 机器人被禁言事件
* @see isBotMuted 判断机器人是否正在被禁言
*/
actual abstract val botMuteRemaining: Int
/**
* 机器人在这个群里的权限
*
* @see Group.checkBotPermission 检查 [Bot] 在这个群里的权限
* @see Group.checkBotPermissionOperator 要求 [Bot] 在这个群里的权限为 [管理员或群主][MemberPermission.isOperator]
*
* @see BotGroupPermissionChangeEvent 机器人群员修改
*/
actual abstract val botPermission: MemberPermission
/**
* 群头像下载链接.
*/
actual val avatarUrl: String
get() = "https://p.qlogo.cn/gh/$id/${id}_1/640"
/**
* 群成员列表, 不含机器人自己, 含群主.
* [Group] 实例创建的时候查询一次. 并与事件同步事件更新
*/
actual abstract val members: ContactList<Member>
/**
* 获取群成员实例. 不存在时抛出 [kotlin.NoSuchElementException]
*/
actual abstract operator fun get(id: Long): Member
/**
* 获取群成员实例, 不存在则 null
*/
actual abstract fun getOrNull(id: Long): Member?
/**
* 检查此 id 的群成员是否存在
*/
actual abstract operator fun contains(id: Long): Boolean
/**
* 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败
*/
@MiraiExperimentalAPI("还未支持")
actual abstract suspend fun quit(): Boolean
/**
* 构造一个 [Member].
* 非特殊情况请不要使用这个函数. 优先使用 [get].
*/
@JvmName("newMember")
@Suppress("INAPPLICABLE_JVM_NAME", "FunctionName")
@MiraiExperimentalAPI("dangerous")
actual abstract fun Member(memberInfo: MemberInfo): Member
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
actual abstract override suspend fun sendMessage(message: MessageChain): MessageReceipt<Group>
actual companion object {
/**
* by @kar98k
*/
actual fun calculateGroupUinByGroupCode(groupCode: Long): Long {
var left: Long = groupCode / 1000000L
when (left) {
in 0..10 -> left += 202
in 11..19 -> left += 480 - 11
in 20..66 -> left += 2100 - 20
in 67..156 -> left += 2010 - 67
in 157..209 -> left += 2147 - 157
in 210..309 -> left += 4100 - 210
in 310..499 -> left += 3800 - 310
}
return left * 1000000L + groupCode % 1000000L
}
actual fun calculateGroupCodeByGroupUin(groupUin: Long): Long {
var left: Long = groupUin / 1000000L
when (left) {
in 0 + 202..10 + 202 -> left -= 202
in 11 + 480 - 11..19 + 480 - 11 -> left -= 480 - 11
in 20 + 2100 - 20..66 + 2100 - 20 -> left -= 2100 - 20
in 67 + 2010 - 67..156 + 2010 - 67 -> left -= 2010 - 67
in 157 + 2147 - 157..209 + 2147 - 157 -> left -= 2147 - 157
in 210 + 4100 - 210..309 + 4100 - 210 -> left -= 4100 - 210
in 310 + 3800 - 310..499 + 3800 - 310 -> left -= 3800 - 310
}
return left * 1000000L + groupUin % 1000000L
}
}
@MiraiExperimentalAPI
actual fun toFullString(): String {
return "Group(id=${this.id}, name=$name, owner=${owner.id}, members=${members.idContentString})"
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.contact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaHappyAPI
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.WeakRefProperty
/**
* 群成员.
*/
@Suppress("INAPPLICABLE_JVM_NAME")
@UseExperimental(MiraiInternalAPI::class, JavaHappyAPI::class)
actual abstract class Member : MemberJavaHappyAPI() {
/**
* 所在的群.
*/
@WeakRefProperty
actual abstract val group: Group
/**
* 成员的权限, 动态更新.
*
* @see MemberPermissionChangeEvent 权限变更事件. 由群主或机器人的操作触发.
*/
actual abstract val permission: MemberPermission
/**
* 群名片. 可能为空.
*
* 管理员和群主都可修改任何人包括群主的群名片.
*
* 在修改时将会异步上传至服务器.
*
* @see [nameCardOrNick] 获取非空群名片或昵称
*
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时
*/
actual abstract var nameCard: String
/**
* 群头衔.
*
* 仅群主可以修改群头衔.
*
* 在修改时将会异步上传至服务器.
*
* @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时
*/
actual abstract var specialTitle: String
/**
* 被禁言剩余时长. 单位为秒.
*
* @see isMuted 判断改成员是否处于禁言状态
* @see mute 设置禁言
* @see unmute 取消禁言
*/
actual abstract val muteTimeRemaining: Int
/**
* 禁言.
*
* QQ 中最小操作和显示的时间都是一分钟.
* 机器人可以实现精确到秒, 会被客户端显示为 1 分钟但不影响实际禁言时间.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 机器人无权限时返回 `false`
*
* @see Int.minutesToSeconds
* @see Int.hoursToSeconds
* @see Int.daysToSeconds
*
* @see MemberMuteEvent 成员被禁言事件
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("muteSuspend")
@JvmSynthetic
actual abstract suspend fun mute(durationSeconds: Int)
/**
* 解除禁言.
*
* 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言.
*
* @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("unmuteSuspend")
@JvmSynthetic
actual abstract suspend fun unmute()
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kickSuspend")
@JvmSynthetic
actual abstract suspend fun kick(message: String)
/**
* 当且仅当 `[other] is [Member] && [other].id == this.id && [other].group == this.group` 时为 true
*/
actual abstract override fun equals(other: Any?): Boolean
/**
* @return `bot.hashCode() * 31 + id.hashCode()`
*/
actual abstract override fun hashCode(): Int
}

View File

@ -0,0 +1,87 @@
@file:Suppress("unused")
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.data.Profile
import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiExperimentalAPI
/**
* QQ 对象.
* 注意: 一个 [QQ] 实例并不是独立的, 它属于一个 [Bot].
* 它不能被直接构造. 任何时候都应从 [Bot.getFriend] 或事件中获取.
*
* 对于同一个 [Bot] 任何一个人的 [QQ] 实例都是单一的.
*
* A QQ instance helps you to receive event from or sendPacket event to.
* Notice that, one QQ instance belong to one [Bot], that is, QQ instances from different [Bot] are NOT the same.
*
* @author Him188moe
*/
@Suppress("INAPPLICABLE_JVM_NAME")
actual abstract class QQ : Contact(), CoroutineScope {
/**
* 请求头像下载链接
*/
// @MiraiExperimentalAPI
//suspend fun queryAvatar(): AvatarLink
/**
* QQ 号码
*/
actual abstract override val id: Long
/**
* 昵称
*/
actual abstract val nick: String
/**
* 查询用户资料
*/
@MiraiExperimentalAPI("还未支持")
actual abstract suspend fun queryProfile(): Profile
/**
* 头像下载链接
*/
actual val avatarUrl: String
get() = "http://q1.qlogo.cn/g?b=qq&nk=$id&s=640"
/**
* 查询曾用名.
*
* 曾用名可能是:
* - 昵称
* - 共同群内的群名片
*/
@MiraiExperimentalAPI("还未支持")
actual abstract suspend fun queryPreviousNameList(): PreviousNameList
/**
* 查询机器人账号给这个人设置的备注
*/
@MiraiExperimentalAPI("还未支持")
actual abstract suspend fun queryRemark(): FriendNameRemark
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
@JvmName("sendMessageSuspend")
@JvmSynthetic
actual abstract override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ>
}

View File

@ -0,0 +1,196 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.message
import android.graphics.Bitmap
import io.ktor.utils.io.core.Input
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.utils.OverFileSizeMaxException
import net.mamoe.mirai.utils.sendTo
import net.mamoe.mirai.utils.toExternalImage
import net.mamoe.mirai.utils.upload
import java.io.File
import java.io.InputStream
import java.net.URL
/*
* 发送图片的一些扩展函数.
*/
// region IMAGE.sendAsImageTo(Contact)
/**
* [Dispatchers.IO] 中将图片发送到指定联系人. 不会创建临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Bitmap.sendTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun URL.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Input.sendAsImageTo(contact: Contact) = withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun InputStream.sendAsImageTo(contact: Contact) =
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
/**
* [Dispatchers.IO] 中将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun File.sendAsImageTo(contact: Contact) {
require(this.exists() && this.canRead())
withContext(Dispatchers.IO) { toExternalImage() }.sendTo(contact)
}
// endregion
// region IMAGE.Upload(Contact): Image
/**
* [Dispatchers.IO] 中将图片上传后构造 [Image]. 不会创建临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Bitmap.upload(contact: Contact): Image = withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun URL.uploadAsImage(contact: Contact): Image =
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun Input.uploadAsImage(contact: Contact): Image =
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun InputStream.uploadAsImage(contact: Contact): Image =
withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
/**
* [Dispatchers.IO] 中将文件作为图片上传后构造 [Image]
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend fun File.uploadAsImage(contact: Contact): Image {
require(this.exists() && this.canRead())
return withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
}
// endregion
// region Contact.sendImage(IMAGE)
/**
* [Dispatchers.IO] 中将图片发送到指定联系人. 不会保存临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.sendImage(bufferedImage: Bitmap) = bufferedImage.sendTo(this)
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.sendImage(imageUrl: URL) = imageUrl.sendAsImageTo(this)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.sendImage(imageInput: Input) = imageInput.sendAsImageTo(this)
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.sendImage(imageStream: InputStream) = imageStream.sendAsImageTo(this)
/**
* [Dispatchers.IO] 中将文件作为图片发送到指定联系人
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.sendImage(file: File) = file.sendAsImageTo(this)
// endregion
// region Contact.uploadImage(IMAGE)
/**
* [Dispatchers.IO] 中将图片上传, 但不发送. 不会保存临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(bufferedImage: Bitmap): Image = bufferedImage.upload(this)
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(imageUrl: URL): Image = imageUrl.uploadAsImage(this)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(imageInput: Input): Image = imageInput.uploadAsImage(this)
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(imageStream: InputStream): Image = imageStream.uploadAsImage(this)
/**
* [Dispatchers.IO] 中将文件作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(file: File): Image = file.uploadAsImage(this)
// endregion

View File

@ -7,15 +7,16 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE")
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
package net.mamoe.mirai.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.core.copyTo
import io.ktor.utils.io.errors.IOException
import io.ktor.utils.io.streams.asInput
import io.ktor.utils.io.streams.asOutput
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
@ -27,6 +28,17 @@ import java.net.URL
* 将各类型图片容器转为 [ExternalImage]
*/
/**
* 读取 [BufferedImage] 的属性, 然后构造 [ExternalImage]
*/
@Throws(IOException::class)
fun Bitmap.toExternalImage(formatName: String = "gif"): Nothing {
TODO()
}
// suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
/**
* 读取文件头识别图片属性, 然后构造 [ExternalImage]
*/
@ -48,8 +60,7 @@ fun File.toExternalImage(): ExternalImage {
/**
* [IO] 中进行 [File.toExternalImage]
*/
@Suppress("unused")
suspend fun File.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
suspend inline fun File.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
/**
* 下载文件到临时目录然后调用 [File.toExternalImage]
@ -61,6 +72,7 @@ fun URL.toExternalImage(): ExternalImage {
openStream().use { input ->
input.copyTo(output)
}
output.flush()
}
return file.toExternalImage()
}
@ -68,8 +80,7 @@ fun URL.toExternalImage(): ExternalImage {
/**
* [IO] 中进行 [URL.toExternalImage]
*/
@Suppress("unused")
suspend fun URL.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
suspend inline fun URL.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
/**
* 保存为临时文件然后调用 [File.toExternalImage]
@ -77,8 +88,9 @@ suspend fun URL.suspendToExternalImage(): ExternalImage = withContext(IO) { toEx
@Throws(IOException::class)
fun InputStream.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() }
file.outputStream().asOutput().use {
this.asInput().copyTo(it)
file.outputStream().use {
this.copyTo(it)
it.flush()
}
this.close()
return file.toExternalImage()
@ -87,17 +99,19 @@ fun InputStream.toExternalImage(): ExternalImage {
/**
* [IO] 中进行 [InputStream.toExternalImage]
*/
@Suppress("unused")
suspend fun InputStream.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
suspend inline fun InputStream.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
/**
* 保存为临时文件然后调用 [File.toExternalImage]
* 保存为临时文件然后调用 [File.toExternalImage].
*
* 需要函数调用者 close [this]
*/
@Throws(IOException::class)
fun Input.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() }
file.outputStream().asOutput().use {
this.copyTo(it)
it.flush()
}
return file.toExternalImage()
}
@ -105,5 +119,17 @@ fun Input.toExternalImage(): ExternalImage {
/**
* [IO] 中进行 [Input.toExternalImage]
*/
@Suppress("unused")
suspend fun Input.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
suspend inline fun Input.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
/**
* 保存为临时文件然后调用 [File.toExternalImage].
*/
suspend fun ByteReadChannel.toExternalImage(): ExternalImage {
val file = createTempFile().apply { deleteOnExit() }
file.outputStream().use {
withContext(IO) { copyTo(it) }
it.flush()
}
return file.suspendToExternalImage()
}

View File

@ -0,0 +1,6 @@
package net.mamoe.mirai.utils
/**
* 图片文件过大
*/
actual class OverFileSizeMaxException : IllegalStateException()

View File

@ -14,6 +14,7 @@ package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaHappyAPI
import net.mamoe.mirai.event.events.BeforeImageUploadEvent
import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.ImageUploadEvent
@ -23,25 +24,26 @@ import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.recall
import net.mamoe.mirai.recallIn
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.OverFileSizeMaxException
import net.mamoe.mirai.utils.WeakRefProperty
import net.mamoe.mirai.utils.*
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
/**
* 联系人. 虽然叫做联系人, 但他的子类有 [QQ] [][Group].
*
* @author Him188moe
*/
interface Contact : CoroutineScope {
*/ // 不要删除多平台结构 !!! kotlin bug
@UseExperimental(MiraiInternalAPI::class, JavaHappyAPI::class)
@Suppress("INAPPLICABLE_JVM_NAME")
expect abstract class Contact() : CoroutineScope, ContactJavaHappyAPI {
/**
* 这个联系人所属 [Bot].
*/
@WeakRefProperty
val bot: Bot
abstract val bot: Bot
/**
* 可以是 QQ 号码或者群号码.
@ -52,7 +54,7 @@ interface Contact : CoroutineScope {
* @see QQ.id
* @see Group.id
*/
val id: Long
abstract val id: Long
/**
* 向这个对象发送消息.
@ -65,11 +67,12 @@ interface Contact : CoroutineScope {
*
* @return 消息回执. [引用回复][MessageReceipt.quote]仅群聊 [撤回][MessageReceipt.recall] 这条消息.
*/
suspend fun sendMessage(message: MessageChain): MessageReceipt<out Contact>
@JvmName("sendMessageSuspend")
@JvmSynthetic
abstract suspend fun sendMessage(message: MessageChain): MessageReceipt<out Contact>
/**
* 上传一个图片以备发送.
* TODO 群图片与好友图片在服务器上是通用的, mirai 目前不通用.
*
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
@ -77,7 +80,9 @@ interface Contact : CoroutineScope {
* @throws EventCancelledException 当发送消息事件被取消
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
*/
suspend fun uploadImage(image: ExternalImage): Image
@JvmName("uploadImageSuspend")
@JvmSynthetic
abstract suspend fun uploadImage(image: ExternalImage): Image
/**
* 判断 `this` [other] 是否是相同的类型, 并且 [id] 相同.
@ -87,17 +92,17 @@ interface Contact : CoroutineScope {
* 因为, [Member] 含义为群员, 必属于一个群.
* [QQ] 含义为一个独立的人, 可以是好友, 也可以是陌生人.
*/
override fun equals(other: Any?): Boolean
abstract override fun equals(other: Any?): Boolean
/**
* @return `bot.hashCode() * 31 + id.hashCode()`
*/
override fun hashCode(): Int
abstract override fun hashCode(): Int
/**
* @return "QQ($id)" or "Group($id)" or "Member($id)"
*/
override fun toString(): String
abstract override fun toString(): String
}
/**

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE")
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
package net.mamoe.mirai.contact
@ -21,11 +21,13 @@ import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
/**
* . QQ Android 中叫做 "Troop"
*/
interface Group : Contact, CoroutineScope {
@Suppress("INAPPLICABLE_JVM_NAME")
expect abstract class Group() : Contact, CoroutineScope {
/**
* 群名称.
*
@ -35,7 +37,7 @@ interface Group : Contact, CoroutineScope {
* @see MemberPermissionChangeEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
var name: String
abstract var name: String
/**
* 入群公告, 没有时为空字符串.
*
@ -44,7 +46,7 @@ interface Group : Contact, CoroutineScope {
* @see GroupEntranceAnnouncementChangeEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
var entranceAnnouncement: String
abstract var entranceAnnouncement: String
/**
* 全体禁言状态. `true` 为开启.
*
@ -53,7 +55,7 @@ interface Group : Contact, CoroutineScope {
* @see GroupMuteAllEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
var isMuteAll: Boolean
abstract var isMuteAll: Boolean
/**
* 坦白说状态. `true` 为允许.
*
@ -62,7 +64,7 @@ interface Group : Contact, CoroutineScope {
* @see GroupAllowConfessTalkEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
var isConfessTalkEnabled: Boolean
abstract var isConfessTalkEnabled: Boolean
/**
* 允许群员邀请好友入群的状态. `true` 为允许
*
@ -71,33 +73,33 @@ interface Group : Contact, CoroutineScope {
* @see GroupAllowMemberInviteEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
var isAllowMemberInvite: Boolean
abstract var isAllowMemberInvite: Boolean
/**
* 自动加群审批
*/
val isAutoApproveEnabled: Boolean
abstract val isAutoApproveEnabled: Boolean
/**
* 匿名聊天
*/
val isAnonymousChatEnabled: Boolean
abstract val isAnonymousChatEnabled: Boolean
/**
* 同为 groupCode, 用户看到的群号码.
*/
override val id: Long
abstract override val id: Long
/**
* 群主.
*
* @return 若机器人是群主, 返回 [botAsMember]. 否则返回相应的成员
*/
val owner: Member
abstract val owner: Member
/**
* [Bot] 在群内的 [Member] 实例
*/
@MiraiExperimentalAPI
val botAsMember: Member
abstract val botAsMember: Member
/**
* 机器人被禁言还剩余多少秒
@ -105,7 +107,7 @@ interface Group : Contact, CoroutineScope {
* @see BotMuteEvent 机器人被禁言事件
* @see isBotMuted 判断机器人是否正在被禁言
*/
val botMuteRemaining: Int
abstract val botMuteRemaining: Int
/**
* 机器人在这个群里的权限
@ -115,39 +117,41 @@ interface Group : Contact, CoroutineScope {
*
* @see BotGroupPermissionChangeEvent 机器人群员修改
*/
val botPermission: MemberPermission
abstract val botPermission: MemberPermission
/**
* 群头像下载链接.
*/
val avatarUrl: String get() = "https://p.qlogo.cn/gh/$id/${id}_1/640"
val avatarUrl: String
/**
* 群成员列表, 不含机器人自己, 含群主.
* [Group] 实例创建的时候查询一次. 并与事件同步事件更新
*/
val members: ContactList<Member>
abstract val members: ContactList<Member>
/**
* 获取群成员实例. 不存在时抛出 [kotlin.NoSuchElementException]
*/
operator fun get(id: Long): Member
abstract operator fun get(id: Long): Member
/**
* 获取群成员实例, 不存在则 null
*/
fun getOrNull(id: Long): Member?
abstract fun getOrNull(id: Long): Member?
/**
* 检查此 id 的群成员是否存在
*/
operator fun contains(id: Long): Boolean
abstract operator fun contains(id: Long): Boolean
/**
* 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败
*/
@JvmName("quitSuspend")
@JvmSynthetic
@MiraiExperimentalAPI("还未支持")
suspend fun quit(): Boolean
abstract suspend fun quit(): Boolean
/**
* 构造一个 [Member].
@ -156,7 +160,7 @@ interface Group : Contact, CoroutineScope {
@MiraiExperimentalAPI("dangerous")
@Suppress("INAPPLICABLE_JVM_NAME", "FunctionName")
@JvmName("newMember")
fun Member(memberInfo: MemberInfo): Member
abstract fun Member(memberInfo: MemberInfo): Member
/**
* 向这个对象发送消息.
@ -169,48 +173,19 @@ interface Group : Contact, CoroutineScope {
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
override suspend fun sendMessage(message: MessageChain): MessageReceipt<Group>
@JvmName("sendMessageSuspend")
@JvmSynthetic
abstract override suspend fun sendMessage(message: MessageChain): MessageReceipt<Group>
companion object {
// don't @JvmStatic: JDK 1.8 required
fun calculateGroupUinByGroupCode(groupCode: Long): Long
/**
* by @kar98k
*/ // don't @JvmStatic: JDK 1.8 required
fun calculateGroupUinByGroupCode(groupCode: Long): Long {
var left: Long = groupCode / 1000000L
when (left) {
in 0..10 -> left += 202
in 11..19 -> left += 480 - 11
in 20..66 -> left += 2100 - 20
in 67..156 -> left += 2010 - 67
in 157..209 -> left += 2147 - 157
in 210..309 -> left += 4100 - 210
in 310..499 -> left += 3800 - 310
}
return left * 1000000L + groupCode % 1000000L
}
fun calculateGroupCodeByGroupUin(groupUin: Long): Long {
var left: Long = groupUin / 1000000L
when (left) {
in 0 + 202..10 + 202 -> left -= 202
in 11 + 480 - 11..19 + 480 - 11 -> left -= 480 - 11
in 20 + 2100 - 20..66 + 2100 - 20 -> left -= 2100 - 20
in 67 + 2010 - 67..156 + 2010 - 67 -> left -= 2010 - 67
in 157 + 2147 - 157..209 + 2147 - 157 -> left -= 2147 - 157
in 210 + 4100 - 210..309 + 4100 - 210 -> left -= 4100 - 210
in 310 + 3800 - 310..499 + 3800 - 310 -> left -= 3800 - 310
}
return left * 1000000L + groupUin % 1000000L
}
fun calculateGroupCodeByGroupUin(groupUin: Long): Long
}
@MiraiExperimentalAPI
fun toFullString(): String = "Group(id=${this.id}, name=$name, owner=${owner.id}, members=${members.idContentString})"
fun toFullString(): String
}
/**

View File

@ -12,27 +12,33 @@
package net.mamoe.mirai.contact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaHappyAPI
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.WeakRefProperty
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
/**
* 群成员.
*/
interface Member : QQ, Contact {
*/ // 不要删除多平台结构, kotlin bug
@Suppress("INAPPLICABLE_JVM_NAME")
@UseExperimental(MiraiInternalAPI::class, JavaHappyAPI::class)
expect abstract class Member() : MemberJavaHappyAPI {
/**
* 所在的群.
*/
@WeakRefProperty
val group: Group
abstract val group: Group
/**
* 成员的权限, 动态更新.
*
* @see MemberPermissionChangeEvent 权限变更事件. 由群主或机器人的操作触发.
*/
val permission: MemberPermission
abstract val permission: MemberPermission
/**
* 群名片. 可能为空.
@ -46,7 +52,7 @@ interface Member : QQ, Contact {
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时
*/
var nameCard: String
abstract var nameCard: String
/**
* 群头衔.
@ -58,7 +64,7 @@ interface Member : QQ, Contact {
* @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时
*/
var specialTitle: String
abstract var specialTitle: String
/**
* 被禁言剩余时长. 单位为秒.
@ -67,7 +73,7 @@ interface Member : QQ, Contact {
* @see mute 设置禁言
* @see unmute 取消禁言
*/
val muteTimeRemaining: Int
abstract val muteTimeRemaining: Int
/**
* 禁言.
@ -87,7 +93,9 @@ interface Member : QQ, Contact {
* @see MemberMuteEvent 成员被禁言事件
* @throws PermissionDeniedException 无权限修改时
*/
suspend fun mute(durationSeconds: Int)
@JvmName("muteSuspend")
@JvmSynthetic
abstract suspend fun mute(durationSeconds: Int)
/**
* 解除禁言.
@ -97,7 +105,9 @@ interface Member : QQ, Contact {
* @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/
suspend fun unmute()
@JvmName("unmuteSuspend")
@JvmSynthetic
abstract suspend fun unmute()
/**
* 踢出该成员.
@ -107,12 +117,19 @@ interface Member : QQ, Contact {
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
suspend fun kick(message: String = "")
@JvmName("kickSuspend")
@JvmSynthetic
abstract suspend fun kick(message: String = "")
/**
* 当且仅当 `[other] is [Member] && [other].id == this.id && [other].group == this.group` 时为 true
*/
override fun equals(other: Any?): Boolean
abstract override fun equals(other: Any?): Boolean
/**
* @return `bot.hashCode() * 31 + id.hashCode()`
*/
abstract override fun hashCode(): Int
}
/**

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("EXPERIMENTAL_API_USAGE")
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused")
package net.mamoe.mirai.contact
@ -22,6 +22,8 @@ import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
/**
* QQ 对象.
@ -35,33 +37,32 @@ import net.mamoe.mirai.utils.MiraiExperimentalAPI
*
* @author Him188moe
*/
interface QQ : Contact, CoroutineScope {
/**
* QQ 号码
*/
override val id: Long
/**
* 昵称
*/
val nick: String
@Suppress("INAPPLICABLE_JVM_NAME")
expect abstract class QQ() : Contact, CoroutineScope {
/**
* 请求头像下载链接
*/
// @MiraiExperimentalAPI
//suspend fun queryAvatar(): AvatarLink
/**
* QQ 号码
*/
abstract override val id: Long
/**
* 昵称
*/
abstract val nick: String
/**
* 查询用户资料
*/
@MiraiExperimentalAPI("还未支持")
suspend fun queryProfile(): Profile
abstract suspend fun queryProfile(): Profile
/**
* 头像下载链接
*/
val avatarUrl: String get() = "http://q1.qlogo.cn/g?b=qq&nk=$id&s=640"
val avatarUrl: String
/**
* 查询曾用名.
@ -71,13 +72,13 @@ interface QQ : Contact, CoroutineScope {
* - 共同群内的群名片
*/
@MiraiExperimentalAPI("还未支持")
suspend fun queryPreviousNameList(): PreviousNameList
abstract suspend fun queryPreviousNameList(): PreviousNameList
/**
* 查询机器人账号给这个人设置的备注
*/
@MiraiExperimentalAPI("还未支持")
suspend fun queryRemark(): FriendNameRemark
abstract suspend fun queryRemark(): FriendNameRemark
/**
* 向这个对象发送消息.
@ -90,5 +91,9 @@ interface QQ : Contact, CoroutineScope {
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ>
@JvmSynthetic
@JvmName("sendMessageSuspend")
abstract override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ>
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.contact
import net.mamoe.mirai.JavaHappyAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
/**
* [Contact] 中为了让 `Java` 更容易调用的 API
*/
@MiraiInternalAPI
@JavaHappyAPI
expect abstract class ContactJavaHappyAPI
/**
* [Member] 中为了让 `Java` 更容易调用的 API
*/
@MiraiInternalAPI
@JavaHappyAPI
expect abstract class MemberJavaHappyAPI : QQ

View File

@ -16,7 +16,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
*/
@MiraiInternalAPI
@Experimental(level = Experimental.Level.ERROR)
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPE, AnnotationTarget.CLASS)
annotation class JavaHappyAPI
/**

View File

@ -17,5 +17,5 @@ import kotlin.jvm.JvmName
/**
* 图片文件过大
*/
class OverFileSizeMaxException : IllegalStateException()
*/ // 不要删除多平台结构, 这是 kotlin 的 bug
expect class OverFileSizeMaxException() : IllegalStateException

View File

@ -170,7 +170,7 @@ actual abstract class BotJavaHappyAPI actual constructor() {
}
@UseExperimental(ExperimentalCoroutinesApi::class)
private fun <R> Bot.future(block: suspend Bot.() -> R): Future<R> {
internal fun <R, C : CoroutineScope> C.future(block: suspend C.() -> R): Future<R> {
val future = object : Future<R> {
val value: CompletableDeferred<R> = CompletableDeferred()

View File

@ -0,0 +1,100 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaHappyAPI
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.MessageReceipt
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.id
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.OverFileSizeMaxException
import net.mamoe.mirai.utils.WeakRefProperty
/**
* 联系人. 虽然叫做联系人, 但他的子类有 [QQ] [][Group].
*
* @author Him188moe
*/
@Suppress("INAPPLICABLE_JVM_NAME")
@UseExperimental(MiraiInternalAPI::class, JavaHappyAPI::class)
actual abstract class Contact : CoroutineScope, ContactJavaHappyAPI() {
/**
* 这个联系人所属 [Bot].
*/
@WeakRefProperty
actual abstract val bot: Bot
/**
* 可以是 QQ 号码或者群号码.
*
* 对于 [QQ], `uin` `id` 是相同的意思.
* 对于 [Group], `groupCode` `id` 是相同的意思.
*
* @see QQ.id
* @see Group.id
*/
actual abstract val id: Long
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. [引用回复][MessageReceipt.quote]仅群聊 [撤回][MessageReceipt.recall] 这条消息.
*/
@JvmName("sendMessageSuspend")
@JvmSynthetic
actual abstract suspend fun sendMessage(message: MessageChain): MessageReceipt<out Contact>
/**
* 上传一个图片以备发送.
*
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
*/
@JvmName("uploadImageSuspend")
@JvmSynthetic
actual abstract suspend fun uploadImage(image: ExternalImage): Image
/**
* 判断 `this` [other] 是否是相同的类型, 并且 [id] 相同.
*
* :
* [id] 相同的 [Member] [QQ], 他们并不 [equals].
* 因为, [Member] 含义为群员, 必属于一个群.
* [QQ] 含义为一个独立的人, 可以是好友, 也可以是陌生人.
*/
actual abstract override fun equals(other: Any?): Boolean
/**
* @return `bot.hashCode() * 31 + id.hashCode()`
*/
actual abstract override fun hashCode(): Int
/**
* @return "QQ($id)" or "Group($id)" or "Member($id)"
*/
actual abstract override fun toString(): String
}

View File

@ -0,0 +1,340 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.contact
import io.ktor.utils.io.core.Input
import kotlinx.coroutines.Dispatchers
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaHappyAPI
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.future
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.uploadImage
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.OverFileSizeMaxException
import java.awt.image.BufferedImage
import java.io.File
import java.io.InputStream
import java.net.URL
import java.util.concurrent.Future
@MiraiInternalAPI
@JavaHappyAPI
@Suppress("INAPPLICABLE_JVM_NAME", "FunctionName", "unused")
actual abstract class ContactJavaHappyAPI {
private inline fun <R> runBlocking(crossinline block: suspend Contact.() -> R): R {
@Suppress("CAST_NEVER_SUCCEEDS")
return kotlinx.coroutines.runBlocking { block(this@ContactJavaHappyAPI as Contact) }
}
private inline fun <R> future(crossinline block: suspend Contact.() -> R): Future<R> {
@Suppress("CAST_NEVER_SUCCEEDS")
return (this as Contact).run { future { block() } }
}
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. [引用回复][MessageReceipt.quote]仅群聊 [撤回][MessageReceipt.recall] 这条消息.
*/
@Throws(EventCancelledException::class, IllegalStateException::class)
@JvmName("sendMessage")
open fun __sendMessageBlockingForJava__(message: Message) {
runBlocking { sendMessage(message) }
}
@JvmName("sendMessage")
open fun __sendMessageBlockingForJava__(message: String) {
runBlocking { sendMessage(message) }
}
/**
* 上传一个图片以备发送.
*
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时. (最大大小约为 20 MB)
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: ExternalImage) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: URL) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: InputStream) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: Input) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中将文件作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: File) {
runBlocking { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中将图片上传, 但不发送. 不会保存临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
@JvmName("uploadImage")
open fun __uploadImageBlockingForJava__(image: BufferedImage) {
runBlocking { uploadImage(image) }
}
/**
* 发送消息
* @see Contact.sendMessage
*/
@JvmName("sendMessageAsync")
open fun __sendMessageAsyncForJava__(message: Message): Future<MessageReceipt<Contact>> {
return future { sendMessage(message) }
}
/**
* 发送消息
* @see Contact.sendMessage
*/
@JvmName("sendMessageAsync")
open fun __sendMessageAsyncForJava__(message: String): Future<MessageReceipt<Contact>> {
return future { sendMessage(message) }
}
/**
* 上传一个图片以备发送.
*
* @see BeforeImageUploadEvent 图片发送前事件, cancellable
* @see ImageUploadEvent 图片发送完成事件
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: ExternalImage): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传, 但不发送
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: URL): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: InputStream): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传, 但不发送
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: Input): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中将文件作为图片上传, 但不发送
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: File): Future<Image> {
return future { uploadImage(image) }
}
/**
* [Dispatchers.IO] 中将图片上传, 但不发送. 不会保存临时文件
*/
@JvmName("uploadImageAsync")
open fun __uploadImageAsyncForJava__(image: BufferedImage): Future<Image> {
return future { uploadImage(image) }
}
}
@Suppress("INAPPLICABLE_JVM_NAME", "FunctionName", "unused", "unused")
@MiraiInternalAPI
@JavaHappyAPI
actual abstract class MemberJavaHappyAPI : QQ() {
private inline fun <R> runBlocking(crossinline block: suspend Member.() -> R): R {
@Suppress("CAST_NEVER_SUCCEEDS")
return kotlinx.coroutines.runBlocking { block(this@MemberJavaHappyAPI as Member) }
}
private inline fun <R> future(crossinline block: suspend Member.() -> R): Future<R> {
@Suppress("CAST_NEVER_SUCCEEDS")
return (this as Member).run { future { block() } }
}
/**
* 禁言.
*
* QQ 中最小操作和显示的时间都是一分钟.
* 机器人可以实现精确到秒, 会被客户端显示为 1 分钟但不影响实际禁言时间.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 机器人无权限时返回 `false`
*
* @see Int.minutesToSeconds
* @see Int.hoursToSeconds
* @see Int.daysToSeconds
*
* @see MemberMuteEvent 成员被禁言事件
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("mute")
open fun __muteBlockingForJava__(seconds: Int) {
runBlocking { mute(seconds) }
}
/**
* 解除禁言.
*
* 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言.
*
* @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("unmute")
open fun __unmuteBlockingForJava__() {
runBlocking { unmute() }
}
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kick")
open fun __kickBlockingForJava__(message: String) {
runBlocking { kick() }
}
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kick")
open fun __kickBlockingForJava__() = __kickBlockingForJava__("")
/**
* 禁言.
*
* QQ 中最小操作和显示的时间都是一分钟.
* 机器人可以实现精确到秒, 会被客户端显示为 1 分钟但不影响实际禁言时间.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 机器人无权限时返回 `false`
*
* @see Int.minutesToSeconds
* @see Int.hoursToSeconds
* @see Int.daysToSeconds
*
* @see MemberMuteEvent 成员被禁言事件
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("muteAsync")
open fun __muteAsyncForJava__(seconds: Int): Future<Unit> {
return future { mute(seconds) }
}
/**
* 解除禁言.
*
* 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言.
*
* @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("unmuteAsync")
open fun __unmuteAsyncForJava__(): Future<Unit> {
return future { unmute() }
}
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kickAsync")
open fun __kickAsyncForJava__(message: String): Future<Unit> {
return future { kick() }
}
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("kickAsync")
open fun __kickAsyncForJava__(): Future<Unit> = __kickAsyncForJava__("")
}

View File

@ -0,0 +1,209 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiExperimentalAPI
/**
* . QQ Android 中叫做 "Troop"
*/
actual abstract class Group : Contact(), CoroutineScope {
/**
* 群名称.
*
* 在修改时将会异步上传至服务器.
* 频繁修改可能会被服务器拒绝.
*
* @see MemberPermissionChangeEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var name: String
/**
* 入群公告, 没有时为空字符串.
*
* 在修改时将会异步上传至服务器.
*
* @see GroupEntranceAnnouncementChangeEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var entranceAnnouncement: String
/**
* 全体禁言状态. `true` 为开启.
*
* 当前仅能修改状态.
*
* @see GroupMuteAllEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var isMuteAll: Boolean
/**
* 坦白说状态. `true` 为允许.
*
* 在修改时将会异步上传至服务器.
*
* @see GroupAllowConfessTalkEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var isConfessTalkEnabled: Boolean
/**
* 允许群员邀请好友入群的状态. `true` 为允许
*
* 在修改时将会异步上传至服务器.
*
* @see GroupAllowMemberInviteEvent
* @throws PermissionDeniedException 无权限修改时将会抛出异常
*/
actual abstract var isAllowMemberInvite: Boolean
/**
* 自动加群审批
*/
actual abstract val isAutoApproveEnabled: Boolean
/**
* 匿名聊天
*/
actual abstract val isAnonymousChatEnabled: Boolean
/**
* 同为 groupCode, 用户看到的群号码.
*/
actual abstract override val id: Long
/**
* 群主.
*
* @return 若机器人是群主, 返回 [botAsMember]. 否则返回相应的成员
*/
actual abstract val owner: Member
/**
* [Bot] 在群内的 [Member] 实例
*/
@MiraiExperimentalAPI
actual abstract val botAsMember: Member
/**
* 机器人被禁言还剩余多少秒
*
* @see BotMuteEvent 机器人被禁言事件
* @see isBotMuted 判断机器人是否正在被禁言
*/
actual abstract val botMuteRemaining: Int
/**
* 机器人在这个群里的权限
*
* @see Group.checkBotPermission 检查 [Bot] 在这个群里的权限
* @see Group.checkBotPermissionOperator 要求 [Bot] 在这个群里的权限为 [管理员或群主][MemberPermission.isOperator]
*
* @see BotGroupPermissionChangeEvent 机器人群员修改
*/
actual abstract val botPermission: MemberPermission
/**
* 群头像下载链接.
*/
actual val avatarUrl: String
get() = "https://p.qlogo.cn/gh/$id/${id}_1/640"
/**
* 群成员列表, 不含机器人自己, 含群主.
* [Group] 实例创建的时候查询一次. 并与事件同步事件更新
*/
actual abstract val members: ContactList<Member>
/**
* 获取群成员实例. 不存在时抛出 [kotlin.NoSuchElementException]
*/
actual abstract operator fun get(id: Long): Member
/**
* 获取群成员实例, 不存在则 null
*/
actual abstract fun getOrNull(id: Long): Member?
/**
* 检查此 id 的群成员是否存在
*/
actual abstract operator fun contains(id: Long): Boolean
/**
* 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败
*/
@MiraiExperimentalAPI("还未支持")
actual abstract suspend fun quit(): Boolean
/**
* 构造一个 [Member].
* 非特殊情况请不要使用这个函数. 优先使用 [get].
*/
@JvmName("newMember")
@Suppress("INAPPLICABLE_JVM_NAME", "FunctionName")
@MiraiExperimentalAPI("dangerous")
actual abstract fun Member(memberInfo: MemberInfo): Member
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
@JvmSynthetic
actual abstract override suspend fun sendMessage(message: MessageChain): MessageReceipt<Group>
actual companion object {
/**
* by @kar98k
*/
actual fun calculateGroupUinByGroupCode(groupCode: Long): Long {
var left: Long = groupCode / 1000000L
when (left) {
in 0..10 -> left += 202
in 11..19 -> left += 480 - 11
in 20..66 -> left += 2100 - 20
in 67..156 -> left += 2010 - 67
in 157..209 -> left += 2147 - 157
in 210..309 -> left += 4100 - 210
in 310..499 -> left += 3800 - 310
}
return left * 1000000L + groupCode % 1000000L
}
actual fun calculateGroupCodeByGroupUin(groupUin: Long): Long {
var left: Long = groupUin / 1000000L
when (left) {
in 0 + 202..10 + 202 -> left -= 202
in 11 + 480 - 11..19 + 480 - 11 -> left -= 480 - 11
in 20 + 2100 - 20..66 + 2100 - 20 -> left -= 2100 - 20
in 67 + 2010 - 67..156 + 2010 - 67 -> left -= 2010 - 67
in 157 + 2147 - 157..209 + 2147 - 157 -> left -= 2147 - 157
in 210 + 4100 - 210..309 + 4100 - 210 -> left -= 4100 - 210
in 310 + 3800 - 310..499 + 3800 - 310 -> left -= 3800 - 310
}
return left * 1000000L + groupUin % 1000000L
}
}
@MiraiExperimentalAPI
actual fun toFullString(): String {
return "Group(id=${this.id}, name=$name, owner=${owner.id}, members=${members.idContentString})"
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.contact
import net.mamoe.mirai.Bot
import net.mamoe.mirai.JavaHappyAPI
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.WeakRefProperty
/**
* 群成员.
*/
@UseExperimental(MiraiInternalAPI::class, JavaHappyAPI::class)
@Suppress("INAPPLICABLE_JVM_NAME")
actual abstract class Member : MemberJavaHappyAPI() {
/**
* 所在的群.
*/
@WeakRefProperty
actual abstract val group: Group
/**
* 成员的权限, 动态更新.
*
* @see MemberPermissionChangeEvent 权限变更事件. 由群主或机器人的操作触发.
*/
actual abstract val permission: MemberPermission
/**
* 群名片. 可能为空.
*
* 管理员和群主都可修改任何人包括群主的群名片.
*
* 在修改时将会异步上传至服务器.
*
* @see [nameCardOrNick] 获取非空群名片或昵称
*
* @see MemberCardChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时
*/
actual abstract var nameCard: String
/**
* 群头衔.
*
* 仅群主可以修改群头衔.
*
* 在修改时将会异步上传至服务器.
*
* @see MemberSpecialTitleChangeEvent 群名片被管理员, 自己或 [Bot] 改动事件. 修改时也会触发此事件.
* @throws PermissionDeniedException 无权限修改时
*/
actual abstract var specialTitle: String
/**
* 被禁言剩余时长. 单位为秒.
*
* @see isMuted 判断改成员是否处于禁言状态
* @see mute 设置禁言
* @see unmute 取消禁言
*/
actual abstract val muteTimeRemaining: Int
/**
* 禁言.
*
* QQ 中最小操作和显示的时间都是一分钟.
* 机器人可以实现精确到秒, 会被客户端显示为 1 分钟但不影响实际禁言时间.
*
* 管理员可禁言成员, 群主可禁言管理员和群员.
*
* @param durationSeconds 持续时间. 精确到秒. 范围区间表示为 `(0s, 30days]`. 超过范围则会抛出异常.
* @return 机器人无权限时返回 `false`
*
* @see Int.minutesToSeconds
* @see Int.hoursToSeconds
* @see Int.daysToSeconds
*
* @see MemberMuteEvent 成员被禁言事件
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("muteSuspend")
@JvmSynthetic
actual abstract suspend fun mute(durationSeconds: Int)
/**
* 解除禁言.
*
* 管理员可解除成员的禁言, 群主可解除管理员和群员的禁言.
*
* @see MemberUnmuteEvent 成员被取消禁言事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("muteSuspend")
@JvmSynthetic
actual abstract suspend fun unmute()
/**
* 踢出该成员.
*
* 管理员可踢出成员, 群主可踢出管理员和群员.
*
* @see MemberLeaveEvent.Kick 成员被踢出事件.
* @throws PermissionDeniedException 无权限修改时
*/
@JvmName("muteSuspend")
@JvmSynthetic
actual abstract suspend fun kick(message: String)
/**
* 当且仅当 `[other] is [Member] && [other].id == this.id && [other].group == this.group` 时为 true
*/
actual abstract override fun equals(other: Any?): Boolean
/**
* @return `bot.hashCode() * 31 + id.hashCode()`
*/
actual abstract override fun hashCode(): Int
}

View File

@ -0,0 +1,88 @@
@file:Suppress("unused")
package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.Bot
import net.mamoe.mirai.data.FriendNameRemark
import net.mamoe.mirai.data.PreviousNameList
import net.mamoe.mirai.data.Profile
import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.MiraiExperimentalAPI
/**
* QQ 对象.
* 注意: 一个 [QQ] 实例并不是独立的, 它属于一个 [Bot].
* 它不能被直接构造. 任何时候都应从 [Bot.getFriend] 或事件中获取.
*
* 对于同一个 [Bot] 任何一个人的 [QQ] 实例都是单一的.
*
* A QQ instance helps you to receive event from or sendPacket event to.
* Notice that, one QQ instance belong to one [Bot], that is, QQ instances from different [Bot] are NOT the same.
*
* @author Him188moe
*/
@Suppress("INAPPLICABLE_JVM_NAME")
actual abstract class QQ : Contact(), CoroutineScope {
/**
* 请求头像下载链接
*/
// @MiraiExperimentalAPI
//suspend fun queryAvatar(): AvatarLink
/**
* QQ 号码
*/
actual abstract override val id: Long
/**
* 昵称
*/
actual abstract val nick: String
/**
* 查询用户资料
*/
@MiraiExperimentalAPI("还未支持")
actual abstract suspend fun queryProfile(): Profile
/**
* 头像下载链接
*/
actual val avatarUrl: String
get() = "http://q1.qlogo.cn/g?b=qq&nk=$id&s=640"
/**
* 查询曾用名.
*
* 曾用名可能是:
* - 昵称
* - 共同群内的群名片
*/
@MiraiExperimentalAPI("还未支持")
actual abstract suspend fun queryPreviousNameList(): PreviousNameList
/**
* 查询机器人账号给这个人设置的备注
*/
@MiraiExperimentalAPI("还未支持")
actual abstract suspend fun queryRemark(): FriendNameRemark
/**
* 向这个对象发送消息.
*
* @see FriendMessageSendEvent 发送好友信息事件, cancellable
* @see GroupMessageSendEvent 发送群消息事件. cancellable
*
* @throws EventCancelledException 当发送消息事件被取消
* @throws IllegalStateException 发送群消息时若 [Bot] 被禁言抛出
*
* @return 消息回执. 可进行撤回 ([MessageReceipt.recall])
*/
@JvmName("sendMessageSuspend")
@JvmSynthetic
actual abstract override suspend fun sendMessage(message: MessageChain): MessageReceipt<QQ>
}

View File

@ -155,35 +155,35 @@ suspend inline fun Contact.sendImage(file: File) = file.sendAsImageTo(this)
// region Contact.uploadImage(IMAGE)
/**
* [Dispatchers.IO] 中将图片发送到指定联系人. 不会保存临时文件
* [Dispatchers.IO] 中将图片上传, 但不发送. 不会保存临时文件
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(bufferedImage: BufferedImage): Image = bufferedImage.upload(this)
/**
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片发送到指定联系人
* [Dispatchers.IO] 中下载 [URL] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(imageUrl: URL): Image = imageUrl.uploadAsImage(this)
/**
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片发送到指定联系人
* [Dispatchers.IO] 中读取 [Input] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(imageInput: Input): Image = imageInput.uploadAsImage(this)
/**
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人
* [Dispatchers.IO] 中读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)
suspend inline fun Contact.uploadImage(imageStream: InputStream): Image = imageStream.uploadAsImage(this)
/**
* [Dispatchers.IO] 中将文件作为图片发送到指定联系人
* [Dispatchers.IO] 中将文件作为图片上传, 但不发送
* @throws OverFileSizeMaxException
*/
@Throws(OverFileSizeMaxException::class)

View File

@ -0,0 +1,6 @@
package net.mamoe.mirai.utils
/**
* 图片文件过大
*/
actual class OverFileSizeMaxException : IllegalStateException()